Enforce uniqueness between pending and verified emails
This commit is contained in:
		
							parent
							
								
									c7d35146ac
								
							
						
					
					
						commit
						80993bbf8c
					
				| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					-- migrate:up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create table "emails" (
 | 
				
			||||||
 | 
					    "id" serial primary key,
 | 
				
			||||||
 | 
					    "uid" integer not null references "users" on delete cascade,
 | 
				
			||||||
 | 
					    "email" varchar(320) unique not null, -- local 64 + domain 255 + '@' 1 per RFC5321
 | 
				
			||||||
 | 
					    "verificationSecret" varchar(255) unique
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create unique index "emails_uid_verified" on "emails" ("uid", ("verificationSecret" is not null));
 | 
				
			||||||
 | 
					-- at most one verified and one pending email per user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					insert into "emails" ("uid", "email", "verificationSecret")
 | 
				
			||||||
 | 
					    select "id", "email", null as "verificationSecret" from "users" where "email" is not null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					insert into "emails" ("uid", "email", "verificationSecret")
 | 
				
			||||||
 | 
					    select "id", "pendingEmail", "emailVerificationSecret" from "users" where "pendingEmail" is not null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alter table "users"
 | 
				
			||||||
 | 
					    drop "email" cascade,
 | 
				
			||||||
 | 
					    drop "pendingEmail" cascade,
 | 
				
			||||||
 | 
					    drop "emailVerificationSecret" cascade;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- migrate:down
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					alter table "users"
 | 
				
			||||||
 | 
					    add "email" varchar(320) unique,
 | 
				
			||||||
 | 
					    add "pendingEmail" varchar(320) unique,
 | 
				
			||||||
 | 
					    add "emailVerificationSecret" varchar(255) unique;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					update "users" set "email" = "emails"."email"
 | 
				
			||||||
 | 
					    from "emails"
 | 
				
			||||||
 | 
					    where "users"."id" = "emails"."uid" and "emails"."verificationSecret" is null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					update "users" set "pendingEmail" = "emails"."email",
 | 
				
			||||||
 | 
					    "emailVerificationSecret" = "emails"."verificationSecret"
 | 
				
			||||||
 | 
					    from "emails"
 | 
				
			||||||
 | 
					    where "users"."id" = "emails"."uid" and "emails"."verificationSecret" is not null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					drop table "emails";
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Server.API (coreApp, runApp, resolver) where
 | 
					module Server.API (coreApp, runApp, resolver) where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Relude hiding (Undefined, void, when)
 | 
					import Relude hiding (Undefined, void, when, get)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "cryptonite" Crypto.Random (getRandomBytes, MonadRandom)
 | 
					import "cryptonite" Crypto.Random (getRandomBytes, MonadRandom)
 | 
				
			||||||
import Control.Monad.Except (MonadError, throwError)
 | 
					import Control.Monad.Except (MonadError, throwError)
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ import Data.Morpheus.Server (deriveApp, runApp)
 | 
				
			||||||
import Data.Morpheus.Server.Types (defaultRootResolver, RootResolver(..), Undefined)
 | 
					import Data.Morpheus.Server.Types (defaultRootResolver, RootResolver(..), Undefined)
 | 
				
			||||||
import Data.Morpheus.Types (Arg(..), GQLType, GQLError, App)
 | 
					import Data.Morpheus.Types (Arg(..), GQLType, GQLError, App)
 | 
				
			||||||
import Data.Time (nominalDay)
 | 
					import Data.Time (nominalDay)
 | 
				
			||||||
import Database.Persist (Entity, entityVal, (=.))
 | 
					import Database.Persist (Entity, entityVal, entityKey, get, (=.))
 | 
				
			||||||
import Server.DB
 | 
					import Server.DB
 | 
				
			||||||
import Server.DB.Queries
 | 
					import Server.DB.Queries
 | 
				
			||||||
import Server.Email (sendVerificationEmail)
 | 
					import Server.Email (sendVerificationEmail)
 | 
				
			||||||
| 
						 | 
					@ -50,15 +50,15 @@ void m = m >> pure Unit
 | 
				
			||||||
when :: Monad m => Bool -> m a -> m Unit
 | 
					when :: Monad m => Bool -> m a -> m Unit
 | 
				
			||||||
when b m = if b then void m else pure Unit
 | 
					when b m = if b then void m else pure Unit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dbUserToUser :: Monad m => Entity DBUser -> User m
 | 
					dbUserToUser :: MonadDB m => Entity DBUser -> User m
 | 
				
			||||||
dbUserToUser user = let id = entityToID user
 | 
					dbUserToUser user = let id = entityToID user
 | 
				
			||||||
                        DBUser {..} = entityVal user
 | 
					                        DBUser {..} = entityVal user
 | 
				
			||||||
                        Success (MemberData {..}) = fromJSON dBUserMemberData
 | 
					                        Success (MemberData {..}) = fromJSON dBUserMemberData
 | 
				
			||||||
                        -- XXX: Explodes if database doesn't contain needed data
 | 
					                        -- XXX: Explodes if database doesn't contain needed data
 | 
				
			||||||
                    in User
 | 
					                    in User
 | 
				
			||||||
                        { id = pure id
 | 
					                        { id = pure id
 | 
				
			||||||
                        , email = pure dBUserEmail
 | 
					                        , email = fmap (dBEmailEmail . entityVal) <$> getUserEmail id
 | 
				
			||||||
                        , pendingEmail = pure dBUserPendingEmail
 | 
					                        , pendingEmail = fmap (dBEmailEmail . entityVal) <$> getUserPendingEmail id
 | 
				
			||||||
                        , phoneNumber = pure phoneNumber
 | 
					                        , phoneNumber = pure phoneNumber
 | 
				
			||||||
                        , name = pure name
 | 
					                        , name = pure name
 | 
				
			||||||
                        , nickname = pure $ fromMaybe (error "db contains empty name") $
 | 
					                        , nickname = pure $ fromMaybe (error "db contains empty name") $
 | 
				
			||||||
| 
						 | 
					@ -114,9 +114,7 @@ newUser (ApplicationData {..}) = do
 | 
				
			||||||
    when (T.null homeplace) $ throwError "Homeplace must not be empty"
 | 
					    when (T.null homeplace) $ throwError "Homeplace must not be empty"
 | 
				
			||||||
    let memberData = MemberData { nickname = nickname >>= \x -> if T.null x then Nothing else Just x, ..}
 | 
					    let memberData = MemberData { nickname = nickname >>= \x -> if T.null x then Nothing else Just x, ..}
 | 
				
			||||||
    user <- addUser $ DBUser
 | 
					    user <- addUser $ DBUser
 | 
				
			||||||
        { dBUserEmail = Nothing
 | 
					        { dBUserRegistered = time
 | 
				
			||||||
        , dBUserPendingEmail = Just email
 | 
					 | 
				
			||||||
        , dBUserRegistered = time
 | 
					 | 
				
			||||||
        , dBUserToBeDeleted = Just $ verificationExpires
 | 
					        , dBUserToBeDeleted = Just $ verificationExpires
 | 
				
			||||||
        , dBUserPasswordCrypt = passwordHash
 | 
					        , dBUserPasswordCrypt = passwordHash
 | 
				
			||||||
        , dBUserPermissions = permissions
 | 
					        , dBUserPermissions = permissions
 | 
				
			||||||
| 
						 | 
					@ -124,9 +122,13 @@ newUser (ApplicationData {..}) = do
 | 
				
			||||||
        , dBUserSeceded = Nothing
 | 
					        , dBUserSeceded = Nothing
 | 
				
			||||||
        , dBUserRejected = Nothing
 | 
					        , dBUserRejected = Nothing
 | 
				
			||||||
        , dBUserMemberData = toJSON memberData
 | 
					        , dBUserMemberData = toJSON memberData
 | 
				
			||||||
        , dBUserEmailVerificationSecret = Just secret
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    sendVerificationSecret user
 | 
					    email <- addEmail $ DBEmail
 | 
				
			||||||
 | 
					        { dBEmailUid = toDBKey user
 | 
				
			||||||
 | 
					        , dBEmailEmail = email
 | 
				
			||||||
 | 
					        , dBEmailVerificationSecret = Just secret
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    sendVerificationSecret email
 | 
				
			||||||
    return user
 | 
					    return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
verificationExpireTime :: MonadTime m => m Time
 | 
					verificationExpireTime :: MonadTime m => m Time
 | 
				
			||||||
| 
						 | 
					@ -135,11 +137,11 @@ verificationExpireTime = addTime (7 * nominalDay) <$> currentTime
 | 
				
			||||||
genVerificationSecret :: MonadRandom m => m Text
 | 
					genVerificationSecret :: MonadRandom m => m Text
 | 
				
			||||||
genVerificationSecret = T.intercalate "-" . T.chunksOf 4 . base32 <$> getRandomBytes 10
 | 
					genVerificationSecret = T.intercalate "-" . T.chunksOf 4 . base32 <$> getRandomBytes 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sendVerificationSecret :: (MonadEmail m, MonadDB m, MonadError GQLError m) => UserID -> m Unit
 | 
					sendVerificationSecret :: (MonadEmail m, MonadDB m, MonadError GQLError m) => Key DBEmail -> m Unit
 | 
				
			||||||
sendVerificationSecret user = void $ do
 | 
					sendVerificationSecret email = void $ do
 | 
				
			||||||
    maybeDBUser <- fmap entityVal <$> getByID user
 | 
					    maybeDBEmail <- runQuery $ get email
 | 
				
			||||||
    let email = dBUserPendingEmail =<< maybeDBUser
 | 
					    let email = dBEmailEmail <$> maybeDBEmail
 | 
				
			||||||
        secret = dBUserEmailVerificationSecret =<< maybeDBUser
 | 
					        secret = dBEmailVerificationSecret =<< maybeDBEmail
 | 
				
			||||||
        args = (,) <$> email <*> secret
 | 
					        args = (,) <$> email <*> secret
 | 
				
			||||||
    maybe (pure ()) (uncurry sendVerificationEmail) args
 | 
					    maybe (pure ()) (uncurry sendVerificationEmail) args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -148,14 +150,16 @@ updateUser :: (MonadRandom m, MonadDB m, MonadEmail m, MonadError GQLError m) =>
 | 
				
			||||||
updateUser user (UpdateData {..}) = do
 | 
					updateUser user (UpdateData {..}) = do
 | 
				
			||||||
    hash <- sequence $ hashPassword <$> password
 | 
					    hash <- sequence $ hashPassword <$> password
 | 
				
			||||||
    -- TODO: assert stuff valid
 | 
					    -- TODO: assert stuff valid
 | 
				
			||||||
    verificationSecretUpdate <- maybe (pure Nothing)
 | 
					 | 
				
			||||||
        (const $ Just . (DBUserEmailVerificationSecret =. ) . Just <$> genVerificationSecret) email
 | 
					 | 
				
			||||||
    user <- updateUserData user
 | 
					    user <- updateUserData user
 | 
				
			||||||
        (catMaybes [(DBUserPendingEmail =. ) . Just <$> email, verificationSecretUpdate,
 | 
					        (catMaybes [(DBUserPasswordCrypt =.) <$> hash])
 | 
				
			||||||
            (DBUserPasswordCrypt =.) <$> hash])
 | 
					 | 
				
			||||||
        (catMaybes [SetUserName <$> name, SetUserNickname . Just <$> nickname,
 | 
					        (catMaybes [SetUserName <$> name, SetUserNickname . Just <$> nickname,
 | 
				
			||||||
            SetUserHomeplace <$> homeplace, SetUserPhoneNumber <$> phoneNumber])
 | 
					            SetUserHomeplace <$> homeplace, SetUserPhoneNumber <$> phoneNumber])
 | 
				
			||||||
    when (isJust email) $ sendVerificationSecret user
 | 
					    case email of
 | 
				
			||||||
 | 
					      Nothing -> pure Unit
 | 
				
			||||||
 | 
					      Just email' -> do
 | 
				
			||||||
 | 
					          verificationSecret <- genVerificationSecret
 | 
				
			||||||
 | 
					          emailKey <- updateEmail user email' verificationSecret
 | 
				
			||||||
 | 
					          sendVerificationSecret emailKey
 | 
				
			||||||
    return user
 | 
					    return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
makeNewToken :: (MonadError GQLError m, MonadDB m, MonadTime m, MonadRandom m, MonadPermissions m) =>
 | 
					makeNewToken :: (MonadError GQLError m, MonadDB m, MonadTime m, MonadRandom m, MonadPermissions m) =>
 | 
				
			||||||
| 
						 | 
					@ -224,7 +228,8 @@ resolveMutation = Mutation
 | 
				
			||||||
        user <- fromMaybeFail "" maybeUser
 | 
					        user <- fromMaybeFail "" maybeUser
 | 
				
			||||||
        pure $ dbUserToUser user
 | 
					        pure $ dbUserToUser user
 | 
				
			||||||
    , verifyEmail = \(Arg secret) -> void $ verifyEmailSecret secret >>= \x -> when (x < 1) $ throwError "Invalid verification secret"
 | 
					    , verifyEmail = \(Arg secret) -> void $ verifyEmailSecret secret >>= \x -> when (x < 1) $ throwError "Invalid verification secret"
 | 
				
			||||||
    , resendVerificationEmail = \(Arg id) -> targetUser id >>= sendVerificationSecret
 | 
					    , resendVerificationEmail = \(Arg id) -> targetUser id >>= getUserPendingEmail >>=
 | 
				
			||||||
 | 
					        maybe (pure Unit) (sendVerificationSecret . entityKey)
 | 
				
			||||||
    , update = \updateData (Arg id) -> targetUser id >>= \user ->
 | 
					    , update = \updateData (Arg id) -> targetUser id >>= \user ->
 | 
				
			||||||
        requirePermission (Profile user) ReadWrite >>
 | 
					        requirePermission (Profile user) ReadWrite >>
 | 
				
			||||||
            updateUser user updateData >> getByID user >>= fmap dbUserToUser . fromMaybeFail ""
 | 
					            updateUser user updateData >> getByID user >>= fmap dbUserToUser . fromMaybeFail ""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,9 +24,6 @@ import Server.Types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mkPersist sqlSettings [persistUpperCase|
 | 
					mkPersist sqlSettings [persistUpperCase|
 | 
				
			||||||
DBUser sql=users
 | 
					DBUser sql=users
 | 
				
			||||||
  email (Maybe Email) sqltype=varchar(255)
 | 
					 | 
				
			||||||
  pendingEmail (Maybe Email) sqltype=varchar(255)
 | 
					 | 
				
			||||||
  emailVerificationSecret (Maybe Text)
 | 
					 | 
				
			||||||
  registered Time
 | 
					  registered Time
 | 
				
			||||||
  passwordCrypt PasswordHash
 | 
					  passwordCrypt PasswordHash
 | 
				
			||||||
  permissions Text
 | 
					  permissions Text
 | 
				
			||||||
| 
						 | 
					@ -36,12 +33,21 @@ DBUser sql=users
 | 
				
			||||||
  toBeDeleted (Maybe Time)
 | 
					  toBeDeleted (Maybe Time)
 | 
				
			||||||
  memberData Value sqltype=jsonb
 | 
					  memberData Value sqltype=jsonb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  UniqueEmail email
 | 
					 | 
				
			||||||
  UniquePendingEmail pendingEmail
 | 
					 | 
				
			||||||
  UniqueVerification emailVerificationSecret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  deriving (Show)
 | 
					  deriving (Show)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DBEmail sql=emails
 | 
				
			||||||
 | 
					  uid DBUserId
 | 
				
			||||||
 | 
					  email Email sqltype=varchar(320)
 | 
				
			||||||
 | 
					  verificationSecret (Maybe Text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  UniqueUserVerified uid verificationSecret
 | 
				
			||||||
 | 
					  -- This enables using persistent functions to get unique verified emails. The real
 | 
				
			||||||
 | 
					  -- constraint is stricter and doesn't allow having more than one null and one non-null
 | 
				
			||||||
 | 
					  -- verification secret but it's too complicated for persistent to understand.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  UniqueEmail email
 | 
				
			||||||
 | 
					  UniqueVerification verificationSecret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DBKey sql=keys
 | 
					DBKey sql=keys
 | 
				
			||||||
  uid DBUserId
 | 
					  uid DBUserId
 | 
				
			||||||
  data ByteString
 | 
					  data ByteString
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,21 +10,22 @@ import Server.DB as DB
 | 
				
			||||||
import Server.Types
 | 
					import Server.Types
 | 
				
			||||||
import Data.Text (Text)
 | 
					import Data.Text (Text)
 | 
				
			||||||
import Database.Esqueleto.Experimental
 | 
					import Database.Esqueleto.Experimental
 | 
				
			||||||
import qualified Database.Persist as Persist (update, (=.))
 | 
					import qualified Database.Persist as Persist (update, (=.), (==.))
 | 
				
			||||||
import qualified Database.Persist.Types as Persist (Update)
 | 
					import qualified Database.Persist.Types as Persist (Update)
 | 
				
			||||||
import Data.Maybe (listToMaybe, isJust)
 | 
					import Data.Maybe (listToMaybe)
 | 
				
			||||||
import Data.Aeson (fromJSON, toJSON, Result(..))
 | 
					import Data.Aeson (fromJSON, toJSON, Result(..))
 | 
				
			||||||
import GHC.Int (Int64)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
getByID :: (MonadDB m, ToDBKey k, PersistEntityBackend (DB k) ~ SqlBackend) => k -> m (Maybe (Entity (DB k)))
 | 
					getByID :: (MonadDB m, ToDBKey k, PersistEntityBackend (DB k) ~ SqlBackend) => k -> m (Maybe (Entity (DB k)))
 | 
				
			||||||
getByID id = let key = toDBKey id in runQuery $ fmap (Entity key) <$> get key
 | 
					getByID id = let key = toDBKey id in runQuery $ fmap (Entity key) <$> get key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
getUserByEmail :: MonadDB m => Email -> m (Maybe (Entity DBUser))
 | 
					getUserByEmail :: MonadDB m => Email -> m (Maybe (Entity DBUser))
 | 
				
			||||||
getUserByEmail email = runQuery $ getBy (UniqueEmail $ Just email) >>=
 | 
					getUserByEmail email = fmap listToMaybe $ runQuery $ select $ do
 | 
				
			||||||
    maybe ((>>= guardUnconfirmed) <$> getBy (UniquePendingEmail $ Just email)) (pure . Just)
 | 
					    (dbUser :& dbEmail) <- from $ table @DBUser `crossJoin` table @DBEmail
 | 
				
			||||||
        where guardUnconfirmed user
 | 
					    where_ $ dbEmail ^. DBEmailEmail ==. val email &&. dbUser ^. DBUserId ==. dbEmail ^. DBEmailUid
 | 
				
			||||||
                | isJust (dBUserEmail $ entityVal user) = Nothing
 | 
					    -- There is only one row in DBEmail with a given email (unique constraint) and a DBEmail only
 | 
				
			||||||
                | otherwise = Just user
 | 
					    -- has one user id and there is only row in DBUser with a given user id (primary key). Thus
 | 
				
			||||||
 | 
					    -- there is at most one combination of rows from DBEmail and DBUser that satisfy this query.
 | 
				
			||||||
 | 
					    pure dbUser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
addUser :: MonadDB m => DBUser -> m UserID
 | 
					addUser :: MonadDB m => DBUser -> m UserID
 | 
				
			||||||
addUser = fmap fromDBKey . runQuery . insert
 | 
					addUser = fmap fromDBKey . runQuery . insert
 | 
				
			||||||
| 
						 | 
					@ -74,19 +75,61 @@ applicants = runQuery $ select $ do
 | 
				
			||||||
    where_ $ isApplicant users
 | 
					    where_ $ isApplicant users
 | 
				
			||||||
    pure $ users
 | 
					    pure $ users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					isVerified :: SqlExpr (Entity DBEmail) -> SqlExpr (Value Bool)
 | 
				
			||||||
 | 
					isVerified email = isNothing (email ^. DBEmailVerificationSecret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					hasVerifiedEmail :: SqlExpr (Value DBUserId) -> SqlExpr (Value Bool)
 | 
				
			||||||
 | 
					hasVerifiedEmail userId = not_ $ isNothing $ subSelect $ do
 | 
				
			||||||
 | 
					    emails <- from $ table @DBEmail
 | 
				
			||||||
 | 
					    where_ $ emails ^. DBEmailUid ==. userId &&. isVerified emails
 | 
				
			||||||
 | 
					    pure $ val True -- This is not used anywhere, there just isn't a PersistField instance for ()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
isApplicant :: SqlExpr (Entity DBUser) -> SqlExpr (Value Bool)
 | 
					isApplicant :: SqlExpr (Entity DBUser) -> SqlExpr (Value Bool)
 | 
				
			||||||
isApplicant user = isNothing (user ^. DBUserAccepted) &&. not_ (isNothing (user ^. DBUserEmail)) &&. isNothing (user ^. DBUserRejected)
 | 
					isApplicant user = isNothing (user ^. DBUserAccepted)
 | 
				
			||||||
 | 
					    &&. hasVerifiedEmail (user ^. DBUserId)
 | 
				
			||||||
 | 
					    &&. isNothing (user ^. DBUserRejected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
isMember :: SqlExpr (Entity DBUser) -> SqlExpr (Value Bool)
 | 
					isMember :: SqlExpr (Entity DBUser) -> SqlExpr (Value Bool)
 | 
				
			||||||
isMember user = not_ (isNothing (user ^. DBUserAccepted)) &&. isNothing (user ^. DBUserSeceded)
 | 
					isMember user = not_ (isNothing (user ^. DBUserAccepted)) &&. isNothing (user ^. DBUserSeceded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
verifyEmailSecret :: MonadDB m => Text -> m Int64
 | 
					verifyEmailSecret :: MonadDB m => Text -> m Integer
 | 
				
			||||||
verifyEmailSecret secret = runQuery $ updateCount $ \user -> do
 | 
					verifyEmailSecret secret = fmap fromIntegral $ runQuery $ updateCount $ \email -> do
 | 
				
			||||||
    set user [ DBUserEmailVerificationSecret =. val Nothing
 | 
					    set email [DBEmailVerificationSecret =. val Nothing]
 | 
				
			||||||
             , DBUserEmail =. user ^. DBUserPendingEmail
 | 
					    where_ $ email ^. DBEmailVerificationSecret ==. val (Just secret)
 | 
				
			||||||
             , DBUserPendingEmail =. val Nothing
 | 
					
 | 
				
			||||||
             ]
 | 
					getUserEmail' :: MonadDB m => UserID -> Bool -> m (Maybe (Entity DBEmail))
 | 
				
			||||||
    where_ $ user ^. DBUserEmailVerificationSecret ==. just (val secret)
 | 
					getUserEmail' user verified = fmap listToMaybe $ runQuery $ select $ do
 | 
				
			||||||
 | 
					    email <- from $ table @DBEmail
 | 
				
			||||||
 | 
					    where_ $ email ^. DBEmailUid ==. val (toDBKey user)
 | 
				
			||||||
 | 
					        &&. isNothing (email ^. DBEmailVerificationSecret) ==. val verified
 | 
				
			||||||
 | 
					    pure email
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					getUserEmail :: MonadDB m => UserID -> m (Maybe (Entity DBEmail))
 | 
				
			||||||
 | 
					getUserEmail user = getUserEmail' user True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					getUserPendingEmail :: MonadDB m => UserID -> m (Maybe (Entity DBEmail))
 | 
				
			||||||
 | 
					getUserPendingEmail user = getUserEmail' user False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					addEmail :: MonadDB m => DBEmail -> m (Key DBEmail)
 | 
				
			||||||
 | 
					addEmail = runQuery . insert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					updateEmail :: MonadDB m => UserID -> Email -> Text -> m (Key DBEmail)
 | 
				
			||||||
 | 
					updateEmail user email secret = runQuery $ do
 | 
				
			||||||
 | 
					    delete $ do
 | 
				
			||||||
 | 
					        dbEmail <- from $ table @DBEmail
 | 
				
			||||||
 | 
					        where_ $ dbEmail ^. DBEmailUid ==. val (toDBKey user) &&. not_ (isVerified dbEmail)
 | 
				
			||||||
 | 
					    verifiedEmail <- fmap listToMaybe $ select $ do
 | 
				
			||||||
 | 
					        dbEmail <- from $ table @DBEmail
 | 
				
			||||||
 | 
					        where_ $ dbEmail ^. DBEmailUid ==. val (toDBKey user)
 | 
				
			||||||
 | 
					            &&. dbEmail ^. DBEmailEmail ==. val email
 | 
				
			||||||
 | 
					        pure dbEmail
 | 
				
			||||||
 | 
					    case verifiedEmail of
 | 
				
			||||||
 | 
					      Just (Entity key _) -> pure key
 | 
				
			||||||
 | 
					      Nothing -> insert DBEmail
 | 
				
			||||||
 | 
					          { dBEmailUid = toDBKey user
 | 
				
			||||||
 | 
					          , dBEmailEmail = email
 | 
				
			||||||
 | 
					          , dBEmailVerificationSecret = Just secret
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
markAsAccepted :: MonadDB m => UserID -> Time -> m ()
 | 
					markAsAccepted :: MonadDB m => UserID -> Time -> m ()
 | 
				
			||||||
markAsAccepted userID time = runQuery $ update $ \user -> do
 | 
					markAsAccepted userID time = runQuery $ update $ \user -> do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue