-- | A module for authenticating to create sessions and handling them module Gargantext.Sessions ( module Gargantext.Sessions.Types , WithSession, WithSessionContext , load, change , Action(..), act, delete, get, post, put, put_ , postAuthRequest, postForgotPasswordRequest , deleteWithBody, postWwwUrlencoded , getCacheState, setCacheState ) where import DOM.Simple.Console (log2) import Data.Either (Either(..), hush) import Data.Map as Map import Data.Maybe (Maybe(..), fromMaybe) import Effect (Effect) import Effect.Aff (Aff) import Reactix as R import Simple.JSON as JSON import Toestand as T import Web.Storage.Storage (getItem, removeItem, setItem) import Gargantext.Prelude import Gargantext.Components.Login.Types (AuthData(..), AuthInvalid(..), AuthRequest(..), AuthResponse(..)) import Gargantext.Components.Nodes.Lists.Types as NT import Gargantext.Config.REST as REST import Gargantext.Ends (class ToUrl, Backend, toUrl) import Gargantext.Sessions.Types (Session(..), Sessions(..), OpenNodes, NodeId, mkNodeId, sessionUrl, sessionId, empty, null, unSessions, lookup, cons, tryCons, update, remove, tryRemove) import Gargantext.Utils.Reactix as R2 type WithSession c = ( session :: Session | c ) type WithSessionContext c = ( session :: R.Context Session | c ) load :: forall c. T.Write c Sessions => c -> Effect Sessions load cell = do sessions <- loadSessions T.write sessions cell change :: forall c . T.Read c Sessions => T.Write c Sessions => Action -> c -> Effect Sessions change action cell = do cur <- T.read cell new <- act cur action saveSessions new *> T.write new cell data Action = Login Session | Logout Session | Update Session act :: Sessions -> Action -> Effect Sessions act ss (Login s) = case tryCons s ss of Right new -> pure new _ -> pure ss <* log2 "Cannot overwrite existing session: " (sessionId s) act old@(Sessions ss) (Logout s) = case tryRemove (sessionId s) old of Right new -> pure $ new _ -> pure old <* log2 "Logged out of stale session:" (sessionId s) act ss (Update s) = saveSessions $ update s ss -- Key we will store the data under localStorageKey :: String localStorageKey = "garg-sessions" getCacheState :: NT.CacheState -> Session -> Int -> NT.CacheState getCacheState defaultCacheState (Session { caches }) nodeId = fromMaybe defaultCacheState $ Map.lookup nodeId caches setCacheState :: Session -> Int -> NT.CacheState -> Session setCacheState (Session session@{ caches }) nodeId cacheState = Session $ session { caches = Map.insert nodeId cacheState caches } -- | Will attempt to load saved sessions from localstorage. should log -- | if decoding fails loadSessions :: Effect Sessions loadSessions = do storage <- R2.getls mItem :: Maybe String <- getItem localStorageKey storage case mItem of Nothing -> pure empty Just val -> do let r = JSON.readJSON val case hush r of Nothing -> pure empty Just p -> pure p -- loadSessions = R2.getls >>= getItem localStorageKey >>= handleMaybe -- where -- -- a localstorage lookup can find nothing -- handleMaybe (Just val) = handleEither (JSON.readJSON val) -- handleMaybe Nothing = pure empty -- -- either parsing or decoding could fail, hence two errors -- handleEither (Left err) = err *> pure empty -- handleEither (Right ss) = pure ss saveSessions :: Sessions -> Effect Sessions saveSessions sessions = effect *> pure sessions where rem = R2.getls >>= removeItem localStorageKey set v = R2.getls >>= setItem localStorageKey v effect | null sessions = rem | otherwise = set (JSON.writeJSON sessions) postAuthRequest :: Backend -> AuthRequest -> Aff (Either String Session) postAuthRequest backend ar@(AuthRequest {username}) = decode <$> REST.post Nothing (toUrl backend "auth") ar where decode (Left err) = Left $ "Error when sending REST.post: " <> show err decode (Right (AuthResponse ar2)) | {inval: Just (AuthInvalid {message})} <- ar2 = Left message | {valid: Just (AuthData {token, tree_id, user_id})} <- ar2 = Right $ Session { backend, caches: Map.empty, token, treeId: tree_id, username, userId: user_id } | otherwise = Left "Invalid response from server" postForgotPasswordRequest :: Backend -> String -> Aff (Either String { status :: String }) postForgotPasswordRequest backend email = decode <$> REST.post Nothing (toUrl backend "async/forgot-password") { email } where decode (Left err) = Left $ "Error when sending REST.post: " <> show err decode (Right s) = Right s get :: forall a p. JSON.ReadForeign a => ToUrl Session p => Session -> p -> REST.AffRESTError a get session@(Session {token}) p = REST.get (Just token) (toUrl session p) put :: forall a b p. JSON.WriteForeign a => JSON.ReadForeign b => ToUrl Session p => Session -> p -> a -> REST.AffRESTError b put session@(Session {token}) p = REST.put (Just token) (toUrl session p) put_ :: forall b p. JSON.ReadForeign b => ToUrl Session p => Session -> p -> REST.AffRESTError b put_ session@(Session {token}) p = REST.put_ (Just token) (toUrl session p) delete :: forall a p. JSON.ReadForeign a => ToUrl Session p => Session -> p -> REST.AffRESTError a delete session@(Session {token}) p = REST.delete (Just token) (toUrl session p) -- This might not be a good idea: -- https://stackoverflow.com/questions/14323716/restful-alternatives-to-delete-request-body deleteWithBody :: forall a b p. JSON.WriteForeign a => JSON.ReadForeign b => ToUrl Session p => Session -> p -> a -> REST.AffRESTError b deleteWithBody session@(Session {token}) p = REST.deleteWithBody (Just token) (toUrl session p) post :: forall a b p. JSON.WriteForeign a => JSON.ReadForeign b => ToUrl Session p => Session -> p -> a -> REST.AffRESTError b post session@(Session {token}) p = REST.post (Just token) (toUrl session p) postWwwUrlencoded :: forall b p. JSON.ReadForeign b => ToUrl Session p => Session -> p -> REST.FormDataParams -> REST.AffRESTError b postWwwUrlencoded session@(Session {token}) p = REST.postWwwUrlencoded (Just token) (toUrl session p)