packages feed

shomei-core-0.2.0.0: src/Shomei/Mfa/Totp/Store.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}

-- | Store effect for a user's TOTP (RFC 6238) credential (EP-7).
--
-- One credential per user (@UNIQUE (user_id)@). The port speaks in /raw/ 'TotpSecret's; the
-- PostgreSQL interpreter encrypts on the way in and decrypts on the way out (AES-256-GCM),
-- while the in-memory interpreter holds the raw bytes. 'UpsertTotpEnrollment' replaces an
-- existing /unconfirmed/ enrollment (re-scanning the QR); refusing to overwrite a /confirmed/
-- credential is the workflow's job, not the store's.
module Shomei.Mfa.Totp.Store
  ( TotpCredentialStore (..),
    upsertTotpEnrollment,
    findTotpByUser,
    confirmTotp,
    setTotpLastUsedCounter,
    deleteTotpByUser,
  )
where

import Data.Int (Int64)
import Effectful (Dispatch (Dynamic), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (send)
import Shomei.Id (TotpCredentialId, UserId)
import Shomei.Mfa.Totp.Domain (NewTotpCredential, TotpCredential)
import Shomei.Prelude

data TotpCredentialStore :: Effect where
  -- | Insert the enrollment, replacing any existing (unconfirmed) row for the user.
  UpsertTotpEnrollment :: NewTotpCredential -> TotpCredentialStore m TotpCredential
  FindTotpByUser :: UserId -> TotpCredentialStore m (Maybe TotpCredential)
  -- | Mark the credential confirmed (activated) at the given time.
  ConfirmTotp :: TotpCredentialId -> UTCTime -> TotpCredentialStore m ()
  -- | Advance the replay-defense high-water counter only when the supplied value is newer.
  -- 'False' means another request already accepted this counter (or a later one).
  SetTotpLastUsedCounter :: TotpCredentialId -> Int64 -> TotpCredentialStore m Bool
  DeleteTotpByUser :: UserId -> TotpCredentialStore m ()

type instance DispatchOf TotpCredentialStore = Dynamic

upsertTotpEnrollment :: (TotpCredentialStore :> es) => NewTotpCredential -> Eff es TotpCredential
upsertTotpEnrollment = send . UpsertTotpEnrollment

findTotpByUser :: (TotpCredentialStore :> es) => UserId -> Eff es (Maybe TotpCredential)
findTotpByUser = send . FindTotpByUser

confirmTotp :: (TotpCredentialStore :> es) => TotpCredentialId -> UTCTime -> Eff es ()
confirmTotp i t = send (ConfirmTotp i t)

setTotpLastUsedCounter :: (TotpCredentialStore :> es) => TotpCredentialId -> Int64 -> Eff es Bool
setTotpLastUsedCounter i c = send (SetTotpLastUsedCounter i c)

deleteTotpByUser :: (TotpCredentialStore :> es) => UserId -> Eff es ()
deleteTotpByUser = send . DeleteTotpByUser