-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement email verification (#7161)
- Loading branch information
Showing
34 changed files
with
522 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package controllers | ||
|
||
import com.mohiva.play.silhouette.api.Silhouette | ||
import com.scalableminds.util.accesscontext.GlobalAccessContext | ||
import com.scalableminds.util.tools.FoxImplicits | ||
import models.user.EmailVerificationService | ||
import oxalis.security.WkEnv | ||
import play.api.mvc.{Action, AnyContent, PlayBodyParsers} | ||
|
||
import javax.inject.Inject | ||
import scala.concurrent.ExecutionContext | ||
|
||
class EmailVerificationController @Inject()(emailVerificationService: EmailVerificationService, sil: Silhouette[WkEnv])( | ||
implicit ec: ExecutionContext, | ||
val bodyParsers: PlayBodyParsers) | ||
extends Controller | ||
with FoxImplicits { | ||
|
||
def verify(key: String): Action[AnyContent] = Action.async { implicit request => | ||
for { | ||
_ <- emailVerificationService.verify(key)(GlobalAccessContext, ec) | ||
} yield Ok | ||
} | ||
|
||
def requestVerificationMail: Action[AnyContent] = sil.SecuredAction.async { implicit request => | ||
for { | ||
_ <- emailVerificationService.sendEmailVerification(request.identity) | ||
} yield Ok | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package models.user | ||
|
||
import com.scalableminds.util.time.Instant | ||
import com.scalableminds.util.tools.Fox | ||
import com.scalableminds.webknossos.schema.Tables | ||
import com.scalableminds.webknossos.schema.Tables.{Emailverificationkeys, EmailverificationkeysRow} | ||
import slick.lifted.{Rep, TableQuery} | ||
import utils.ObjectId | ||
import utils.sql.{SQLDAO, SqlClient} | ||
|
||
import javax.inject.Inject | ||
import scala.concurrent.ExecutionContext | ||
|
||
case class EmailVerificationKey(_id: ObjectId, | ||
key: String, | ||
email: String, | ||
_multiUser: ObjectId, | ||
validUntil: Option[Instant], | ||
isUsed: Boolean) | ||
|
||
class EmailVerificationKeyDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) | ||
extends SQLDAO[EmailVerificationKey, EmailverificationkeysRow, Emailverificationkeys](sqlClient) { | ||
override protected def collection: TableQuery[Tables.Emailverificationkeys] = Emailverificationkeys | ||
|
||
override protected def idColumn(x: Tables.Emailverificationkeys): Rep[String] = x._Id | ||
|
||
override protected def isDeletedColumn(x: Tables.Emailverificationkeys): Rep[Boolean] = ??? | ||
|
||
override protected def parse( | ||
row: _root_.com.scalableminds.webknossos.schema.Tables.EmailverificationkeysRow): Fox[EmailVerificationKey] = | ||
Fox.successful( | ||
EmailVerificationKey( | ||
ObjectId(row._Id), | ||
row.key, | ||
row.email, | ||
ObjectId(row._Multiuser), | ||
row.validuntil.map(Instant.fromSql), | ||
row.isused | ||
) | ||
) | ||
|
||
def insertOne(evk: EmailVerificationKey): Fox[Unit] = | ||
for { | ||
_ <- run(q"""insert into webknossos.emailVerificationKeys(_id, key, email, _multiUser, validUntil, isUsed) | ||
values(${evk._id}, ${evk.key}, ${evk.email}, ${evk._multiUser}, ${evk.validUntil}, ${evk.isUsed})""".asUpdate) | ||
} yield () | ||
|
||
def findOneByKey(key: String): Fox[EmailVerificationKey] = | ||
for { | ||
r <- run(q"select $columns from webknossos.emailVerificationKeys where key = $key".as[EmailverificationkeysRow]) | ||
parsed <- parseFirst(r, key) | ||
} yield parsed | ||
|
||
def markAsUsed(emailVerificationKeyId: ObjectId): Fox[Unit] = | ||
for { | ||
_ <- run(q"""update webknossos.emailVerificationKeys set | ||
isused = true | ||
where _id = $emailVerificationKeyId""".asUpdate) | ||
} yield () | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package models.user | ||
|
||
import akka.actor.ActorSystem | ||
import com.scalableminds.util.accesscontext.DBAccessContext | ||
import com.scalableminds.util.time.Instant | ||
import com.scalableminds.util.tools.Fox | ||
import com.typesafe.scalalogging.LazyLogging | ||
import oxalis.mail.{DefaultMails, Send} | ||
import oxalis.security.RandomIDGenerator | ||
import utils.{ObjectId, WkConf} | ||
|
||
import javax.inject.Inject | ||
import scala.concurrent.ExecutionContext | ||
|
||
class EmailVerificationService @Inject()(conf: WkConf, | ||
emailVerificationKeyDAO: EmailVerificationKeyDAO, | ||
multiUserDAO: MultiUserDAO, | ||
defaultMails: DefaultMails, | ||
actorSystem: ActorSystem) | ||
extends LazyLogging { | ||
|
||
private lazy val Mailer = | ||
actorSystem.actorSelection("/user/mailActor") | ||
|
||
def sendEmailVerification(user: User)(implicit ctx: DBAccessContext): Fox[Unit] = | ||
for { | ||
multiUser <- multiUserDAO.findOne(user._multiUser)(ctx) | ||
key: String = RandomIDGenerator.generateBlocking(32) | ||
expiration = conf.WebKnossos.User.EmailVerification.linkExpiry.map(Instant.now + _) | ||
evk: EmailVerificationKey = EmailVerificationKey(ObjectId.generate, | ||
key, | ||
multiUser.email, | ||
multiUser._id, | ||
expiration, | ||
isUsed = false) | ||
_ <- emailVerificationKeyDAO.insertOne(evk) | ||
fullVerificationLink = s"${conf.Http.uri}/verifyEmail/$key" | ||
_ = logger.info(s"Sending email verification mail for user with email ${multiUser.email}") | ||
_ = Mailer ! Send(defaultMails.emailVerificationMail(user, multiUser.email, fullVerificationLink)) | ||
} yield () | ||
|
||
def verify(key: String)(implicit ctx: DBAccessContext, ec: ExecutionContext): Fox[Unit] = | ||
for { | ||
isEmailVerified <- isEmailAlreadyVerifiedByKey(key) | ||
_ <- Fox.runIf(!isEmailVerified)(checkAndVerify(key)) | ||
} yield () | ||
|
||
private def isEmailAlreadyVerifiedByKey(key: String)(implicit ctx: DBAccessContext): Fox[Boolean] = | ||
for { | ||
evk <- emailVerificationKeyDAO.findOneByKey(key) ?~> "user.email.verification.keyInvalid" | ||
multiUser <- multiUserDAO.findOne(evk._multiUser) ?~> "user.notFound" | ||
} yield multiUser.isEmailVerified | ||
|
||
private def checkAndVerify(key: String)(implicit ctx: DBAccessContext, ec: ExecutionContext): Fox[Unit] = | ||
for { | ||
evk <- emailVerificationKeyDAO.findOneByKey(key) ?~> "user.email.verification.keyInvalid" | ||
multiUser <- multiUserDAO.findOne(evk._multiUser) ?~> "user.notFound" | ||
_ <- Fox.bool2Fox(!evk.isUsed) ?~> "user.email.verification.keyUsed" | ||
_ <- Fox.bool2Fox(evk.validUntil.forall(!_.isPast)) ?~> "user.email.verification.linkExpired" | ||
_ <- Fox.bool2Fox(evk.email == multiUser.email) ?~> "user.email.verification.emailDoesNotMatch" | ||
_ = multiUserDAO.updateEmailVerification(evk._multiUser, verified = true) | ||
_ <- emailVerificationKeyDAO.markAsUsed(evk._id) | ||
} yield () | ||
|
||
def assertEmailVerifiedOrResendVerificationMail(user: User)( | ||
implicit ctx: DBAccessContext, | ||
ec: ExecutionContext | ||
): Fox[Unit] = | ||
for { | ||
emailVerificationOk <- userHasVerifiedEmail(user) | ||
_ <- Fox.runIf(!emailVerificationOk)(sendEmailVerification(user)) | ||
_ <- Fox.bool2Fox(emailVerificationOk) ?~> "user.email.notVerified" | ||
} yield () | ||
|
||
private def userHasVerifiedEmail(user: User)( | ||
implicit ctx: DBAccessContext | ||
): Fox[Boolean] = | ||
for { | ||
multiUser: MultiUser <- multiUserDAO.findOne(user._multiUser) ?~> "user.notFound" | ||
endOfGracePeriod: Instant = multiUser.created + conf.WebKnossos.User.EmailVerification.gracePeriod | ||
overGracePeriod = endOfGracePeriod.isPast | ||
} yield !conf.WebKnossos.User.EmailVerification.required || multiUser.isEmailVerified || !overGracePeriod | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.