Add expiry time to email verification secrets
This commit is contained in:
parent
ae3f33f8c1
commit
3211296e9c
|
@ -0,0 +1,36 @@
|
||||||
|
-- migrate:up
|
||||||
|
|
||||||
|
create table "emailVerifications" (
|
||||||
|
"id" serial primary key,
|
||||||
|
"secret" varchar(255) unique not null,
|
||||||
|
"expires" timestamp not null
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table "emails" add "verification" integer unique references "emailVerifications" on delete cascade;
|
||||||
|
|
||||||
|
insert into "emailVerifications" ("secret", "expires")
|
||||||
|
select "verificationSecret", (localtimestamp + '7 days') as "expires" from "emails"
|
||||||
|
where "verificationSecret" is not null;
|
||||||
|
|
||||||
|
update "emails" set
|
||||||
|
"verification" = (select "id" from "emailVerifications" where "secret" = "verificationSecret");
|
||||||
|
|
||||||
|
alter table "emails" drop "verificationSecret" cascade;
|
||||||
|
|
||||||
|
create unique index "emails_uid_verified" on "emails" ("uid", ("verification" is not null));
|
||||||
|
-- at most one verified and one pending email per user
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
|
||||||
|
alter table "emails" add "verificationSecret" varchar(255) unique;
|
||||||
|
|
||||||
|
update "emails" set
|
||||||
|
"verificationSecret" = (select "secret" from "emailVerifications"
|
||||||
|
where "emailVerifications"."id" = "emails"."verification");
|
||||||
|
|
||||||
|
alter table "emails" drop "verification";
|
||||||
|
|
||||||
|
drop table "emailVerifications";
|
||||||
|
|
||||||
|
create unique index "emails_uid_verified" on "emails" ("uid", ("verificationSecret" is not null));
|
||||||
|
-- at most one verified and one pending email per user
|
|
@ -23,6 +23,7 @@ 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)
|
||||||
import Data.Aeson (fromJSON, Result(..), toJSON)
|
import Data.Aeson (fromJSON, Result(..), toJSON)
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
import Data.Morpheus.Server (deriveApp, runApp)
|
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)
|
||||||
|
@ -124,10 +125,11 @@ newUser (ApplicationData {..}) = do
|
||||||
, dBUserRejected = Nothing
|
, dBUserRejected = Nothing
|
||||||
, dBUserMemberData = toJSON memberData
|
, dBUserMemberData = toJSON memberData
|
||||||
}
|
}
|
||||||
|
verification <- addEmailVerification secret
|
||||||
email <- addEmail $ DBEmail
|
email <- addEmail $ DBEmail
|
||||||
{ dBEmailUid = toDBKey user
|
{ dBEmailUid = toDBKey user
|
||||||
, dBEmailEmail = email
|
, dBEmailEmail = email
|
||||||
, dBEmailVerificationSecret = Just secret
|
, dBEmailVid = Just verification
|
||||||
}
|
}
|
||||||
sendVerificationSecret email
|
sendVerificationSecret email
|
||||||
return user
|
return user
|
||||||
|
@ -141,12 +143,17 @@ genVerificationSecret = T.intercalate "-" . T.chunksOf 4 . base32 <$> getRandomB
|
||||||
sendVerificationSecret :: (MonadEmail m, MonadDB m, MonadError GQLError m) => Key DBEmail -> m Unit
|
sendVerificationSecret :: (MonadEmail m, MonadDB m, MonadError GQLError m) => Key DBEmail -> m Unit
|
||||||
sendVerificationSecret email = void $ do
|
sendVerificationSecret email = void $ do
|
||||||
maybeDBEmail <- runQuery $ get email
|
maybeDBEmail <- runQuery $ get email
|
||||||
let email = dBEmailEmail <$> maybeDBEmail
|
case maybeDBEmail of
|
||||||
secret = dBEmailVerificationSecret =<< maybeDBEmail
|
Nothing -> pure Unit
|
||||||
args = (,) <$> secret <*> email
|
Just dbEmail -> do
|
||||||
maybe (pure ()) (uncurry sendVerificationEmail) args
|
case dBEmailVid dbEmail of
|
||||||
|
Nothing -> pure Unit
|
||||||
|
Just dbVerificationId -> do
|
||||||
|
secret <- fmap (dBEmailVerificationSecret . fromJust) $ runQuery $ get dbVerificationId
|
||||||
|
let email = dBEmailEmail dbEmail
|
||||||
|
void $ sendVerificationEmail secret email
|
||||||
|
|
||||||
updateUser :: (MonadRandom m, MonadDB m, MonadEmail m, MonadError GQLError m) =>
|
updateUser :: (MonadRandom m, MonadDB m, MonadEmail m, MonadError GQLError m, MonadTime m) =>
|
||||||
UserID -> UpdateData -> m UserID
|
UserID -> UpdateData -> m UserID
|
||||||
updateUser user (UpdateData {..}) = do
|
updateUser user (UpdateData {..}) = do
|
||||||
hash <- sequence $ hashPassword <$> password
|
hash <- sequence $ hashPassword <$> password
|
||||||
|
@ -238,7 +245,7 @@ resolveMutation = Mutation
|
||||||
maybeUser <- getByID userID
|
maybeUser <- getByID userID
|
||||||
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 (not x) $ throwError "Invalid verification secret"
|
||||||
, resendVerificationEmail = \(Arg id) -> targetUser id >>= getUserPendingEmail >>=
|
, resendVerificationEmail = \(Arg id) -> targetUser id >>= getUserPendingEmail >>=
|
||||||
maybe (pure Unit) (sendVerificationSecret . entityKey)
|
maybe (pure Unit) (sendVerificationSecret . entityKey)
|
||||||
, update = \updateData (Arg id) -> targetUser id >>= \user ->
|
, update = \updateData (Arg id) -> targetUser id >>= \user ->
|
||||||
|
|
|
@ -39,15 +39,21 @@ DBUser sql=users
|
||||||
DBEmail sql=emails
|
DBEmail sql=emails
|
||||||
uid DBUserId
|
uid DBUserId
|
||||||
email Email sqltype=varchar(320)
|
email Email sqltype=varchar(320)
|
||||||
verificationSecret (Maybe Text)
|
vid (Maybe DBEmailVerificationId) sql=verification
|
||||||
|
|
||||||
UniqueUserVerified uid verificationSecret
|
UniqueUserVerified uid vid
|
||||||
-- This enables using persistent functions to get unique verified emails. The real
|
-- 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
|
-- 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.
|
-- verification but it's too complicated for persistent to understand.
|
||||||
|
|
||||||
UniqueEmail email
|
UniqueEmail email
|
||||||
UniqueVerification verificationSecret
|
UniqueVerification vid
|
||||||
|
|
||||||
|
DBEmailVerification sql=emailVerifications
|
||||||
|
secret Text sqltype=varchar(255)
|
||||||
|
expires Time
|
||||||
|
|
||||||
|
UniqueVerificationSecret secret
|
||||||
|
|
||||||
DBKey sql=keys
|
DBKey sql=keys
|
||||||
uid DBUserId
|
uid DBUserId
|
||||||
|
|
|
@ -11,10 +11,11 @@ import Datarekisteri.Backend.Types
|
||||||
import Datarekisteri.Core.Types
|
import Datarekisteri.Core.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)
|
import Data.Maybe (listToMaybe)
|
||||||
import Data.Aeson (fromJSON, toJSON, Result(..))
|
import Data.Aeson (fromJSON, toJSON, Result(..))
|
||||||
|
import Data.Time (nominalDay)
|
||||||
|
|
||||||
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
|
||||||
|
@ -77,7 +78,7 @@ applicants = runQuery $ select $ do
|
||||||
pure $ users
|
pure $ users
|
||||||
|
|
||||||
isVerified :: SqlExpr (Entity DBEmail) -> SqlExpr (Value Bool)
|
isVerified :: SqlExpr (Entity DBEmail) -> SqlExpr (Value Bool)
|
||||||
isVerified email = isNothing (email ^. DBEmailVerificationSecret)
|
isVerified email = isNothing (email ^. DBEmailVid)
|
||||||
|
|
||||||
hasVerifiedEmail :: SqlExpr (Value DBUserId) -> SqlExpr (Value Bool)
|
hasVerifiedEmail :: SqlExpr (Value DBUserId) -> SqlExpr (Value Bool)
|
||||||
hasVerifiedEmail userId = not_ $ isNothing $ subSelect $ do
|
hasVerifiedEmail userId = not_ $ isNothing $ subSelect $ do
|
||||||
|
@ -93,16 +94,22 @@ isApplicant user = isNothing (user ^. DBUserAccepted)
|
||||||
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 Integer
|
verifyEmailSecret :: MonadDB m => Text -> m Bool
|
||||||
verifyEmailSecret secret = fmap fromIntegral $ runQuery $ updateCount $ \email -> do
|
verifyEmailSecret secret = runQuery $ do
|
||||||
set email [DBEmailVerificationSecret =. val Nothing]
|
update $ \email -> do
|
||||||
where_ $ email ^. DBEmailVerificationSecret ==. val (Just secret)
|
verification <- from $ table @DBEmailVerification
|
||||||
|
set email [DBEmailVid =. val Nothing]
|
||||||
|
where_ $ email ^. DBEmailVid ==. just (verification ^. DBEmailVerificationId)
|
||||||
|
&&. verification ^. DBEmailVerificationSecret ==. val secret
|
||||||
|
fmap (> 0) $ deleteCount $ do
|
||||||
|
verification <- from (table @DBEmailVerification)
|
||||||
|
where_ $ verification ^. DBEmailVerificationSecret ==. val secret
|
||||||
|
|
||||||
getUserEmail' :: MonadDB m => UserID -> Bool -> m (Maybe (Entity DBEmail))
|
getUserEmail' :: MonadDB m => UserID -> Bool -> m (Maybe (Entity DBEmail))
|
||||||
getUserEmail' user verified = fmap listToMaybe $ runQuery $ select $ do
|
getUserEmail' user verified = fmap listToMaybe $ runQuery $ select $ do
|
||||||
email <- from $ table @DBEmail
|
email <- from $ table @DBEmail
|
||||||
where_ $ email ^. DBEmailUid ==. val (toDBKey user)
|
where_ $ email ^. DBEmailUid ==. val (toDBKey user)
|
||||||
&&. isNothing (email ^. DBEmailVerificationSecret) ==. val verified
|
&&. isNothing (email ^. DBEmailVid) ==. val verified
|
||||||
pure email
|
pure email
|
||||||
|
|
||||||
getUserEmail :: MonadDB m => UserID -> m (Maybe (Entity DBEmail))
|
getUserEmail :: MonadDB m => UserID -> m (Maybe (Entity DBEmail))
|
||||||
|
@ -114,8 +121,19 @@ getUserPendingEmail user = getUserEmail' user False
|
||||||
addEmail :: MonadDB m => DBEmail -> m (Key DBEmail)
|
addEmail :: MonadDB m => DBEmail -> m (Key DBEmail)
|
||||||
addEmail = runQuery . insert
|
addEmail = runQuery . insert
|
||||||
|
|
||||||
updateEmail :: MonadDB m => UserID -> Email -> Text -> m (Key DBEmail)
|
getExpireTime :: MonadTime m => m Time
|
||||||
updateEmail user email secret = runQuery $ do
|
getExpireTime = addTime (7 * nominalDay) <$> currentTime
|
||||||
|
|
||||||
|
addEmailVerification :: (MonadDB m, MonadTime m) => Text -> m (Key DBEmailVerification)
|
||||||
|
addEmailVerification secret = do
|
||||||
|
expires <- getExpireTime
|
||||||
|
runQuery $ insert $ DBEmailVerification
|
||||||
|
{ dBEmailVerificationSecret = secret
|
||||||
|
, dBEmailVerificationExpires = expires
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmail :: (MonadDB m, MonadTime m) => UserID -> Email -> Text -> m (Key DBEmail)
|
||||||
|
updateEmail user email secret = getExpireTime >>= \expires -> runQuery $ do
|
||||||
delete $ do
|
delete $ do
|
||||||
dbEmail <- from $ table @DBEmail
|
dbEmail <- from $ table @DBEmail
|
||||||
where_ $ dbEmail ^. DBEmailUid ==. val (toDBKey user) &&. not_ (isVerified dbEmail)
|
where_ $ dbEmail ^. DBEmailUid ==. val (toDBKey user) &&. not_ (isVerified dbEmail)
|
||||||
|
@ -126,11 +144,16 @@ updateEmail user email secret = runQuery $ do
|
||||||
pure dbEmail
|
pure dbEmail
|
||||||
case verifiedEmail of
|
case verifiedEmail of
|
||||||
Just (Entity key _) -> pure key
|
Just (Entity key _) -> pure key
|
||||||
Nothing -> insert DBEmail
|
Nothing -> do
|
||||||
{ dBEmailUid = toDBKey user
|
verificationId <- insert DBEmailVerification
|
||||||
, dBEmailEmail = email
|
{ dBEmailVerificationSecret = secret
|
||||||
, dBEmailVerificationSecret = Just secret
|
, dBEmailVerificationExpires = expires
|
||||||
}
|
}
|
||||||
|
insert DBEmail
|
||||||
|
{ dBEmailUid = toDBKey user
|
||||||
|
, dBEmailEmail = email
|
||||||
|
, dBEmailVid = Just verificationId
|
||||||
|
}
|
||||||
|
|
||||||
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