servant-auth-token 0.2.0.1 → 0.3.0.0
raw patch · 6 files changed
+276/−19 lines, 6 filesdep ~servant-auth-token-apiPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: servant-auth-token-api
API changes (from Hackage documentation)
+ Servant.Server.Auth.Token: authGetSingleUseCodes :: AuthMonad m => UserId -> Maybe Word -> MToken' '["auth-single-codes"] -> m (OnlyField "codes" [SingleUseCode])
+ Servant.Server.Auth.Token: authSigninGetCode :: AuthMonad m => Maybe Login -> m Unit
+ Servant.Server.Auth.Token: authSigninPostCode :: AuthMonad m => Maybe Login -> Maybe SingleUseCode -> Maybe Seconds -> m (OnlyField "token" SimpleToken)
+ Servant.Server.Auth.Token: getAuthToken :: AuthMonad m => UserImplId -> Maybe Seconds -> m SimpleToken
+ Servant.Server.Auth.Token.Config: [singleUseCodeDefaultCount] :: AuthConfig -> !Word
+ Servant.Server.Auth.Token.Config: [singleUseCodeExpire] :: AuthConfig -> !NominalDiffTime
+ Servant.Server.Auth.Token.Config: [singleUseCodeGenerator] :: AuthConfig -> !(IO SingleUseCode)
+ Servant.Server.Auth.Token.Config: [singleUseCodePermamentMaximum] :: AuthConfig -> !Word
+ Servant.Server.Auth.Token.Config: [singleUseCodeSender] :: AuthConfig -> !(RespUserInfo -> SingleUseCode -> IO ())
+ Servant.Server.Auth.Token.Model: UserSingleUseCode :: !SingleUseCode -> !(Key UserImpl) -> !(Maybe UTCTime) -> !(Maybe UTCTime) -> UserSingleUseCode
+ Servant.Server.Auth.Token.Model: [userSingleUseCodeExpire] :: UserSingleUseCode -> !(Maybe UTCTime)
+ Servant.Server.Auth.Token.Model: [userSingleUseCodeUsed] :: UserSingleUseCode -> !(Maybe UTCTime)
+ Servant.Server.Auth.Token.Model: [userSingleUseCodeUser] :: UserSingleUseCode -> !(Key UserImpl)
+ Servant.Server.Auth.Token.Model: [userSingleUseCodeValue] :: UserSingleUseCode -> !SingleUseCode
+ Servant.Server.Auth.Token.Model: data UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance (Database.Persist.Class.PersistQuery.PersistQuery backend, Database.Persist.Class.PersistEntity.PersistEntityBackend Servant.Server.Auth.Token.Model.UserSingleUseCode ~ backend) => Database.Persist.Class.DeleteCascade.DeleteCascade Servant.Server.Auth.Token.Model.UserSingleUseCode backend
+ Servant.Server.Auth.Token.Model: instance Data.Aeson.Types.Class.FromJSON (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Data.Aeson.Types.Class.ToJSON (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Class.PersistEntity.PersistEntity Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Class.PersistField.PersistField (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Class.PersistField.PersistField Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Class.PersistStore.ToBackendKey Database.Persist.Sql.Types.SqlBackend Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Sql.Class.PersistFieldSql (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Database.Persist.Sql.Class.PersistFieldSql Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Classes.Eq (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance GHC.Classes.Ord (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Constructor Servant.Server.Auth.Token.Model.C1_0UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Datatype Servant.Server.Auth.Token.Model.D1UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Generic Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Selector Servant.Server.Auth.Token.Model.S1_0_0UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Selector Servant.Server.Auth.Token.Model.S1_0_1UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Selector Servant.Server.Auth.Token.Model.S1_0_2UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Generics.Selector Servant.Server.Auth.Token.Model.S1_0_3UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance GHC.Read.Read (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance GHC.Show.Show (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance GHC.Show.Show Servant.Server.Auth.Token.Model.UserSingleUseCode
+ Servant.Server.Auth.Token.Model: instance Web.HttpApiData.Internal.FromHttpApiData (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Web.HttpApiData.Internal.ToHttpApiData (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: instance Web.PathPieces.PathPiece (Database.Persist.Class.PersistEntity.Key Servant.Server.Auth.Token.Model.UserSingleUseCode)
+ Servant.Server.Auth.Token.Model: readUserInfoByLogin :: Login -> SqlPersistT IO (Maybe RespUserInfo)
+ Servant.Server.Auth.Token.Model: type UserSingleUseCodeId = Key UserSingleUseCode
+ Servant.Server.Auth.Token.SingleUse: generateSingleUsedCodes :: MonadIO m => UserImplId -> IO SingleUseCode -> Word -> SqlPersistT m [SingleUseCode]
+ Servant.Server.Auth.Token.SingleUse: invalideSingleUseCode :: MonadIO m => UserSingleUseCodeId -> SqlPersistT m ()
+ Servant.Server.Auth.Token.SingleUse: makeSingleUseExpire :: MonadIO m => NominalDiffTime -> m UTCTime
+ Servant.Server.Auth.Token.SingleUse: registerSingleUseCode :: MonadIO m => UserImplId -> SingleUseCode -> Maybe UTCTime -> SqlPersistT m ()
+ Servant.Server.Auth.Token.SingleUse: validateSingleUseCode :: MonadIO m => UserImplId -> SingleUseCode -> SqlPersistT m Bool
- Servant.Server.Auth.Token.Config: AuthConfig :: ConnectionPool -> !NominalDiffTime -> !NominalDiffTime -> !(RespUserInfo -> RestoreCode -> IO ()) -> !(IO RestoreCode) -> !(Maybe NominalDiffTime) -> !Int -> !(Text -> Maybe Text) -> !(ServantErr -> ServantErr) -> !Word -> AuthConfig
+ Servant.Server.Auth.Token.Config: AuthConfig :: ConnectionPool -> !NominalDiffTime -> !NominalDiffTime -> !(RespUserInfo -> RestoreCode -> IO ()) -> !(IO RestoreCode) -> !(Maybe NominalDiffTime) -> !Int -> !(Text -> Maybe Text) -> !(ServantErr -> ServantErr) -> !Word -> !(RespUserInfo -> SingleUseCode -> IO ()) -> !NominalDiffTime -> !(IO SingleUseCode) -> !Word -> !Word -> AuthConfig
Files
- CHANGELOG.md +5/−0
- servant-auth-token.cabal +4/−3
- src/Servant/Server/Auth/Token.hs +125/−9
- src/Servant/Server/Auth/Token/Config.hs +32/−0
- src/Servant/Server/Auth/Token/Model.hs +25/−7
- src/Servant/Server/Auth/Token/SingleUse.hs +85/−0
CHANGELOG.md view
@@ -1,3 +1,8 @@+0.3.0.0+=======++* Add authorisation by single usage codes.+ 0.2.0.1 =======
servant-auth-token.cabal view
@@ -1,5 +1,5 @@ name: servant-auth-token-version: 0.2.0.1+version: 0.3.0.0 synopsis: Servant based API and server for token based authorisation description: Please see README.md homepage: https://github.com/ncrashed/servant-auth-token#readme@@ -18,6 +18,7 @@ library hs-source-dirs: src exposed-modules:+ Servant.Server.Auth.Token Servant.Server.Auth.Token.Common Servant.Server.Auth.Token.Config Servant.Server.Auth.Token.Error@@ -26,7 +27,7 @@ Servant.Server.Auth.Token.Pagination Servant.Server.Auth.Token.Patch Servant.Server.Auth.Token.Restore - Servant.Server.Auth.Token+ Servant.Server.Auth.Token.SingleUse build-depends: base >= 4.7 && < 5 , aeson-injector >= 1.0.4 && < 1.1@@ -37,7 +38,7 @@ , persistent-postgresql >= 2.2 && < 2.6 , persistent-template >= 2.1 && < 2.6 , pwstore-fast >= 2.4 && < 2.5- , servant-auth-token-api >= 0.2.0 && < 0.3+ , servant-auth-token-api >= 0.3.0 && < 0.4 , servant-server >= 0.7 && < 0.9 , text >= 1.2 && < 1.3 , time >= 1.5 && < 1.7
src/Servant/Server/Auth/Token.hs view
@@ -70,6 +70,8 @@ , authUserByToken -- * API methods , authSignin+ , authSigninGetCode+ , authSigninPostCode , authTouch , authToken , authSignout@@ -80,12 +82,15 @@ , authUserPut , authUserDelete , authRestore+ , authGetSingleUseCodes , authGroupGet , authGroupPost , authGroupPut , authGroupPatch , authGroupDelete , authGroupList+ -- * Low-level API+ , getAuthToken ) where import Control.Monad @@ -111,6 +116,7 @@ import Servant.Server.Auth.Token.Monad import Servant.Server.Auth.Token.Pagination import Servant.Server.Auth.Token.Restore+import Servant.Server.Auth.Token.SingleUse import qualified Data.ByteString.Lazy as BS @@ -123,7 +129,7 @@ convertAuthHandler cfg = Nat (flip runReaderT cfg . runAuthHandler) -- | The interface your application should implement to be able to use--- token aurhorisation API.+-- token authorisation API. class Monad m => AuthMonad m where getAuthConfig :: m AuthConfig liftAuthAction :: ExceptT ServantErr IO a -> m a @@ -143,6 +149,8 @@ authServer :: AuthConfig -> Server AuthAPI authServer cfg = enter (convertAuthHandler cfg) ( authSignin+ :<|> authSigninGetCode+ :<|> authSigninPostCode :<|> authTouch :<|> authToken :<|> authSignout@@ -153,6 +161,7 @@ :<|> authUserPut :<|> authUserDelete :<|> authRestore+ :<|> authGetSingleUseCodes :<|> authGroupGet :<|> authGroupPost :<|> authGroupPut @@ -165,16 +174,12 @@ => Maybe Login -- ^ Login query parameter -> Maybe Password -- ^ Password query parameter -> Maybe Seconds -- ^ Expire query parameter, how many seconds the token is valid- -> m (OnlyField "token" SimpleToken) -- ^ If everthing is OK, return token+ -> m (OnlyField "token" SimpleToken) -- ^ If everything is OK, return token authSignin mlogin mpass mexpire = runAuth $ do login <- require "login" mlogin pass <- require "pass" mpass Entity uid UserImpl{..} <- guardLogin login pass- expire <- calcExpire mexpire- mt <- getExistingToken uid -- check whether there is already existing token- OnlyField <$> case mt of - Nothing -> createToken uid expire -- create new token- Just t -> touchToken t expire -- prolong token expiration time+ OnlyField <$> getAuthToken uid mexpire where guardLogin login pass = do -- check login and password, return passed user muser <- runDB $ selectFirst [UserImplLogin ==. login] []@@ -185,11 +190,23 @@ then return user else err - getExistingToken uid = do -- return active token for specified user id+-- | Helper to get or generate new token for user+getAuthToken :: AuthMonad m+ => UserImplId -- ^ User for whom we want token+ -> Maybe Seconds -- ^ Expiration duration, 'Nothing' means default+ -> m SimpleToken -- ^ Old token (if it doesn't expire) or new one+getAuthToken uid mexpire = runAuth $ do + expire <- calcExpire mexpire+ mt <- getExistingToken -- check whether there is already existing token+ case mt of + Nothing -> createToken expire -- create new token+ Just t -> touchToken t expire -- prolong token expiration time+ where+ getExistingToken = do -- return active token for specified user id t <- liftIO getCurrentTime runDB $ selectFirst [AuthTokenUser ==. uid, AuthTokenExpire >. t] [] - createToken uid expire = do -- generate and save fresh token + createToken expire = do -- generate and save fresh token token <- toText <$> liftIO nextRandom _ <- runDB $ insert AuthToken { authTokenValue = token @@ -198,6 +215,90 @@ } return token +-- | Authorisation via code of single usage.+--+-- Implementation of 'AuthSigninGetCodeMethod' endpoint.+--+-- Logic of authorisation via this method is:+-- +-- * Client sends GET request to 'AuthSigninGetCodeMethod' endpoint+--+-- * Server generates single use token and sends it via+-- SMS or email, defined in configuration by 'singleUseCodeSender' field.+--+-- * Client sends POST request to 'AuthSigninPostCodeMethod' endpoint+--+-- * Server responds with auth token.+--+-- * Client uses the token with other requests as authorisation+-- header+--+-- * Client can extend lifetime of token by periodically pinging+-- of 'AuthTouchMethod' endpoint+--+-- * Client can invalidate token instantly by 'AuthSignoutMethod'+--+-- * Client can get info about user with 'AuthTokenInfoMethod' endpoint.+--+-- See also: 'authSigninPostCode'+authSigninGetCode :: AuthMonad m + => Maybe Login -- ^ User login, required+ -> m Unit +authSigninGetCode mlogin = runAuth $ do + login <- require "login" mlogin + uinfo <- runDB404 "user" $ readUserInfoByLogin login+ let uid = toKey $ respUserId uinfo ++ AuthConfig{..} <- getConfig+ code <- liftIO singleUseCodeGenerator + expire <- makeSingleUseExpire singleUseCodeExpire+ runDB $ registerSingleUseCode uid code (Just expire)+ liftIO $ singleUseCodeSender uinfo code ++ return Unit ++-- | Authorisation via code of single usage.+--+-- Logic of authorisation via this method is:+-- +-- * Client sends GET request to 'AuthSigninGetCodeMethod' endpoint+--+-- * Server generates single use token and sends it via+-- SMS or email, defined in configuration by 'singleUseCodeSender' field.+--+-- * Client sends POST request to 'AuthSigninPostCodeMethod' endpoint+--+-- * Server responds with auth token.+--+-- * Client uses the token with other requests as authorisation+-- header+--+-- * Client can extend lifetime of token by periodically pinging+-- of 'AuthTouchMethod' endpoint+--+-- * Client can invalidate token instantly by 'AuthSignoutMethod'+--+-- * Client can get info about user with 'AuthTokenInfoMethod' endpoint.+--+-- See also: 'authSigninGetCode'+authSigninPostCode :: AuthMonad m + => Maybe Login -- ^ User login, required+ -> Maybe SingleUseCode -- ^ Received single usage code, required+ -> Maybe Seconds + -- ^ Time interval after which the token expires, 'Nothing' means + -- some default value+ -> m (OnlyField "token" SimpleToken)+authSigninPostCode mlogin mcode mexpire = runAuth $ do + login <- require "login" mlogin + code <- require "code" mcode++ uinfo <- runDB404 "user" $ readUserInfoByLogin login+ let uid = toKey $ respUserId uinfo + isValid <- runDB $ validateSingleUseCode uid code + unless isValid $ throw401 "Single usage code doesn't match"++ OnlyField <$> getAuthToken uid mexpire+ -- | Calculate expiration timestamp for token calcExpire :: Maybe Seconds -> AuthHandler UTCTime calcExpire mexpire = do @@ -383,6 +484,21 @@ user' <- setUserPassword pass user runDB $ replace uid user' return Unit ++-- | Implementation of 'AuthGetSingleUseCodes' endpoint.+authGetSingleUseCodes :: AuthMonad m + => UserId -- ^ Id of user+ -> Maybe Word -- ^ Number of codes. 'Nothing' means that server generates some default count of codes.+ -- And server can define maximum count of codes that user can have at once.+ -> MToken' '["auth-single-codes"]+ -> m (OnlyField "codes" [SingleUseCode])+authGetSingleUseCodes uid mcount token = runAuth $ do + guardAuthToken token + let uid' = toKey uid+ _ <- runDB404 "user" $ readUserInfo uid+ AuthConfig{..} <- getConfig + let n = min singleUseCodePermamentMaximum $ fromMaybe singleUseCodeDefaultCount mcount + runDB $ OnlyField <$> generateSingleUsedCodes uid' singleUseCodeGenerator n -- | Getting user by id, throw 404 response if not found guardUser :: UserImplId -> AuthHandler UserImpl
src/Servant/Server/Auth/Token/Config.hs view
@@ -59,6 +59,29 @@ , servantErrorFormer :: !(ServantErr -> ServantErr) -- | Default size of page for pagination , defaultPageSize :: !Word + -- | User specified method of sending single usage code for authorisation.+ -- + -- See also: endpoints 'AuthSigninGetCodeMethod' and 'AuthSigninPostCodeMethod'.+ --+ -- By default does nothing.+ , singleUseCodeSender :: !(RespUserInfo -> SingleUseCode -> IO ())+ -- | Time the generated single usage code expires after.+ --+ -- By default 1 hour.+ , singleUseCodeExpire :: !NominalDiffTime+ -- | User specified generator for single use codes. + --+ -- By default the server generates UUID that can be unacceptable for SMS way of sending.+ , singleUseCodeGenerator :: !(IO SingleUseCode)+ -- | Number of not expiring single use codes that user can have at once.+ --+ -- Used by 'AuthGetSingleUseCodes' endpoint. Default is 100.+ , singleUseCodePermamentMaximum :: !Word + -- | Number of not expiring single use codes that generated by default when client doesn't + -- specify the value.+ --+ -- Used by 'AuthGetSingleUseCodes' endpoint. Default is 20.+ , singleUseCodeDefaultCount :: !Word } -- | Default configuration for authorisation server@@ -74,8 +97,17 @@ , passwordValidator = const Nothing , servantErrorFormer = id , defaultPageSize = 50+ , singleUseCodeSender = const $ const $ return ()+ , singleUseCodeExpire = fromIntegral (60 * 60 :: Int) -- 1 hour+ , singleUseCodeGenerator = uuidSingleUseCodeGenerate+ , singleUseCodePermamentMaximum = 100+ , singleUseCodeDefaultCount = 20 } -- | Default generator of restore codes uuidCodeGenerate :: IO RestoreCode uuidCodeGenerate = toText <$> liftIO nextRandom++-- | Default generator of restore codes+uuidSingleUseCodeGenerate :: IO RestoreCode+uuidSingleUseCodeGenerate = toText <$> liftIO nextRandom
src/Servant/Server/Auth/Token/Model.hs view
@@ -19,6 +19,7 @@ , AuthUserGroupUsers(..) , AuthUserGroupPerms(..) , EntityField(..)+ , UserSingleUseCode(..) -- * IDs of entities , UserImplId , UserPermId@@ -27,6 +28,7 @@ , AuthUserGroupId , AuthUserGroupUsersId , AuthUserGroupPermsId+ , UserSingleUseCodeId -- * Operations , runDB , migrateAll@@ -35,6 +37,7 @@ -- ** User , userToUserInfo , readUserInfo+ , readUserInfoByLogin , getUserPermissions , setUserPermissions , createUser@@ -109,6 +112,13 @@ expire UTCTime deriving Generic Show +UserSingleUseCode+ value SingleUseCode + user UserImplId + expire UTCTime Maybe -- Nothing is code that never expires+ used UTCTime Maybe+ deriving Generic Show+ AuthUserGroup name Text parent AuthUserGroupId Maybe@@ -149,18 +159,26 @@ , respUserGroups = groups } +-- | Low level operation for collecting info about user+makeUserInfo :: Entity UserImpl -> SqlPersistT IO RespUserInfo+makeUserInfo euser = do + let uid = entityKey euser+ perms <- getUserPermissions uid + groups <- getUserGroups uid + return $ userToUserInfo euser perms groups+ -- | Get user by id readUserInfo :: UserId -> SqlPersistT IO (Maybe RespUserInfo) readUserInfo uid' = do let uid = toKey uid' muser <- get uid - case muser of - Nothing -> return Nothing - Just user -> do - perms <- getUserPermissions uid - groups <- getUserGroups uid - return . Just $ - userToUserInfo (Entity uid user) perms groups+ maybe (return Nothing) (fmap Just . makeUserInfo . Entity uid) $ muser++-- | Get user by login+readUserInfoByLogin :: Login -> SqlPersistT IO (Maybe RespUserInfo)+readUserInfoByLogin login = do + muser <- getBy $ UniqueLogin login + maybe (return Nothing) (fmap Just . makeUserInfo) muser -- | Return list of permissions for the given user (only permissions that are assigned to him directly) getUserPermissions :: UserImplId -> SqlPersistT IO [Permission]
+ src/Servant/Server/Auth/Token/SingleUse.hs view
@@ -0,0 +1,85 @@+{-|+Module : Servant.Server.Auth.Token.SingleUse+Description : Specific functions to work with single usage codes.+Copyright : (c) Anton Gushcha, 2016+License : MIT+Maintainer : ncrashed@gmail.com+Stability : experimental+Portability : Portable+-}+module Servant.Server.Auth.Token.SingleUse(+ makeSingleUseExpire+ , registerSingleUseCode+ , invalideSingleUseCode+ , validateSingleUseCode+ , generateSingleUsedCodes+ ) where ++import Control.Monad+import Control.Monad.IO.Class +import Data.Time +import Database.Persist.Sql +import Servant.API.Auth.Token+import Servant.Server.Auth.Token.Common+import Servant.Server.Auth.Token.Model ++-- | Calculate expire date for single usage code+makeSingleUseExpire :: MonadIO m => NominalDiffTime -- ^ Duration of code+ -> m UTCTime -- ^ Time when the code expires+makeSingleUseExpire dt = do + t <- liftIO getCurrentTime+ return $ dt `addUTCTime` t++-- | Register single use code in DB+registerSingleUseCode :: MonadIO m => UserImplId -- ^ Id of user+ -> SingleUseCode -- ^ Single usage code+ -> Maybe UTCTime -- ^ Time when the code expires, 'Nothing' is never expiring code+ -> SqlPersistT m () +registerSingleUseCode uid code expire = void $ insert + $ UserSingleUseCode code uid expire Nothing++-- | Marks single use code that it cannot be used again+invalideSingleUseCode :: MonadIO m => UserSingleUseCodeId -- ^ Id of code+ -> SqlPersistT m ()+invalideSingleUseCode i = do+ t <- liftIO getCurrentTime+ update i [UserSingleUseCodeUsed =. Just t] ++-- | Check single use code and return 'True' on success.+--+-- On success invalidates single use code.+validateSingleUseCode :: MonadIO m => UserImplId -- ^ Id of user + -> SingleUseCode -- ^ Single usage code + -> SqlPersistT m Bool+validateSingleUseCode uid code = do + t <- liftIO getCurrentTime+ mcode <- selectFirst ([+ UserSingleUseCodeValue ==. code+ , UserSingleUseCodeUser ==. uid+ , UserSingleUseCodeUsed ==. Nothing+ ] ++ (+ [UserSingleUseCodeExpire ==. Nothing]+ ||. [UserSingleUseCodeExpire >=. Just t]+ )) [Desc UserSingleUseCodeExpire]+ whenJust mcode $ invalideSingleUseCode . entityKey+ return $ maybe False (const True) mcode++-- | Generates a set single use codes that doesn't expire.+--+-- Note: previous codes without expiration are invalidated.+generateSingleUsedCodes :: MonadIO m => UserImplId -- ^ Id of user+ -> IO SingleUseCode -- ^ Generator of codes+ -> Word -- Count of codes+ -> SqlPersistT m [SingleUseCode]+generateSingleUsedCodes uid gen n = do + t <- liftIO getCurrentTime+ updateWhere [+ UserSingleUseCodeUser ==. uid+ , UserSingleUseCodeUsed ==. Nothing+ , UserSingleUseCodeExpire ==. Nothing + ] + [UserSingleUseCodeUsed =. Just t]+ replicateM (fromIntegral n) $ do + code <- liftIO gen + _ <- insert $ UserSingleUseCode code uid Nothing Nothing+ return code