polysemy-account-0.2.0.0: lib/Polysemy/Account/Interpreter/Accounts.hs
{-# options_haddock prune #-}
-- | Description: Interpreters for 'Accounts' and 'Password'
module Polysemy.Account.Interpreter.Accounts where
import Chronos (Datetime)
import Polysemy.Db (Id, Query, Store, newId)
import qualified Polysemy.Db.Effect.Query as Query
import qualified Polysemy.Db.Effect.Store as Store
import Sqel (Uid (Uid))
import Polysemy.Account.Data.Account (Account (Account))
import Polysemy.Account.Data.AccountAuth (AccountAuth (AccountAuth))
import Polysemy.Account.Data.AccountAuthDescription (AccountAuthDescription)
import Polysemy.Account.Data.AccountByName (AccountByName (AccountByName))
import Polysemy.Account.Data.AccountName (AccountName)
import qualified Polysemy.Account.Data.AccountStatus as AccountStatus
import Polysemy.Account.Data.AccountStatus (AccountStatus)
import qualified Polysemy.Account.Data.AccountsConfig as AccountsConfig
import Polysemy.Account.Data.AccountsConfig (AccountsConfig (AccountsConfig))
import Polysemy.Account.Data.AccountsError (
AccountsClientError (Conflict, InvalidAuth, NoAccountId, NoAccountName),
AccountsError (Client, Internal),
)
import Polysemy.Account.Data.AuthForAccount (AuthForAccount (AuthForAccount))
import Polysemy.Account.Data.AuthedAccount (AuthedAccount (AuthedAccount))
import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword (GeneratedPassword))
import Polysemy.Account.Data.RawPassword (RawPassword (UnsafeRawPassword))
import Polysemy.Account.Effect.Accounts (Accounts (..))
import qualified Polysemy.Account.Effect.Password as Password
import Polysemy.Account.Effect.Password (Password)
import Polysemy.Account.Interpreter.AccountByName (interpretAccountByNameState)
import Polysemy.Account.Interpreter.AuthForAccount (interpretAuthForAccountState)
import Polysemy.Account.Interpreter.Password (interpretPasswordId)
dbError ::
∀ eff e r .
Show e =>
Members [eff !! e, Stop AccountsError] r =>
InterpreterFor eff r
dbError =
resumeHoist (Internal . show)
storeError ::
∀ a e i r .
Show e =>
Members [Store i a !! e, Stop AccountsError] r =>
InterpreterFor (Store i a) r
storeError =
resumeHoist (Internal . show)
queryError ::
∀ a q e r .
Show e =>
Members [Query q a !! e, Stop AccountsError] r =>
InterpreterFor (Query q a) r
queryError =
resumeHoist (Internal . show)
config ::
Show e =>
Members [Reader (AccountsConfig p) !! e, Stop AccountsError] r =>
Sem r (AccountsConfig p)
config =
dbError ask
byId ::
∀ i r a .
Members [Store i a, Stop AccountsError] r =>
i ->
Sem r (Uid i a)
byId accountId =
stopNote (Client NoAccountId) =<< Store.fetch accountId
byName ::
∀ i r a .
Members [Query AccountByName (Maybe (Uid i a)), Stop AccountsError] r =>
AccountName ->
Sem r (Uid i a)
byName name =
stopNote (Client NoAccountName) =<< Query.query (AccountByName name)
authedAccount ::
∀ i p r .
Members [Store i (Account p), Store i (AccountAuth i), Stop AccountsError] r =>
i ->
Sem r (AuthedAccount i p)
authedAccount authId = do
aa <- Store.fetch authId
Uid _ (AccountAuth accountId _ _ _) <- stopNote (Client InvalidAuth) aa
Uid _ (Account name status privs) <- byId accountId
pure (AuthedAccount accountId authId name status privs)
-- TODO see if Query for AccountAuth can be used without Uid, extracting it in the interpreter
authenticate ::
Show e =>
Member (Query AccountByName (Maybe (Uid i a)) !! e) r =>
Member (Query (AuthForAccount i) [Uid i (AccountAuth i)] !! e) r =>
Members [Stop AccountsError, Password] r =>
AccountName ->
RawPassword ->
Sem r (Uid i (AccountAuth i))
authenticate name password = do
Uid id' _ <- notFound =<< queryError (Query.query (AccountByName name))
auths <- queryError (Query.query (AuthForAccount id'))
invalid =<< findM check auths
where
notFound =
stopNote (Client NoAccountName)
check (Uid _ (AccountAuth _ _ hash _)) =
Password.check password hash
invalid =
stopNote (Client InvalidAuth)
privileges ::
∀ i p r .
Members [Store i (Account p), Stop AccountsError] r =>
i ->
Sem r p
privileges i =
Store.fetch i >>= \case
Just (Uid _ (Account _ _ privs)) -> pure privs
Nothing -> stop (Client NoAccountId)
addPassword ::
Members [Password, Store i (AccountAuth i), Id i, Stop AccountsError] r =>
AccountAuthDescription ->
i ->
RawPassword ->
Maybe Datetime ->
Sem r (Uid i (AccountAuth i))
addPassword desc accountId password expiry = do
hashedPassword <- Password.hash password
authId <- newId
let auth = Uid authId (AccountAuth accountId desc hashedPassword expiry)
auth <$ Store.insert auth
generatePassword ::
Show e =>
Members [Password, Store i (AccountAuth i), Reader (AccountsConfig p) !! e, Id i, Stop AccountsError] r =>
i ->
Maybe Datetime ->
Sem r GeneratedPassword
generatePassword accountId expiry = do
AccountsConfig {..} <- config
pw@(GeneratedPassword raw) <- Password.generate passwordLength
coerce pw <$ addPassword "auth token" accountId (UnsafeRawPassword raw) expiry
-- | Fail if the account name is already present in the store.
-- If the account status is `AccountStatus.Creating', however, a previous attempt has failed critically and the account
-- can be overwritten.
deletePreviousFailure ::
Members [Store i (Account p), Stop AccountsError] r =>
Uid i (Account p) ->
Sem r ()
deletePreviousFailure (Uid i (Account _ AccountStatus.Creating _)) =
void (Store.delete i)
deletePreviousFailure _ =
stop (Client Conflict)
create ::
∀ i p e r .
Members [Store i (Account p), Query AccountByName (Maybe (Uid i (Account p))), Reader (AccountsConfig p) !! e] r =>
Members [Id i, Stop AccountsError] r =>
AccountName ->
p ->
Sem r (Uid i (Account p))
create name privs = do
traverse_ deletePreviousFailure =<< Query.query (AccountByName name)
accountId <- newId
let account = Uid accountId (Account name AccountStatus.Creating privs)
account <$ Store.upsert account
finishCreate ::
∀ i p r .
Members [Store i (Account p), Stop AccountsError] r =>
Bool ->
i ->
Sem r (Uid i (Account p))
finishCreate active accountId = do
account :: Uid i (Account p) <- stopNote (Internal "Account absent after password creation") =<< Store.fetch accountId
let updatedAccount = account & #payload . #status .~ status
updatedAccount <$ Store.upsert (account & #payload . #status .~ status)
where
status = if active then AccountStatus.Active else AccountStatus.Pending
setStatus ::
Members [Store i (Account p), Stop AccountsError] r =>
i ->
AccountStatus ->
Sem r ()
setStatus accountId status = do
account <- stopNote (Client NoAccountId) =<< Store.fetch accountId
Store.upsert (account & #payload . #status .~ status)
updatePrivileges ::
∀ i p e r .
Show e =>
Members [Store i (Account p) !! e, Stop AccountsError] r =>
i ->
(p -> p) ->
Sem r ()
updatePrivileges i f =
dbError (Store.fetch i) >>= \case
Just account ->
dbError (Store.upsert (account & #payload . #privileges %~ f))
Nothing ->
stop (Client NoAccountId)
-- | Interpret 'Accounts' using 'Store' and 'Query' from [Polysemy.Db]("Polysemy.Db") as the storage backend.
interpretAccounts ::
∀ e i p r .
Show e =>
Member (Query AccountByName (Maybe (Uid i (Account p))) !! e) r =>
Member (Query (AuthForAccount i) [Uid i (AccountAuth i)] !! e) r =>
Members [Password, Store i (Account p) !! e, Store i (AccountAuth i) !! e, Reader (AccountsConfig p) !! e, Id i] r =>
InterpreterFor (Accounts i p !! AccountsError) r
interpretAccounts =
interpretResumable \case
Authenticate name password ->
authenticate name password
GeneratePassword accountId expiry ->
storeError (generatePassword accountId expiry)
Create name -> do
AccountsConfig {..} <- config
queryError (storeError (create name defaultPrivileges))
FinalizeCreate accountId -> do
AccountsConfig {..} <- config
storeError (finishCreate initActive accountId)
AddPassword accountId password expiry ->
storeError (addPassword "user login" accountId password expiry)
SetStatus accountId status ->
storeError (setStatus accountId status)
ById accountId ->
storeError (byId accountId)
ByName name ->
queryError (byName name)
Authed authId ->
storeError (storeError @(Account _) (authedAccount authId))
Update account ->
storeError (Store.upsert account)
Privileges i ->
storeError (privileges i)
UpdatePrivileges i f ->
updatePrivileges i f
All ->
storeError Store.fetchAll
AllAuths ->
storeError Store.fetchAll
-- | Interpret 'Accounts' and 'Password' using 'AtomicState' as storage backend.
interpretAccountsState ::
∀ i p r .
Ord i =>
Show i =>
Members [Log, Id i, Embed IO] r =>
AccountsConfig p ->
[Uid i (Account p)] ->
[Uid i (AccountAuth i)] ->
InterpretersFor [Accounts i p !! AccountsError, Password] r
interpretAccountsState conf accounts auths =
interpretPasswordId .
raiseResumable (runReader conf) .
interpretAccountByNameState accounts .
interpretAuthForAccountState auths .
interpretAccounts .
insertAt @1