Skip to content

Instantly share code, notes, and snippets.

@maxhoffmann
Last active October 9, 2020 14:40
Show Gist options
  • Save maxhoffmann/117e0d31e7976a40477738975f4903d3 to your computer and use it in GitHub Desktop.
Save maxhoffmann/117e0d31e7976a40477738975f4903d3 to your computer and use it in GitHub Desktop.
Elm effect manager that handles token authentication
effect module TokenRequest where { command = MyCmd } exposing (request)
import Task exposing (..)
import Http
import Process
import Json.Decode as Json exposing ((:=))
import Native.Api
import Json.Decode as Json
import Time exposing (Time)
-- COMMANDS
type MyCmd msg
= ApiRequest (String -> Task Never msg)
request : (Http.Error -> msg) -> (a -> msg) -> (String -> Task Http.Error a) -> Cmd msg
request onFail onSuccess request =
command
<| ApiRequest
<| request
>> Task.map onSuccess
>> flip Task.onError (succeed << onFail)
cmdMap : (msg -> msg') -> MyCmd msg -> MyCmd msg'
cmdMap func (ApiRequest requestWithoutToken) =
ApiRequest (Task.map func << requestWithoutToken)
-- MANAGER
type alias State msg =
{ token : Token
, requestQueue : List (MyCmd msg)
, retries : Float
}
type Token
= Valid String Time
| Invalid
| Refreshing
init : Task Never (State msg)
init =
succeed (State Invalid [] 0)
-- HANDLE APP MESSAGES
onEffects :
Platform.Router msg Msg
-> List (MyCmd msg)
-> State msg
-> Task Never (State msg)
onEffects router requests ({ token, requestQueue } as state) =
let
updatedState =
{ state | requestQueue = List.append requests requestQueue }
in
case token of
Invalid ->
Process.spawn (Platform.sendToSelf router RefreshToken)
&> succeed updatedState
Refreshing ->
succeed updatedState
Valid secret expireTime ->
sendRequests router secret expireTime
&> succeed updatedState
sendRequests : Platform.Router msg Msg -> String -> Float -> Task Never Process.Id
sendRequests router token expireTime =
let
sendRequestsOrRefreshToken now =
if now >= expireTime then
Platform.sendToSelf router RefreshToken
else
Platform.sendToSelf router (SendRequests token)
in
Process.spawn (Time.now `andThen` sendRequestsOrRefreshToken)
-- HANDLE SELF MESSAGES
type Msg
= RefreshToken
| RefreshSuccess ( String, Time )
| RefreshFailure Http.Error
| SendRequests String
onSelfMsg : Platform.Router msg Msg -> Msg -> State msg -> Task Never (State msg)
onSelfMsg router selfMsg state =
case selfMsg of
RefreshToken ->
refreshToken router
&> succeed { state | token = Refreshing }
RefreshSuccess ( secret, expirationTime ) ->
Process.spawn (Platform.sendToSelf router (SendRequests secret))
&> succeed { state | token = Valid secret expirationTime, retries = 0 }
RefreshFailure error ->
case error of
Http.UnexpectedPayload payload ->
Process.spawn (redirect "/sign_in")
&> succeed state
_ ->
if state.retries < 3 then
Process.spawn
(Process.sleep (state.retries * 5000)
&> Platform.sendToSelf router RefreshToken
)
&> succeed { state | retries = state.retries + 1 }
else
succeed state
SendRequests token ->
sequence (List.map (sendCmd router token) state.requestQueue)
&> succeed { state | requestQueue = [] }
refreshToken : Platform.Router msg Msg -> Task Never Process.Id
refreshToken router =
let
refreshSuccess tokenValues =
Platform.sendToSelf router (RefreshSuccess tokenValues)
refreshFailure error =
Platform.sendToSelf router (RefreshFailure error)
attemptRefresh =
map2 calculateExpireTime requestNewToken Time.now
`andThen` refreshSuccess
`onError` refreshFailure
in
Process.spawn attemptRefresh
calculateExpireTime : ( String, Time ) -> Time -> ( String, Time )
calculateExpireTime ( token, expires_in ) now =
( token, now + (expires_in * 1000) )
sendCmd : Platform.Router msg Msg -> String -> MyCmd msg -> Task Never Process.Id
sendCmd router token (ApiRequest requestWithoutToken) =
Process.spawn (requestWithoutToken token `andThen` Platform.sendToApp router)
requestNewToken : Task Http.Error ( String, Time )
requestNewToken =
Http.send Http.defaultSettings
{ verb = "POST"
, headers =
[ ( "Accept", "application/json" )
, ( "Content-Type", "application/json" )
]
, url = "/token_endpoint"
, body = Http.empty
}
|> Http.fromJson
(Json.object2 (,)
("token" := Json.string)
("expires_in" := Json.float)
)
-- Helpers
(&>) : Task a b -> Task a c -> Task a c
(&>) task1 task2 =
task1 `andThen` \_ -> task2
-- Native
redirect : String -> Task x ()
redirect path =
Native.Api.redirect path
-- Usage
Api.request ResponseFail ResponseSuccess fetchData
-- Example for fetchData
fetchData : String -> Task Http.Error DataType
fetchData token =
Http.send Http.defaultSettings
{ verb = "GET"
, headers = defaultApiHeaders token
, url = "https://api.com/get_data"
, body = Http.empty
}
|> Http.fromJson dataTypeDecoder
defaultApiHeaders : String -> List ( String, String )
defaultApiHeaders token =
[ ( "Authorization", "Bearer " ++ token )
, ( "Accept", "application/json" )
, ( "Content-Type", "application/json" )
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment