packages feed

servant-auth-token-leveldb (empty) → 0.4.1.0

raw patch · 7 files changed

+820/−0 lines, 7 filesdep +aeson-injectordep +basedep +bytestringsetup-changed

Dependencies added: aeson-injector, base, bytestring, concurrent-extra, containers, exceptions, lens, leveldb-haskell, monad-control, mtl, resourcet, safe, safecopy-store, servant-auth-token, servant-auth-token-api, servant-server, store, text, time, transformers, transformers-base, uuid, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,4 @@+0.4.1.0+=======++* Initial release.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Anton Gushcha (c) 2016-2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of NCrashed nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,60 @@+# servant-auth-token-acid++Storage backend on acid for [servant-auth-token](https://github.com/NCrashed/servant-auth-token) server.++As `acid-state` is bad at composing states, the integration of the library in your project requires a massive TH mixins.+The authentification state's queries are simply copied to your app.++First, define you own global `acid-state` type:++``` haskell+import Data.SafeCopy+import Servant.Server.Auth.Token.Acid.Schema as A++-- | Application global state for acid-state+data DB = DB {+  dbAuth :: A.Model -- ^ Storage for Auth state+, dbCustom :: () -- ^ Demo of custom state+}++-- | Generation of inital state+newDB :: DB+newDB = DB {+    dbAuth = A.newModel+  , dbCustom = ()+  }++-- | Extraction of Auth model from global state+instance HasModelRead DB where+  askModel = dbAuth++-- | Extraction of Auth model from global state+instance HasModelWrite DB where+  putModel db m = db { dbAuth = m }++deriveSafeCopy 0 'base ''DB++-- Mixin auth state queries and derive acid-state instances for them+A.deriveQueries ''DB+A.makeModelAcidic ''DB+```++Next, define your monad stack for the authorization actions:+``` haskell+-- Derive HasStorage for 'AcidBackendT' with your 'DB'.+-- It is important that it is come before the below newtype+deriveAcidHasStorage ''DB++-- | Special monad for authorisation actions+newtype AuthM a = AuthM { unAuthM :: AcidBackendT DB IO a }+  deriving (Functor, Applicative, Monad, MonadIO, MonadError ServantErr, HasAuthConfig, HasStorage)++-- | Execution of authorisation actions that require 'AuthHandler' context+runAuth :: AuthM a -> ServerM a+runAuth m = do+  cfg <- asks envAuthConfig+  db <- asks envDB+  liftHandler $ ExceptT $ runAcidBackendT cfg db $ unAuthM m+```++See a full example in [servant-auth-token-example-acid](https://github.com/NCrashed/servant-auth-token/tree/master/example/acid).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ servant-auth-token-leveldb.cabal view
@@ -0,0 +1,74 @@+name:                servant-auth-token-leveldb+version:             0.4.1.0+synopsis:            Leveldb backend for servant-auth-token server+description:         Please see README.md+homepage:            https://github.com/ncrashed/servant-auth-token#readme+license:             BSD3+license-file:        LICENSE+author:              NCrashed+maintainer:          ncrashed@gmail.com+copyright:           2017 Anton Gushcha+category:            Web+build-type:          Simple+extra-source-files:+  README.md+  CHANGELOG.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:+    Servant.Server.Auth.Token.LevelDB+    Servant.Server.Auth.Token.LevelDB.Schema+  build-depends:+      base                    >= 4.7    && < 5+    , aeson-injector          >= 1.0    && < 1.1+    , bytestring              >= 0.10   && < 0.11+    , concurrent-extra        >= 0.7    && < 0.8+    , containers              >= 0.5    && < 0.6+    , exceptions              >= 0.8    && < 0.9+    , lens                    >= 4.15   && < 4.16+    , leveldb-haskell         >= 0.6    && < 0.7+    , monad-control           >= 1.0    && < 1.1+    , mtl                     >= 2.2    && < 2.3+    , resourcet               >= 1.1    && < 1.2+    , safe                    >= 0.3    && < 0.4+    , safecopy-store          >= 0.9    && < 0.10+    , servant-auth-token      >= 0.4    && < 0.5+    , servant-auth-token-api  >= 0.4    && < 0.5+    , servant-server          >= 0.9    && < 0.10+    , store                   >= 0.3    && < 0.4+    , text                    >= 1.2    && < 1.3+    , time                    >= 1.5    && < 1.7+    , transformers            >= 0.4    && < 0.6+    , transformers-base       >= 0.4    && < 0.5+    , uuid                    >= 1.3    && < 1.4+    , vector                  >= 0.11   && < 0.12+  default-language:    Haskell2010+  default-extensions:+    BangPatterns+    DataKinds+    DefaultSignatures+    DeriveGeneric+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    KindSignatures+    MultiParamTypeClasses+    OverloadedStrings+    QuasiQuotes+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StandaloneDeriving+    TemplateHaskell+    TupleSections+    TypeFamilies+    TypeOperators+    UndecidableInstances++source-repository head+  type:     git+  location: https://github.com/ncrashed/servant-auth-token
+ src/Servant/Server/Auth/Token/LevelDB.hs view
@@ -0,0 +1,129 @@+module Servant.Server.Auth.Token.LevelDB(+    LevelDBBackendT+  , runLevelDBBackendT+  , LevelDBEnv+  , newLevelDBEnv+  ) where++import Control.Monad.Base+import Control.Monad.Catch+import Control.Monad.Except+import Control.Monad.Reader+import Control.Monad.Trans.Control+import Control.Monad.Trans.Resource+import Database.LevelDB+import Servant.Server+import Servant.Server.Auth.Token.Config+import Servant.Server.Auth.Token.LevelDB.Schema (LevelDBEnv, newLevelDBEnv)+import Servant.Server.Auth.Token.Model++import qualified Servant.Server.Auth.Token.LevelDB.Schema as S++-- | Monad transformer that implements storage backend+newtype LevelDBBackendT m a = LevelDBBackendT { unLevelDBBackendT :: ReaderT (AuthConfig, LevelDBEnv) (ExceptT ServantErr (ResourceT m)) a }+  deriving (Functor, Applicative, Monad, MonadIO, MonadError ServantErr, MonadReader (AuthConfig, LevelDBEnv), MonadThrow, MonadCatch)++deriving instance MonadBase IO m => MonadBase IO (LevelDBBackendT m)+deriving instance (MonadBase IO m, MonadThrow m, MonadIO m) => MonadResource (LevelDBBackendT m)++instance Monad m => HasAuthConfig (LevelDBBackendT m) where+  getAuthConfig = fst <$> LevelDBBackendT ask++newtype StMLevelDBBackendT m a = StMLevelDBBackendT { unStMLevelDBBackendT :: StM (ReaderT (AuthConfig, LevelDBEnv) (ExceptT ServantErr m)) a }++instance MonadBaseControl IO m => MonadBaseControl IO (LevelDBBackendT m) where+    type StM (LevelDBBackendT m) a = StMLevelDBBackendT m a+    liftBaseWith f = LevelDBBackendT $ liftBaseWith $ \q -> f (fmap StMLevelDBBackendT . q . unLevelDBBackendT)+    restoreM = LevelDBBackendT . restoreM . unStMLevelDBBackendT++-- | Execute backend action with given connection pool.+runLevelDBBackendT :: MonadBaseControl IO m => AuthConfig -> LevelDBEnv -> LevelDBBackendT m a -> m (Either ServantErr a)+runLevelDBBackendT cfg db ma = runResourceT . runExceptT $ runReaderT (unLevelDBBackendT ma) (cfg, db)++-- | Helper to extract LevelDB reference+getEnv :: Monad m => LevelDBBackendT m LevelDBEnv+getEnv  = snd <$> LevelDBBackendT ask++-- | Helper to lift low-level LevelDB queries to backend monad+liftEnv :: Monad m => (LevelDBEnv -> ResourceT m a) -> LevelDBBackendT m a+liftEnv f = do+  e <- getEnv+  LevelDBBackendT . lift . lift $ f e++instance (MonadBase IO m, MonadIO m, MonadThrow m, MonadMask m) => HasStorage (LevelDBBackendT m) where+  getUserImpl = liftEnv . flip S.load+  getUserImplByLogin = liftEnv . S.getUserImplByLogin+  listUsersPaged page size = liftEnv $ S.listUsersPaged page size+  getUserImplPermissions = liftEnv . S.getUserImplPermissions+  deleteUserPermissions = liftEnv . S.deleteUserPermissions+  insertUserPerm = liftEnv . S.insertUserPerm+  insertUserImpl = liftEnv . S.insertUserImpl+  replaceUserImpl i v = liftEnv $ S.replaceUserImpl i v+  deleteUserImpl = liftEnv . S.deleteUserImpl+  hasPerm i p = liftEnv $ S.hasPerm i p+  getFirstUserByPerm = liftEnv . S.getFirstUserByPerm+  selectUserImplGroups = liftEnv . S.selectUserImplGroups+  clearUserImplGroups = liftEnv . S.clearUserImplGroups+  insertAuthUserGroup = liftEnv . S.insertAuthUserGroup+  insertAuthUserGroupUsers = liftEnv . S.insertAuthUserGroupUsers+  insertAuthUserGroupPerms = liftEnv . S.insertAuthUserGroupPerms+  getAuthUserGroup = liftEnv . flip S.load+  listAuthUserGroupPermissions = liftEnv . S.listAuthUserGroupPermissions+  listAuthUserGroupUsers = liftEnv . S.listAuthUserGroupUsers+  replaceAuthUserGroup i v = liftEnv $ S.replaceAuthUserGroup i v+  clearAuthUserGroupUsers = liftEnv . S.clearAuthUserGroupUsers+  clearAuthUserGroupPerms = liftEnv . S.clearAuthUserGroupPerms+  deleteAuthUserGroup = liftEnv . S.deleteAuthUserGroup+  listGroupsPaged page size = liftEnv $ S.listGroupsPaged page size+  setAuthUserGroupName i n = liftEnv $ S.setAuthUserGroupName i n+  setAuthUserGroupParent i mp = liftEnv $ S.setAuthUserGroupParent i mp+  insertSingleUseCode = liftEnv . S.insertSingleUseCode+  setSingleUseCodeUsed i mt = liftEnv $ S.setSingleUseCodeUsed i mt+  getUnusedCode c i t = liftEnv $ S.getUnusedCode c i t+  invalidatePermamentCodes i t = liftEnv $ S.invalidatePermamentCodes i t+  selectLastRestoreCode i t = liftEnv $ S.selectLastRestoreCode i t+  insertUserRestore = liftEnv . S.insertUserRestore+  findRestoreCode i rc t = liftEnv $ S.findRestoreCode i rc t+  replaceRestoreCode i v = liftEnv $ S.replaceRestoreCode i v+  findAuthToken i t = liftEnv $ S.findAuthToken i t+  findAuthTokenByValue t = liftEnv $ S.findAuthTokenByValue t+  insertAuthToken = liftEnv . S.insertAuthToken+  replaceAuthToken i v = liftEnv $ S.replaceAuthToken i v+  {-# INLINE getUserImpl #-}+  {-# INLINE getUserImplByLogin #-}+  {-# INLINE listUsersPaged #-}+  {-# INLINE getUserImplPermissions #-}+  {-# INLINE deleteUserPermissions #-}+  {-# INLINE insertUserPerm #-}+  {-# INLINE insertUserImpl #-}+  {-# INLINE replaceUserImpl #-}+  {-# INLINE deleteUserImpl #-}+  {-# INLINE hasPerm #-}+  {-# INLINE getFirstUserByPerm #-}+  {-# INLINE selectUserImplGroups #-}+  {-# INLINE clearUserImplGroups #-}+  {-# INLINE insertAuthUserGroup #-}+  {-# INLINE insertAuthUserGroupUsers #-}+  {-# INLINE insertAuthUserGroupPerms #-}+  {-# INLINE getAuthUserGroup #-}+  {-# INLINE listAuthUserGroupPermissions #-}+  {-# INLINE listAuthUserGroupUsers #-}+  {-# INLINE replaceAuthUserGroup #-}+  {-# INLINE clearAuthUserGroupUsers #-}+  {-# INLINE clearAuthUserGroupPerms #-}+  {-# INLINE deleteAuthUserGroup #-}+  {-# INLINE listGroupsPaged #-}+  {-# INLINE setAuthUserGroupName #-}+  {-# INLINE setAuthUserGroupParent #-}+  {-# INLINE insertSingleUseCode #-}+  {-# INLINE setSingleUseCodeUsed #-}+  {-# INLINE getUnusedCode #-}+  {-# INLINE invalidatePermamentCodes #-}+  {-# INLINE selectLastRestoreCode #-}+  {-# INLINE insertUserRestore #-}+  {-# INLINE findRestoreCode #-}+  {-# INLINE replaceRestoreCode #-}+  {-# INLINE findAuthToken #-}+  {-# INLINE findAuthTokenByValue #-}+  {-# INLINE insertAuthToken #-}+  {-# INLINE replaceAuthToken #-}
+ src/Servant/Server/Auth/Token/LevelDB/Schema.hs view
@@ -0,0 +1,521 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+module Servant.Server.Auth.Token.LevelDB.Schema where++import Control.Concurrent.RLock+import Control.Lens+import Control.Monad+import Control.Monad.Catch+import Control.Monad.IO.Class+import Data.Aeson.WithField+import Data.ByteString (ByteString)+import Data.Int+import Data.List (sort, sortBy)+import Data.Map.Strict (Map)+import Data.Maybe+import Data.Ord+import Data.SafeCopy.Store+import Data.SafeCopy.Store.Internal+import Data.Set (Set)+import Data.Store+import Data.Text (Text)+import Data.Time+import Data.Typeable hiding (Proxy)+import Data.Vector (Vector)+import Database.LevelDB+import Safe++import Servant.API.Auth.Token+import Servant.API.Auth.Token.Pagination+import Servant.Server.Auth.Token.Common+import Servant.Server.Auth.Token.Model(+    UserImplId+  , UserImpl(..)+  , UserPermId+  , UserPerm(..)+  , AuthTokenId+  , AuthToken(..)+  , UserRestoreId+  , UserRestore(..)+  , UserSingleUseCodeId+  , UserSingleUseCode(..)+  , AuthUserGroupId+  , AuthUserGroup(..)+  , AuthUserGroupUsersId+  , AuthUserGroupUsers(..)+  , AuthUserGroupPermsId+  , AuthUserGroupPerms(..)+  )++import qualified Data.Foldable as F+import qualified Data.Map.Strict as M+import qualified Data.Set as S+import qualified Data.Vector as V++-- | ID of global model index+newtype ModelId = ModelId { unModelId :: Int64 }+  deriving (Show, Read, Ord, Eq)++-- | Global id of model index+modelId :: ModelId+modelId = ModelId 0++-- | Holds all data for auth server in acid-state container+data Model = Model {+  -- | Holds users by id+  _modelUsers                    :: !(Set UserImplId)+  -- | Holds users by login (same content as 'modelUsers')+, _modelUsersByLogin             :: !(Map Login UserImplId)+  -- | Holds 'UserPerm'+, _modelUserPerms                :: !(Set UserPermId)+  -- | Holds 'AuthToken'+, _modelAuthTokens               :: !(Set AuthTokenId)+  -- | Holds 'UserRestore'+, _modelUserRestores             :: !(Set UserRestoreId)+  -- | Holds 'UserSingleUseCode'+, _modelUserSingleUseCodes       :: !(Set UserSingleUseCodeId)+  -- | Holds 'AuthUserGroup'+, _modelAuthUserGroups           :: !(Set AuthUserGroupId)+  -- | Holds 'AuthUserGroupUsers'+, _modelAuthUserGroupUsers       :: !(Set AuthUserGroupUsersId)+  -- | Holds 'AuthUserGroupPerms'+, _modelAuthUserGroupPerms       :: !(Set AuthUserGroupPermsId)+  -- | Holds next id for entities+, _modelNextUserImplId           :: !Int64+-- | Holds next id for entities+, _modelNextUserPermId           :: !Int64+-- | Holds next id for entities+, _modelNextAuthTokenId          :: !Int64+-- | Holds next id for entities+, _modelNextUserRestoreId        :: !Int64+-- | Holds next id for entities+, _modelNextUserSingleUseCodeId  :: !Int64+-- | Holds next id for entities+, _modelNextAuthUserGroupId      :: !Int64+-- | Holds next id for entities+, _modelNextAuthUserGroupUserId  :: !Int64+-- | Holds next id for entities+, _modelNextAuthUserGroupPermId  :: !Int64+}++makeLenses ''Model++-- | Defines empty model for new database+newModel :: Model+newModel = Model {+    _modelUsers = mempty+  , _modelUsersByLogin = mempty+  , _modelUserPerms = mempty+  , _modelAuthTokens = mempty+  , _modelUserRestores = mempty+  , _modelUserSingleUseCodes = mempty+  , _modelAuthUserGroups = mempty+  , _modelAuthUserGroupUsers = mempty+  , _modelAuthUserGroupPerms = mempty+  , _modelNextUserImplId = 0+  , _modelNextUserPermId = 0+  , _modelNextAuthTokenId = 0+  , _modelNextUserRestoreId = 0+  , _modelNextUserSingleUseCodeId = 0+  , _modelNextAuthUserGroupId = 0+  , _modelNextAuthUserGroupUserId = 0+  , _modelNextAuthUserGroupPermId = 0+  }++-- | Helper that defines bijection between key and record+class Key i a | i -> a, a -> i where+  encodeKey :: i -> ByteString++  default encodeKey :: (SafeCopy i, Typeable i) => i -> ByteString+  encodeKey i = runEncode $ do+    pokeE tname+    safePut i+    where+      tname = show $ typeRep (Proxy :: Proxy i)++instance Key AuthTokenId AuthToken+instance Key AuthUserGroupId AuthUserGroup+instance Key AuthUserGroupPermsId AuthUserGroupPerms+instance Key AuthUserGroupUsersId AuthUserGroupUsers+instance Key ModelId Model+instance Key UserImplId UserImpl+instance Key UserPermId UserPerm+instance Key UserRestoreId UserRestore+instance Key UserSingleUseCodeId UserSingleUseCode++-- | Holds together db reference and options for read/write and mutex+data LevelDBEnv = LevelDBEnv !DB !ReadOptions !WriteOptions !RLock++-- | Make new environment for execution of LevelDB operations+newLevelDBEnv :: MonadIO m => DB -> ReadOptions -> WriteOptions -> m LevelDBEnv+newLevelDBEnv db rops wopts = do+  rlock <- liftIO new+  return $ LevelDBEnv db rops wopts rlock++-- | Load object by id from leveldb+load :: (MonadResource m, Key i a, SafeCopy a) => LevelDBEnv -> i -> m (Maybe a)+load (LevelDBEnv db ropts _ _) i = do+  mbs <- get db ropts (encodeKey i)+  return $ decodeExWith safeGet <$> mbs++-- | Store object by id in leveldb+store :: (MonadResource m, Key i a, SafeCopy a) => LevelDBEnv -> i -> a -> m ()+store (LevelDBEnv db _ wopts _) i a = put db wopts (encodeKey i) (runEncode $ safePut a)++-- | Remove object by given id in leveldb+remove :: (MonadResource m, Key i a) => LevelDBEnv -> i -> m ()+remove (LevelDBEnv db _ wopts _) i = delete db wopts (encodeKey i)++-- | Modify value by id in leveldb+modify :: (MonadResource m, MonadMask m, Key i a, SafeCopy a) => LevelDBEnv -> i -> (a -> a) -> m ()+modify db@(LevelDBEnv _ _ _ mut) i f = bracket_ (liftIO $ acquire mut) (liftIO $ release mut) $ do+  ma <- load db i+  case ma of+    Nothing -> return ()+    Just a -> store db i (f a)++-- | Modify value by id in leveldb+modifyM :: (MonadResource m, MonadMask m, Key i a, SafeCopy a) => LevelDBEnv -> i -> (a -> m a) -> m ()+modifyM db@(LevelDBEnv _ _ _ mut) i f = bracket_ (liftIO $ acquire mut) (liftIO $ release mut) $ do+  ma <- load db i+  case ma of+    Nothing -> return ()+    Just a -> store db i =<< f a++-- | Load global index from leveldb+loadModel :: MonadResource m => LevelDBEnv -> m Model+loadModel db = do+  mm <- load db modelId+  return $ fromMaybe newModel mm++-- | Store glogal index to leveldb+storeModel :: MonadResource m => LevelDBEnv -> Model -> m ()+storeModel db = store db modelId++-- | Modify global index+modifyModel :: MonadResource m => LevelDBEnv -> (Model -> Model) -> m ()+modifyModel db f = do+  m <- loadModel db+  storeModel db $ f m++-- | Modify global index+modifyModelM :: (MonadResource m, MonadMask m) => LevelDBEnv -> (Model -> m (Model, a)) -> m a+modifyModelM db@(LevelDBEnv _ _ _ mut) f = bracket_ (liftIO $ acquire mut) (liftIO $ release mut) $ do+  m <- loadModel db+  (m', a) <- f m+  storeModel db m'+  return a++-- | Helper to get paged list of entities+getPagedList :: (MonadResource m, Ord i, Key i a, SafeCopy a) => LevelDBEnv -> Page -> PageSize -> Set i -> m ([WithId i a], Word)+getPagedList db p s is = do+  let is' = take (fromIntegral s) . drop (fromIntegral $ p * s) . sort . F.toList $ is+  es <- traverse (\i -> fmap (i,) <$> load db i) is'+  return (fmap (uncurry WithField) . catMaybes $ es, fromIntegral $ F.length is)++-- | Generic way to insert record in the leveldb with track in global registry+insertRecord :: (MonadResource m, MonadMask m, Key i a, ConvertableKey i, Ord i, SafeCopy a)+  => Lens' Model Int64 -- ^ Field of model that store counter of the record ids+  -> Lens' Model (Set i) -- ^ Field of model that store a registry of the record ids+  -> a -> LevelDBEnv -> m i+insertRecord counterL registryL v db = modifyModelM db $ \m -> do+  let+    i = toKey $ view counterL m+    m' = m & over counterL (+1)+           & over registryL (S.insert i)+  store db i v+  return (m', i)++-- | Generic way to select all records that satisfies given predicate+selectRecords :: (MonadResource m, Key i a, SafeCopy a)+  => Lens' Model (Set i) -- ^ Model field with registry of the records+  -> (i -> a -> Bool) -- ^ Predicate+  -> LevelDBEnv -> m [WithId i a]+selectRecords registryL f db = do+  is <- view registryL <$> loadModel db+  fmap catMaybes $ forM (F.toList is) $ \i -> do+    ma <- load db i+    return $ case ma of+      Just a | f i a -> Just $ WithField i a+      _ -> Nothing++-- | Generic way to delete several records with respect of global registry+deleteRecords :: (MonadResource m, MonadMask m, Key i a, Ord i, Foldable f)+  => Lens' Model (Set i) -- ^ Model field with registry of the records+  -> f i -- ^ Set of ids of records that should be deleted+  -> LevelDBEnv -> m ()+deleteRecords registryL is db = modifyModelM db $ \m -> do+  F.traverse_ (remove db) is+  return . (, ()) $ m & over registryL (`S.difference` (S.fromList . F.toList) is)++-- | Generic way to replace record in registry and leveldb+replaceRecord :: (MonadResource m, MonadMask m, Key i a, Ord i, SafeCopy a)+  => Lens' Model (Set i) -- ^ Model field with registry of the records+  -> i -- ^ ID of record+  -> a -- ^ Value of record+  -> LevelDBEnv -> m ()+replaceRecord registryL i v db = modifyModelM db $ \m -> do+  store db i v+  return . (, ()) $ m & over registryL (S.insert i)++-- | Extract id+withId :: WithField s i a -> i+withId (WithField i _) = i++-- | Extract value+withVal :: WithField s i a -> a+withVal (WithField _ v) = v++--------------------------------------------------------------------------------+-- End of generic helpers+--------------------------------------------------------------------------------++-- | Getting user from storage by login+getUserImplByLogin :: MonadResource m => Login -> LevelDBEnv -> m (Maybe (WithId UserImplId UserImpl))+getUserImplByLogin login db = do+  Model{..} <- loadModel db+  case M.lookup login _modelUsersByLogin of+    Nothing -> return Nothing+    Just i -> fmap (WithField i) <$> load db i++-- | Get paged list of users and total count of users+listUsersPaged :: MonadResource m => Page -> PageSize -> LevelDBEnv -> m ([WithId UserImplId UserImpl], Word)+listUsersPaged p s db = getPagedList db p s =<< (_modelUsers <$> loadModel db)++-- | Get user permissions, ascending by tag+getUserImplPermissions :: MonadResource m => UserImplId -> LevelDBEnv -> m [WithId UserPermId UserPerm]+getUserImplPermissions i = selectRecords modelUserPerms $ \ _ perm -> userPermUser perm == i++-- | Delete user permissions+deleteUserPermissions :: (MonadResource m, MonadMask m) => UserImplId -> LevelDBEnv -> m ()+deleteUserPermissions i db = do+  is <- fmap withId <$> getUserImplPermissions i db+  deleteRecords modelUserPerms is db++-- | Insertion of new user permission+insertUserPerm :: (MonadResource m, MonadMask m) => UserPerm -> LevelDBEnv -> m UserPermId+insertUserPerm = insertRecord modelNextUserPermId modelUserPerms++-- | Insertion of new user+insertUserImpl :: (MonadResource m, MonadMask m) => UserImpl -> LevelDBEnv -> m UserImplId+insertUserImpl v db = modifyModelM db $ \m -> do+  let+    i = toKey $ view modelNextUserImplId m+    m' = m & over modelNextUserImplId (+1)+           & over modelUsers (S.insert i)+           & over modelUsersByLogin (M.insert (userImplLogin v) i)+  store db i v+  return (m', i)++-- | Replace user with new value+replaceUserImpl :: (MonadResource m, MonadMask m) => UserImplId -> UserImpl -> LevelDBEnv -> m ()+replaceUserImpl i v db = modifyModelM db $ \m -> do+  muser <- load db i+  let cleanOld = case muser of+        Nothing -> id+        Just v' -> M.delete (userImplLogin v')+  store db i v+  return . (, ()) $ m & over modelUsersByLogin (M.insert (userImplLogin v) i . cleanOld)++-- | Delete user by id+deleteUserImpl :: (MonadResource m, MonadMask m) => UserImplId -> LevelDBEnv -> m ()+deleteUserImpl i db = do+  muser <- load db i+  case muser of+    Nothing -> return ()+    Just u  -> modifyModelM db $ \m -> do+      deleteUserPermissions i db+      remove db i+      return . (, ()) $ m+        & over modelUsers (S.delete i)+        & over modelUsersByLogin (M.delete (userImplLogin u))+++-- | Check whether the user has particular permission+hasPerm :: MonadResource m => UserImplId -> Permission -> LevelDBEnv -> m Bool+hasPerm i p db = do+  ps <- getUserImplPermissions i db+  return $ (> 0) . F.length . filter (\(WithField _ p') -> userPermUser p' == i && userPermPermission p' == p) $ ps++-- | Get any user with given permission+getFirstUserByPerm :: MonadResource m => Permission -> LevelDBEnv -> m (Maybe (WithId UserImplId UserImpl))+getFirstUserByPerm perm db = do+  ps <- view modelUserPerms <$> loadModel db+  let+    go _ v@Just{} = pure v+    go i Nothing  = do+      mp <- load db i+      case mp of+        Just p | userPermPermission p == perm -> fmap (WithField (userPermUser p)) <$> load db (userPermUser p)+        Nothing -> pure Nothing+  F.foldrM go Nothing ps++-- | Select user groups and sort them by ascending name+selectUserImplGroups :: MonadResource m => UserImplId -> LevelDBEnv -> m [WithId AuthUserGroupUsersId AuthUserGroupUsers]+selectUserImplGroups i = selectRecords modelAuthUserGroupUsers $ \_ g -> authUserGroupUsersUser g == i++-- | Remove user from all groups+clearUserImplGroups :: (MonadResource m, MonadMask m) => UserImplId -> LevelDBEnv -> m ()+clearUserImplGroups i db = do+  is <- fmap withId <$> selectUserImplGroups i db+  deleteRecords modelAuthUserGroupUsers is db++-- | Add new user group+insertAuthUserGroup :: (MonadResource m, MonadMask m) => AuthUserGroup -> LevelDBEnv -> m AuthUserGroupId+insertAuthUserGroup = insertRecord modelNextAuthUserGroupId modelAuthUserGroups++-- | Add user to given group+insertAuthUserGroupUsers :: (MonadResource m, MonadMask m) => AuthUserGroupUsers -> LevelDBEnv -> m AuthUserGroupUsersId+insertAuthUserGroupUsers = insertRecord modelNextAuthUserGroupUserId modelAuthUserGroupUsers++-- | Add permission to given group+insertAuthUserGroupPerms :: (MonadResource m, MonadMask m) => AuthUserGroupPerms -> LevelDBEnv -> m AuthUserGroupPermsId+insertAuthUserGroupPerms = insertRecord modelNextAuthUserGroupPermId modelAuthUserGroupPerms++-- | Get list of permissions of given group+listAuthUserGroupPermissions :: MonadResource m => AuthUserGroupId -> LevelDBEnv -> m [WithId AuthUserGroupPermsId AuthUserGroupPerms]+listAuthUserGroupPermissions i = selectRecords modelAuthUserGroupPerms $ \_ p -> authUserGroupPermsGroup p == i++-- | Get list of all users of the group+listAuthUserGroupUsers :: MonadResource m => AuthUserGroupId -> LevelDBEnv -> m [WithId AuthUserGroupUsersId AuthUserGroupUsers]+listAuthUserGroupUsers i = selectRecords modelAuthUserGroupUsers $ \_ p -> authUserGroupUsersGroup p == i++-- | Replace record of user group+replaceAuthUserGroup :: (MonadResource m, MonadMask m) => AuthUserGroupId -> AuthUserGroup -> LevelDBEnv -> m ()+replaceAuthUserGroup = replaceRecord modelAuthUserGroups++-- | Remove all users from group+clearAuthUserGroupUsers :: (MonadResource m, MonadMask m) => AuthUserGroupId -> LevelDBEnv -> m ()+clearAuthUserGroupUsers i db = do+  is <- fmap withId <$> listAuthUserGroupUsers i db+  deleteRecords modelAuthUserGroupUsers is db++-- | Remove all permissions from group+clearAuthUserGroupPerms :: (MonadResource m, MonadMask m) => AuthUserGroupId -> LevelDBEnv -> m ()+clearAuthUserGroupPerms i db = do+  is <- fmap withId <$> listAuthUserGroupPermissions i db+  deleteRecords modelAuthUserGroupPerms is db++-- | Delete user group from storage+deleteAuthUserGroup :: (MonadResource m, MonadMask m) => AuthUserGroupId -> LevelDBEnv -> m ()+deleteAuthUserGroup i db = modifyModelM db $ \m -> do+  clearAuthUserGroupUsers i db+  clearAuthUserGroupPerms i db+  remove db i+  return . (, ()) $ m & over modelAuthUserGroups (S.delete i)++-- | Get paged list of user groups with total count+listGroupsPaged :: MonadResource m => Page -> PageSize -> LevelDBEnv -> m ([WithId AuthUserGroupId AuthUserGroup], Word)+listGroupsPaged p s db = getPagedList db p s =<< (view modelAuthUserGroups <$> loadModel db)++-- | Set group name+setAuthUserGroupName :: (MonadResource m, MonadMask m) => AuthUserGroupId -> Text -> LevelDBEnv -> m ()+setAuthUserGroupName i n db = modify db i $ \v -> v { authUserGroupName = n }++-- | Set group parent+setAuthUserGroupParent :: (MonadResource m, MonadMask m) => AuthUserGroupId -> Maybe AuthUserGroupId -> LevelDBEnv -> m ()+setAuthUserGroupParent i p db = modify db i $ \v -> v { authUserGroupParent = p }++-- | Add new single use code+insertSingleUseCode :: (MonadResource m, MonadMask m) => UserSingleUseCode -> LevelDBEnv -> m UserSingleUseCodeId+insertSingleUseCode = insertRecord modelNextUserSingleUseCodeId modelUserSingleUseCodes++-- | Set usage time of the single use code+setSingleUseCodeUsed :: (MonadResource m, MonadMask m) => UserSingleUseCodeId -> Maybe UTCTime -> LevelDBEnv -> m ()+setSingleUseCodeUsed i mt db = modify db i $ \v -> v { userSingleUseCodeUsed = mt }++-- | Find unused code for the user and expiration time greater than the given time+getUnusedCode :: MonadResource m => SingleUseCode -> UserImplId -> UTCTime -> LevelDBEnv -> m (Maybe (WithId UserSingleUseCodeId UserSingleUseCode))+getUnusedCode c i t db = headMay . sorting <$> selectRecords modelUserSingleUseCodes f db+  where+    sorting = sortBy (comparing $ Down . userSingleUseCodeExpire . (\(WithField _ v) -> v))+    f _ usc =+      userSingleUseCodeValue usc == c+      && userSingleUseCodeUser usc == i+      && isNothing (userSingleUseCodeUsed usc)+      && (isNothing (userSingleUseCodeExpire usc) || userSingleUseCodeExpire usc >= Just t)++-- | Invalidate all permament codes for user and set use time for them+invalidatePermamentCodes :: (MonadResource m, MonadMask m) => UserImplId -> UTCTime -> LevelDBEnv -> m ()+invalidatePermamentCodes i t db = do+  cs <- view modelUserSingleUseCodes <$> loadModel db+  forM_ (F.toList cs) $ \cid -> do+    mc <- load db cid+    case mc of+      Just usc | isPermament usc -> modify db cid invalidate+      _ -> return ()+  where+    invalidate su = su { userSingleUseCodeUsed = Just t }+    isPermament usc =+         userSingleUseCodeUser usc == i+      && isNothing (userSingleUseCodeUsed usc)+      && isNothing (userSingleUseCodeExpire usc)++-- | Select last valid restoration code by the given current time+selectLastRestoreCode :: MonadResource m => UserImplId -> UTCTime -> LevelDBEnv -> m (Maybe (WithId UserRestoreId UserRestore))+selectLastRestoreCode i t db = headMay . sorting <$> selectRecords modelUserRestores (const f) db+  where+    sorting = sortBy (comparing $ Down . userRestoreExpire . withVal)+    f ur = userRestoreUser ur == i && userRestoreExpire ur > t++-- | Insert new restore code+insertUserRestore :: (MonadResource m, MonadMask m) => UserRestore -> LevelDBEnv -> m UserRestoreId+insertUserRestore = insertRecord modelNextUserRestoreId modelUserRestores++-- | Find unexpired by the time restore code+findRestoreCode :: MonadResource m => UserImplId -> RestoreCode -> UTCTime -> LevelDBEnv -> m (Maybe (WithId UserRestoreId UserRestore))+findRestoreCode i rc t db = headMay . sorting <$> selectRecords modelUserRestores (const f) db+  where+    sorting = sortBy (comparing $ Down . userRestoreExpire . (\(WithField _ v) -> v ))+    f ur = userRestoreUser ur == i && userRestoreValue ur == rc && userRestoreExpire ur > t++-- | Replace restore code with new value+replaceRestoreCode :: (MonadResource m, MonadMask m) => UserRestoreId -> UserRestore ->  LevelDBEnv -> m ()+replaceRestoreCode = replaceRecord modelUserRestores++-- | Find first non-expired by the time token for user+findAuthToken :: MonadResource m => UserImplId -> UTCTime -> LevelDBEnv -> m (Maybe (WithId AuthTokenId AuthToken))+findAuthToken i t db = headMay <$> selectRecords modelAuthTokens (const f) db+  where+    f atok = authTokenUser atok == i && authTokenExpire atok > t++-- | Find token by value+findAuthTokenByValue :: MonadResource m => SimpleToken -> LevelDBEnv -> m (Maybe (WithId AuthTokenId AuthToken))+findAuthTokenByValue v db = headMay <$> selectRecords modelAuthTokens (const f) db+  where+    f atok = authTokenValue atok == v++-- | Insert new token+insertAuthToken :: (MonadResource m, MonadMask m) => AuthToken -> LevelDBEnv -> m AuthTokenId+insertAuthToken = insertRecord modelNextAuthTokenId modelAuthTokens++-- | Replace auth token with new value+replaceAuthToken :: (MonadResource m, MonadMask m) => AuthTokenId -> AuthToken -> LevelDBEnv -> m ()+replaceAuthToken = replaceRecord modelAuthTokens++deriveSafeCopy 0 'base ''UserImplId+deriveSafeCopy 0 'base ''UserImpl+deriveSafeCopy 0 'base ''UserPermId+deriveSafeCopy 0 'base ''UserPerm+deriveSafeCopy 0 'base ''AuthTokenId+deriveSafeCopy 0 'base ''AuthToken+deriveSafeCopy 0 'base ''UserRestoreId+deriveSafeCopy 0 'base ''UserRestore+deriveSafeCopy 0 'base ''UserSingleUseCodeId+deriveSafeCopy 0 'base ''UserSingleUseCode+deriveSafeCopy 0 'base ''AuthUserGroupId+deriveSafeCopy 0 'base ''AuthUserGroup+deriveSafeCopy 0 'base ''AuthUserGroupUsersId+deriveSafeCopy 0 'base ''AuthUserGroupUsers+deriveSafeCopy 0 'base ''AuthUserGroupPermsId+deriveSafeCopy 0 'base ''AuthUserGroupPerms+deriveSafeCopy 0 'base ''ModelId+deriveSafeCopy 0 'base ''Model++instance (SafeCopy k, SafeCopy v) => SafeCopy (WithField i k v) where+  putCopy a@(WithField k v) = contain $ do+    safePut k+    safePut v+    return a+  getCopy = contain $ WithField+    <$> safeGet+    <*> safeGet