From ebef28cf23f5754c91eab5cebfd5c6180f64161e Mon Sep 17 00:00:00 2001 From: Alexander Myltsev Date: Fri, 27 Dec 2019 09:22:06 +0300 Subject: [PATCH 1/3] Migrate to g8 (except source files) --- .travis.yml | 25 + Procfile | 1 - README.markdown | 12 + app/controllers/AbstractAuthController.scala | 64 -- .../ActivateAccountController.scala | 85 --- app/controllers/ApplicationController.scala | 55 -- .../ChangePasswordController.scala | 78 --- .../ForgotPasswordController.scala | 84 --- app/controllers/ResetPasswordController.scala | 82 --- app/controllers/SignInController.scala | 87 --- app/controllers/SignUpController.scala | 119 ---- app/controllers/SocialAuthController.scala | 67 -- app/controllers/TotpController.scala | 126 ---- app/controllers/TotpRecoveryController.scala | 91 --- app/forms/ChangePasswordForm.scala | 18 - app/forms/ForgotPasswordForm.scala | 17 - app/forms/ResetPasswordForm.scala | 17 - app/forms/SignInForm.scala | 33 - app/forms/SignUpForm.scala | 36 - app/forms/TotpForm.scala | 36 - app/forms/TotpRecoveryForm.scala | 36 - app/forms/TotpSetupForm.scala | 40 -- app/jobs/AuthTokenCleaner.scala | 55 -- app/jobs/Scheduler.scala | 18 - app/models/AuthToken.scala | 17 - app/models/User.scala | 42 -- app/models/daos/AuthTokenDAO.scala | 45 -- app/models/daos/AuthTokenDAOImpl.scala | 69 -- app/models/daos/UserDAO.scala | 38 -- app/models/daos/UserDAOImpl.scala | 56 -- app/models/services/AuthTokenService.scala | 39 -- .../services/AuthTokenServiceImpl.scala | 60 -- app/models/services/UserService.scala | 41 -- app/models/services/UserServiceImpl.scala | 76 --- app/modules/BaseModule.scala | 20 - app/modules/JobModule.scala | 19 - app/modules/SilhouetteModule.scala | 475 -------------- app/utils/Filters.scala | 15 - app/utils/Logger.scala | 12 - .../auth/CustomSecuredErrorHandler.scala | 42 -- .../auth/CustomUnsecuredErrorHandler.scala | 25 - app/utils/auth/Env.scala | 13 - app/utils/auth/WithProvider.scala | 32 - app/utils/route/Binders.scala | 24 - app/views/activateAccount.scala.html | 19 - app/views/changePassword.scala.html | 27 - app/views/emails/activateAccount.scala.html | 11 - app/views/emails/activateAccount.scala.txt | 6 - app/views/emails/alreadySignedUp.scala.html | 11 - app/views/emails/alreadySignedUp.scala.txt | 6 - app/views/emails/resetPassword.scala.html | 11 - app/views/emails/resetPassword.scala.txt | 6 - app/views/emails/signUp.scala.html | 11 - app/views/emails/signUp.scala.txt | 6 - app/views/forgotPassword.scala.html | 25 - app/views/home.scala.html | 93 --- app/views/main.scala.html | 88 --- app/views/passwordStrength.scala.html | 13 - app/views/resetPassword.scala.html | 24 - app/views/signIn.scala.html | 44 -- app/views/signUp.scala.html | 31 - app/views/totp.scala.html | 35 - app/views/totpRecovery.scala.html | 34 - build.sbt | 83 +-- conf/application.conf | 69 -- conf/application.prod.conf | 53 -- conf/messages | 127 ---- conf/routes | 37 -- conf/silhouette.conf | 103 --- default.properties | 2 + project/build.properties | 2 +- project/plugins.sbt | 8 +- public/images/favicon.png | Bin 687 -> 0 bytes public/images/providers/facebook.png | Bin 449 -> 0 bytes public/images/providers/google.png | Bin 843 -> 0 bytes public/images/providers/twitter.png | Bin 675 -> 0 bytes public/images/providers/vk.png | Bin 955 -> 0 bytes public/images/providers/xing.png | Bin 684 -> 0 bytes public/images/providers/yahoo.png | Bin 684 -> 0 bytes public/images/silhouette.png | Bin 10664 -> 0 bytes public/javascripts/zxcvbnShim.js | 31 - public/styles/main.css | 135 ---- scripts/reformat | 21 - scripts/sbt | 621 ------------------ .../main/g8/CONTRIBUTING.md | 0 src/main/g8/LICENSE | 202 ++++++ README.md => src/main/g8/README.md | 0 app.json => src/main/g8/app.json | 0 src/main/g8/build.sbt | 70 ++ src/main/g8/project/build.properties | 1 + src/main/g8/project/plugins.sbt | 6 + .../ApplicationControllerSpec.scala | 96 --- tutorial/index.html | 137 ---- 93 files changed, 334 insertions(+), 4413 deletions(-) create mode 100644 .travis.yml delete mode 100644 Procfile create mode 100644 README.markdown delete mode 100644 app/controllers/AbstractAuthController.scala delete mode 100644 app/controllers/ActivateAccountController.scala delete mode 100644 app/controllers/ApplicationController.scala delete mode 100644 app/controllers/ChangePasswordController.scala delete mode 100644 app/controllers/ForgotPasswordController.scala delete mode 100644 app/controllers/ResetPasswordController.scala delete mode 100644 app/controllers/SignInController.scala delete mode 100644 app/controllers/SignUpController.scala delete mode 100644 app/controllers/SocialAuthController.scala delete mode 100644 app/controllers/TotpController.scala delete mode 100644 app/controllers/TotpRecoveryController.scala delete mode 100644 app/forms/ChangePasswordForm.scala delete mode 100644 app/forms/ForgotPasswordForm.scala delete mode 100644 app/forms/ResetPasswordForm.scala delete mode 100644 app/forms/SignInForm.scala delete mode 100644 app/forms/SignUpForm.scala delete mode 100644 app/forms/TotpForm.scala delete mode 100644 app/forms/TotpRecoveryForm.scala delete mode 100644 app/forms/TotpSetupForm.scala delete mode 100644 app/jobs/AuthTokenCleaner.scala delete mode 100644 app/jobs/Scheduler.scala delete mode 100644 app/models/AuthToken.scala delete mode 100644 app/models/User.scala delete mode 100644 app/models/daos/AuthTokenDAO.scala delete mode 100644 app/models/daos/AuthTokenDAOImpl.scala delete mode 100644 app/models/daos/UserDAO.scala delete mode 100644 app/models/daos/UserDAOImpl.scala delete mode 100644 app/models/services/AuthTokenService.scala delete mode 100644 app/models/services/AuthTokenServiceImpl.scala delete mode 100644 app/models/services/UserService.scala delete mode 100644 app/models/services/UserServiceImpl.scala delete mode 100644 app/modules/BaseModule.scala delete mode 100644 app/modules/JobModule.scala delete mode 100644 app/modules/SilhouetteModule.scala delete mode 100644 app/utils/Filters.scala delete mode 100644 app/utils/Logger.scala delete mode 100644 app/utils/auth/CustomSecuredErrorHandler.scala delete mode 100644 app/utils/auth/CustomUnsecuredErrorHandler.scala delete mode 100644 app/utils/auth/Env.scala delete mode 100644 app/utils/auth/WithProvider.scala delete mode 100644 app/utils/route/Binders.scala delete mode 100644 app/views/activateAccount.scala.html delete mode 100644 app/views/changePassword.scala.html delete mode 100644 app/views/emails/activateAccount.scala.html delete mode 100644 app/views/emails/activateAccount.scala.txt delete mode 100644 app/views/emails/alreadySignedUp.scala.html delete mode 100644 app/views/emails/alreadySignedUp.scala.txt delete mode 100644 app/views/emails/resetPassword.scala.html delete mode 100644 app/views/emails/resetPassword.scala.txt delete mode 100644 app/views/emails/signUp.scala.html delete mode 100644 app/views/emails/signUp.scala.txt delete mode 100644 app/views/forgotPassword.scala.html delete mode 100644 app/views/home.scala.html delete mode 100644 app/views/main.scala.html delete mode 100644 app/views/passwordStrength.scala.html delete mode 100644 app/views/resetPassword.scala.html delete mode 100644 app/views/signIn.scala.html delete mode 100644 app/views/signUp.scala.html delete mode 100644 app/views/totp.scala.html delete mode 100644 app/views/totpRecovery.scala.html delete mode 100644 conf/application.conf delete mode 100644 conf/application.prod.conf delete mode 100644 conf/messages delete mode 100644 conf/routes delete mode 100644 conf/silhouette.conf create mode 100644 default.properties delete mode 100644 public/images/favicon.png delete mode 100644 public/images/providers/facebook.png delete mode 100644 public/images/providers/google.png delete mode 100644 public/images/providers/twitter.png delete mode 100644 public/images/providers/vk.png delete mode 100644 public/images/providers/xing.png delete mode 100644 public/images/providers/yahoo.png delete mode 100644 public/images/silhouette.png delete mode 100644 public/javascripts/zxcvbnShim.js delete mode 100644 public/styles/main.css delete mode 100755 scripts/reformat delete mode 100755 scripts/sbt rename CONTRIBUTING.md => src/main/g8/CONTRIBUTING.md (100%) create mode 100644 src/main/g8/LICENSE rename README.md => src/main/g8/README.md (100%) rename app.json => src/main/g8/app.json (100%) create mode 100644 src/main/g8/build.sbt create mode 100644 src/main/g8/project/build.properties create mode 100644 src/main/g8/project/plugins.sbt delete mode 100644 test/controllers/ApplicationControllerSpec.scala delete mode 100644 tutorial/index.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fb88952 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: scala + +dist: trusty + +scala: +- 2.12.10 + +# These directories are cached to S3 at the end of the build +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/ + - $HOME/.sbt/boot/ + - $HOME/.sbt/launchers + +jdk: + - oraclejdk8 + +script: + ## This runs the template with the default parameters, and runs test within the templated app. + - sbt -Dfile.encoding=UTF8 -J-XX:ReservedCodeCacheSize=256M test + + # Tricks to avoid unnecessary cache updates + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm diff --git a/Procfile b/Procfile deleted file mode 100644 index 2038709..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: target/universal/stage/bin/play-silhouette-seed -Dhttp.port=${PORT} -Dconfig.resource=${PLAY_CONF_FILE} diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..0157809 --- /dev/null +++ b/README.markdown @@ -0,0 +1,12 @@ +A [Giter8][g8] template for ...! + +Template license +---------------- +Written in by +[other author/contributor lines as appropriate] + +To the extent possible under law, the author(s) have dedicated all copyright and related +and neighboring rights to this template to the public domain worldwide. +This template is distributed without any warranty. See . + +[g8]: http://www.foundweekends.org/giter8/ diff --git a/app/controllers/AbstractAuthController.scala b/app/controllers/AbstractAuthController.scala deleted file mode 100644 index a04a363..0000000 --- a/app/controllers/AbstractAuthController.scala +++ /dev/null @@ -1,64 +0,0 @@ -package controllers - -import com.mohiva.play.silhouette.api.Authenticator.Implicits._ -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.services.AuthenticatorResult -import com.mohiva.play.silhouette.api.util.Clock -import models.User -import net.ceedubs.ficus.Ficus._ -import org.webjars.play.WebJarsUtil -import play.api.Configuration -import play.api.i18n.I18nSupport -import play.api.mvc._ -import utils.auth.DefaultEnv - -import scala.concurrent.duration._ -import scala.concurrent.{ ExecutionContext, Future } - -/** - * `AbstractAuthController` base with support methods to authenticate an user. - * - * @param silhouette The Silhouette stack. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - */ -abstract class AbstractAuthController( - silhouette: Silhouette[DefaultEnv], - configuration: Configuration, - clock: Clock -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends InjectedController with I18nSupport { - - /** - * Performs user authentication - * @param user User data - * @param rememberMe Remember me flag - * @param request Initial request - * @return The result to display. - */ - protected def authenticateUser(user: User, rememberMe: Boolean)(implicit request: Request[_]): Future[AuthenticatorResult] = { - val c = configuration.underlying - val result = Redirect(routes.ApplicationController.index()) - silhouette.env.authenticatorService.create(user.loginInfo).map { - case authenticator if rememberMe => - authenticator.copy( - expirationDateTime = clock.now + c.as[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorExpiry"), - idleTimeout = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorIdleTimeout"), - cookieMaxAge = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.cookieMaxAge") - ) - case authenticator => authenticator - }.flatMap { authenticator => - silhouette.env.eventBus.publish(LoginEvent(user, request)) - silhouette.env.authenticatorService.init(authenticator).flatMap { v => - silhouette.env.authenticatorService.embed(v, result) - } - } - } -} diff --git a/app/controllers/ActivateAccountController.scala b/app/controllers/ActivateAccountController.scala deleted file mode 100644 index 1a436a4..0000000 --- a/app/controllers/ActivateAccountController.scala +++ /dev/null @@ -1,85 +0,0 @@ -package controllers - -import java.net.URLDecoder -import java.util.UUID -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.impl.providers.CredentialsProvider -import models.services.{ AuthTokenService, UserService } -import play.api.i18n.{ I18nSupport, Messages } -import play.api.libs.mailer.{ Email, MailerClient } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Activate Account` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param authTokenService The auth token service implementation. - * @param mailerClient The mailer client. - * @param ex The execution context. - */ -class ActivateAccountController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authTokenService: AuthTokenService, - mailerClient: MailerClient -)( - implicit - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Sends an account activation email to the user with the given email. - * - * @param email The email address of the user to send the activation mail to. - * @return The result to display. - */ - def send(email: String) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - val decodedEmail = URLDecoder.decode(email, "UTF-8") - val loginInfo = LoginInfo(CredentialsProvider.ID, decodedEmail) - val result = Redirect(routes.SignInController.view()).flashing("info" -> Messages("activation.email.sent", decodedEmail)) - - userService.retrieve(loginInfo).flatMap { - case Some(user) if !user.activated => - authTokenService.create(user.userID).map { authToken => - val url = routes.ActivateAccountController.activate(authToken.id).absoluteURL() - - mailerClient.send(Email( - subject = Messages("email.activate.account.subject"), - from = Messages("email.from"), - to = Seq(decodedEmail), - bodyText = Some(views.txt.emails.activateAccount(user, url).body), - bodyHtml = Some(views.html.emails.activateAccount(user, url).body) - )) - result - } - case None => Future.successful(result) - } - } - - /** - * Activates an account. - * - * @param token The token to identify a user. - * @return The result to display. - */ - def activate(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - authTokenService.validate(token).flatMap { - case Some(authToken) => userService.retrieve(authToken.userID).flatMap { - case Some(user) if user.loginInfo.providerID == CredentialsProvider.ID => - userService.save(user.copy(activated = true)).map { _ => - Redirect(routes.SignInController.view()).flashing("success" -> Messages("account.activated")) - } - case _ => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link"))) - } - case None => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link"))) - } - } -} diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala deleted file mode 100644 index 04330fb..0000000 --- a/app/controllers/ApplicationController.scala +++ /dev/null @@ -1,55 +0,0 @@ -package controllers - -import com.mohiva.play.silhouette.api.actions.SecuredRequest -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.{ LogoutEvent, Silhouette } -import com.mohiva.play.silhouette.impl.providers.GoogleTotpInfo -import javax.inject.Inject -import org.webjars.play.WebJarsUtil -import play.api.i18n.I18nSupport -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents } -import utils.auth.DefaultEnv - -import scala.concurrent.ExecutionContext - -/** - * The basic application controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - */ -class ApplicationController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - authInfoRepository: AuthInfoRepository -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Handles the index action. - * - * @return The result to display. - */ - def index = silhouette.SecuredAction.async { implicit request: SecuredRequest[DefaultEnv, AnyContent] => - authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => - Ok(views.html.home(request.identity, totpInfoOpt)) - } - } - - /** - * Handles the Sign Out action. - * - * @return The result to display. - */ - def signOut = silhouette.SecuredAction.async { implicit request: SecuredRequest[DefaultEnv, AnyContent] => - val result = Redirect(routes.ApplicationController.index()) - silhouette.env.eventBus.publish(LogoutEvent(request.identity, request)) - silhouette.env.authenticatorService.discard(request.authenticator, result) - } -} diff --git a/app/controllers/ChangePasswordController.scala b/app/controllers/ChangePasswordController.scala deleted file mode 100644 index fb55c67..0000000 --- a/app/controllers/ChangePasswordController.scala +++ /dev/null @@ -1,78 +0,0 @@ -package controllers - -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.actions.SecuredRequest -import com.mohiva.play.silhouette.api.exceptions.ProviderException -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.util.{ Credentials, PasswordHasherRegistry, PasswordInfo } -import com.mohiva.play.silhouette.impl.providers.CredentialsProvider -import forms.ChangePasswordForm -import org.webjars.play.WebJarsUtil -import play.api.i18n.{ I18nSupport, Messages } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents } -import utils.auth.{ DefaultEnv, WithProvider } - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Change Password` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param credentialsProvider The credentials provider. - * @param authInfoRepository The auth info repository. - * @param passwordHasherRegistry The password hasher registry. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - */ -class ChangePasswordController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - credentialsProvider: CredentialsProvider, - authInfoRepository: AuthInfoRepository, - passwordHasherRegistry: PasswordHasherRegistry -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Views the `Change Password` page. - * - * @return The result to display. - */ - def view = silhouette.SecuredAction(WithProvider[DefaultEnv#A](CredentialsProvider.ID)) { - implicit request: SecuredRequest[DefaultEnv, AnyContent] => - Ok(views.html.changePassword(ChangePasswordForm.form, request.identity)) - } - - /** - * Changes the password. - * - * @return The result to display. - */ - def submit = silhouette.SecuredAction(WithProvider[DefaultEnv#A](CredentialsProvider.ID)).async { - implicit request: SecuredRequest[DefaultEnv, AnyContent] => - ChangePasswordForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.changePassword(form, request.identity))), - password => { - val (currentPassword, newPassword) = password - val credentials = Credentials(request.identity.email.getOrElse(""), currentPassword) - credentialsProvider.authenticate(credentials).flatMap { loginInfo => - val passwordInfo = passwordHasherRegistry.current.hash(newPassword) - authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo).map { _ => - Redirect(routes.ChangePasswordController.view()).flashing("success" -> Messages("password.changed")) - } - }.recover { - case _: ProviderException => - Redirect(routes.ChangePasswordController.view()).flashing("error" -> Messages("current.password.invalid")) - } - } - ) - } -} diff --git a/app/controllers/ForgotPasswordController.scala b/app/controllers/ForgotPasswordController.scala deleted file mode 100644 index d2965b5..0000000 --- a/app/controllers/ForgotPasswordController.scala +++ /dev/null @@ -1,84 +0,0 @@ -package controllers - -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.impl.providers.CredentialsProvider -import forms.ForgotPasswordForm -import models.services.{ AuthTokenService, UserService } -import org.webjars.play.WebJarsUtil -import play.api.i18n.{ I18nSupport, Messages } -import play.api.libs.mailer.{ Email, MailerClient } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Forgot Password` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param authTokenService The auth token service implementation. - * @param mailerClient The mailer client. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - */ -class ForgotPasswordController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authTokenService: AuthTokenService, - mailerClient: MailerClient -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Views the `Forgot Password` page. - * - * @return The result to display. - */ - def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - Future.successful(Ok(views.html.forgotPassword(ForgotPasswordForm.form))) - } - - /** - * Sends an email with password reset instructions. - * - * It sends an email to the given address if it exists in the database. Otherwise we do not show the user - * a notice for not existing email addresses to prevent the leak of existing email addresses. - * - * @return The result to display. - */ - def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - ForgotPasswordForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.forgotPassword(form))), - email => { - val loginInfo = LoginInfo(CredentialsProvider.ID, email) - val result = Redirect(routes.SignInController.view()).flashing("info" -> Messages("reset.email.sent")) - userService.retrieve(loginInfo).flatMap { - case Some(user) if user.email.isDefined => - authTokenService.create(user.userID).map { authToken => - val url = routes.ResetPasswordController.view(authToken.id).absoluteURL() - - mailerClient.send(Email( - subject = Messages("email.reset.password.subject"), - from = Messages("email.from"), - to = Seq(email), - bodyText = Some(views.txt.emails.resetPassword(user, url).body), - bodyHtml = Some(views.html.emails.resetPassword(user, url).body) - )) - result - } - case None => Future.successful(result) - } - } - ) - } -} diff --git a/app/controllers/ResetPasswordController.scala b/app/controllers/ResetPasswordController.scala deleted file mode 100644 index 2991e96..0000000 --- a/app/controllers/ResetPasswordController.scala +++ /dev/null @@ -1,82 +0,0 @@ -package controllers - -import java.util.UUID -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.util.{ PasswordHasherRegistry, PasswordInfo } -import com.mohiva.play.silhouette.impl.providers.CredentialsProvider -import forms.ResetPasswordForm -import models.services.{ AuthTokenService, UserService } -import org.webjars.play.WebJarsUtil -import play.api.i18n.{ I18nSupport, Messages } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Reset Password` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param authInfoRepository The auth info repository. - * @param passwordHasherRegistry The password hasher registry. - * @param authTokenService The auth token service implementation. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - */ -class ResetPasswordController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authInfoRepository: AuthInfoRepository, - passwordHasherRegistry: PasswordHasherRegistry, - authTokenService: AuthTokenService -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Views the `Reset Password` page. - * - * @param token The token to identify a user. - * @return The result to display. - */ - def view(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - authTokenService.validate(token).map { - case Some(_) => Ok(views.html.resetPassword(ResetPasswordForm.form, token)) - case None => Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link")) - } - } - - /** - * Resets the password. - * - * @param token The token to identify a user. - * @return The result to display. - */ - def submit(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - authTokenService.validate(token).flatMap { - case Some(authToken) => - ResetPasswordForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.resetPassword(form, token))), - password => userService.retrieve(authToken.userID).flatMap { - case Some(user) if user.loginInfo.providerID == CredentialsProvider.ID => - val passwordInfo = passwordHasherRegistry.current.hash(password) - authInfoRepository.update[PasswordInfo](user.loginInfo, passwordInfo).map { _ => - Redirect(routes.SignInController.view()).flashing("success" -> Messages("password.reset")) - } - case _ => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link"))) - } - ) - case None => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link"))) - } - } -} diff --git a/app/controllers/SignInController.scala b/app/controllers/SignInController.scala deleted file mode 100644 index cc82b34..0000000 --- a/app/controllers/SignInController.scala +++ /dev/null @@ -1,87 +0,0 @@ -package controllers - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.exceptions.ProviderException -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.util.{ Clock, Credentials } -import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException -import com.mohiva.play.silhouette.impl.providers._ -import forms.{ SignInForm, TotpForm } -import javax.inject.Inject -import models.services.UserService -import org.webjars.play.WebJarsUtil -import play.api.Configuration -import play.api.i18n.{ I18nSupport, Messages } -import play.api.mvc.{ AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Sign In` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param credentialsProvider The credentials provider. - * @param socialProviderRegistry The social provider registry. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - */ -class SignInController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authInfoRepository: AuthInfoRepository, - credentialsProvider: CredentialsProvider, - socialProviderRegistry: SocialProviderRegistry, - configuration: Configuration, - clock: Clock -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { - - /** - * Views the `Sign In` page. - * - * @return The result to display. - */ - def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - Future.successful(Ok(views.html.signIn(SignInForm.form, socialProviderRegistry))) - } - - /** - * Handles the submitted form. - * - * @return The result to display. - */ - def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - SignInForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.signIn(form, socialProviderRegistry))), - data => { - val credentials = Credentials(data.email, data.password) - credentialsProvider.authenticate(credentials).flatMap { loginInfo => - userService.retrieve(loginInfo).flatMap { - case Some(user) if !user.activated => - Future.successful(Ok(views.html.activateAccount(data.email))) - case Some(user) => - authInfoRepository.find[GoogleTotpInfo](user.loginInfo).flatMap { - case Some(totpInfo) => Future.successful(Ok(views.html.totp(TotpForm.form.fill(TotpForm.Data( - user.userID, totpInfo.sharedKey, data.rememberMe))))) - case _ => authenticateUser(user, data.rememberMe) - } - case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) - } - }.recover { - case _: ProviderException => - Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.credentials")) - } - } - ) - } -} diff --git a/app/controllers/SignUpController.scala b/app/controllers/SignUpController.scala deleted file mode 100644 index a9bd25f..0000000 --- a/app/controllers/SignUpController.scala +++ /dev/null @@ -1,119 +0,0 @@ -package controllers - -import java.util.UUID -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.services.AvatarService -import com.mohiva.play.silhouette.api.util.PasswordHasherRegistry -import com.mohiva.play.silhouette.impl.providers._ -import forms.SignUpForm -import models.User -import models.services.{ AuthTokenService, UserService } -import org.webjars.play.WebJarsUtil -import play.api.i18n.{ I18nSupport, Messages } -import play.api.libs.mailer.{ Email, MailerClient } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `Sign Up` controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param authInfoRepository The auth info repository implementation. - * @param authTokenService The auth token service implementation. - * @param avatarService The avatar service implementation. - * @param passwordHasherRegistry The password hasher registry. - * @param mailerClient The mailer client. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - */ -class SignUpController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authInfoRepository: AuthInfoRepository, - authTokenService: AuthTokenService, - avatarService: AvatarService, - passwordHasherRegistry: PasswordHasherRegistry, - mailerClient: MailerClient -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport { - - /** - * Views the `Sign Up` page. - * - * @return The result to display. - */ - def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - Future.successful(Ok(views.html.signUp(SignUpForm.form))) - } - - /** - * Handles the submitted form. - * - * @return The result to display. - */ - def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => - SignUpForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.signUp(form))), - data => { - val result = Redirect(routes.SignUpController.view()).flashing("info" -> Messages("sign.up.email.sent", data.email)) - val loginInfo = LoginInfo(CredentialsProvider.ID, data.email) - userService.retrieve(loginInfo).flatMap { - case Some(user) => - val url = routes.SignInController.view().absoluteURL() - mailerClient.send(Email( - subject = Messages("email.already.signed.up.subject"), - from = Messages("email.from"), - to = Seq(data.email), - bodyText = Some(views.txt.emails.alreadySignedUp(user, url).body), - bodyHtml = Some(views.html.emails.alreadySignedUp(user, url).body) - )) - - Future.successful(result) - case None => - val authInfo = passwordHasherRegistry.current.hash(data.password) - val user = User( - userID = UUID.randomUUID(), - loginInfo = loginInfo, - firstName = Some(data.firstName), - lastName = Some(data.lastName), - fullName = Some(data.firstName + " " + data.lastName), - email = Some(data.email), - avatarURL = None, - activated = false - ) - for { - avatar <- avatarService.retrieveURL(data.email) - user <- userService.save(user.copy(avatarURL = avatar)) - authInfo <- authInfoRepository.add(loginInfo, authInfo) - authToken <- authTokenService.create(user.userID) - } yield { - val url = routes.ActivateAccountController.activate(authToken.id).absoluteURL() - mailerClient.send(Email( - subject = Messages("email.sign.up.subject"), - from = Messages("email.from"), - to = Seq(data.email), - bodyText = Some(views.txt.emails.signUp(user, url).body), - bodyHtml = Some(views.html.emails.signUp(user, url).body) - )) - - silhouette.env.eventBus.publish(SignUpEvent(user, request)) - result - } - } - } - ) - } -} diff --git a/app/controllers/SocialAuthController.scala b/app/controllers/SocialAuthController.scala deleted file mode 100644 index bca4776..0000000 --- a/app/controllers/SocialAuthController.scala +++ /dev/null @@ -1,67 +0,0 @@ -package controllers - -import javax.inject.Inject - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.exceptions.ProviderException -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.impl.providers._ -import models.services.UserService -import play.api.i18n.{ I18nSupport, Messages } -import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The social auth controller. - * - * @param components The Play controller components. - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param authInfoRepository The auth info service implementation. - * @param socialProviderRegistry The social provider registry. - * @param ex The execution context. - */ -class SocialAuthController @Inject() ( - components: ControllerComponents, - silhouette: Silhouette[DefaultEnv], - userService: UserService, - authInfoRepository: AuthInfoRepository, - socialProviderRegistry: SocialProviderRegistry -)( - implicit - ex: ExecutionContext -) extends AbstractController(components) with I18nSupport with Logger { - - /** - * Authenticates a user against a social provider. - * - * @param provider The ID of the provider to authenticate against. - * @return The result to display. - */ - def authenticate(provider: String) = Action.async { implicit request: Request[AnyContent] => - (socialProviderRegistry.get[SocialProvider](provider) match { - case Some(p: SocialProvider with CommonSocialProfileBuilder) => - p.authenticate().flatMap { - case Left(result) => Future.successful(result) - case Right(authInfo) => for { - profile <- p.retrieveProfile(authInfo) - user <- userService.save(profile) - authInfo <- authInfoRepository.save(profile.loginInfo, authInfo) - authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo) - value <- silhouette.env.authenticatorService.init(authenticator) - result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.ApplicationController.index())) - } yield { - silhouette.env.eventBus.publish(LoginEvent(user, request)) - result - } - } - case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) - }).recover { - case e: ProviderException => - logger.error("Unexpected provider error", e) - Redirect(routes.SignInController.view()).flashing("error" -> Messages("could.not.authenticate")) - } - } -} diff --git a/app/controllers/TotpController.scala b/app/controllers/TotpController.scala deleted file mode 100644 index 9781df3..0000000 --- a/app/controllers/TotpController.scala +++ /dev/null @@ -1,126 +0,0 @@ -package controllers - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.exceptions.ProviderException -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.util.Clock -import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException -import com.mohiva.play.silhouette.impl.providers._ -import forms.{ TotpForm, TotpSetupForm } -import javax.inject.Inject -import models.services.UserService -import org.webjars.play.WebJarsUtil -import play.api.Configuration -import play.api.i18n.{ I18nSupport, Messages } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `TOTP` controller. - * - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param totpProvider The totp provider. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - * @param authInfoRepository The auth info repository. - */ -class TotpController @Inject() ( - silhouette: Silhouette[DefaultEnv], - userService: UserService, - totpProvider: GoogleTotpProvider, - configuration: Configuration, - clock: Clock -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext, - authInfoRepository: AuthInfoRepository -) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { - - /** - * Views the `TOTP` page. - * @return The result to display. - */ - def view(userId: java.util.UUID, sharedKey: String, rememberMe: Boolean) = silhouette.UnsecuredAction.async { implicit request => - Future.successful(Ok(views.html.totp(TotpForm.form.fill(TotpForm.Data(userId, sharedKey, rememberMe))))) - } - - /** - * Enable TOTP. - * @return The result to display. - */ - def enableTotp = silhouette.SecuredAction.async { implicit request => - val user = request.identity - val credentials = totpProvider.createCredentials(user.email.get) - val totpInfo = credentials.totpInfo - val formData = TotpSetupForm.form.fill(TotpSetupForm.Data(totpInfo.sharedKey, totpInfo.scratchCodes, credentials.scratchCodesPlain)) - authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => - Ok(views.html.home(user, totpInfoOpt, Some((formData, credentials)))) - } - } - - /** - * Disable TOTP. - * @return The result to display. - */ - def disableTotp = silhouette.SecuredAction.async { implicit request => - val user = request.identity - authInfoRepository.remove[GoogleTotpInfo](user.loginInfo) - Future(Redirect(routes.ApplicationController.index()).flashing("info" -> Messages("totp.disabling.info"))) - } - - /** - * Handles the submitted form with TOTP initial data. - * @return The result to display. - */ - def enableTotpSubmit = silhouette.SecuredAction.async { implicit request => - val user = request.identity - TotpSetupForm.form.bindFromRequest.fold( - form => authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => - BadRequest(views.html.home(user, totpInfoOpt)) - }, - data => { - totpProvider.authenticate(data.sharedKey, data.verificationCode).flatMap { - case Some(loginInfo: LoginInfo) => { - authInfoRepository.add[GoogleTotpInfo](user.loginInfo, GoogleTotpInfo(data.sharedKey, data.scratchCodes)) - Future(Redirect(routes.ApplicationController.index()).flashing("success" -> Messages("totp.enabling.info"))) - } - case _ => Future.successful(Redirect(routes.ApplicationController.index()).flashing("error" -> Messages("invalid.verification.code"))) - }.recover { - case _: ProviderException => - Redirect(routes.TotpController.view(user.userID, data.sharedKey, request.authenticator.cookieMaxAge.isDefined)).flashing("error" -> Messages("invalid.unexpected.totp")) - } - } - ) - } - - /** - * Handles the submitted form with TOTP verification key. - * @return The result to display. - */ - def submit = silhouette.UnsecuredAction.async { implicit request => - TotpForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.totp(form))), - data => { - val totpControllerRoute = routes.TotpController.view(data.userID, data.sharedKey, data.rememberMe) - userService.retrieve(data.userID).flatMap { - case Some(user) => - totpProvider.authenticate(data.sharedKey, data.verificationCode).flatMap { - case Some(_) => authenticateUser(user, data.rememberMe) - case _ => Future.successful(Redirect(totpControllerRoute).flashing("error" -> Messages("invalid.verification.code"))) - }.recover { - case _: ProviderException => - Redirect(totpControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp")) - } - case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) - } - } - ) - } -} diff --git a/app/controllers/TotpRecoveryController.scala b/app/controllers/TotpRecoveryController.scala deleted file mode 100644 index e1e3b22..0000000 --- a/app/controllers/TotpRecoveryController.scala +++ /dev/null @@ -1,91 +0,0 @@ -package controllers - -import java.util.UUID - -import com.mohiva.play.silhouette.api._ -import com.mohiva.play.silhouette.api.exceptions.ProviderException -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.util.Clock -import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException -import com.mohiva.play.silhouette.impl.providers._ -import forms.TotpRecoveryForm -import javax.inject.Inject -import models.services.UserService -import org.webjars.play.WebJarsUtil -import play.api.Configuration -import play.api.i18n.{ I18nSupport, Messages } -import utils.auth.DefaultEnv - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * The `TOTP` controller. - * - * @param silhouette The Silhouette stack. - * @param userService The user service implementation. - * @param totpProvider The totp provider. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @param webJarsUtil The webjar util. - * @param assets The Play assets finder. - * @param ex The execution context. - * @param authInfoRepository The auth info repository. - */ -class TotpRecoveryController @Inject() ( - silhouette: Silhouette[DefaultEnv], - userService: UserService, - totpProvider: GoogleTotpProvider, - configuration: Configuration, - clock: Clock -)( - implicit - webJarsUtil: WebJarsUtil, - assets: AssetsFinder, - ex: ExecutionContext, - authInfoRepository: AuthInfoRepository -) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { - - /** - * Views the TOTP recovery page. - * - * @param userID the user ID. - * @param sharedKey the shared key associated to the user. - * @param rememberMe the remember me flag. - * @return The result to display. - */ - def view(userID: UUID, sharedKey: String, rememberMe: Boolean) = silhouette.UnsecuredAction.async { implicit request => - Future.successful(Ok(views.html.totpRecovery(TotpRecoveryForm.form.fill(TotpRecoveryForm.Data(userID, sharedKey, rememberMe))))) - } - - /** - * Handles the submitted form with TOTP verification key. - * @return The result to display. - */ - def submit = silhouette.UnsecuredAction.async { implicit request => - TotpRecoveryForm.form.bindFromRequest.fold( - form => Future.successful(BadRequest(views.html.totpRecovery(form))), - data => { - val totpRecoveryControllerRoute = routes.TotpRecoveryController.view(data.userID, data.sharedKey, data.rememberMe) - userService.retrieve(data.userID).flatMap { - case Some(user) => { - authInfoRepository.find[GoogleTotpInfo](user.loginInfo).flatMap { - case Some(totpInfo) => - totpProvider.authenticate(totpInfo, data.recoveryCode).flatMap { - case Some(updated) => { - authInfoRepository.update[GoogleTotpInfo](user.loginInfo, updated._2) - authenticateUser(user, data.rememberMe) - } - case _ => Future.successful(Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.recovery.code"))) - }.recover { - case _: ProviderException => - Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp")) - } - case _ => Future.successful(Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp"))) - } - } - case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) - } - } - ) - } -} diff --git a/app/forms/ChangePasswordForm.scala b/app/forms/ChangePasswordForm.scala deleted file mode 100644 index cf640c4..0000000 --- a/app/forms/ChangePasswordForm.scala +++ /dev/null @@ -1,18 +0,0 @@ -package forms - -import play.api.data.Forms._ -import play.api.data._ - -/** - * The `Change Password` form. - */ -object ChangePasswordForm { - - /** - * A play framework form. - */ - val form = Form(tuple( - "current-password" -> nonEmptyText, - "new-password" -> nonEmptyText - )) -} diff --git a/app/forms/ForgotPasswordForm.scala b/app/forms/ForgotPasswordForm.scala deleted file mode 100644 index 70dbaf3..0000000 --- a/app/forms/ForgotPasswordForm.scala +++ /dev/null @@ -1,17 +0,0 @@ -package forms - -import play.api.data.Forms._ -import play.api.data._ - -/** - * The `Forgot Password` form. - */ -object ForgotPasswordForm { - - /** - * A play framework form. - */ - val form = Form( - "email" -> email - ) -} diff --git a/app/forms/ResetPasswordForm.scala b/app/forms/ResetPasswordForm.scala deleted file mode 100644 index 9d0a020..0000000 --- a/app/forms/ResetPasswordForm.scala +++ /dev/null @@ -1,17 +0,0 @@ -package forms - -import play.api.data.Forms._ -import play.api.data._ - -/** - * The `Reset Password` form. - */ -object ResetPasswordForm { - - /** - * A play framework form. - */ - val form = Form( - "password" -> nonEmptyText - ) -} diff --git a/app/forms/SignInForm.scala b/app/forms/SignInForm.scala deleted file mode 100644 index e4183c4..0000000 --- a/app/forms/SignInForm.scala +++ /dev/null @@ -1,33 +0,0 @@ -package forms - -import play.api.data.Form -import play.api.data.Forms._ - -/** - * The form which handles the submission of the credentials. - */ -object SignInForm { - - /** - * A play framework form. - */ - val form = Form( - mapping( - "email" -> email, - "password" -> nonEmptyText, - "rememberMe" -> boolean - )(Data.apply)(Data.unapply) - ) - - /** - * The form data. - * - * @param email The email of the user. - * @param password The password of the user. - * @param rememberMe Indicates if the user should stay logged in on the next visit. - */ - case class Data( - email: String, - password: String, - rememberMe: Boolean) -} diff --git a/app/forms/SignUpForm.scala b/app/forms/SignUpForm.scala deleted file mode 100644 index 9cfb02e..0000000 --- a/app/forms/SignUpForm.scala +++ /dev/null @@ -1,36 +0,0 @@ -package forms - -import play.api.data.Form -import play.api.data.Forms._ - -/** - * The form which handles the sign up process. - */ -object SignUpForm { - - /** - * A play framework form. - */ - val form = Form( - mapping( - "firstName" -> nonEmptyText, - "lastName" -> nonEmptyText, - "email" -> email, - "password" -> nonEmptyText - )(Data.apply)(Data.unapply) - ) - - /** - * The form data. - * - * @param firstName The first name of a user. - * @param lastName The last name of a user. - * @param email The email of the user. - * @param password The password of the user. - */ - case class Data( - firstName: String, - lastName: String, - email: String, - password: String) -} diff --git a/app/forms/TotpForm.scala b/app/forms/TotpForm.scala deleted file mode 100644 index ce8c6de..0000000 --- a/app/forms/TotpForm.scala +++ /dev/null @@ -1,36 +0,0 @@ -package forms - -import java.util.UUID - -import play.api.data.Form -import play.api.data.Forms._ - -/** - * The form which handles the submission of the credentials plus verification code for TOTP-authentication - */ -object TotpForm { - /** - * A play framework form. - */ - val form = Form( - mapping( - "userID" -> uuid, - "sharedKey" -> nonEmptyText, - "rememberMe" -> boolean, - "verificationCode" -> nonEmptyText(minLength = 6, maxLength = 6) - )(Data.apply)(Data.unapply) - ) - - /** - * The form data. - * @param userID The unique identifier of the user. - * @param sharedKey the TOTP shared key - * @param rememberMe Indicates if the user should stay logged in on the next visit. - * @param verificationCode Verification code for TOTP-authentication - */ - case class Data( - userID: UUID, - sharedKey: String, - rememberMe: Boolean, - verificationCode: String = "") -} diff --git a/app/forms/TotpRecoveryForm.scala b/app/forms/TotpRecoveryForm.scala deleted file mode 100644 index 9c50f98..0000000 --- a/app/forms/TotpRecoveryForm.scala +++ /dev/null @@ -1,36 +0,0 @@ -package forms - -import java.util.UUID - -import play.api.data.Form -import play.api.data.Forms._ - -/** - * The form which handles the submission of the credentials plus verification code for TOTP-authentication - */ -object TotpRecoveryForm { - /** - * A play framework form. - */ - val form = Form( - mapping( - "userID" -> uuid, - "sharedKey" -> nonEmptyText, - "rememberMe" -> boolean, - "recoveryCode" -> nonEmptyText(minLength = 8, maxLength = 8) - )(Data.apply)(Data.unapply) - ) - - /** - * The form data. - * @param userID The unique identifier of the user. - * @param sharedKey the TOTP shared key - * @param rememberMe Indicates if the user should stay logged in on the next visit. - * @param recoveryCode Verification code for TOTP-authentication - */ - case class Data( - userID: UUID, - sharedKey: String, - rememberMe: Boolean, - recoveryCode: String = "") -} diff --git a/app/forms/TotpSetupForm.scala b/app/forms/TotpSetupForm.scala deleted file mode 100644 index 183513d..0000000 --- a/app/forms/TotpSetupForm.scala +++ /dev/null @@ -1,40 +0,0 @@ -package forms - -import com.mohiva.play.silhouette.api.util.PasswordInfo -import play.api.data.Form -import play.api.data.Forms._ - -/** - * The form which handles the submission of the form with data for TOTP-authentication enabling - */ -object TotpSetupForm { - /** - * A play framework form. - */ - val form = Form( - mapping( - "sharedKey" -> nonEmptyText, - "scratchCodes" -> seq( - mapping( - "hasher" -> nonEmptyText, - "password" -> nonEmptyText, - "salt" -> optional(nonEmptyText) - )(PasswordInfo.apply)(PasswordInfo.unapply) - ), - "scratchCodesPlain" -> seq(nonEmptyText), - "verificationCode" -> nonEmptyText(minLength = 6, maxLength = 6) - )(Data.apply)(Data.unapply) - ) - - /** - * The form data. - * @param sharedKey Shared user key for TOTP authentication. - * @param scratchCodes Scratch or recovery codes used for one time TOTP authentication. - * @param verificationCode Verification code for TOTP-authentication - */ - case class Data( - sharedKey: String, - scratchCodes: Seq[PasswordInfo], - scratchCodesPlain: Seq[String], - verificationCode: String = "") -} diff --git a/app/jobs/AuthTokenCleaner.scala b/app/jobs/AuthTokenCleaner.scala deleted file mode 100644 index e34053e..0000000 --- a/app/jobs/AuthTokenCleaner.scala +++ /dev/null @@ -1,55 +0,0 @@ -package jobs - -import javax.inject.Inject - -import akka.actor._ -import com.mohiva.play.silhouette.api.util.Clock -import jobs.AuthTokenCleaner.Clean -import models.services.AuthTokenService -import utils.Logger - -import scala.concurrent.ExecutionContext.Implicits.global - -/** - * A job which cleanup invalid auth tokens. - * - * @param service The auth token service implementation. - * @param clock The clock implementation. - */ -class AuthTokenCleaner @Inject() ( - service: AuthTokenService, - clock: Clock) - extends Actor with Logger { - - /** - * Process the received messages. - */ - def receive: Receive = { - case Clean => - val start = clock.now.getMillis - val msg = new StringBuffer("\n") - msg.append("=================================\n") - msg.append("Start to cleanup auth tokens\n") - msg.append("=================================\n") - service.clean.map { deleted => - val seconds = (clock.now.getMillis - start) / 1000 - msg.append("Total of %s auth tokens(s) were deleted in %s seconds".format(deleted.length, seconds)).append("\n") - msg.append("=================================\n") - - msg.append("=================================\n") - logger.info(msg.toString) - }.recover { - case e => - msg.append("Couldn't cleanup auth tokens because of unexpected error\n") - msg.append("=================================\n") - logger.error(msg.toString, e) - } - } -} - -/** - * The companion object. - */ -object AuthTokenCleaner { - case object Clean -} diff --git a/app/jobs/Scheduler.scala b/app/jobs/Scheduler.scala deleted file mode 100644 index 59c7cd8..0000000 --- a/app/jobs/Scheduler.scala +++ /dev/null @@ -1,18 +0,0 @@ -package jobs - -import akka.actor.{ ActorRef, ActorSystem } -import com.google.inject.Inject -import com.google.inject.name.Named -import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension - -/** - * Schedules the jobs. - */ -class Scheduler @Inject() ( - system: ActorSystem, - @Named("auth-token-cleaner") authTokenCleaner: ActorRef) { - - QuartzSchedulerExtension(system).schedule("AuthTokenCleaner", authTokenCleaner, AuthTokenCleaner.Clean) - - authTokenCleaner ! AuthTokenCleaner.Clean -} diff --git a/app/models/AuthToken.scala b/app/models/AuthToken.scala deleted file mode 100644 index 2acb397..0000000 --- a/app/models/AuthToken.scala +++ /dev/null @@ -1,17 +0,0 @@ -package models - -import java.util.UUID - -import org.joda.time.DateTime - -/** - * A token to authenticate a user against an endpoint for a short time period. - * - * @param id The unique token ID. - * @param userID The unique ID of the user the token is associated with. - * @param expiry The date-time the token expires. - */ -case class AuthToken( - id: UUID, - userID: UUID, - expiry: DateTime) diff --git a/app/models/User.scala b/app/models/User.scala deleted file mode 100644 index 00f6618..0000000 --- a/app/models/User.scala +++ /dev/null @@ -1,42 +0,0 @@ -package models - -import java.util.UUID - -import com.mohiva.play.silhouette.api.{ Identity, LoginInfo } - -/** - * The user object. - * - * @param userID The unique ID of the user. - * @param loginInfo The linked login info. - * @param firstName Maybe the first name of the authenticated user. - * @param lastName Maybe the last name of the authenticated user. - * @param fullName Maybe the full name of the authenticated user. - * @param email Maybe the email of the authenticated provider. - * @param avatarURL Maybe the avatar URL of the authenticated provider. - * @param activated Indicates that the user has activated its registration. - */ -case class User( - userID: UUID, - loginInfo: LoginInfo, - firstName: Option[String], - lastName: Option[String], - fullName: Option[String], - email: Option[String], - avatarURL: Option[String], - activated: Boolean) extends Identity { - - /** - * Tries to construct a name. - * - * @return Maybe a name. - */ - def name = fullName.orElse { - firstName -> lastName match { - case (Some(f), Some(l)) => Some(f + " " + l) - case (Some(f), None) => Some(f) - case (None, Some(l)) => Some(l) - case _ => None - } - } -} diff --git a/app/models/daos/AuthTokenDAO.scala b/app/models/daos/AuthTokenDAO.scala deleted file mode 100644 index d1cbf8b..0000000 --- a/app/models/daos/AuthTokenDAO.scala +++ /dev/null @@ -1,45 +0,0 @@ -package models.daos - -import java.util.UUID - -import models.AuthToken -import org.joda.time.DateTime - -import scala.concurrent.Future - -/** - * Give access to the [[AuthToken]] object. - */ -trait AuthTokenDAO { - - /** - * Finds a token by its ID. - * - * @param id The unique token ID. - * @return The found token or None if no token for the given ID could be found. - */ - def find(id: UUID): Future[Option[AuthToken]] - - /** - * Finds expired tokens. - * - * @param dateTime The current date time. - */ - def findExpired(dateTime: DateTime): Future[Seq[AuthToken]] - - /** - * Saves a token. - * - * @param token The token to save. - * @return The saved token. - */ - def save(token: AuthToken): Future[AuthToken] - - /** - * Removes the token for the given ID. - * - * @param id The ID for which the token should be removed. - * @return A future to wait for the process to be completed. - */ - def remove(id: UUID): Future[Unit] -} diff --git a/app/models/daos/AuthTokenDAOImpl.scala b/app/models/daos/AuthTokenDAOImpl.scala deleted file mode 100644 index 95b5173..0000000 --- a/app/models/daos/AuthTokenDAOImpl.scala +++ /dev/null @@ -1,69 +0,0 @@ -package models.daos - -import java.util.UUID - -import models.AuthToken -import models.daos.AuthTokenDAOImpl._ -import org.joda.time.DateTime - -import scala.collection.mutable -import scala.concurrent.Future - -/** - * Give access to the [[AuthToken]] object. - */ -class AuthTokenDAOImpl extends AuthTokenDAO { - - /** - * Finds a token by its ID. - * - * @param id The unique token ID. - * @return The found token or None if no token for the given ID could be found. - */ - def find(id: UUID) = Future.successful(tokens.get(id)) - - /** - * Finds expired tokens. - * - * @param dateTime The current date time. - */ - def findExpired(dateTime: DateTime) = Future.successful { - tokens.filter { - case (_, token) => - token.expiry.isBefore(dateTime) - }.values.toSeq - } - - /** - * Saves a token. - * - * @param token The token to save. - * @return The saved token. - */ - def save(token: AuthToken) = { - tokens += (token.id -> token) - Future.successful(token) - } - - /** - * Removes the token for the given ID. - * - * @param id The ID for which the token should be removed. - * @return A future to wait for the process to be completed. - */ - def remove(id: UUID) = { - tokens -= id - Future.successful(()) - } -} - -/** - * The companion object. - */ -object AuthTokenDAOImpl { - - /** - * The list of tokens. - */ - val tokens: mutable.HashMap[UUID, AuthToken] = mutable.HashMap() -} diff --git a/app/models/daos/UserDAO.scala b/app/models/daos/UserDAO.scala deleted file mode 100644 index be0ecb0..0000000 --- a/app/models/daos/UserDAO.scala +++ /dev/null @@ -1,38 +0,0 @@ -package models.daos - -import java.util.UUID - -import com.mohiva.play.silhouette.api.LoginInfo -import models.User - -import scala.concurrent.Future - -/** - * Give access to the user object. - */ -trait UserDAO { - - /** - * Finds a user by its login info. - * - * @param loginInfo The login info of the user to find. - * @return The found user or None if no user for the given login info could be found. - */ - def find(loginInfo: LoginInfo): Future[Option[User]] - - /** - * Finds a user by its user ID. - * - * @param userID The ID of the user to find. - * @return The found user or None if no user for the given ID could be found. - */ - def find(userID: UUID): Future[Option[User]] - - /** - * Saves a user. - * - * @param user The user to save. - * @return The saved user. - */ - def save(user: User): Future[User] -} diff --git a/app/models/daos/UserDAOImpl.scala b/app/models/daos/UserDAOImpl.scala deleted file mode 100644 index 54d6cc0..0000000 --- a/app/models/daos/UserDAOImpl.scala +++ /dev/null @@ -1,56 +0,0 @@ -package models.daos - -import java.util.UUID - -import com.mohiva.play.silhouette.api.LoginInfo -import models.User -import models.daos.UserDAOImpl._ - -import scala.collection.mutable -import scala.concurrent.Future - -/** - * Give access to the user object. - */ -class UserDAOImpl extends UserDAO { - - /** - * Finds a user by its login info. - * - * @param loginInfo The login info of the user to find. - * @return The found user or None if no user for the given login info could be found. - */ - def find(loginInfo: LoginInfo) = Future.successful( - users.find { case (_, user) => user.loginInfo == loginInfo }.map(_._2) - ) - - /** - * Finds a user by its user ID. - * - * @param userID The ID of the user to find. - * @return The found user or None if no user for the given ID could be found. - */ - def find(userID: UUID) = Future.successful(users.get(userID)) - - /** - * Saves a user. - * - * @param user The user to save. - * @return The saved user. - */ - def save(user: User) = { - users += (user.userID -> user) - Future.successful(user) - } -} - -/** - * The companion object. - */ -object UserDAOImpl { - - /** - * The list of users. - */ - val users: mutable.HashMap[UUID, User] = mutable.HashMap() -} diff --git a/app/models/services/AuthTokenService.scala b/app/models/services/AuthTokenService.scala deleted file mode 100644 index a26b02a..0000000 --- a/app/models/services/AuthTokenService.scala +++ /dev/null @@ -1,39 +0,0 @@ -package models.services - -import java.util.UUID - -import models.AuthToken - -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.language.postfixOps - -/** - * Handles actions to auth tokens. - */ -trait AuthTokenService { - - /** - * Creates a new auth token and saves it in the backing store. - * - * @param userID The user ID for which the token should be created. - * @param expiry The duration a token expires. - * @return The saved auth token. - */ - def create(userID: UUID, expiry: FiniteDuration = 5 minutes): Future[AuthToken] - - /** - * Validates a token ID. - * - * @param id The token ID to validate. - * @return The token if it's valid, None otherwise. - */ - def validate(id: UUID): Future[Option[AuthToken]] - - /** - * Cleans expired tokens. - * - * @return The list of deleted tokens. - */ - def clean: Future[Seq[AuthToken]] -} diff --git a/app/models/services/AuthTokenServiceImpl.scala b/app/models/services/AuthTokenServiceImpl.scala deleted file mode 100644 index 11ab9fe..0000000 --- a/app/models/services/AuthTokenServiceImpl.scala +++ /dev/null @@ -1,60 +0,0 @@ -package models.services - -import java.util.UUID -import javax.inject.Inject - -import com.mohiva.play.silhouette.api.util.Clock -import models.AuthToken -import models.daos.AuthTokenDAO -import org.joda.time.DateTimeZone - -import scala.concurrent.{ ExecutionContext, Future } -import scala.concurrent.duration._ -import scala.language.postfixOps - -/** - * Handles actions to auth tokens. - * - * @param authTokenDAO The auth token DAO implementation. - * @param clock The clock instance. - * @param ex The execution context. - */ -class AuthTokenServiceImpl @Inject() ( - authTokenDAO: AuthTokenDAO, - clock: Clock -)( - implicit - ex: ExecutionContext -) extends AuthTokenService { - - /** - * Creates a new auth token and saves it in the backing store. - * - * @param userID The user ID for which the token should be created. - * @param expiry The duration a token expires. - * @return The saved auth token. - */ - def create(userID: UUID, expiry: FiniteDuration = 5 minutes) = { - val token = AuthToken(UUID.randomUUID(), userID, clock.now.withZone(DateTimeZone.UTC).plusSeconds(expiry.toSeconds.toInt)) - authTokenDAO.save(token) - } - - /** - * Validates a token ID. - * - * @param id The token ID to validate. - * @return The token if it's valid, None otherwise. - */ - def validate(id: UUID) = authTokenDAO.find(id) - - /** - * Cleans expired tokens. - * - * @return The list of deleted tokens. - */ - def clean = authTokenDAO.findExpired(clock.now.withZone(DateTimeZone.UTC)).flatMap { tokens => - Future.sequence(tokens.map { token => - authTokenDAO.remove(token.id).map(_ => token) - }) - } -} diff --git a/app/models/services/UserService.scala b/app/models/services/UserService.scala deleted file mode 100644 index d263012..0000000 --- a/app/models/services/UserService.scala +++ /dev/null @@ -1,41 +0,0 @@ -package models.services - -import java.util.UUID - -import com.mohiva.play.silhouette.api.services.IdentityService -import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile -import models.User - -import scala.concurrent.Future - -/** - * Handles actions to users. - */ -trait UserService extends IdentityService[User] { - - /** - * Retrieves a user that matches the specified ID. - * - * @param id The ID to retrieve a user. - * @return The retrieved user or None if no user could be retrieved for the given ID. - */ - def retrieve(id: UUID): Future[Option[User]] - - /** - * Saves a user. - * - * @param user The user to save. - * @return The saved user. - */ - def save(user: User): Future[User] - - /** - * Saves the social profile for a user. - * - * If a user exists for this profile then update the user, otherwise create a new user with the given profile. - * - * @param profile The social profile to save. - * @return The user for whom the profile was saved. - */ - def save(profile: CommonSocialProfile): Future[User] -} diff --git a/app/models/services/UserServiceImpl.scala b/app/models/services/UserServiceImpl.scala deleted file mode 100644 index c914451..0000000 --- a/app/models/services/UserServiceImpl.scala +++ /dev/null @@ -1,76 +0,0 @@ -package models.services - -import java.util.UUID -import javax.inject.Inject - -import com.mohiva.play.silhouette.api.LoginInfo -import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile -import models.User -import models.daos.UserDAO - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * Handles actions to users. - * - * @param userDAO The user DAO implementation. - * @param ex The execution context. - */ -class UserServiceImpl @Inject() (userDAO: UserDAO)(implicit ex: ExecutionContext) extends UserService { - - /** - * Retrieves a user that matches the specified ID. - * - * @param id The ID to retrieve a user. - * @return The retrieved user or None if no user could be retrieved for the given ID. - */ - def retrieve(id: UUID) = userDAO.find(id) - - /** - * Retrieves a user that matches the specified login info. - * - * @param loginInfo The login info to retrieve a user. - * @return The retrieved user or None if no user could be retrieved for the given login info. - */ - def retrieve(loginInfo: LoginInfo): Future[Option[User]] = userDAO.find(loginInfo) - - /** - * Saves a user. - * - * @param user The user to save. - * @return The saved user. - */ - def save(user: User) = userDAO.save(user) - - /** - * Saves the social profile for a user. - * - * If a user exists for this profile then update the user, otherwise create a new user with the given profile. - * - * @param profile The social profile to save. - * @return The user for whom the profile was saved. - */ - def save(profile: CommonSocialProfile) = { - userDAO.find(profile.loginInfo).flatMap { - case Some(user) => // Update user with profile - userDAO.save(user.copy( - firstName = profile.firstName, - lastName = profile.lastName, - fullName = profile.fullName, - email = profile.email, - avatarURL = profile.avatarURL - )) - case None => // Insert a new user - userDAO.save(User( - userID = UUID.randomUUID(), - loginInfo = profile.loginInfo, - firstName = profile.firstName, - lastName = profile.lastName, - fullName = profile.fullName, - email = profile.email, - avatarURL = profile.avatarURL, - activated = true - )) - } - } -} diff --git a/app/modules/BaseModule.scala b/app/modules/BaseModule.scala deleted file mode 100644 index 2bb22a0..0000000 --- a/app/modules/BaseModule.scala +++ /dev/null @@ -1,20 +0,0 @@ -package modules - -import com.google.inject.AbstractModule -import models.daos.{ AuthTokenDAO, AuthTokenDAOImpl } -import models.services.{ AuthTokenService, AuthTokenServiceImpl } -import net.codingwell.scalaguice.ScalaModule - -/** - * The base Guice module. - */ -class BaseModule extends AbstractModule with ScalaModule { - - /** - * Configures the module. - */ - override def configure(): Unit = { - bind[AuthTokenDAO].to[AuthTokenDAOImpl] - bind[AuthTokenService].to[AuthTokenServiceImpl] - } -} diff --git a/app/modules/JobModule.scala b/app/modules/JobModule.scala deleted file mode 100644 index c74e22f..0000000 --- a/app/modules/JobModule.scala +++ /dev/null @@ -1,19 +0,0 @@ -package modules - -import jobs.{ AuthTokenCleaner, Scheduler } -import net.codingwell.scalaguice.ScalaModule -import play.api.libs.concurrent.AkkaGuiceSupport - -/** - * The job module. - */ -class JobModule extends ScalaModule with AkkaGuiceSupport { - - /** - * Configures the module. - */ - override def configure() = { - bindActor[AuthTokenCleaner]("auth-token-cleaner") - bind[Scheduler].asEagerSingleton() - } -} diff --git a/app/modules/SilhouetteModule.scala b/app/modules/SilhouetteModule.scala deleted file mode 100644 index 1431e6c..0000000 --- a/app/modules/SilhouetteModule.scala +++ /dev/null @@ -1,475 +0,0 @@ -package modules - -import com.google.inject.name.Named -import com.google.inject.{ AbstractModule, Provides } -import com.mohiva.play.silhouette.api.actions.{ SecuredErrorHandler, UnsecuredErrorHandler } -import com.mohiva.play.silhouette.api.crypto._ -import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository -import com.mohiva.play.silhouette.api.services._ -import com.mohiva.play.silhouette.api.util._ -import com.mohiva.play.silhouette.api.{ Environment, EventBus, Silhouette, SilhouetteProvider } -import com.mohiva.play.silhouette.crypto.{ JcaCrypter, JcaCrypterSettings, JcaSigner, JcaSignerSettings } -import com.mohiva.play.silhouette.impl.authenticators._ -import com.mohiva.play.silhouette.impl.providers._ -import com.mohiva.play.silhouette.impl.providers.oauth1._ -import com.mohiva.play.silhouette.impl.providers.oauth1.secrets.{ CookieSecretProvider, CookieSecretSettings } -import com.mohiva.play.silhouette.impl.providers.oauth1.services.PlayOAuth1Service -import com.mohiva.play.silhouette.impl.providers.oauth2._ -import com.mohiva.play.silhouette.impl.providers.openid.YahooProvider -import com.mohiva.play.silhouette.impl.providers.openid.services.PlayOpenIDService -import com.mohiva.play.silhouette.impl.providers.state.{ CsrfStateItemHandler, CsrfStateSettings } -import com.mohiva.play.silhouette.impl.services._ -import com.mohiva.play.silhouette.impl.util._ -import com.mohiva.play.silhouette.password.{ BCryptPasswordHasher, BCryptSha256PasswordHasher } -import com.mohiva.play.silhouette.persistence.daos.{ DelegableAuthInfoDAO, InMemoryAuthInfoDAO } -import com.mohiva.play.silhouette.persistence.repositories.DelegableAuthInfoRepository -import com.typesafe.config.Config -import models.daos._ -import models.services.{ UserService, UserServiceImpl } -import net.ceedubs.ficus.Ficus._ -import net.ceedubs.ficus.readers.ArbitraryTypeReader._ -import net.ceedubs.ficus.readers.ValueReader -import net.codingwell.scalaguice.ScalaModule -import play.api.Configuration -import play.api.libs.openid.OpenIdClient -import play.api.libs.ws.WSClient -import play.api.mvc.{ Cookie, CookieHeaderEncoding } -import utils.auth.{ CustomSecuredErrorHandler, CustomUnsecuredErrorHandler, DefaultEnv } - -import scala.concurrent.ExecutionContext.Implicits.global - -/** - * The Guice module which wires all Silhouette dependencies. - */ -class SilhouetteModule extends AbstractModule with ScalaModule { - - /** - * A very nested optional reader, to support these cases: - * Not set, set None, will use default ('Lax') - * Set to null, set Some(None), will use 'No Restriction' - * Set to a string value try to match, Some(Option(string)) - */ - implicit val sameSiteReader: ValueReader[Option[Option[Cookie.SameSite]]] = - (config: Config, path: String) => { - if (config.hasPathOrNull(path)) { - if (config.getIsNull(path)) - Some(None) - else { - Some(Cookie.SameSite.parse(config.getString(path))) - } - } else { - None - } - } - - /** - * Configures the module. - */ - override def configure() { - bind[Silhouette[DefaultEnv]].to[SilhouetteProvider[DefaultEnv]] - bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler] - bind[SecuredErrorHandler].to[CustomSecuredErrorHandler] - bind[UserService].to[UserServiceImpl] - bind[UserDAO].to[UserDAOImpl] - bind[CacheLayer].to[PlayCacheLayer] - bind[IDGenerator].toInstance(new SecureRandomIDGenerator()) - bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false)) - bind[EventBus].toInstance(EventBus()) - bind[Clock].toInstance(Clock()) - - // Replace this with the bindings to your concrete DAOs - bind[DelegableAuthInfoDAO[GoogleTotpInfo]].toInstance(new InMemoryAuthInfoDAO[GoogleTotpInfo]) - bind[DelegableAuthInfoDAO[PasswordInfo]].toInstance(new InMemoryAuthInfoDAO[PasswordInfo]) - bind[DelegableAuthInfoDAO[OAuth1Info]].toInstance(new InMemoryAuthInfoDAO[OAuth1Info]) - bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info]) - bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo]) - } - - /** - * Provides the HTTP layer implementation. - * - * @param client Play's WS client. - * @return The HTTP layer implementation. - */ - @Provides - def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client) - - /** - * Provides the Silhouette environment. - * - * @param userService The user service implementation. - * @param authenticatorService The authentication service implementation. - * @param eventBus The event bus instance. - * @return The Silhouette environment. - */ - @Provides - def provideEnvironment( - userService: UserService, - authenticatorService: AuthenticatorService[CookieAuthenticator], - eventBus: EventBus): Environment[DefaultEnv] = { - - Environment[DefaultEnv]( - userService, - authenticatorService, - Seq(), - eventBus - ) - } - - /** - * Provides the social provider registry. - * - * @param facebookProvider The Facebook provider implementation. - * @param googleProvider The Google provider implementation. - * @param vkProvider The VK provider implementation. - * @param twitterProvider The Twitter provider implementation. - * @param xingProvider The Xing provider implementation. - * @param yahooProvider The Yahoo provider implementation. - * @return The Silhouette environment. - */ - @Provides - def provideSocialProviderRegistry( - facebookProvider: FacebookProvider, - googleProvider: GoogleProvider, - vkProvider: VKProvider, - twitterProvider: TwitterProvider, - xingProvider: XingProvider, - yahooProvider: YahooProvider): SocialProviderRegistry = { - - SocialProviderRegistry(Seq( - googleProvider, - facebookProvider, - twitterProvider, - vkProvider, - xingProvider, - yahooProvider - )) - } - - /** - * Provides the signer for the OAuth1 token secret provider. - * - * @param configuration The Play configuration. - * @return The signer for the OAuth1 token secret provider. - */ - @Provides @Named("oauth1-token-secret-signer") - def provideOAuth1TokenSecretSigner(configuration: Configuration): Signer = { - val config = configuration.underlying.as[JcaSignerSettings]("silhouette.oauth1TokenSecretProvider.signer") - - new JcaSigner(config) - } - - /** - * Provides the crypter for the OAuth1 token secret provider. - * - * @param configuration The Play configuration. - * @return The crypter for the OAuth1 token secret provider. - */ - @Provides @Named("oauth1-token-secret-crypter") - def provideOAuth1TokenSecretCrypter(configuration: Configuration): Crypter = { - val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.oauth1TokenSecretProvider.crypter") - - new JcaCrypter(config) - } - - /** - * Provides the signer for the CSRF state item handler. - * - * @param configuration The Play configuration. - * @return The signer for the CSRF state item handler. - */ - @Provides @Named("csrf-state-item-signer") - def provideCSRFStateItemSigner(configuration: Configuration): Signer = { - val config = configuration.underlying.as[JcaSignerSettings]("silhouette.csrfStateItemHandler.signer") - - new JcaSigner(config) - } - - /** - * Provides the signer for the social state handler. - * - * @param configuration The Play configuration. - * @return The signer for the social state handler. - */ - @Provides @Named("social-state-signer") - def provideSocialStateSigner(configuration: Configuration): Signer = { - val config = configuration.underlying.as[JcaSignerSettings]("silhouette.socialStateHandler.signer") - - new JcaSigner(config) - } - - /** - * Provides the signer for the authenticator. - * - * @param configuration The Play configuration. - * @return The signer for the authenticator. - */ - @Provides @Named("authenticator-signer") - def provideAuthenticatorSigner(configuration: Configuration): Signer = { - val config = configuration.underlying.as[JcaSignerSettings]("silhouette.authenticator.signer") - - new JcaSigner(config) - } - - /** - * Provides the crypter for the authenticator. - * - * @param configuration The Play configuration. - * @return The crypter for the authenticator. - */ - @Provides @Named("authenticator-crypter") - def provideAuthenticatorCrypter(configuration: Configuration): Crypter = { - val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.authenticator.crypter") - - new JcaCrypter(config) - } - - /** - * Provides the auth info repository. - * - * @param totpInfoDAO The implementation of the delegable totp auth info DAO. - * @param passwordInfoDAO The implementation of the delegable password auth info DAO. - * @param oauth1InfoDAO The implementation of the delegable OAuth1 auth info DAO. - * @param oauth2InfoDAO The implementation of the delegable OAuth2 auth info DAO. - * @param openIDInfoDAO The implementation of the delegable OpenID auth info DAO. - * @return The auth info repository instance. - */ - @Provides - def provideAuthInfoRepository( - totpInfoDAO: DelegableAuthInfoDAO[GoogleTotpInfo], - passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo], - oauth1InfoDAO: DelegableAuthInfoDAO[OAuth1Info], - oauth2InfoDAO: DelegableAuthInfoDAO[OAuth2Info], - openIDInfoDAO: DelegableAuthInfoDAO[OpenIDInfo]): AuthInfoRepository = { - - new DelegableAuthInfoRepository(totpInfoDAO, passwordInfoDAO, oauth1InfoDAO, oauth2InfoDAO, openIDInfoDAO) - } - - /** - * Provides the authenticator service. - * - * @param signer The signer implementation. - * @param crypter The crypter implementation. - * @param cookieHeaderEncoding Logic for encoding and decoding `Cookie` and `Set-Cookie` headers. - * @param fingerprintGenerator The fingerprint generator implementation. - * @param idGenerator The ID generator implementation. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @return The authenticator service. - */ - @Provides - def provideAuthenticatorService( - @Named("authenticator-signer") signer: Signer, - @Named("authenticator-crypter") crypter: Crypter, - cookieHeaderEncoding: CookieHeaderEncoding, - fingerprintGenerator: FingerprintGenerator, - idGenerator: IDGenerator, - configuration: Configuration, - clock: Clock): AuthenticatorService[CookieAuthenticator] = { - - val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator") - val authenticatorEncoder = new CrypterAuthenticatorEncoder(crypter) - - new CookieAuthenticatorService(config, None, signer, cookieHeaderEncoding, authenticatorEncoder, fingerprintGenerator, idGenerator, clock) - } - - /** - * Provides the avatar service. - * - * @param httpLayer The HTTP layer implementation. - * @return The avatar service implementation. - */ - @Provides - def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer) - - /** - * Provides the OAuth1 token secret provider. - * - * @param signer The signer implementation. - * @param crypter The crypter implementation. - * @param configuration The Play configuration. - * @param clock The clock instance. - * @return The OAuth1 token secret provider implementation. - */ - @Provides - def provideOAuth1TokenSecretProvider( - @Named("oauth1-token-secret-signer") signer: Signer, - @Named("oauth1-token-secret-crypter") crypter: Crypter, - configuration: Configuration, - clock: Clock): OAuth1TokenSecretProvider = { - - val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider") - new CookieSecretProvider(settings, signer, crypter, clock) - } - - /** - * Provides the CSRF state item handler. - * - * @param idGenerator The ID generator implementation. - * @param signer The signer implementation. - * @param configuration The Play configuration. - * @return The CSRF state item implementation. - */ - @Provides - def provideCsrfStateItemHandler( - idGenerator: IDGenerator, - @Named("csrf-state-item-signer") signer: Signer, - configuration: Configuration): CsrfStateItemHandler = { - val settings = configuration.underlying.as[CsrfStateSettings]("silhouette.csrfStateItemHandler") - new CsrfStateItemHandler(settings, idGenerator, signer) - } - - /** - * Provides the social state handler. - * - * @param signer The signer implementation. - * @return The social state handler implementation. - */ - @Provides - def provideSocialStateHandler( - @Named("social-state-signer") signer: Signer, - csrfStateItemHandler: CsrfStateItemHandler): SocialStateHandler = { - - new DefaultSocialStateHandler(Set(csrfStateItemHandler), signer) - } - - /** - * Provides the password hasher registry. - * - * @return The password hasher registry. - */ - @Provides - def providePasswordHasherRegistry(): PasswordHasherRegistry = { - PasswordHasherRegistry(new BCryptSha256PasswordHasher(), Seq(new BCryptPasswordHasher())) - } - - /** - * Provides the credentials provider. - * - * @param authInfoRepository The auth info repository implementation. - * @param passwordHasherRegistry The password hasher registry. - * @return The credentials provider. - */ - @Provides - def provideCredentialsProvider( - authInfoRepository: AuthInfoRepository, - passwordHasherRegistry: PasswordHasherRegistry): CredentialsProvider = { - - new CredentialsProvider(authInfoRepository, passwordHasherRegistry) - } - - /** - * Provides the TOTP provider. - * - * @return The credentials provider. - */ - @Provides - def provideTotpProvider(passwordHasherRegistry: PasswordHasherRegistry): GoogleTotpProvider = { - new GoogleTotpProvider(passwordHasherRegistry) - } - - /** - * Provides the Facebook provider. - * - * @param httpLayer The HTTP layer implementation. - * @param socialStateHandler The social state handler implementation. - * @param configuration The Play configuration. - * @return The Facebook provider. - */ - @Provides - def provideFacebookProvider( - httpLayer: HTTPLayer, - socialStateHandler: SocialStateHandler, - configuration: Configuration): FacebookProvider = { - - new FacebookProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.facebook")) - } - - /** - * Provides the Google provider. - * - * @param httpLayer The HTTP layer implementation. - * @param socialStateHandler The social state handler implementation. - * @param configuration The Play configuration. - * @return The Google provider. - */ - @Provides - def provideGoogleProvider( - httpLayer: HTTPLayer, - socialStateHandler: SocialStateHandler, - configuration: Configuration): GoogleProvider = { - - new GoogleProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.google")) - } - - /** - * Provides the VK provider. - * - * @param httpLayer The HTTP layer implementation. - * @param socialStateHandler The social state handler implementation. - * @param configuration The Play configuration. - * @return The VK provider. - */ - @Provides - def provideVKProvider( - httpLayer: HTTPLayer, - socialStateHandler: SocialStateHandler, - configuration: Configuration): VKProvider = { - - new VKProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.vk")) - } - - /** - * Provides the Twitter provider. - * - * @param httpLayer The HTTP layer implementation. - * @param tokenSecretProvider The token secret provider implementation. - * @param configuration The Play configuration. - * @return The Twitter provider. - */ - @Provides - def provideTwitterProvider( - httpLayer: HTTPLayer, - tokenSecretProvider: OAuth1TokenSecretProvider, - configuration: Configuration): TwitterProvider = { - - val settings = configuration.underlying.as[OAuth1Settings]("silhouette.twitter") - new TwitterProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) - } - - /** - * Provides the Xing provider. - * - * @param httpLayer The HTTP layer implementation. - * @param tokenSecretProvider The token secret provider implementation. - * @param configuration The Play configuration. - * @return The Xing provider. - */ - @Provides - def provideXingProvider( - httpLayer: HTTPLayer, - tokenSecretProvider: OAuth1TokenSecretProvider, - configuration: Configuration): XingProvider = { - - val settings = configuration.underlying.as[OAuth1Settings]("silhouette.xing") - new XingProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) - } - - /** - * Provides the Yahoo provider. - * - * @param httpLayer The HTTP layer implementation. - * @param client The OpenID client implementation. - * @param configuration The Play configuration. - * @return The Yahoo provider. - */ - @Provides - def provideYahooProvider( - httpLayer: HTTPLayer, - client: OpenIdClient, - configuration: Configuration): YahooProvider = { - - val settings = configuration.underlying.as[OpenIDSettings]("silhouette.yahoo") - new YahooProvider(httpLayer, new PlayOpenIDService(client, settings), settings) - } -} diff --git a/app/utils/Filters.scala b/app/utils/Filters.scala deleted file mode 100644 index 93d384a..0000000 --- a/app/utils/Filters.scala +++ /dev/null @@ -1,15 +0,0 @@ -package utils - -import javax.inject.Inject - -import play.api.http.HttpFilters -import play.api.mvc.EssentialFilter -import play.filters.csrf.CSRFFilter -import play.filters.headers.SecurityHeadersFilter - -/** - * Provides filters. - */ -class Filters @Inject() (csrfFilter: CSRFFilter, securityHeadersFilter: SecurityHeadersFilter) extends HttpFilters { - override def filters: Seq[EssentialFilter] = Seq(csrfFilter, securityHeadersFilter) -} diff --git a/app/utils/Logger.scala b/app/utils/Logger.scala deleted file mode 100644 index 191c223..0000000 --- a/app/utils/Logger.scala +++ /dev/null @@ -1,12 +0,0 @@ -package utils - -/** - * Implement this to get a named logger in scope. - */ -trait Logger { - - /** - * A named logger instance. - */ - val logger = play.api.Logger(this.getClass) -} diff --git a/app/utils/auth/CustomSecuredErrorHandler.scala b/app/utils/auth/CustomSecuredErrorHandler.scala deleted file mode 100644 index 89ba1c3..0000000 --- a/app/utils/auth/CustomSecuredErrorHandler.scala +++ /dev/null @@ -1,42 +0,0 @@ -package utils.auth - -import javax.inject.Inject - -import com.mohiva.play.silhouette.api.actions.SecuredErrorHandler -import play.api.i18n.{ MessagesApi, I18nSupport, Messages } -import play.api.mvc.RequestHeader -import play.api.mvc.Results._ - -import scala.concurrent.Future - -/** - * Custom secured error handler. - * - * @param messagesApi The Play messages API. - */ -class CustomSecuredErrorHandler @Inject() (val messagesApi: MessagesApi) extends SecuredErrorHandler with I18nSupport { - - /** - * Called when a user is not authenticated. - * - * As defined by RFC 2616, the status code of the response should be 401 Unauthorized. - * - * @param request The request header. - * @return The result to send to the client. - */ - override def onNotAuthenticated(implicit request: RequestHeader) = { - Future.successful(Redirect(controllers.routes.SignInController.view())) - } - - /** - * Called when a user is authenticated but not authorized. - * - * As defined by RFC 2616, the status code of the response should be 403 Forbidden. - * - * @param request The request header. - * @return The result to send to the client. - */ - override def onNotAuthorized(implicit request: RequestHeader) = { - Future.successful(Redirect(controllers.routes.SignInController.view()).flashing("error" -> Messages("access.denied"))) - } -} diff --git a/app/utils/auth/CustomUnsecuredErrorHandler.scala b/app/utils/auth/CustomUnsecuredErrorHandler.scala deleted file mode 100644 index 0c27eb3..0000000 --- a/app/utils/auth/CustomUnsecuredErrorHandler.scala +++ /dev/null @@ -1,25 +0,0 @@ -package utils.auth - -import com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandler -import play.api.mvc.RequestHeader -import play.api.mvc.Results._ - -import scala.concurrent.Future - -/** - * Custom unsecured error handler. - */ -class CustomUnsecuredErrorHandler extends UnsecuredErrorHandler { - - /** - * Called when a user is authenticated but not authorized. - * - * As defined by RFC 2616, the status code of the response should be 403 Forbidden. - * - * @param request The request header. - * @return The result to send to the client. - */ - override def onNotAuthorized(implicit request: RequestHeader) = { - Future.successful(Redirect(controllers.routes.ApplicationController.index())) - } -} diff --git a/app/utils/auth/Env.scala b/app/utils/auth/Env.scala deleted file mode 100644 index a88c872..0000000 --- a/app/utils/auth/Env.scala +++ /dev/null @@ -1,13 +0,0 @@ -package utils.auth - -import com.mohiva.play.silhouette.api.Env -import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator -import models.User - -/** - * The default env. - */ -trait DefaultEnv extends Env { - type I = User - type A = CookieAuthenticator -} diff --git a/app/utils/auth/WithProvider.scala b/app/utils/auth/WithProvider.scala deleted file mode 100644 index 329626a..0000000 --- a/app/utils/auth/WithProvider.scala +++ /dev/null @@ -1,32 +0,0 @@ -package utils.auth - -import com.mohiva.play.silhouette.api.{ Authenticator, Authorization } -import models.User -import play.api.mvc.Request - -import scala.concurrent.Future - -/** - * Grants only access if a user has authenticated with the given provider. - * - * @param provider The provider ID the user must authenticated with. - * @tparam A The type of the authenticator. - */ -case class WithProvider[A <: Authenticator](provider: String) extends Authorization[User, A] { - - /** - * Indicates if a user is authorized to access an action. - * - * @param user The usr object. - * @param authenticator The authenticator instance. - * @param request The current request. - * @tparam B The type of the request body. - * @return True if the user is authorized, false otherwise. - */ - override def isAuthorized[B](user: User, authenticator: A)( - implicit - request: Request[B]): Future[Boolean] = { - - Future.successful(user.loginInfo.providerID == provider) - } -} diff --git a/app/utils/route/Binders.scala b/app/utils/route/Binders.scala deleted file mode 100644 index 740287a..0000000 --- a/app/utils/route/Binders.scala +++ /dev/null @@ -1,24 +0,0 @@ -package utils.route - -import java.util.UUID - -import play.api.mvc.PathBindable - -/** - * Some route binders. - */ -object Binders { - - /** - * A `java.util.UUID` bindable. - */ - implicit object UUIDPathBindable extends PathBindable[UUID] { - def bind(key: String, value: String) = try { - Right(UUID.fromString(value)) - } catch { - case _: Exception => Left("Cannot parse parameter '" + key + "' with value '" + value + "' as UUID") - } - - def unbind(key: String, value: UUID): String = value.toString - } -} diff --git a/app/views/activateAccount.scala.html b/app/views/activateAccount.scala.html deleted file mode 100644 index c9db833..0000000 --- a/app/views/activateAccount.scala.html +++ /dev/null @@ -1,19 +0,0 @@ -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import play.twirl.api.Html -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder - -@(email: String)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@main(messages("activate.account.title")) { -
- @messages("activate.account") -
-

@messages("activate.account.text1")

-

@email

-

@messages("activate.account.text2")

-

@Html(messages("activate.account.text3", controllers.routes.ActivateAccountController.send(helper.urlEncode(email))))

-
-
-} diff --git a/app/views/changePassword.scala.html b/app/views/changePassword.scala.html deleted file mode 100644 index 2552da9..0000000 --- a/app/views/changePassword.scala.html +++ /dev/null @@ -1,27 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import b3.inline.fieldConstructor - -@(changePasswordForm: Form[(String, String)], user: models.User)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -@main(messages("change.password.title"), Some(user)) { -
- @messages("change.password") - @helper.form(action = controllers.routes.ChangePasswordController.submit, 'autocomplete -> "off") { -

@messages("strong.password.info")

- @helper.CSRF.formField - @b3.password(changePasswordForm("current-password"), '_hiddenLabel -> messages("current.password"), 'placeholder -> messages("current.password"), 'class -> "form-control input-lg") - @passwordStrength(changePasswordForm("new-password"), '_hiddenLabel -> messages("new.password"), 'placeholder -> messages("new.password"), 'class -> "form-control input-lg") -
-
- -
-
- } -
-} diff --git a/app/views/emails/activateAccount.scala.html b/app/views/emails/activateAccount.scala.html deleted file mode 100644 index ce46247..0000000 --- a/app/views/emails/activateAccount.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@import play.api.i18n.Messages -@import play.twirl.api.Html - -@(user: models.User, url: String)(implicit messages: Messages) - - - -

@messages("email.activate.account.hello", user.name.getOrElse("user"))

-

@Html(messages("email.activate.account.html.text", url))

- - diff --git a/app/views/emails/activateAccount.scala.txt b/app/views/emails/activateAccount.scala.txt deleted file mode 100644 index 2cd4b2c..0000000 --- a/app/views/emails/activateAccount.scala.txt +++ /dev/null @@ -1,6 +0,0 @@ -@import play.api.i18n.Messages - -@(user: models.User, url: String)(implicit messages: Messages) -@messages("email.activate.account.hello", user.name.getOrElse("user")) - -@messages("email.activate.account.txt.text", url) diff --git a/app/views/emails/alreadySignedUp.scala.html b/app/views/emails/alreadySignedUp.scala.html deleted file mode 100644 index 66a3ed9..0000000 --- a/app/views/emails/alreadySignedUp.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@import play.api.i18n.Messages -@import play.twirl.api.Html - -@(user: models.User, url: String)(implicit messages: Messages) - - - -

@messages("email.already.signed.up.hello", user.name.getOrElse("user"))

-

@Html(messages("email.already.signed.up.html.text", url))

- - diff --git a/app/views/emails/alreadySignedUp.scala.txt b/app/views/emails/alreadySignedUp.scala.txt deleted file mode 100644 index bd40e9b..0000000 --- a/app/views/emails/alreadySignedUp.scala.txt +++ /dev/null @@ -1,6 +0,0 @@ -@import play.api.i18n.Messages - -@(user: models.User, url: String)(implicit messages: Messages) -@messages("email.already.signed.up.hello", user.name.getOrElse("user")) - -@messages("email.already.signed.up.txt.text", url) diff --git a/app/views/emails/resetPassword.scala.html b/app/views/emails/resetPassword.scala.html deleted file mode 100644 index 126ce68..0000000 --- a/app/views/emails/resetPassword.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@import play.api.i18n.Messages -@import play.twirl.api.Html - -@(user: models.User, url: String)(implicit messages: Messages) - - - -

@messages("email.reset.password.hello", user.name.getOrElse("user"))

-

@Html(messages("email.reset.password.html.text", url))

- - diff --git a/app/views/emails/resetPassword.scala.txt b/app/views/emails/resetPassword.scala.txt deleted file mode 100644 index fae4c96..0000000 --- a/app/views/emails/resetPassword.scala.txt +++ /dev/null @@ -1,6 +0,0 @@ -@import play.api.i18n.Messages - -@(user: models.User, url: String)(implicit messages: Messages) -@messages("email.reset.password.hello", user.name.getOrElse("user")) - -@messages("email.reset.password.txt.text", url) diff --git a/app/views/emails/signUp.scala.html b/app/views/emails/signUp.scala.html deleted file mode 100644 index 7663114..0000000 --- a/app/views/emails/signUp.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@import play.api.i18n.Messages -@import play.twirl.api.Html - -@(user: models.User, url: String)(implicit messages: Messages) - - - -

@messages("email.sign.up.hello", user.name.getOrElse("user"))

-

@Html(messages("email.sign.up.html.text", url))

- - diff --git a/app/views/emails/signUp.scala.txt b/app/views/emails/signUp.scala.txt deleted file mode 100644 index 37f119d..0000000 --- a/app/views/emails/signUp.scala.txt +++ /dev/null @@ -1,6 +0,0 @@ -@import play.api.i18n.Messages - -@(user: models.User, url: String)(implicit messages: Messages) -@messages("email.sign.up.hello", user.name.getOrElse("user")) - -@messages("email.sign.up.txt.text", url) diff --git a/app/views/forgotPassword.scala.html b/app/views/forgotPassword.scala.html deleted file mode 100644 index c2f9359..0000000 --- a/app/views/forgotPassword.scala.html +++ /dev/null @@ -1,25 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder - -@(forgotPasswordForm: Form[String])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -@main(messages("forgot.password.title")) { -
- @messages("forgot.password") - @helper.form(action = controllers.routes.ForgotPasswordController.submit(), 'autocomplete -> "off") { -

@messages("forgot.password.info")

- @helper.CSRF.formField - @b3.text(forgotPasswordForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") -
-
- -
-
- } -
-} diff --git a/app/views/home.scala.html b/app/views/home.scala.html deleted file mode 100644 index 0954580..0000000 --- a/app/views/home.scala.html +++ /dev/null @@ -1,93 +0,0 @@ -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import play.api.data._ -@import forms.TotpSetupForm.Data -@import com.mohiva.play.silhouette.impl.providers.GoogleTotpCredentials -@import com.mohiva.play.silhouette.impl.providers.GoogleTotpInfo - -@(user: models.User, totpInfoOpt: Option[GoogleTotpInfo], totpDataOpt: Option[(Form[Data], GoogleTotpCredentials)] = None)(implicit request: RequestHeader, - messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ - b3.vertical.fieldConstructor() -} - -@main(messages("home.title"), Some(user)) { -
-
-
-

@messages("welcome.signed.in")

-
- -
-
-
-
-
-
-

@messages("first.name") - :

@user.firstName.getOrElse("None")

-
-
-

@messages("last.name") - :

@user.lastName.getOrElse("None")

-
-
-

@messages("full.name") - :

@user.fullName.getOrElse("None")

-
-
-

@messages("email") - :

@user.email.getOrElse("None")

-
-
-
-
- @if(totpInfoOpt.nonEmpty) { -

@messages("totp.enabled.title")

- - - - } else { - @totpDataOpt match { - case Some((totpForm, credentials)) => { -

@messages("totp.enabling.title")

-

@messages("totp.shared.key.title")

- -

@messages("totp.recovery.tokens.title")

-
    - @for(scratchCodePlain <- credentials.scratchCodesPlain) { -
  • @{ - scratchCodePlain - }
  • - } -
- @helper.form(action = controllers.routes.TotpController.enableTotpSubmit()) { - @helper.CSRF.formField - @b3.text(totpForm("verificationCode"), '_hiddenLabel -> messages("totp.verification.code "), 'placeholder -> messages("totp.verification.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") - @b3.hidden(totpForm("sharedKey")) - @helper.repeat(totpForm("scratchCodes"), min = 1) { scratchCodeField => - @b3.hidden(scratchCodeField("hasher")) - @b3.hidden(scratchCodeField("password")) - @b3.hidden(scratchCodeField("salt")) - } -
-
- -
-
- } - } - case None => { -

@messages("totp.disabled.title")

- - - - } - } - } -
-
-} diff --git a/app/views/main.scala.html b/app/views/main.scala.html deleted file mode 100644 index c601ae2..0000000 --- a/app/views/main.scala.html +++ /dev/null @@ -1,88 +0,0 @@ -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import play.twirl.api.Html -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder - -@(title: String, user: Option[models.User] = None)(content: Html)(implicit request: RequestHeader, messages: Messages, assets: AssetsFinder, webJarsUtil: WebJarsUtil) - - - - - - - - - @title - - - @webJarsUtil.locate("bootstrap.min.css").css() - @webJarsUtil.locate("bootstrap-theme.min.css").css() - - - - - - -
-
- @request.flash.get("error").map { msg => -
- × - @messages("error") @msg -
- } - @request.flash.get("info").map { msg => -
- × - @messages("info") @msg -
- } - @request.flash.get("success").map { msg => -
- × - @messages("success") @msg -
- } - @content -
-
- @webJarsUtil.locate("jquery.min.js").script() - @webJarsUtil.locate("bootstrap.min.js").script() - - - - diff --git a/app/views/passwordStrength.scala.html b/app/views/passwordStrength.scala.html deleted file mode 100644 index 7748402..0000000 --- a/app/views/passwordStrength.scala.html +++ /dev/null @@ -1,13 +0,0 @@ -@import play.api.data.Field -@import play.api.i18n.MessagesProvider - -@(field: Field, options: (Symbol, Any)*)(implicit messagesProvider: MessagesProvider) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -
- @b3.password(field, (Symbol("data-pwd"), "true") +: options:_*) - - -

-
diff --git a/app/views/resetPassword.scala.html b/app/views/resetPassword.scala.html deleted file mode 100644 index 855670c..0000000 --- a/app/views/resetPassword.scala.html +++ /dev/null @@ -1,24 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import java.util.UUID - -@(form: Form[String], token: UUID)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@main(messages("reset.password.title")) { -
- @messages("reset.password") - @helper.form(action = controllers.routes.ResetPasswordController.submit(token), 'autocomplete -> "off") { -

@messages("strong.password.info")

- @helper.CSRF.formField - @passwordStrength(form("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") -
-
- -
-
- } -
-} diff --git a/app/views/signIn.scala.html b/app/views/signIn.scala.html deleted file mode 100644 index 14be42a..0000000 --- a/app/views/signIn.scala.html +++ /dev/null @@ -1,44 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import com.mohiva.play.silhouette.impl.providers.SocialProviderRegistry -@import forms.SignInForm.Data - -@(signInForm: Form[Data], socialProviders: SocialProviderRegistry)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -@main(messages("sign.in.title")) { -
- @messages("sign.in.credentials") - @helper.form(action = controllers.routes.SignInController.submit()) { - @helper.CSRF.formField - @b3.email(signInForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") - @b3.password(signInForm("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") - @b3.checkbox(signInForm("rememberMe"), '_text -> messages("remember.me"), 'checked -> true) -
-
- -
-
- } - - - - @if(socialProviders.providers.nonEmpty) { - - } - -
-} diff --git a/app/views/signUp.scala.html b/app/views/signUp.scala.html deleted file mode 100644 index c4b6d5f..0000000 --- a/app/views/signUp.scala.html +++ /dev/null @@ -1,31 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import forms.SignUpForm.Data - -@(signUpForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -@main(messages("sign.up.title")) { -
- @messages("sign.up.account") - @helper.form(action = controllers.routes.SignUpController.submit()) { - @helper.CSRF.formField - @b3.text(signUpForm("firstName"), '_hiddenLabel -> messages("first.name"), 'placeholder -> messages("first.name"), 'class -> "form-control input-lg") - @b3.text(signUpForm("lastName"), '_hiddenLabel -> messages("last.name"), 'placeholder -> messages("last.name"), 'class -> "form-control input-lg") - @b3.text(signUpForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") - @passwordStrength(signUpForm("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") -
-
- -
-
- - } -
-} diff --git a/app/views/totp.scala.html b/app/views/totp.scala.html deleted file mode 100644 index b391b76..0000000 --- a/app/views/totp.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import forms.TotpForm.Data -@import forms.TotpRecoveryForm -@import java.util.UUID - -@(totpForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - -@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } - -@main(messages("sign.in.title")) { -
- @messages("sign.in.totp") - @helper.form(action = controllers.routes.TotpController.submit()) { - @helper.CSRF.formField - @b3.text(totpForm("verificationCode"), '_hiddenLabel -> messages("totp.verification.code"), 'placeholder -> messages("totp.verification.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") - @b3.hidden(totpForm("userID")) - @b3.hidden(totpForm("sharedKey")) - @b3.hidden(totpForm("rememberMe")) -
-
- -
-
- } - - @messages("totp.open.the.app.for.2fa") -
-

@messages("totp.dont.have.your.phone") @messages("totp.use.recovery.code")

-
-
-} diff --git a/app/views/totpRecovery.scala.html b/app/views/totpRecovery.scala.html deleted file mode 100644 index b898edc..0000000 --- a/app/views/totpRecovery.scala.html +++ /dev/null @@ -1,34 +0,0 @@ -@import play.api.data.Form -@import play.api.i18n.Messages -@import play.api.mvc.RequestHeader -@import org.webjars.play.WebJarsUtil -@import controllers.AssetsFinder -@import forms.TotpRecoveryForm.Data - -@(totpRecoveryForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) - - @implicitFieldConstructor = @{ - b3.vertical.fieldConstructor() - } - - @main(messages("sign.in.title")) { -
- @messages("sign.in.totp.recovery") - @helper.form(action = controllers.routes.TotpRecoveryController.submit()) { - @helper.CSRF.formField - @b3.text(totpRecoveryForm("recoveryCode"), '_hiddenLabel -> messages("totp.recovery.code"), 'placeholder -> messages("totp.recovery.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") - @b3.hidden(totpRecoveryForm("userID")) - @b3.hidden(totpRecoveryForm("sharedKey")) - @b3.hidden(totpRecoveryForm("rememberMe")) -
-
- -
-
- } - -
-

@messages("totp.lost.your.recovery.codes") @messages("totp.contact.support")

-
-
- } diff --git a/build.sbt b/build.sbt index 4ea9582..0edb3e6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,70 +1,13 @@ -import com.typesafe.sbt.SbtScalariform._ - -import scalariform.formatter.preferences._ - -name := "play-silhouette-seed" - -version := "6.0.0" - -scalaVersion := "2.12.8" - -resolvers += Resolver.jcenterRepo - -resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" - -libraryDependencies ++= Seq( - "com.mohiva" %% "play-silhouette" % "6.1.0", - "com.mohiva" %% "play-silhouette-password-bcrypt" % "6.1.0", - "com.mohiva" %% "play-silhouette-persistence" % "6.1.0", - "com.mohiva" %% "play-silhouette-crypto-jca" % "6.1.0", - "com.mohiva" %% "play-silhouette-totp" % "6.1.0", - "org.webjars" %% "webjars-play" % "2.7.0", - "org.webjars" % "bootstrap" % "3.3.7-1" exclude("org.webjars", "jquery"), - "org.webjars" % "jquery" % "3.2.1", - "net.codingwell" %% "scala-guice" % "4.1.0", - "com.iheart" %% "ficus" % "1.4.3", - "com.typesafe.play" %% "play-mailer" % "7.0.0", - "com.typesafe.play" %% "play-mailer-guice" % "7.0.0", - "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.1-akka-2.5.x", - "com.adrianhurt" %% "play-bootstrap" % "1.5-P27-B3-SNAPSHOT", - "com.mohiva" %% "play-silhouette-testkit" % "6.1.0" % "test", - specs2 % Test, - ehcache, - guice, - filters -) - -lazy val root = (project in file(".")).enablePlugins(PlayScala) - -routesImport += "utils.route.Binders._" - -// https://github.com/playframework/twirl/issues/105 -TwirlKeys.templateImports := Seq() - -scalacOptions ++= Seq( - "-deprecation", // Emit warning and location for usages of deprecated APIs. - "-feature", // Emit warning and location for usages of features that should be imported explicitly. - "-unchecked", // Enable additional warnings where generated code depends on assumptions. - "-Xfatal-warnings", // Fail the compilation if there are any warnings. - //"-Xlint", // Enable recommended additional warnings. - "-Ywarn-adapted-args", // Warn if an argument list is modified to match the receiver. - "-Ywarn-dead-code", // Warn when dead code is identified. - "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. - "-Ywarn-nullary-override", // Warn when non-nullary overrides nullary, e.g. def foo() over def foo. - "-Ywarn-numeric-widen", // Warn when numerics are widened. - // Play has a lot of issues with unused imports and unsued params - // https://github.com/playframework/playframework/issues/6690 - // https://github.com/playframework/twirl/issues/105 - "-Xlint:-unused,_" -) - -//******************************************************** -// Scalariform settings -//******************************************************** - -scalariformAutoformat := true - -ScalariformKeys.preferences := ScalariformKeys.preferences.value - .setPreference(FormatXml, false) - .setPreference(DoubleIndentConstructorArguments, false) - .setPreference(DanglingCloseParenthesis, Preserve) +// This build is for this Giter8 template. +// To test the template run `g8` or `g8Test` from the sbt session. +// See http://www.foundweekends.org/giter8/testing.html#Using+the+Giter8Plugin for more details. +lazy val root = (project in file(".")) + .enablePlugins(ScriptedPlugin) + .settings( + name := "play-silhouette-seed", + test in Test := { + val _ = (g8Test in Test).toTask("").value + }, + scriptedLaunchOpts ++= List("-Xms1024m", "-Xmx1024m", "-XX:ReservedCodeCacheSize=128m", "-Xss2m", "-Dfile.encoding=UTF-8"), + resolvers += Resolver.url("typesafe", url("http://repo.typesafe.com/typesafe/ivy-releases/"))(Resolver.ivyStylePatterns) + ) diff --git a/conf/application.conf b/conf/application.conf deleted file mode 100644 index 7fb9279..0000000 --- a/conf/application.conf +++ /dev/null @@ -1,69 +0,0 @@ -# This is the main configuration file for the application. -# ~~~~~ - -# Secret key -# ~~~~~ -# The secret key is used to secure cryptographics functions. -# If you deploy your application to several instances be sure to use the same key! -play.http.secret.key="changeme" - -# The application languages -# ~~~~~ -play.i18n.langs=["en"] - -# Registers the request handler -# ~~~~~ -play.http.requestHandler = "play.api.http.DefaultHttpRequestHandler" - -# Registers the filters -# ~~~~~ -play.http.filters = "utils.Filters" - -# The application DI modules -# ~~~~~ -play.modules.enabled += "modules.BaseModule" -play.modules.enabled += "modules.JobModule" -play.modules.enabled += "modules.SilhouetteModule" -play.modules.enabled += "play.api.libs.mailer.MailerModule" - -play.modules.disabled += "com.mohiva.play.silhouette.api.actions.SecuredErrorHandlerModule" -play.modules.disabled += "com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandlerModule" - -# The asset configuration -# ~~~~~ -play.assets { - path = "/public" - urlPrefix = "/assets" -} - -# Akka config -akka { - loglevel = "INFO" - jvm-exit-on-fatal-error=off - - # Auth token cleaner - quartz.schedules.AuthTokenCleaner { - expression = "0 0 */1 * * ?" - timezone = "UTC" - description = "cleanup the auth tokens on every hour" - } -} - -# Play mailer -play.mailer { - host = "localhost" - port = 25 - mock = true -} - -# Security Filter Configuration - Content Security Policy -play.filters.csp { - CSPFilter = "default-src 'self';" - CSPFilter = ${play.filters.headers.contentSecurityPolicy}" img-src 'self' *.fbcdn.net *.twimg.com *.googleusercontent.com *.xingassets.com vk.com *.yimg.com secure.gravatar.com chart.googleapis.com;" - CSPFilter = ${play.filters.headers.contentSecurityPolicy}" style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com maxcdn.bootstrapcdn.com cdn.jsdelivr.net fonts.googleapis.com;" - CSPFilter = ${play.filters.headers.contentSecurityPolicy}" font-src 'self' fonts.gstatic.com fonts.googleapis.com cdnjs.cloudflare.com;" - CSPFilter = ${play.filters.headers.contentSecurityPolicy}" script-src 'self' cdnjs.cloudflare.com;" - CSPFilter = ${play.filters.headers.contentSecurityPolicy}" connect-src 'self' twitter.com *.xing.com;" -} - -include "silhouette.conf" diff --git a/conf/application.prod.conf b/conf/application.prod.conf deleted file mode 100644 index dbe8ebb..0000000 --- a/conf/application.prod.conf +++ /dev/null @@ -1,53 +0,0 @@ -include "application.conf" - -play.crypto.secret=${?PLAY_APP_SECRET} - -# Allow all proxies for Heroku so that X-Forwarded headers can be read by Play -# ~~~~~ -play.http.forwarded.trustedProxies=["0.0.0.0/0", "::/0"] - -# Play mailer -play.mailer { - host = "smtp.sendgrid.net" - port = 587 - tls = true - mock = false - user = "" - user = ${?SENDGRID_USERNAME} - password = "" - password = ${?SENDGRID_PASSWORD} -} - -silhouette { - - # Authenticator settings - authenticator.cookieDomain="play-silhouette-seed.herokuapp.com" - authenticator.secureCookie=true - - # OAuth1 token secret provider settings - oauth1TokenSecretProvider.cookieDomain="play-silhouette-seed.herokuapp.com" - oauth1TokenSecretProvider.secureCookie=true - - # OAuth2 state provider settings - oauth2StateProvider.cookieDomain="play-silhouette-seed.herokuapp.com" - oauth2StateProvider.secureCookie=true - - # Facebook provider - facebook.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/facebook" - - # Google provider - google.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/google" - - # VK provider - vk.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/vk" - - # Twitter provider - twitter.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/twitter" - - # Xing provider - xing.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/xing" - - # Yahoo provider - yahoo.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/yahoo" - yahoo.realm="https://play-silhouette-seed.herokuapp.com" -} diff --git a/conf/messages b/conf/messages deleted file mode 100644 index acf08bf..0000000 --- a/conf/messages +++ /dev/null @@ -1,127 +0,0 @@ -error.email = Valid email required -error.required = This field is required -invalid.credentials = Invalid credentials! -invalid.unexpected.totp = Unexpected TOTP exception! -invalid.verification.code = Invalid verification code! -invalid.recovery.code = Invalid recovery code! -access.denied = Access denied! -could.not.authenticate = Could not authenticate with social provider! Please try again! - -home.title = Silhouette - Home -sign.up.title = Silhouette - Sign Up -sign.in.title = Silhouette - Sign In -totp.title = Silhouette - TOTP -forgot.password.title = Silhouette - Forgot Password -reset.password.title = Silhouette - Reset Password -change.password.title = Silhouette - Change Password -activate.account.title = Silhouette - Activate Account - -toggle.navigation = Toggle navigation -welcome.signed.in = Welcome, you are now signed in! - -totp.enable = Enable two-factor authentication -totp.disable = Disable two-factor authentication -totp.enabling.title = 2 factor auth enabling -totp.disabled.title = 2 factor auth is not enabled -totp.enabled.title = 2 factor auth is enabled -totp.shared.key.title = Shared key: -totp.recovery.tokens.title = Recovery tokens: -totp.enabling.info = 2 factor auth enabled successfully! -totp.disabling.info = 2 factor auth disabled successfully! -totp.recovery.code = Recovery code -totp.verification.code = Verification Code -totp.verify = Verify -totp.open.the.app.for.2fa = Open the two-factor authentication app on your device to view your authentication code and verify your identity. -totp.dont.have.your.phone = Don't have your phone? -totp.use.recovery.code = Enter a two-factor recovery code -totp.lost.your.recovery.codes = Lost your recovery codes? -totp.contact.support = Contact support - -sign.up.account = Sign up for a new account -sign.in.credentials = Sign in with your credentials -sign.in.totp = Two-factor authentication -sign.in.totp.recovery = Two-factor authentication using recovery code - -error = Error! -info = Info! -success = Success! -home = Home -first.name = First name -last.name = Last name -full.name = Full name -email = Email -password = Password -send = Send -change = Change -reset = Reset -sign.up = Sign up -sign.in = Sign in -sign.out = Sign out -sign.in.now = Sign in now -sign.up.now = Sign up now -already.a.member = Already a member? -not.a.member = Not a member? -forgot.your.password = Forgot your password? -forgot.password = Forgot password -reset.password = Reset password -change.password = Change password -activate.account = Activate Account -current.password = Current password -new.password = New password - -remember.me = Remember my login on this computer -or.use.social = Or use your existing account on one of the following services to sign in: -forgot.password.info = Please enter your email address and we will send you an email with further instructions to reset your password. -strong.password.info = Strong passwords include numbers, letters and punctuation marks. -current.password.invalid = The entered password is invalid. Please enter the correct password! -activate.account.text1 = You can''t log in yet. We previously sent an activation email to you at: -activate.account.text2 = Please follow the instructions in that email to activate your account. -activate.account.text3 = Click here to send the activation email again. - -sign.up.email.sent = You''re almost done! We sent an activation mail to {0}. Please follow the instructions in the email to activate your account. If it doesn''t arrive, check your spam folder, or try to log in again to send another activation mail. -activation.email.sent = We sent another activation email to you at {0}. It might take a few minutes for it to arrive; be sure to check your spam folder. -reset.email.sent = We have sent you an email with further instructions to reset your password, on condition that the address was found in our system. If you do not receive an email within the next 5 minutes, then please recheck your entered email address and try it again. - -invalid.activation.link = The link isn't valid anymore! Please sign in to send the activation email again. -invalid.reset.link = The link isn't valid anymore! Please request a new link to reset your password. - -password.reset = We have reset your password. You can now sign in with your credentials. -account.activated = Your account is now activated! Please sign in to use your new account. -password.changed = Your password has been changed. - -google = Google -facebook = Facebook -twitter = Twitter -vk = VK -xing = Xing -yahoo = Yahoo - -########## -# Emails -########## - -email.from = Silhouette - -# Sign Up -email.sign.up.subject = Welcome -email.sign.up.hello = Hello {0}, -email.sign.up.html.text = Please follow this link to confirm and activate your new account. -email.sign.up.txt.text = Please follow the link to confirm and activate your new account: {0} - -# Already Signed Up -email.already.signed.up.subject = Welcome -email.already.signed.up.hello = Hello {0}, -email.already.signed.up.html.text = You already have an account registered. Please follow this link to sign in into your account. -email.already.signed.up.txt.text = You already have an account registered. Please follow the link to sign in into your account: {0} - -# Reset Password -email.reset.password.subject = Reset password -email.reset.password.hello = Hello {0}, -email.reset.password.html.text = Please follow this link to reset your password. -email.reset.password.txt.text = Please follow the link to reset your password: {0} - -# Activate Account -email.activate.account.subject = Activate account -email.activate.account.hello = Hello {0}, -email.activate.account.html.text = Please follow this link to confirm and activate your new account. -email.activate.account.txt.text = Please follow the link to confirm and activate your new account: {0} diff --git a/conf/routes b/conf/routes deleted file mode 100644 index 87db0e3..0000000 --- a/conf/routes +++ /dev/null @@ -1,37 +0,0 @@ -# Routes -# This file defines all application routes (Higher priority routes first) -# ~~~~ - -# Home page -GET / controllers.ApplicationController.index -GET /signOut controllers.ApplicationController.signOut -GET /authenticate/:provider controllers.SocialAuthController.authenticate(provider) - -GET /signUp controllers.SignUpController.view -POST /signUp controllers.SignUpController.submit - -GET /signIn controllers.SignInController.view -POST /signIn controllers.SignInController.submit - -GET /totp controllers.TotpController.view(userId: java.util.UUID, sharedKey: String, rememberMe: Boolean) -GET /enableTotp controllers.TotpController.enableTotp -GET /disableTotp controllers.TotpController.disableTotp -POST /totpSubmit controllers.TotpController.submit -POST /enableTotpSubmit controllers.TotpController.enableTotpSubmit - -GET /totpRecovery controllers.TotpRecoveryController.view(userID: java.util.UUID, sharedKey: String, rememberMe: Boolean) -POST /totpRecoverySubmit controllers.TotpRecoveryController.submit - -GET /password/forgot controllers.ForgotPasswordController.view -POST /password/forgot controllers.ForgotPasswordController.submit -GET /password/reset/:token controllers.ResetPasswordController.view(token: java.util.UUID) -POST /password/reset/:token controllers.ResetPasswordController.submit(token: java.util.UUID) -GET /password/change controllers.ChangePasswordController.view -POST /password/change controllers.ChangePasswordController.submit - -GET /account/email/:email controllers.ActivateAccountController.send(email: String) -GET /account/activate/:token controllers.ActivateAccountController.activate(token: java.util.UUID) - -# Map static resources from the /public folder to the /assets URL path -GET /assets/*file controllers.Assets.versioned(file) --> /webjars webjars.Routes diff --git a/conf/silhouette.conf b/conf/silhouette.conf deleted file mode 100644 index 9492d97..0000000 --- a/conf/silhouette.conf +++ /dev/null @@ -1,103 +0,0 @@ -silhouette { - - # Authenticator settings - authenticator.cookieName="authenticator" - authenticator.cookiePath="/" - authenticator.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set - authenticator.httpOnlyCookie=true - authenticator.sameSite="Lax" - authenticator.useFingerprinting=true - authenticator.authenticatorIdleTimeout=30 minutes - authenticator.authenticatorExpiry=12 hours - - authenticator.rememberMe.cookieMaxAge=30 days - authenticator.rememberMe.authenticatorIdleTimeout=5 days - authenticator.rememberMe.authenticatorExpiry=30 days - - authenticator.signer.key = "[changeme]" // A unique encryption key - authenticator.crypter.key = "[changeme]" // A unique encryption key - - # OAuth1 token secret provider settings - oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret" - oauth1TokenSecretProvider.cookiePath="/" - oauth1TokenSecretProvider.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set - oauth1TokenSecretProvider.httpOnlyCookie=true - oauth1TokenSecretProvider.sameSite="Lax" - oauth1TokenSecretProvider.expirationTime=5 minutes - - oauth1TokenSecretProvider.signer.key = "[changeme]" // A unique encryption key - oauth1TokenSecretProvider.crypter.key = "[changeme]" // A unique encryption key - - # Social state handler - socialStateHandler.signer.key = "[changeme]" // A unique encryption key - - # CSRF state item handler settings - csrfStateItemHandler.cookieName="OAuth2State" - csrfStateItemHandler.cookiePath="/" - csrfStateItemHandler.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set - csrfStateItemHandler.httpOnlyCookie=true - csrfStateItemHandler.sameSite="Lax" - csrfStateItemHandler.expirationTime=5 minutes - - csrfStateItemHandler.signer.key = "[changeme]" // A unique encryption key - - # Facebook provider - facebook.authorizationURL="https://graph.facebook.com/v2.3/oauth/authorize" - facebook.accessTokenURL="https://graph.facebook.com/v2.3/oauth/access_token" - facebook.redirectURL="http://localhost:9000/authenticate/facebook" - facebook.clientID="" - facebook.clientID=${?FACEBOOK_CLIENT_ID} - facebook.clientSecret="" - facebook.clientSecret=${?FACEBOOK_CLIENT_SECRET} - facebook.scope="email" - - # Google provider - google.authorizationURL="https://accounts.google.com/o/oauth2/auth" - google.accessTokenURL="https://accounts.google.com/o/oauth2/token" - google.redirectURL="http://localhost:9000/authenticate/google" - google.clientID="" - google.clientID=${?GOOGLE_CLIENT_ID} - google.clientSecret="" - google.clientSecret=${?GOOGLE_CLIENT_SECRET} - google.scope="profile email" - - # VK provider - vk.authorizationURL="http://oauth.vk.com/authorize" - vk.accessTokenURL="https://oauth.vk.com/access_token" - vk.redirectURL="http://localhost:9000/authenticate/vk" - vk.clientID="" - vk.clientID=${?VK_CLIENT_ID} - vk.clientSecret="" - vk.clientSecret=${?VK_CLIENT_SECRET} - vk.scope="email" - - # Twitter provider - twitter.requestTokenURL="https://twitter.com/oauth/request_token" - twitter.accessTokenURL="https://twitter.com/oauth/access_token" - twitter.authorizationURL="https://twitter.com/oauth/authenticate" - twitter.callbackURL="http://localhost:9000/authenticate/twitter" - twitter.consumerKey="" - twitter.consumerKey=${?TWITTER_CONSUMER_KEY} - twitter.consumerSecret="" - twitter.consumerSecret=${?TWITTER_CONSUMER_SECRET} - - # Xing provider - xing.requestTokenURL="https://api.xing.com/v1/request_token" - xing.accessTokenURL="https://api.xing.com/v1/access_token" - xing.authorizationURL="https://api.xing.com/v1/authorize" - xing.callbackURL="http://localhost:9000/authenticate/xing" - xing.consumerKey="" - xing.consumerKey=${?XING_CONSUMER_KEY} - xing.consumerSecret="" - xing.consumerSecret=${?XING_CONSUMER_SECRET} - - # Yahoo provider - yahoo.providerURL="https://me.yahoo.com/" - yahoo.callbackURL="http://localhost:9000/authenticate/yahoo" - yahoo.axRequired={ - "fullname": "http://axschema.org/namePerson", - "email": "http://axschema.org/contact/email", - "image": "http://axschema.org/media/image/default" - } - yahoo.realm="http://localhost:9000" -} diff --git a/default.properties b/default.properties new file mode 100644 index 0000000..f8194fb --- /dev/null +++ b/default.properties @@ -0,0 +1,2 @@ +name=My Something Project +description=Say something about this template. diff --git a/project/build.properties b/project/build.properties index 8522443..5a9ed92 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.2 +sbt.version=1.3.4 diff --git a/project/plugins.sbt b/project/plugins.sbt index b4fad31..69ca7f5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,2 @@ -// Comment to get more information during initialization -logLevel := Level.Warn - -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.2") - -addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2") +addSbtPlugin("org.foundweekends.giter8" %% "sbt-giter8" % "0.12.0") +libraryDependencies += { "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value } diff --git a/public/images/favicon.png b/public/images/favicon.png deleted file mode 100644 index c7d92d2ae47434d9a61c90bc205e099b673b9dd5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 687 zcmV;g0#N;lP)ezT{T_ZJ?}AL z5NC{NW(ESID=>(O3&Eg8 zmA9J&6c`h4_f6L;=bU>_H8aNG`kfvCj9zomNt)?O;rzWqZs0LEt%1WB218%1fo9uB zsW^yhBR7C(mqN%GEK9&msg0~ zWY?#bf4q8G-~2KttQZ($odJvy&_-~f?9*ThK@fwR$U^1)p*8=_+^3BXx0$i1BC8XC zr21u6D5nVK&^!dOAw&|1E;qC3uFNj3*Jj#&%Oje@0D-nhfmM*o%^5f}-pxQ07(95H z3|LoV>V19w#rLgmRmtVy9!T3M3FUE3><0T8&b3yEsWcLW`0(=1+qsqc(k(ymBLK0h zK!6(6$7MX~M`-QA2$wk7n(7hhkJ}4Rwi-Vd(_ZFX1Yk7TXuB0IJYpo@kLb2G8m)E{ z`9v=!hi}fOytKckfN^C@6+Z*+MVI9-W_p@_3yyR#UYc0FTpD}i#k>c!wYCS)4v@E$ zchZCo=zV@)`v^$;V18ixdjFMY#q^2$wEX%{f(XD8POnsn$bpbClpC@hPxjzyO>pY|*pF3UU2tYcCN?rUk{Sskej70Mmu9vPwMYhO1m{AxAt(zqDT|0jP7FaX=6 V`?~}E4H^Id002ovPDHLkV1hC)G==~G diff --git a/public/images/providers/facebook.png b/public/images/providers/facebook.png deleted file mode 100644 index 06dbaa0d71c6f29021d21814015f31ff894bf2a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 449 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVC?X8aSW-r_4d|YFJ?!PwucTL zJhwciMMOq$OmpK_bX7>mT)ejF0Y96IzJu>`(JBpn4&u#N0hQCFfejBFfa);Ft8{zM7oB!HY$e(7Dh1mmb5xu zDPAHFyLeh|gV8SMb^GKlboe$f*jn>^Xj02z{vylP5zxypk8#i9>rZ~IWPQN&b`h&d ztJ>t(JhR+??_+YwFgSgQ(K(0B`(-Mv?MtSmH)J+f z1|6?tTanICuuAqpknsVHs!M;Q1s3%0xzF0iHt9Xn{rk6{S~jRVLVN`E+yi?CrE~&QInIC5lt7M z3(y7V0(1eo09}ACKo_73&;{rMbOCd00lW?@MPiU^#<;bB^{9j&0ra5JcZ0=2XitYQ#HkyR9v^=H|O2AxqcP#^Ho3UO| zeuz&HB{xFpXoE}}jv(3Q-I0ykVtlFUTVXFs&fIAz<<6307v?rR@5WXTmP5ZTQeS0nQI#5BPv3puXt=qTTj z@>zTX+vOQY)tJTacg6@E4_4VYNwOtJETD6K#7U8DRXcuE=_uYmvQd)pibBTX2D2sU zV!uDdycYa)R=Z?@_~j0TgvAZtuDt)*wb^prX7 z>u%NYge5S}+|*DPk{-ESJZ_T1@+)=;JU2IW{I=-qvR`wNSHDtur(l+g-ps=+>mJHP z2^a?)U^_WCfnVh_lVc}SRH2-80kfyalg=x;7v^nquWxhpNQqe0FxSJ$Dt1zR)8h$# zlT#Sm6=wQtOts_R!LyQQXOY4s_RcN4N|Z_j_zl>lJCoOHv%Wbl>GDy)TcbotZ!YJx zLuqLOMQ;PP_4nJ zu)P6cM!8Z8Q=p(^iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$QVa~t7M?DS zAr*7p&OGlO>L_yDKFfY$U4WpNZp(^)4NK%(c4W;AoVFw5a=)wIAr%ddLmE*cNgWrI zUOr!>uqY~`TO*3)(V_*ul9Anu?kTN!;W%rSZ}(3BGw-UupV?D>pK}gB&-cpbbBv$Y z-Jff2e4Hzp$9SgKTb3=HOd0A6d<7h=8FQN$ZgFM^vt00Vn61!I#&nB?af|Q;HI4;y z8_XOTz)GiN&iQ&k_5u3`<7KIh;ttU>Kh0cnXrtN;rP?y(sgS?VYTqQ zx0}5^$o8XR>>xdA9{LwKS#|Jso!guVwq-(9?nzT(UNodi}_JK<;q)5$!8WkzjRIV=#BK8#-puz*JCcP;!JCFdoa8G{q7f4k~f(D z6n)6;pEbjzdy(@2u?M}2~9WCyQx z{j9rgfl$Pp<^PShv0c>nU2%u$PkBpyZzl1{5twq%_J}{=zrXyo(ao)EK-rYR)78&q Iol`;+0Qg0gLjV8( diff --git a/public/images/providers/xing.png b/public/images/providers/xing.png deleted file mode 100644 index 29131e58f5f78e4947e600fb825109e36c94f11c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 684 zcmV;d0#p5oP)#E`yOiN{QMpGZ0Gm5-_PUT-?`_WymUH!pfLd^zyz28 z6JP>NfC(@GCcte9K$$>Nvw$(M6vHnts8v8akj5G~rcpoM{fIzw%JHf_&U0oCA-5aM-)+DWGE`LG{whX`_MTg^qX?N#|8pb~5d zVP56X_HO&KBQ!{axe%G($+kTo1e^j}LYU`~`Q>1VZF{Z@$OltGkY!LAsku*tp8|mo z0Ai}g{+4g>eDjx(igH#qv;1aU! zb@}jNBn0n-Ad8?PQgf0Bp91wh2)IFnxxT}j<0w%#=B@yK-TxLG+M%`vtPv8YufkN`z76bkhv;2U!bxhP;i7f%b|uvP&fcuzy% zDdnA025#WOn?b60Vco%m;e)yGyec$;Qeam Sw|->+0000aSW-r_4cN{$CX5x;~&pi zn|Pk}nf%YkS5TzTn2Bg<{P~Sx7@c7E6V4c_sEwe|Kf9}28(%zx5&(LdRO17q5Wuqc7Aj92gYT#+&jNB zZ90{G=I|EQGfLf(OJb_|ELu`$lr24E9A(H}tID(BTs!+hm1_cbtR^Xbmpd;0;`E~x z`Tk9N4RrG>1-uq=)bb0M$5abF-qFAMg{!%cA4|ZygIhEYZ?%nJO|I$np0@eC@{T#d zj8igalsWHSCO`Yucd?6tMei(Gz8^Vi-!^Nab3y&Z*2EXp{f9;8REr&qEzxV3bi}IZ zH*;;KNmxai`Ip=W9g`WSB;J^I?^$$=_?NT4KU6xfEz^~nFY-3%!uJd94A&EGxwLFP z=Mdk|&&ZZ|E2U+#)z;tFFT8VZsJmT!&}{CtclFOtr#ICaymg(rz9BH4iRZu;pO($H z3dQ>iPfcfVHpneguhF^JQ8?opC+~{zvc+Pi(>~`Z_8QJ9Fg&1~V$j^Lad4aHln=tL ze-2D|T4S5GJeyG};DDOhhOT&x1kTDQCY3cuH+|y0UwHGAao+0cX$1$iG8ArED5`s7 z_1+`d6CG-}IS>5PUT|e0r{T}*GjPW+P)778`>gANEeGO7rvOtSgQu&X%Q~loCIF}f BEkytT diff --git a/public/images/silhouette.png b/public/images/silhouette.png deleted file mode 100644 index bc2e3656d1238eff6f2e5f461fbf97228ee64e2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10664 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 diff --git a/public/javascripts/zxcvbnShim.js b/public/javascripts/zxcvbnShim.js deleted file mode 100644 index 3335e90..0000000 --- a/public/javascripts/zxcvbnShim.js +++ /dev/null @@ -1,31 +0,0 @@ -$(function() { - var strength = { - 0: "Worst", - 1: "Bad", - 2: "Weak", - 3: "Good", - 4: "Strong" - }; - - var password = $('[data-pwd="true"]'); - var meter = $('#password-strength-meter'); - var msg = $('#password-strength-text'); - - function showFeedback() { - var val = this.value; - var result = zxcvbn(val); - - // Update the password strength meter - meter.val(result.score); - - // Update the text indicator - if (val !== "") { - msg.text("Strength: " + strength[result.score]); - } else { - msg.text(""); - } - } - - password.change(showFeedback); - password.keyup(showFeedback); -}); diff --git a/public/styles/main.css b/public/styles/main.css deleted file mode 100644 index 683225e..0000000 --- a/public/styles/main.css +++ /dev/null @@ -1,135 +0,0 @@ -body { - padding-top: 50px; - font-size: 16px; - background: #f5f7f9; -} - -h1 { - text-align: center; - font-size: 30px; -} -.starter-template { - padding: 40px 15px; -} - -input, button { - margin: 5px 0; -} - -input:hover, input:focus { - outline: 0; - transition: all .5s linear; - box-shadow: inset 0 0 10px #ccc; -} - -fieldset { - margin-top: 100px; -} -legend { - font-family: 'Montserrat', sans-serif; - text-align: center; - font-size: 20px; - padding: 15px; -} -a { - cursor: pointer; -} - -.provider { - display: inline-block; - width: 64px; - height: 64px; - border-radius: 4px; - outline: none; -} -.facebook { background: #3B5998; } -.google { background: #D14836; } -.twitter { background: #00ACED; } -.yahoo { background: #731A8B; } -.xing { background: #006567; } -.vk { background: #567ca4; } - -.social-providers, -.sign-in-now, -.already-member, -.not-a-member { - text-align: center; - margin-top: 20px; -} - -.user { - margin-top: 50px; -} -.user .data { - margin-top: 10px; -} - -.form-control { - border-radius: 0; -} - -[class^='ion-'] { - font-size: 1.2em; -} - -.has-feedback .form-control-feedback { - top: 0; - left: 0; - width: 46px; - height: 46px; - line-height: 46px; - color: #555; -} - -.has-feedback .form-control { - padding-left: 42.5px; -} - -.btn { - font-weight: bold; - border-radius: 2px; - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26); -} - -.btn-lg { - font-size: 18px; -} - -meter { - /* Reset the default appearance */ - -moz-appearance: none; - appearance: none; - - margin: 0 auto 1em; - width: 100%; - height: .5em; - - /* Applicable only to Firefox */ - background: none; - background-color: rgba(0,0,0,0.1); -} - -meter::-webkit-meter-bar { - background: none; - background-color: rgba(0,0,0,0.1); -} - -meter[value="0"]::-webkit-meter-optimum-value, -meter[value="1"]::-webkit-meter-optimum-value { background: red; } -meter[value="2"]::-webkit-meter-optimum-value { background: orange; } -meter[value="3"]::-webkit-meter-optimum-value { background: yellow; } -meter[value="4"]::-webkit-meter-optimum-value { background: green; } - -meter::-webkit-meter-even-less-good-value { background: red; } -meter::-webkit-meter-suboptimum-value { background: orange; } -meter::-webkit-meter-optimum-value { background: green; } - -meter[value="1"]::-moz-meter-bar, -meter[value="1"]::-moz-meter-bar { background: red; } -meter[value="2"]::-moz-meter-bar { background: orange; } -meter[value="3"]::-moz-meter-bar { background: yellow; } -meter[value="4"]::-moz-meter-bar { background: green; } - -meter::-webkit-meter-optimum-value { - transition: width .4s ease-out; -} diff --git a/scripts/reformat b/scripts/reformat deleted file mode 100755 index 87e60b6..0000000 --- a/scripts/reformat +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# -# Reformats source code. -# -# Copyright 2015 Mohiva Organisation (license at mohiva dot com) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -set -o nounset -o errexit - -scripts/sbt scalariformFormat test:scalariformFormat diff --git a/scripts/sbt b/scripts/sbt deleted file mode 100755 index fc7e522..0000000 --- a/scripts/sbt +++ /dev/null @@ -1,621 +0,0 @@ -#!/usr/bin/env bash -# -# A more capable sbt runner, coincidentally also called sbt. -# Author: Paul Phillips -# https://github.com/paulp/sbt-extras - -set -o pipefail - -declare -r sbt_release_version="1.3.2" -declare -r sbt_unreleased_version="1.3.2" - -declare -r latest_213="2.13.1" -declare -r latest_212="2.12.10" -declare -r latest_211="2.11.12" -declare -r latest_210="2.10.7" -declare -r latest_29="2.9.3" -declare -r latest_28="2.8.2" - -declare -r buildProps="project/build.properties" - -declare -r sbt_launch_ivy_release_repo="https://repo.typesafe.com/typesafe/ivy-releases" -declare -r sbt_launch_ivy_snapshot_repo="https://repo.scala-sbt.org/scalasbt/ivy-snapshots" -declare -r sbt_launch_mvn_release_repo="https://repo.scala-sbt.org/scalasbt/maven-releases" -declare -r sbt_launch_mvn_snapshot_repo="https://repo.scala-sbt.org/scalasbt/maven-snapshots" - -declare -r default_jvm_opts_common="-Xms512m -Xss2m" -declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" - -declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new -declare sbt_explicit_version -declare verbose noshare batch trace_level -declare debugUs - -declare java_cmd="java" -declare sbt_launch_dir="$HOME/.sbt/launchers" -declare sbt_launch_repo - -# pull -J and -D options to give to java. -declare -a java_args scalac_args sbt_commands residual_args - -# args to jvm/sbt via files or environment variables -declare -a extra_jvm_opts extra_sbt_opts - -echoerr () { echo >&2 "$@"; } -vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } -die () { echo "Aborting: $*" ; exit 1; } - -setTrapExit () { - # save stty and trap exit, to ensure echo is re-enabled if we are interrupted. - SBT_STTY="$(stty -g 2>/dev/null)" - export SBT_STTY - - # restore stty settings (echo in particular) - onSbtRunnerExit() { - [ -t 0 ] || return - vlog "" - vlog "restoring stty: $SBT_STTY" - stty "$SBT_STTY" - } - - vlog "saving stty: $SBT_STTY" - trap onSbtRunnerExit EXIT -} - -# this seems to cover the bases on OSX, and someone will -# have to tell me about the others. -get_script_path () { - local path="$1" - [[ -L "$path" ]] || { echo "$path" ; return; } - - local -r target="$(readlink "$path")" - if [[ "${target:0:1}" == "/" ]]; then - echo "$target" - else - echo "${path%/*}/$target" - fi -} - -script_path="$(get_script_path "${BASH_SOURCE[0]}")" -declare -r script_path -script_name="${script_path##*/}" -declare -r script_name - -init_default_option_file () { - local overriding_var="${!1}" - local default_file="$2" - if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then - local envvar_file="${BASH_REMATCH[1]}" - if [[ -r "$envvar_file" ]]; then - default_file="$envvar_file" - fi - fi - echo "$default_file" -} - -sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" -jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" - -build_props_sbt () { - [[ -r "$buildProps" ]] && \ - grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' -} - -set_sbt_version () { - sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" - [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version - export sbt_version -} - -url_base () { - local version="$1" - - case "$version" in - 0.7.*) echo "https://simple-build-tool.googlecode.com" ;; - 0.10.* ) echo "$sbt_launch_ivy_release_repo" ;; - 0.11.[12]) echo "$sbt_launch_ivy_release_repo" ;; - 0.*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" - echo "$sbt_launch_ivy_snapshot_repo" ;; - 0.*) echo "$sbt_launch_ivy_release_repo" ;; - *-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" - echo "$sbt_launch_mvn_snapshot_repo" ;; - *) echo "$sbt_launch_mvn_release_repo" ;; - esac -} - -make_url () { - local version="$1" - - local base="${sbt_launch_repo:-$(url_base "$version")}" - - case "$version" in - 0.7.*) echo "$base/files/sbt-launch-0.7.7.jar" ;; - 0.10.* ) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; - 0.11.[12]) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; - 0.*) echo "$base/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; - *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch-${version}.jar" ;; - esac -} - -addJava () { vlog "[addJava] arg = '$1'" ; java_args+=("$1"); } -addSbt () { vlog "[addSbt] arg = '$1'" ; sbt_commands+=("$1"); } -addScalac () { vlog "[addScalac] arg = '$1'" ; scalac_args+=("$1"); } -addResidual () { vlog "[residual] arg = '$1'" ; residual_args+=("$1"); } - -addResolver () { addSbt "set resolvers += $1"; } -addDebugger () { addJava "-Xdebug" ; addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"; } -setThisBuild () { - vlog "[addBuild] args = '$*'" - local key="$1" && shift - addSbt "set $key in ThisBuild := $*" -} -setScalaVersion () { - [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' - addSbt "++ $1" -} -setJavaHome () { - java_cmd="$1/bin/java" - setThisBuild javaHome "_root_.scala.Some(file(\"$1\"))" - export JAVA_HOME="$1" - export JDK_HOME="$1" - export PATH="$JAVA_HOME/bin:$PATH" -} - -getJavaVersion() { - local -r str=$("$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d '"') - - # java -version on java8 says 1.8.x - # but on 9 and 10 it's 9.x.y and 10.x.y. - if [[ "$str" =~ ^1\.([0-9]+)\..*$ ]]; then - echo "${BASH_REMATCH[1]}" - elif [[ "$str" =~ ^([0-9]+)\..*$ ]]; then - echo "${BASH_REMATCH[1]}" - elif [[ -n "$str" ]]; then - echoerr "Can't parse java version from: $str" - fi -} - -checkJava() { - # Warn if there is a Java version mismatch between PATH and JAVA_HOME/JDK_HOME - - [[ -n "$JAVA_HOME" && -e "$JAVA_HOME/bin/java" ]] && java="$JAVA_HOME/bin/java" - [[ -n "$JDK_HOME" && -e "$JDK_HOME/lib/tools.jar" ]] && java="$JDK_HOME/bin/java" - - if [[ -n "$java" ]]; then - pathJavaVersion=$(getJavaVersion java) - homeJavaVersion=$(getJavaVersion "$java") - if [[ "$pathJavaVersion" != "$homeJavaVersion" ]]; then - echoerr "Warning: Java version mismatch between PATH and JAVA_HOME/JDK_HOME, sbt will use the one in PATH" - echoerr " Either: fix your PATH, remove JAVA_HOME/JDK_HOME or use -java-home" - echoerr " java version from PATH: $pathJavaVersion" - echoerr " java version from JAVA_HOME/JDK_HOME: $homeJavaVersion" - fi - fi -} - -java_version () { - local -r version=$(getJavaVersion "$java_cmd") - vlog "Detected Java version: $version" - echo "$version" -} - -# MaxPermSize critical on pre-8 JVMs but incurs noisy warning on 8+ -default_jvm_opts () { - local -r v="$(java_version)" - if [[ $v -ge 8 ]]; then - echo "$default_jvm_opts_common" - else - echo "-XX:MaxPermSize=384m $default_jvm_opts_common" - fi -} - -build_props_scala () { - if [[ -r "$buildProps" ]]; then - versionLine="$(grep '^build.scala.versions' "$buildProps")" - versionString="${versionLine##build.scala.versions=}" - echo "${versionString%% .*}" - fi -} - -execRunner () { - # print the arguments one to a line, quoting any containing spaces - vlog "# Executing command line:" && { - for arg; do - if [[ -n "$arg" ]]; then - if printf "%s\n" "$arg" | grep -q ' '; then - printf >&2 "\"%s\"\n" "$arg" - else - printf >&2 "%s\n" "$arg" - fi - fi - done - vlog "" - } - - setTrapExit - - if [[ -n "$batch" ]]; then - "$@" < /dev/null - else - "$@" - fi -} - -jar_url () { make_url "$1"; } - -is_cygwin () { [[ "$(uname -a)" == "CYGWIN"* ]]; } - -jar_file () { - is_cygwin \ - && cygpath -w "$sbt_launch_dir/$1/sbt-launch.jar" \ - || echo "$sbt_launch_dir/$1/sbt-launch.jar" -} - -download_url () { - local url="$1" - local jar="$2" - - mkdir -p "${jar%/*}" && { - if command -v curl > /dev/null 2>&1; then - curl --fail --silent --location "$url" --output "$jar" - elif command -v wget > /dev/null 2>&1; then - wget -q -O "$jar" "$url" - fi - } && [[ -r "$jar" ]] -} - -acquire_sbt_jar () { - { - sbt_jar="$(jar_file "$sbt_version")" - [[ -r "$sbt_jar" ]] - } || { - sbt_jar="$HOME/.ivy2/local/org.scala-sbt/sbt-launch/$sbt_version/jars/sbt-launch.jar" - [[ -r "$sbt_jar" ]] - } || { - sbt_jar="$(jar_file "$sbt_version")" - jar_url="$(make_url "$sbt_version")" - - echoerr "Downloading sbt launcher for ${sbt_version}:" - echoerr " From ${jar_url}" - echoerr " To ${sbt_jar}" - - download_url "${jar_url}" "${sbt_jar}" - - case "${sbt_version}" in - 0.*) vlog "SBT versions < 1.0 do not have published MD5 checksums, skipping check"; echo "" ;; - *) verify_sbt_jar "${sbt_jar}" ;; - esac - } -} - -verify_sbt_jar() { - local jar="${1}" - local md5="${jar}.md5" - - download_url "$(make_url "${sbt_version}").md5" "${md5}" > /dev/null 2>&1 - - if command -v md5sum > /dev/null 2>&1; then - if echo "$(cat "${md5}") ${jar}" | md5sum -c -; then - rm -rf "${md5}" - return 0 - else - echoerr "Checksum does not match" - return 1 - fi - elif command -v md5 > /dev/null 2>&1; then - if [ "$(md5 -q "${jar}")" == "$(cat "${md5}")" ]; then - rm -rf "${md5}" - return 0 - else - echoerr "Checksum does not match" - return 1 - fi - elif command -v openssl > /dev/null 2>&1; then - if [ "$(openssl md5 -r "${jar}" | awk '{print $1}')" == "$(cat "${md5}")" ]; then - rm -rf "${md5}" - return 0 - else - echoerr "Checksum does not match" - return 1 - fi - else - echoerr "Could not find an MD5 command" - return 1 - fi -} - -usage () { - set_sbt_version - cat < display stack traces with a max of frames (default: -1, traces suppressed) - -debug-inc enable debugging log for the incremental compiler - -no-colors disable ANSI color codes - -sbt-create start sbt even if current directory contains no sbt project - -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) - -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) - -ivy path to local Ivy repository (default: ~/.ivy2) - -no-share use all local caches; no sharing - -offline put sbt in offline mode - -jvm-debug Turn on JVM debugging, open at the given port. - -batch Disable interactive mode - -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted - -script Run the specified file as a scala script - - # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) - -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version - -sbt-version use the specified version of sbt (default: $sbt_release_version) - -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version - -sbt-jar use the specified jar as the sbt launcher - -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) - -sbt-launch-repo repo url for downloading sbt launcher jar (default: $(url_base "$sbt_version")) - - # scala version (default: as chosen by sbt) - -28 use $latest_28 - -29 use $latest_29 - -210 use $latest_210 - -211 use $latest_211 - -212 use $latest_212 - -213 use $latest_213 - -scala-home use the scala build at the specified directory - -scala-version use the specified version of scala - -binary-version use the specified scala version when searching for dependencies - - # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) - -java-home alternate JAVA_HOME - - # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution - # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found - $(default_jvm_opts) - JVM_OPTS environment variable holding either the jvm args directly, or - the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') - Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. - -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) - -Dkey=val pass -Dkey=val directly to the jvm - -J-X pass option -X directly to the jvm (-J is stripped) - - # passing options to sbt, OR to this runner - SBT_OPTS environment variable holding either the sbt args directly, or - the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') - Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. - -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) - -S-X add -X to sbt's scalacOptions (-S is stripped) -EOM -} - -process_args () { - require_arg () { - local type="$1" - local opt="$2" - local arg="$3" - - if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then - die "$opt requires <$type> argument" - fi - } - while [[ $# -gt 0 ]]; do - case "$1" in - -h|-help) usage; exit 0 ;; - -v) verbose=true && shift ;; - -d) addSbt "--debug" && shift ;; - -w) addSbt "--warn" && shift ;; - -q) addSbt "--error" && shift ;; - -x) debugUs=true && shift ;; - -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; - -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; - -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; - -no-share) noshare=true && shift ;; - -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; - -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; - -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; - -offline) addSbt "set offline in Global := true" && shift ;; - -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; - -batch) batch=true && shift ;; - -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; - -script) require_arg file "$1" "$2" && sbt_script="$2" && addJava "-Dsbt.main.class=sbt.ScriptMain" && shift 2 ;; - - -sbt-create) sbt_create=true && shift ;; - -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; - -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; - -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; - -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; - -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; - -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; - -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; - -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; - -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "_root_.scala.Some(file(\"$2\"))" && shift 2 ;; - -java-home) require_arg path "$1" "$2" && setJavaHome "$2" && shift 2 ;; - -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; - -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; - - -D*) addJava "$1" && shift ;; - -J*) addJava "${1:2}" && shift ;; - -S*) addScalac "${1:2}" && shift ;; - -28) setScalaVersion "$latest_28" && shift ;; - -29) setScalaVersion "$latest_29" && shift ;; - -210) setScalaVersion "$latest_210" && shift ;; - -211) setScalaVersion "$latest_211" && shift ;; - -212) setScalaVersion "$latest_212" && shift ;; - -213) setScalaVersion "$latest_213" && shift ;; - new) sbt_new=true && : ${sbt_explicit_version:=$sbt_release_version} && addResidual "$1" && shift ;; - *) addResidual "$1" && shift ;; - esac - done -} - -# process the direct command line arguments -process_args "$@" - -# skip #-styled comments and blank lines -readConfigFile() { - local end=false - until $end; do - read -r || end=true - [[ $REPLY =~ ^# ]] || [[ -z $REPLY ]] || echo "$REPLY" - done < "$1" -} - -# if there are file/environment sbt_opts, process again so we -# can supply args to this runner -if [[ -r "$sbt_opts_file" ]]; then - vlog "Using sbt options defined in file $sbt_opts_file" - while read -r opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") -elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then - vlog "Using sbt options defined in variable \$SBT_OPTS" - IFS=" " read -r -a extra_sbt_opts <<< "$SBT_OPTS" -else - vlog "No extra sbt options have been defined" -fi - -[[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" - -# reset "$@" to the residual args -set -- "${residual_args[@]}" -argumentCount=$# - -# set sbt version -set_sbt_version - -checkJava - -# only exists in 0.12+ -setTraceLevel() { - case "$sbt_version" in - "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; - *) setThisBuild traceLevel "$trace_level" ;; - esac -} - -# set scalacOptions if we were given any -S opts -[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[*]}\"" - -[[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && addJava "-Dsbt.version=$sbt_explicit_version" -vlog "Detected sbt version $sbt_version" - -if [[ -n "$sbt_script" ]]; then - residual_args=( "$sbt_script" "${residual_args[@]}" ) -else - # no args - alert them there's stuff in here - (( argumentCount > 0 )) || { - vlog "Starting $script_name: invoke with -help for other options" - residual_args=( shell ) - } -fi - -# verify this is an sbt dir, -create was given or user attempts to run a scala script -[[ -r ./build.sbt || -d ./project || -n "$sbt_create" || -n "$sbt_script" || -n "$sbt_new" ]] || { - cat < identity)) - - /** - * The application. - */ - lazy val application = new GuiceApplicationBuilder() - .overrides(new FakeModule) - .build() - } -} diff --git a/tutorial/index.html b/tutorial/index.html deleted file mode 100644 index e7500e1..0000000 --- a/tutorial/index.html +++ /dev/null @@ -1,137 +0,0 @@ - -
-

Introduction

-

What is the purpose of this tutorial ?

-

- This tutorial is not really meant as a manual to use this seed, but more as a first step to begin to understand how Silhouette works. You should refer to the documentation for detailed explanations and you can ask questions on the Silhouette Forum. -

- -

Overview

- -
-
-

Run Your Application

-

- You can already run your app through the activator ui or with the activator run command and visit http://localhost:9000. But you will not be able to sign up because the application tries to send an email through SendGrid, which has to be configured. And the same goes for the other authentication providers (Google, Facebook, ...) : you have to register your application and set the provider key and secret in silhouette.conf for each of them to work. -
- Therefore, if you just want to experiment with this project, you can make two small changes allowing you to signup. First you have to set the Play mailer into mock mode by adding the following line to your application.conf file : -

play.mailer.mock = true
- This tells the Play mailer to log the email instead of trying to send it. But now you will not be able to activate your account after signing up, so you have to initialize new accounts as already activated by slightly modifiying the SignUpController : -
val user = User(
-  userID = UUID.randomUUID(),
-  loginInfo = loginInfo,
-  firstName = Some(data.firstName),
-  lastName = Some(data.lastName),
-  fullName = Some(data.firstName + " " + data.lastName),
-  email = Some(data.email),
-  avatarURL = None,
-  //activated = false
-  activated = true // TODO delete to avoid activating all users by default
-)
- You should now be able to sign up and then sign in. -

-
-
-

Endpoints

-

- As explained in the documentation, the endpoints are the Actions and the WebSockets that are managed by Silhouette. This means, for example, that to make sure that only registered users can access to an endpoint of your application, you simply have to use a silhouette.SecuredAction instead of a standard Action, like for index in the ApplicationController. -

-

- These endpoints are provided by the silhouette: Silhouette[DefaultEnv] object which is injected in each controller using dependency injection. To see where it comes from you have to examine the modules (next section). -

-
-
-

Modules

-

- Silhouette uses dependency injection to separate the API definition from its implementation. The modules to customize the bindings from the traits to their implementation can be found in the modules package. We are in particular interested by the SilhouetteModule. -
- This file contains all the bindings related to Silhouette and is therefore rather long. We are going to look at the part concerning the Silhouette[DefaultEnv] object, which provides the endpoints. After that, the structure of the file should be clear enough for you to find what you need. -

-

- We see in the first line of the configure method that the Silhouette[DefaultEnv] we came across in the controller is bound to the class SilhouetteProvider[DefaultEnv]. To discover from where this one comes from you have to look into the Silhouette library itself. At the bottom of the Silhouette.scala file, you can observe that SilhouetteProvider[E <: Env] depends on Environment[E], SecuredAction, UnsecuredAction and UserAwareAction. The three Actions already have a default implementation bound (in SecuredAction for example), so the only one left to be bound is the Environment. We can now go back to the SilhouetteModule to find the provideEnvironment method. -
- The @Provides annotation means that every time an intance of Environment[DefaultEnv] has to be injected, this method will provide it. The dependencies of provideEnvironment are themselves bound in the configure method. The Environment provided here has the type parameter DefaultEnv, an arbitrary name chosen for the environment type of this application, defined in Env.scala. This trait defines which Identity (i.e. structure of a user, doc) and which Authenticator (i.e. mean of authentication, doc) are used in this project. See the documentation about the environment for more information. -

-

- In summary, a module has a configure method where all the bindings are declared and other methods annotated with @Provides to provide instances with specific arguments. -

-
- -
-

Handle errors in endpoints

-

- If a user tries to access a secured enpoint without being authenticated, the default behaviour of Silhouette is to send a simple "not authenticated" message. The same goes if an authenticated user requests a page he is not authorized to access. To customize this behaviour, this seed defines two custom error handlers, CustomSecuredErrorHandler and CustomUnsecuredErrorHandler. -
- To enable them, the defauld handlers have first to be disabled. This is done in the application.conf file with the following lines : -

play.modules.disabled += "com.mohiva.play.silhouette.api.actions.SecuredErrorHandlerModule"
- play.modules.disabled += "com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandlerModule"
- The new handlers can then be bound to the error handler traits in the SilhouetteModule : -
bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler]
- bind[SecuredErrorHandler].to[CustomSecuredErrorHandler]
-

-
- -
-

Identity/IdentityService

-

- Identity is a trait in Silhouette representing a user (documentation). Its implementation in this seed is the User class. This class has to be specified in the environment type, here DefaultEnv (see the documentation about the environment). -

-

- Silhouette also needs an IdentityService, extended here by UserService and then implemented by UserServiceImpl. You can find the binding concerning these in the SilhouetteModule. The IdentityService is needed in the authentication process to get a user given his identity provider and his identifier, bundled in a LoginInfo object. Here a UserService also allows to save a user, making it an additional layer of abstraction above the user data access object (UserDAO) to simplify the saving process. -

-

- The identity of a user is accessible in every secured or user aware endpoint via its Request object. In the case of a SecuredAction you directly get the Identity from request.identity, and for an UserAwareAction it is an Option[Identity] since this type of endpoint also accepts unauthentified users. -

-
- -
-

DAOs

-

- As you can see in the daos package, this seed contains data access objects (DAOs) for Users and for AuthTokens (tokens to activate a user via email or change the password). They are implemented to store the data in an in-memory HashMap for the example. To keep the data over a restart of your application you have to give these DAOs an other implementation storing the values in a database. You can then replace the implementation bound in the SilhouetteModule (or BaseModule for the AuthToken). -

-

- There are other DAOs in this application, but their implementation comes from Silhouette. You can find the bindings in the SilhouetteModule : -

// Replace this with the bindings to your concrete DAOs
-bind[DelegableAuthInfoDAO[PasswordInfo]].toInstance(new InMemoryAuthInfoDAO[PasswordInfo])
-bind[DelegableAuthInfoDAO[OAuth1Info]].toInstance(new InMemoryAuthInfoDAO[OAuth1Info])
-bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info])
-bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo])
-

- These are all AuthInfoDAOs which store the information concerning authentication, like the password for users signing in with credentials. You may wonder why not directly store the password in the User object, but remember that we want users to be able to sign in via other authentication providers like Google or Facebook. Our application will not have to store a password for these users, but other authentication information depending on the protocol (OAuth1, OAuth2 or OpenID). Therefore it makes more sense to split the auth info from the rest of the user and store it in a different DAO. -
- Like for the UserDAO, you will have to provide other implementations to persist the data. -

-
- -
-

Authorization

-

- To specify which endpoints a user can access in your application, you can implement an Authorization (documentation). WithProvider is an example allowing only users which are authenticated via a specified provider. You can implement your own logic and combine authorizations with logical operators like described in the documentation. -

-
- -
-

Conclusion

-

- If you are planning to use Silhouette in your project, you may think that this seed contains quite a lot of features and maybe you want to take only what is necessary. So here is a list of the classes and files you absolutely need to make Silhouette work : -

-
    -
  • A module binding the necessary implementations, or providing the instances when you need to specify the constructor arguments
  • -
  • An implementation of Identity
  • -
  • An implementation of IdentityService
  • -
  • A trait extending Env, like DefaultEnv
  • -
-

- But if you are planning to use your application in production, this project contains a few things that come in handy (AuthTokenCleaner, security headers, ...) and it may therefore be easier to use it as seed from the beginning. -

-
- From 62d9c0a44a0c424a8c9c9ec5123c582014ae63e0 Mon Sep 17 00:00:00 2001 From: Alexander Myltsev Date: Fri, 27 Dec 2019 23:37:13 +0300 Subject: [PATCH 2/3] Add rest of source files --- src/main/g8/Procfile | 1 + .../controllers/AbstractAuthController.scala | 64 ++ .../ActivateAccountController.scala | 85 +++ .../controllers/ApplicationController.scala | 55 ++ .../ChangePasswordController.scala | 78 +++ .../ForgotPasswordController.scala | 84 +++ .../controllers/ResetPasswordController.scala | 82 +++ .../g8/app/controllers/SignInController.scala | 87 +++ .../g8/app/controllers/SignUpController.scala | 119 ++++ .../controllers/SocialAuthController.scala | 67 ++ .../g8/app/controllers/TotpController.scala | 126 ++++ .../controllers/TotpRecoveryController.scala | 91 +++ .../g8/app/forms/ChangePasswordForm.scala | 18 + .../g8/app/forms/ForgotPasswordForm.scala | 17 + src/main/g8/app/forms/ResetPasswordForm.scala | 17 + src/main/g8/app/forms/SignInForm.scala | 33 + src/main/g8/app/forms/SignUpForm.scala | 36 + src/main/g8/app/forms/TotpForm.scala | 36 + src/main/g8/app/forms/TotpRecoveryForm.scala | 36 + src/main/g8/app/forms/TotpSetupForm.scala | 40 ++ src/main/g8/app/jobs/AuthTokenCleaner.scala | 55 ++ src/main/g8/app/jobs/Scheduler.scala | 18 + src/main/g8/app/models/AuthToken.scala | 17 + src/main/g8/app/models/User.scala | 42 ++ .../g8/app/models/daos/AuthTokenDAO.scala | 45 ++ .../g8/app/models/daos/AuthTokenDAOImpl.scala | 69 ++ src/main/g8/app/models/daos/UserDAO.scala | 38 ++ src/main/g8/app/models/daos/UserDAOImpl.scala | 56 ++ .../models/services/AuthTokenService.scala | 39 ++ .../services/AuthTokenServiceImpl.scala | 60 ++ .../g8/app/models/services/UserService.scala | 41 ++ .../app/models/services/UserServiceImpl.scala | 76 +++ src/main/g8/app/modules/BaseModule.scala | 20 + src/main/g8/app/modules/JobModule.scala | 19 + .../g8/app/modules/SilhouetteModule.scala | 475 ++++++++++++++ src/main/g8/app/utils/Filters.scala | 15 + src/main/g8/app/utils/Logger.scala | 12 + .../auth/CustomSecuredErrorHandler.scala | 42 ++ .../auth/CustomUnsecuredErrorHandler.scala | 25 + src/main/g8/app/utils/auth/Env.scala | 13 + src/main/g8/app/utils/auth/WithProvider.scala | 32 + src/main/g8/app/utils/route/Binders.scala | 24 + .../g8/app/views/activateAccount.scala.html | 19 + .../g8/app/views/changePassword.scala.html | 27 + .../views/emails/activateAccount.scala.html | 11 + .../views/emails/activateAccount.scala.txt | 6 + .../views/emails/alreadySignedUp.scala.html | 11 + .../views/emails/alreadySignedUp.scala.txt | 6 + .../app/views/emails/resetPassword.scala.html | 11 + .../app/views/emails/resetPassword.scala.txt | 6 + .../g8/app/views/emails/signUp.scala.html | 11 + src/main/g8/app/views/emails/signUp.scala.txt | 6 + .../g8/app/views/forgotPassword.scala.html | 25 + src/main/g8/app/views/home.scala.html | 93 +++ src/main/g8/app/views/main.scala.html | 88 +++ .../g8/app/views/passwordStrength.scala.html | 13 + .../g8/app/views/resetPassword.scala.html | 24 + src/main/g8/app/views/signIn.scala.html | 44 ++ src/main/g8/app/views/signUp.scala.html | 31 + src/main/g8/app/views/totp.scala.html | 35 + src/main/g8/app/views/totpRecovery.scala.html | 34 + src/main/g8/conf/application.conf | 69 ++ src/main/g8/conf/application.prod.conf | 53 ++ src/main/g8/conf/messages | 127 ++++ src/main/g8/conf/routes | 37 ++ src/main/g8/conf/silhouette.conf | 103 +++ src/main/g8/public/images/favicon.png | Bin 0 -> 687 bytes .../g8/public/images/providers/facebook.png | Bin 0 -> 449 bytes .../g8/public/images/providers/google.png | Bin 0 -> 843 bytes .../g8/public/images/providers/twitter.png | Bin 0 -> 675 bytes src/main/g8/public/images/providers/vk.png | Bin 0 -> 955 bytes src/main/g8/public/images/providers/xing.png | Bin 0 -> 684 bytes src/main/g8/public/images/providers/yahoo.png | Bin 0 -> 684 bytes src/main/g8/public/images/silhouette.png | Bin 0 -> 10664 bytes src/main/g8/public/javascripts/zxcvbnShim.js | 31 + src/main/g8/public/styles/main.css | 135 ++++ src/main/g8/scripts/reformat | 21 + src/main/g8/scripts/sbt | 621 ++++++++++++++++++ .../ApplicationControllerSpec.scala | 96 +++ src/main/g8/tutorial/index.html | 137 ++++ 80 files changed, 4336 insertions(+) create mode 100644 src/main/g8/Procfile create mode 100644 src/main/g8/app/controllers/AbstractAuthController.scala create mode 100644 src/main/g8/app/controllers/ActivateAccountController.scala create mode 100644 src/main/g8/app/controllers/ApplicationController.scala create mode 100644 src/main/g8/app/controllers/ChangePasswordController.scala create mode 100644 src/main/g8/app/controllers/ForgotPasswordController.scala create mode 100644 src/main/g8/app/controllers/ResetPasswordController.scala create mode 100644 src/main/g8/app/controllers/SignInController.scala create mode 100644 src/main/g8/app/controllers/SignUpController.scala create mode 100644 src/main/g8/app/controllers/SocialAuthController.scala create mode 100644 src/main/g8/app/controllers/TotpController.scala create mode 100644 src/main/g8/app/controllers/TotpRecoveryController.scala create mode 100644 src/main/g8/app/forms/ChangePasswordForm.scala create mode 100644 src/main/g8/app/forms/ForgotPasswordForm.scala create mode 100644 src/main/g8/app/forms/ResetPasswordForm.scala create mode 100644 src/main/g8/app/forms/SignInForm.scala create mode 100644 src/main/g8/app/forms/SignUpForm.scala create mode 100644 src/main/g8/app/forms/TotpForm.scala create mode 100644 src/main/g8/app/forms/TotpRecoveryForm.scala create mode 100644 src/main/g8/app/forms/TotpSetupForm.scala create mode 100644 src/main/g8/app/jobs/AuthTokenCleaner.scala create mode 100644 src/main/g8/app/jobs/Scheduler.scala create mode 100644 src/main/g8/app/models/AuthToken.scala create mode 100644 src/main/g8/app/models/User.scala create mode 100644 src/main/g8/app/models/daos/AuthTokenDAO.scala create mode 100644 src/main/g8/app/models/daos/AuthTokenDAOImpl.scala create mode 100644 src/main/g8/app/models/daos/UserDAO.scala create mode 100644 src/main/g8/app/models/daos/UserDAOImpl.scala create mode 100644 src/main/g8/app/models/services/AuthTokenService.scala create mode 100644 src/main/g8/app/models/services/AuthTokenServiceImpl.scala create mode 100644 src/main/g8/app/models/services/UserService.scala create mode 100644 src/main/g8/app/models/services/UserServiceImpl.scala create mode 100644 src/main/g8/app/modules/BaseModule.scala create mode 100644 src/main/g8/app/modules/JobModule.scala create mode 100644 src/main/g8/app/modules/SilhouetteModule.scala create mode 100644 src/main/g8/app/utils/Filters.scala create mode 100644 src/main/g8/app/utils/Logger.scala create mode 100644 src/main/g8/app/utils/auth/CustomSecuredErrorHandler.scala create mode 100644 src/main/g8/app/utils/auth/CustomUnsecuredErrorHandler.scala create mode 100644 src/main/g8/app/utils/auth/Env.scala create mode 100644 src/main/g8/app/utils/auth/WithProvider.scala create mode 100644 src/main/g8/app/utils/route/Binders.scala create mode 100644 src/main/g8/app/views/activateAccount.scala.html create mode 100644 src/main/g8/app/views/changePassword.scala.html create mode 100644 src/main/g8/app/views/emails/activateAccount.scala.html create mode 100644 src/main/g8/app/views/emails/activateAccount.scala.txt create mode 100644 src/main/g8/app/views/emails/alreadySignedUp.scala.html create mode 100644 src/main/g8/app/views/emails/alreadySignedUp.scala.txt create mode 100644 src/main/g8/app/views/emails/resetPassword.scala.html create mode 100644 src/main/g8/app/views/emails/resetPassword.scala.txt create mode 100644 src/main/g8/app/views/emails/signUp.scala.html create mode 100644 src/main/g8/app/views/emails/signUp.scala.txt create mode 100644 src/main/g8/app/views/forgotPassword.scala.html create mode 100644 src/main/g8/app/views/home.scala.html create mode 100644 src/main/g8/app/views/main.scala.html create mode 100644 src/main/g8/app/views/passwordStrength.scala.html create mode 100644 src/main/g8/app/views/resetPassword.scala.html create mode 100644 src/main/g8/app/views/signIn.scala.html create mode 100644 src/main/g8/app/views/signUp.scala.html create mode 100644 src/main/g8/app/views/totp.scala.html create mode 100644 src/main/g8/app/views/totpRecovery.scala.html create mode 100644 src/main/g8/conf/application.conf create mode 100644 src/main/g8/conf/application.prod.conf create mode 100644 src/main/g8/conf/messages create mode 100644 src/main/g8/conf/routes create mode 100644 src/main/g8/conf/silhouette.conf create mode 100644 src/main/g8/public/images/favicon.png create mode 100644 src/main/g8/public/images/providers/facebook.png create mode 100644 src/main/g8/public/images/providers/google.png create mode 100644 src/main/g8/public/images/providers/twitter.png create mode 100644 src/main/g8/public/images/providers/vk.png create mode 100644 src/main/g8/public/images/providers/xing.png create mode 100644 src/main/g8/public/images/providers/yahoo.png create mode 100644 src/main/g8/public/images/silhouette.png create mode 100644 src/main/g8/public/javascripts/zxcvbnShim.js create mode 100644 src/main/g8/public/styles/main.css create mode 100755 src/main/g8/scripts/reformat create mode 100755 src/main/g8/scripts/sbt create mode 100644 src/main/g8/test/controllers/ApplicationControllerSpec.scala create mode 100644 src/main/g8/tutorial/index.html diff --git a/src/main/g8/Procfile b/src/main/g8/Procfile new file mode 100644 index 0000000..2038709 --- /dev/null +++ b/src/main/g8/Procfile @@ -0,0 +1 @@ +web: target/universal/stage/bin/play-silhouette-seed -Dhttp.port=${PORT} -Dconfig.resource=${PLAY_CONF_FILE} diff --git a/src/main/g8/app/controllers/AbstractAuthController.scala b/src/main/g8/app/controllers/AbstractAuthController.scala new file mode 100644 index 0000000..a04a363 --- /dev/null +++ b/src/main/g8/app/controllers/AbstractAuthController.scala @@ -0,0 +1,64 @@ +package controllers + +import com.mohiva.play.silhouette.api.Authenticator.Implicits._ +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.services.AuthenticatorResult +import com.mohiva.play.silhouette.api.util.Clock +import models.User +import net.ceedubs.ficus.Ficus._ +import org.webjars.play.WebJarsUtil +import play.api.Configuration +import play.api.i18n.I18nSupport +import play.api.mvc._ +import utils.auth.DefaultEnv + +import scala.concurrent.duration._ +import scala.concurrent.{ ExecutionContext, Future } + +/** + * `AbstractAuthController` base with support methods to authenticate an user. + * + * @param silhouette The Silhouette stack. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + */ +abstract class AbstractAuthController( + silhouette: Silhouette[DefaultEnv], + configuration: Configuration, + clock: Clock +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends InjectedController with I18nSupport { + + /** + * Performs user authentication + * @param user User data + * @param rememberMe Remember me flag + * @param request Initial request + * @return The result to display. + */ + protected def authenticateUser(user: User, rememberMe: Boolean)(implicit request: Request[_]): Future[AuthenticatorResult] = { + val c = configuration.underlying + val result = Redirect(routes.ApplicationController.index()) + silhouette.env.authenticatorService.create(user.loginInfo).map { + case authenticator if rememberMe => + authenticator.copy( + expirationDateTime = clock.now + c.as[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorExpiry"), + idleTimeout = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorIdleTimeout"), + cookieMaxAge = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.cookieMaxAge") + ) + case authenticator => authenticator + }.flatMap { authenticator => + silhouette.env.eventBus.publish(LoginEvent(user, request)) + silhouette.env.authenticatorService.init(authenticator).flatMap { v => + silhouette.env.authenticatorService.embed(v, result) + } + } + } +} diff --git a/src/main/g8/app/controllers/ActivateAccountController.scala b/src/main/g8/app/controllers/ActivateAccountController.scala new file mode 100644 index 0000000..1a436a4 --- /dev/null +++ b/src/main/g8/app/controllers/ActivateAccountController.scala @@ -0,0 +1,85 @@ +package controllers + +import java.net.URLDecoder +import java.util.UUID +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.impl.providers.CredentialsProvider +import models.services.{ AuthTokenService, UserService } +import play.api.i18n.{ I18nSupport, Messages } +import play.api.libs.mailer.{ Email, MailerClient } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Activate Account` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param authTokenService The auth token service implementation. + * @param mailerClient The mailer client. + * @param ex The execution context. + */ +class ActivateAccountController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authTokenService: AuthTokenService, + mailerClient: MailerClient +)( + implicit + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Sends an account activation email to the user with the given email. + * + * @param email The email address of the user to send the activation mail to. + * @return The result to display. + */ + def send(email: String) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + val decodedEmail = URLDecoder.decode(email, "UTF-8") + val loginInfo = LoginInfo(CredentialsProvider.ID, decodedEmail) + val result = Redirect(routes.SignInController.view()).flashing("info" -> Messages("activation.email.sent", decodedEmail)) + + userService.retrieve(loginInfo).flatMap { + case Some(user) if !user.activated => + authTokenService.create(user.userID).map { authToken => + val url = routes.ActivateAccountController.activate(authToken.id).absoluteURL() + + mailerClient.send(Email( + subject = Messages("email.activate.account.subject"), + from = Messages("email.from"), + to = Seq(decodedEmail), + bodyText = Some(views.txt.emails.activateAccount(user, url).body), + bodyHtml = Some(views.html.emails.activateAccount(user, url).body) + )) + result + } + case None => Future.successful(result) + } + } + + /** + * Activates an account. + * + * @param token The token to identify a user. + * @return The result to display. + */ + def activate(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + authTokenService.validate(token).flatMap { + case Some(authToken) => userService.retrieve(authToken.userID).flatMap { + case Some(user) if user.loginInfo.providerID == CredentialsProvider.ID => + userService.save(user.copy(activated = true)).map { _ => + Redirect(routes.SignInController.view()).flashing("success" -> Messages("account.activated")) + } + case _ => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link"))) + } + case None => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link"))) + } + } +} diff --git a/src/main/g8/app/controllers/ApplicationController.scala b/src/main/g8/app/controllers/ApplicationController.scala new file mode 100644 index 0000000..04330fb --- /dev/null +++ b/src/main/g8/app/controllers/ApplicationController.scala @@ -0,0 +1,55 @@ +package controllers + +import com.mohiva.play.silhouette.api.actions.SecuredRequest +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.{ LogoutEvent, Silhouette } +import com.mohiva.play.silhouette.impl.providers.GoogleTotpInfo +import javax.inject.Inject +import org.webjars.play.WebJarsUtil +import play.api.i18n.I18nSupport +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents } +import utils.auth.DefaultEnv + +import scala.concurrent.ExecutionContext + +/** + * The basic application controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + */ +class ApplicationController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + authInfoRepository: AuthInfoRepository +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Handles the index action. + * + * @return The result to display. + */ + def index = silhouette.SecuredAction.async { implicit request: SecuredRequest[DefaultEnv, AnyContent] => + authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => + Ok(views.html.home(request.identity, totpInfoOpt)) + } + } + + /** + * Handles the Sign Out action. + * + * @return The result to display. + */ + def signOut = silhouette.SecuredAction.async { implicit request: SecuredRequest[DefaultEnv, AnyContent] => + val result = Redirect(routes.ApplicationController.index()) + silhouette.env.eventBus.publish(LogoutEvent(request.identity, request)) + silhouette.env.authenticatorService.discard(request.authenticator, result) + } +} diff --git a/src/main/g8/app/controllers/ChangePasswordController.scala b/src/main/g8/app/controllers/ChangePasswordController.scala new file mode 100644 index 0000000..fb55c67 --- /dev/null +++ b/src/main/g8/app/controllers/ChangePasswordController.scala @@ -0,0 +1,78 @@ +package controllers + +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.actions.SecuredRequest +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.util.{ Credentials, PasswordHasherRegistry, PasswordInfo } +import com.mohiva.play.silhouette.impl.providers.CredentialsProvider +import forms.ChangePasswordForm +import org.webjars.play.WebJarsUtil +import play.api.i18n.{ I18nSupport, Messages } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents } +import utils.auth.{ DefaultEnv, WithProvider } + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Change Password` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param credentialsProvider The credentials provider. + * @param authInfoRepository The auth info repository. + * @param passwordHasherRegistry The password hasher registry. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + */ +class ChangePasswordController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + credentialsProvider: CredentialsProvider, + authInfoRepository: AuthInfoRepository, + passwordHasherRegistry: PasswordHasherRegistry +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Views the `Change Password` page. + * + * @return The result to display. + */ + def view = silhouette.SecuredAction(WithProvider[DefaultEnv#A](CredentialsProvider.ID)) { + implicit request: SecuredRequest[DefaultEnv, AnyContent] => + Ok(views.html.changePassword(ChangePasswordForm.form, request.identity)) + } + + /** + * Changes the password. + * + * @return The result to display. + */ + def submit = silhouette.SecuredAction(WithProvider[DefaultEnv#A](CredentialsProvider.ID)).async { + implicit request: SecuredRequest[DefaultEnv, AnyContent] => + ChangePasswordForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.changePassword(form, request.identity))), + password => { + val (currentPassword, newPassword) = password + val credentials = Credentials(request.identity.email.getOrElse(""), currentPassword) + credentialsProvider.authenticate(credentials).flatMap { loginInfo => + val passwordInfo = passwordHasherRegistry.current.hash(newPassword) + authInfoRepository.update[PasswordInfo](loginInfo, passwordInfo).map { _ => + Redirect(routes.ChangePasswordController.view()).flashing("success" -> Messages("password.changed")) + } + }.recover { + case _: ProviderException => + Redirect(routes.ChangePasswordController.view()).flashing("error" -> Messages("current.password.invalid")) + } + } + ) + } +} diff --git a/src/main/g8/app/controllers/ForgotPasswordController.scala b/src/main/g8/app/controllers/ForgotPasswordController.scala new file mode 100644 index 0000000..d2965b5 --- /dev/null +++ b/src/main/g8/app/controllers/ForgotPasswordController.scala @@ -0,0 +1,84 @@ +package controllers + +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.impl.providers.CredentialsProvider +import forms.ForgotPasswordForm +import models.services.{ AuthTokenService, UserService } +import org.webjars.play.WebJarsUtil +import play.api.i18n.{ I18nSupport, Messages } +import play.api.libs.mailer.{ Email, MailerClient } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Forgot Password` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param authTokenService The auth token service implementation. + * @param mailerClient The mailer client. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + */ +class ForgotPasswordController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authTokenService: AuthTokenService, + mailerClient: MailerClient +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Views the `Forgot Password` page. + * + * @return The result to display. + */ + def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + Future.successful(Ok(views.html.forgotPassword(ForgotPasswordForm.form))) + } + + /** + * Sends an email with password reset instructions. + * + * It sends an email to the given address if it exists in the database. Otherwise we do not show the user + * a notice for not existing email addresses to prevent the leak of existing email addresses. + * + * @return The result to display. + */ + def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + ForgotPasswordForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.forgotPassword(form))), + email => { + val loginInfo = LoginInfo(CredentialsProvider.ID, email) + val result = Redirect(routes.SignInController.view()).flashing("info" -> Messages("reset.email.sent")) + userService.retrieve(loginInfo).flatMap { + case Some(user) if user.email.isDefined => + authTokenService.create(user.userID).map { authToken => + val url = routes.ResetPasswordController.view(authToken.id).absoluteURL() + + mailerClient.send(Email( + subject = Messages("email.reset.password.subject"), + from = Messages("email.from"), + to = Seq(email), + bodyText = Some(views.txt.emails.resetPassword(user, url).body), + bodyHtml = Some(views.html.emails.resetPassword(user, url).body) + )) + result + } + case None => Future.successful(result) + } + } + ) + } +} diff --git a/src/main/g8/app/controllers/ResetPasswordController.scala b/src/main/g8/app/controllers/ResetPasswordController.scala new file mode 100644 index 0000000..2991e96 --- /dev/null +++ b/src/main/g8/app/controllers/ResetPasswordController.scala @@ -0,0 +1,82 @@ +package controllers + +import java.util.UUID +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.util.{ PasswordHasherRegistry, PasswordInfo } +import com.mohiva.play.silhouette.impl.providers.CredentialsProvider +import forms.ResetPasswordForm +import models.services.{ AuthTokenService, UserService } +import org.webjars.play.WebJarsUtil +import play.api.i18n.{ I18nSupport, Messages } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Reset Password` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param authInfoRepository The auth info repository. + * @param passwordHasherRegistry The password hasher registry. + * @param authTokenService The auth token service implementation. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + */ +class ResetPasswordController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authInfoRepository: AuthInfoRepository, + passwordHasherRegistry: PasswordHasherRegistry, + authTokenService: AuthTokenService +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Views the `Reset Password` page. + * + * @param token The token to identify a user. + * @return The result to display. + */ + def view(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + authTokenService.validate(token).map { + case Some(_) => Ok(views.html.resetPassword(ResetPasswordForm.form, token)) + case None => Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link")) + } + } + + /** + * Resets the password. + * + * @param token The token to identify a user. + * @return The result to display. + */ + def submit(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + authTokenService.validate(token).flatMap { + case Some(authToken) => + ResetPasswordForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.resetPassword(form, token))), + password => userService.retrieve(authToken.userID).flatMap { + case Some(user) if user.loginInfo.providerID == CredentialsProvider.ID => + val passwordInfo = passwordHasherRegistry.current.hash(password) + authInfoRepository.update[PasswordInfo](user.loginInfo, passwordInfo).map { _ => + Redirect(routes.SignInController.view()).flashing("success" -> Messages("password.reset")) + } + case _ => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link"))) + } + ) + case None => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.reset.link"))) + } + } +} diff --git a/src/main/g8/app/controllers/SignInController.scala b/src/main/g8/app/controllers/SignInController.scala new file mode 100644 index 0000000..cc82b34 --- /dev/null +++ b/src/main/g8/app/controllers/SignInController.scala @@ -0,0 +1,87 @@ +package controllers + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.util.{ Clock, Credentials } +import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException +import com.mohiva.play.silhouette.impl.providers._ +import forms.{ SignInForm, TotpForm } +import javax.inject.Inject +import models.services.UserService +import org.webjars.play.WebJarsUtil +import play.api.Configuration +import play.api.i18n.{ I18nSupport, Messages } +import play.api.mvc.{ AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Sign In` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param credentialsProvider The credentials provider. + * @param socialProviderRegistry The social provider registry. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + */ +class SignInController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authInfoRepository: AuthInfoRepository, + credentialsProvider: CredentialsProvider, + socialProviderRegistry: SocialProviderRegistry, + configuration: Configuration, + clock: Clock +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { + + /** + * Views the `Sign In` page. + * + * @return The result to display. + */ + def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + Future.successful(Ok(views.html.signIn(SignInForm.form, socialProviderRegistry))) + } + + /** + * Handles the submitted form. + * + * @return The result to display. + */ + def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + SignInForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.signIn(form, socialProviderRegistry))), + data => { + val credentials = Credentials(data.email, data.password) + credentialsProvider.authenticate(credentials).flatMap { loginInfo => + userService.retrieve(loginInfo).flatMap { + case Some(user) if !user.activated => + Future.successful(Ok(views.html.activateAccount(data.email))) + case Some(user) => + authInfoRepository.find[GoogleTotpInfo](user.loginInfo).flatMap { + case Some(totpInfo) => Future.successful(Ok(views.html.totp(TotpForm.form.fill(TotpForm.Data( + user.userID, totpInfo.sharedKey, data.rememberMe))))) + case _ => authenticateUser(user, data.rememberMe) + } + case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) + } + }.recover { + case _: ProviderException => + Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.credentials")) + } + } + ) + } +} diff --git a/src/main/g8/app/controllers/SignUpController.scala b/src/main/g8/app/controllers/SignUpController.scala new file mode 100644 index 0000000..a9bd25f --- /dev/null +++ b/src/main/g8/app/controllers/SignUpController.scala @@ -0,0 +1,119 @@ +package controllers + +import java.util.UUID +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.services.AvatarService +import com.mohiva.play.silhouette.api.util.PasswordHasherRegistry +import com.mohiva.play.silhouette.impl.providers._ +import forms.SignUpForm +import models.User +import models.services.{ AuthTokenService, UserService } +import org.webjars.play.WebJarsUtil +import play.api.i18n.{ I18nSupport, Messages } +import play.api.libs.mailer.{ Email, MailerClient } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `Sign Up` controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param authInfoRepository The auth info repository implementation. + * @param authTokenService The auth token service implementation. + * @param avatarService The avatar service implementation. + * @param passwordHasherRegistry The password hasher registry. + * @param mailerClient The mailer client. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + */ +class SignUpController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authInfoRepository: AuthInfoRepository, + authTokenService: AuthTokenService, + avatarService: AvatarService, + passwordHasherRegistry: PasswordHasherRegistry, + mailerClient: MailerClient +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport { + + /** + * Views the `Sign Up` page. + * + * @return The result to display. + */ + def view = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + Future.successful(Ok(views.html.signUp(SignUpForm.form))) + } + + /** + * Handles the submitted form. + * + * @return The result to display. + */ + def submit = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] => + SignUpForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.signUp(form))), + data => { + val result = Redirect(routes.SignUpController.view()).flashing("info" -> Messages("sign.up.email.sent", data.email)) + val loginInfo = LoginInfo(CredentialsProvider.ID, data.email) + userService.retrieve(loginInfo).flatMap { + case Some(user) => + val url = routes.SignInController.view().absoluteURL() + mailerClient.send(Email( + subject = Messages("email.already.signed.up.subject"), + from = Messages("email.from"), + to = Seq(data.email), + bodyText = Some(views.txt.emails.alreadySignedUp(user, url).body), + bodyHtml = Some(views.html.emails.alreadySignedUp(user, url).body) + )) + + Future.successful(result) + case None => + val authInfo = passwordHasherRegistry.current.hash(data.password) + val user = User( + userID = UUID.randomUUID(), + loginInfo = loginInfo, + firstName = Some(data.firstName), + lastName = Some(data.lastName), + fullName = Some(data.firstName + " " + data.lastName), + email = Some(data.email), + avatarURL = None, + activated = false + ) + for { + avatar <- avatarService.retrieveURL(data.email) + user <- userService.save(user.copy(avatarURL = avatar)) + authInfo <- authInfoRepository.add(loginInfo, authInfo) + authToken <- authTokenService.create(user.userID) + } yield { + val url = routes.ActivateAccountController.activate(authToken.id).absoluteURL() + mailerClient.send(Email( + subject = Messages("email.sign.up.subject"), + from = Messages("email.from"), + to = Seq(data.email), + bodyText = Some(views.txt.emails.signUp(user, url).body), + bodyHtml = Some(views.html.emails.signUp(user, url).body) + )) + + silhouette.env.eventBus.publish(SignUpEvent(user, request)) + result + } + } + } + ) + } +} diff --git a/src/main/g8/app/controllers/SocialAuthController.scala b/src/main/g8/app/controllers/SocialAuthController.scala new file mode 100644 index 0000000..bca4776 --- /dev/null +++ b/src/main/g8/app/controllers/SocialAuthController.scala @@ -0,0 +1,67 @@ +package controllers + +import javax.inject.Inject + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.impl.providers._ +import models.services.UserService +import play.api.i18n.{ I18nSupport, Messages } +import play.api.mvc.{ AbstractController, AnyContent, ControllerComponents, Request } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The social auth controller. + * + * @param components The Play controller components. + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param authInfoRepository The auth info service implementation. + * @param socialProviderRegistry The social provider registry. + * @param ex The execution context. + */ +class SocialAuthController @Inject() ( + components: ControllerComponents, + silhouette: Silhouette[DefaultEnv], + userService: UserService, + authInfoRepository: AuthInfoRepository, + socialProviderRegistry: SocialProviderRegistry +)( + implicit + ex: ExecutionContext +) extends AbstractController(components) with I18nSupport with Logger { + + /** + * Authenticates a user against a social provider. + * + * @param provider The ID of the provider to authenticate against. + * @return The result to display. + */ + def authenticate(provider: String) = Action.async { implicit request: Request[AnyContent] => + (socialProviderRegistry.get[SocialProvider](provider) match { + case Some(p: SocialProvider with CommonSocialProfileBuilder) => + p.authenticate().flatMap { + case Left(result) => Future.successful(result) + case Right(authInfo) => for { + profile <- p.retrieveProfile(authInfo) + user <- userService.save(profile) + authInfo <- authInfoRepository.save(profile.loginInfo, authInfo) + authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo) + value <- silhouette.env.authenticatorService.init(authenticator) + result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.ApplicationController.index())) + } yield { + silhouette.env.eventBus.publish(LoginEvent(user, request)) + result + } + } + case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) + }).recover { + case e: ProviderException => + logger.error("Unexpected provider error", e) + Redirect(routes.SignInController.view()).flashing("error" -> Messages("could.not.authenticate")) + } + } +} diff --git a/src/main/g8/app/controllers/TotpController.scala b/src/main/g8/app/controllers/TotpController.scala new file mode 100644 index 0000000..9781df3 --- /dev/null +++ b/src/main/g8/app/controllers/TotpController.scala @@ -0,0 +1,126 @@ +package controllers + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.util.Clock +import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException +import com.mohiva.play.silhouette.impl.providers._ +import forms.{ TotpForm, TotpSetupForm } +import javax.inject.Inject +import models.services.UserService +import org.webjars.play.WebJarsUtil +import play.api.Configuration +import play.api.i18n.{ I18nSupport, Messages } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `TOTP` controller. + * + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param totpProvider The totp provider. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + * @param authInfoRepository The auth info repository. + */ +class TotpController @Inject() ( + silhouette: Silhouette[DefaultEnv], + userService: UserService, + totpProvider: GoogleTotpProvider, + configuration: Configuration, + clock: Clock +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext, + authInfoRepository: AuthInfoRepository +) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { + + /** + * Views the `TOTP` page. + * @return The result to display. + */ + def view(userId: java.util.UUID, sharedKey: String, rememberMe: Boolean) = silhouette.UnsecuredAction.async { implicit request => + Future.successful(Ok(views.html.totp(TotpForm.form.fill(TotpForm.Data(userId, sharedKey, rememberMe))))) + } + + /** + * Enable TOTP. + * @return The result to display. + */ + def enableTotp = silhouette.SecuredAction.async { implicit request => + val user = request.identity + val credentials = totpProvider.createCredentials(user.email.get) + val totpInfo = credentials.totpInfo + val formData = TotpSetupForm.form.fill(TotpSetupForm.Data(totpInfo.sharedKey, totpInfo.scratchCodes, credentials.scratchCodesPlain)) + authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => + Ok(views.html.home(user, totpInfoOpt, Some((formData, credentials)))) + } + } + + /** + * Disable TOTP. + * @return The result to display. + */ + def disableTotp = silhouette.SecuredAction.async { implicit request => + val user = request.identity + authInfoRepository.remove[GoogleTotpInfo](user.loginInfo) + Future(Redirect(routes.ApplicationController.index()).flashing("info" -> Messages("totp.disabling.info"))) + } + + /** + * Handles the submitted form with TOTP initial data. + * @return The result to display. + */ + def enableTotpSubmit = silhouette.SecuredAction.async { implicit request => + val user = request.identity + TotpSetupForm.form.bindFromRequest.fold( + form => authInfoRepository.find[GoogleTotpInfo](request.identity.loginInfo).map { totpInfoOpt => + BadRequest(views.html.home(user, totpInfoOpt)) + }, + data => { + totpProvider.authenticate(data.sharedKey, data.verificationCode).flatMap { + case Some(loginInfo: LoginInfo) => { + authInfoRepository.add[GoogleTotpInfo](user.loginInfo, GoogleTotpInfo(data.sharedKey, data.scratchCodes)) + Future(Redirect(routes.ApplicationController.index()).flashing("success" -> Messages("totp.enabling.info"))) + } + case _ => Future.successful(Redirect(routes.ApplicationController.index()).flashing("error" -> Messages("invalid.verification.code"))) + }.recover { + case _: ProviderException => + Redirect(routes.TotpController.view(user.userID, data.sharedKey, request.authenticator.cookieMaxAge.isDefined)).flashing("error" -> Messages("invalid.unexpected.totp")) + } + } + ) + } + + /** + * Handles the submitted form with TOTP verification key. + * @return The result to display. + */ + def submit = silhouette.UnsecuredAction.async { implicit request => + TotpForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.totp(form))), + data => { + val totpControllerRoute = routes.TotpController.view(data.userID, data.sharedKey, data.rememberMe) + userService.retrieve(data.userID).flatMap { + case Some(user) => + totpProvider.authenticate(data.sharedKey, data.verificationCode).flatMap { + case Some(_) => authenticateUser(user, data.rememberMe) + case _ => Future.successful(Redirect(totpControllerRoute).flashing("error" -> Messages("invalid.verification.code"))) + }.recover { + case _: ProviderException => + Redirect(totpControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp")) + } + case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) + } + } + ) + } +} diff --git a/src/main/g8/app/controllers/TotpRecoveryController.scala b/src/main/g8/app/controllers/TotpRecoveryController.scala new file mode 100644 index 0000000..e1e3b22 --- /dev/null +++ b/src/main/g8/app/controllers/TotpRecoveryController.scala @@ -0,0 +1,91 @@ +package controllers + +import java.util.UUID + +import com.mohiva.play.silhouette.api._ +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.util.Clock +import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException +import com.mohiva.play.silhouette.impl.providers._ +import forms.TotpRecoveryForm +import javax.inject.Inject +import models.services.UserService +import org.webjars.play.WebJarsUtil +import play.api.Configuration +import play.api.i18n.{ I18nSupport, Messages } +import utils.auth.DefaultEnv + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * The `TOTP` controller. + * + * @param silhouette The Silhouette stack. + * @param userService The user service implementation. + * @param totpProvider The totp provider. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @param webJarsUtil The webjar util. + * @param assets The Play assets finder. + * @param ex The execution context. + * @param authInfoRepository The auth info repository. + */ +class TotpRecoveryController @Inject() ( + silhouette: Silhouette[DefaultEnv], + userService: UserService, + totpProvider: GoogleTotpProvider, + configuration: Configuration, + clock: Clock +)( + implicit + webJarsUtil: WebJarsUtil, + assets: AssetsFinder, + ex: ExecutionContext, + authInfoRepository: AuthInfoRepository +) extends AbstractAuthController(silhouette, configuration, clock) with I18nSupport { + + /** + * Views the TOTP recovery page. + * + * @param userID the user ID. + * @param sharedKey the shared key associated to the user. + * @param rememberMe the remember me flag. + * @return The result to display. + */ + def view(userID: UUID, sharedKey: String, rememberMe: Boolean) = silhouette.UnsecuredAction.async { implicit request => + Future.successful(Ok(views.html.totpRecovery(TotpRecoveryForm.form.fill(TotpRecoveryForm.Data(userID, sharedKey, rememberMe))))) + } + + /** + * Handles the submitted form with TOTP verification key. + * @return The result to display. + */ + def submit = silhouette.UnsecuredAction.async { implicit request => + TotpRecoveryForm.form.bindFromRequest.fold( + form => Future.successful(BadRequest(views.html.totpRecovery(form))), + data => { + val totpRecoveryControllerRoute = routes.TotpRecoveryController.view(data.userID, data.sharedKey, data.rememberMe) + userService.retrieve(data.userID).flatMap { + case Some(user) => { + authInfoRepository.find[GoogleTotpInfo](user.loginInfo).flatMap { + case Some(totpInfo) => + totpProvider.authenticate(totpInfo, data.recoveryCode).flatMap { + case Some(updated) => { + authInfoRepository.update[GoogleTotpInfo](user.loginInfo, updated._2) + authenticateUser(user, data.rememberMe) + } + case _ => Future.successful(Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.recovery.code"))) + }.recover { + case _: ProviderException => + Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp")) + } + case _ => Future.successful(Redirect(totpRecoveryControllerRoute).flashing("error" -> Messages("invalid.unexpected.totp"))) + } + } + case None => Future.failed(new IdentityNotFoundException("Couldn't find user")) + } + } + ) + } +} diff --git a/src/main/g8/app/forms/ChangePasswordForm.scala b/src/main/g8/app/forms/ChangePasswordForm.scala new file mode 100644 index 0000000..cf640c4 --- /dev/null +++ b/src/main/g8/app/forms/ChangePasswordForm.scala @@ -0,0 +1,18 @@ +package forms + +import play.api.data.Forms._ +import play.api.data._ + +/** + * The `Change Password` form. + */ +object ChangePasswordForm { + + /** + * A play framework form. + */ + val form = Form(tuple( + "current-password" -> nonEmptyText, + "new-password" -> nonEmptyText + )) +} diff --git a/src/main/g8/app/forms/ForgotPasswordForm.scala b/src/main/g8/app/forms/ForgotPasswordForm.scala new file mode 100644 index 0000000..70dbaf3 --- /dev/null +++ b/src/main/g8/app/forms/ForgotPasswordForm.scala @@ -0,0 +1,17 @@ +package forms + +import play.api.data.Forms._ +import play.api.data._ + +/** + * The `Forgot Password` form. + */ +object ForgotPasswordForm { + + /** + * A play framework form. + */ + val form = Form( + "email" -> email + ) +} diff --git a/src/main/g8/app/forms/ResetPasswordForm.scala b/src/main/g8/app/forms/ResetPasswordForm.scala new file mode 100644 index 0000000..9d0a020 --- /dev/null +++ b/src/main/g8/app/forms/ResetPasswordForm.scala @@ -0,0 +1,17 @@ +package forms + +import play.api.data.Forms._ +import play.api.data._ + +/** + * The `Reset Password` form. + */ +object ResetPasswordForm { + + /** + * A play framework form. + */ + val form = Form( + "password" -> nonEmptyText + ) +} diff --git a/src/main/g8/app/forms/SignInForm.scala b/src/main/g8/app/forms/SignInForm.scala new file mode 100644 index 0000000..e4183c4 --- /dev/null +++ b/src/main/g8/app/forms/SignInForm.scala @@ -0,0 +1,33 @@ +package forms + +import play.api.data.Form +import play.api.data.Forms._ + +/** + * The form which handles the submission of the credentials. + */ +object SignInForm { + + /** + * A play framework form. + */ + val form = Form( + mapping( + "email" -> email, + "password" -> nonEmptyText, + "rememberMe" -> boolean + )(Data.apply)(Data.unapply) + ) + + /** + * The form data. + * + * @param email The email of the user. + * @param password The password of the user. + * @param rememberMe Indicates if the user should stay logged in on the next visit. + */ + case class Data( + email: String, + password: String, + rememberMe: Boolean) +} diff --git a/src/main/g8/app/forms/SignUpForm.scala b/src/main/g8/app/forms/SignUpForm.scala new file mode 100644 index 0000000..9cfb02e --- /dev/null +++ b/src/main/g8/app/forms/SignUpForm.scala @@ -0,0 +1,36 @@ +package forms + +import play.api.data.Form +import play.api.data.Forms._ + +/** + * The form which handles the sign up process. + */ +object SignUpForm { + + /** + * A play framework form. + */ + val form = Form( + mapping( + "firstName" -> nonEmptyText, + "lastName" -> nonEmptyText, + "email" -> email, + "password" -> nonEmptyText + )(Data.apply)(Data.unapply) + ) + + /** + * The form data. + * + * @param firstName The first name of a user. + * @param lastName The last name of a user. + * @param email The email of the user. + * @param password The password of the user. + */ + case class Data( + firstName: String, + lastName: String, + email: String, + password: String) +} diff --git a/src/main/g8/app/forms/TotpForm.scala b/src/main/g8/app/forms/TotpForm.scala new file mode 100644 index 0000000..ce8c6de --- /dev/null +++ b/src/main/g8/app/forms/TotpForm.scala @@ -0,0 +1,36 @@ +package forms + +import java.util.UUID + +import play.api.data.Form +import play.api.data.Forms._ + +/** + * The form which handles the submission of the credentials plus verification code for TOTP-authentication + */ +object TotpForm { + /** + * A play framework form. + */ + val form = Form( + mapping( + "userID" -> uuid, + "sharedKey" -> nonEmptyText, + "rememberMe" -> boolean, + "verificationCode" -> nonEmptyText(minLength = 6, maxLength = 6) + )(Data.apply)(Data.unapply) + ) + + /** + * The form data. + * @param userID The unique identifier of the user. + * @param sharedKey the TOTP shared key + * @param rememberMe Indicates if the user should stay logged in on the next visit. + * @param verificationCode Verification code for TOTP-authentication + */ + case class Data( + userID: UUID, + sharedKey: String, + rememberMe: Boolean, + verificationCode: String = "") +} diff --git a/src/main/g8/app/forms/TotpRecoveryForm.scala b/src/main/g8/app/forms/TotpRecoveryForm.scala new file mode 100644 index 0000000..9c50f98 --- /dev/null +++ b/src/main/g8/app/forms/TotpRecoveryForm.scala @@ -0,0 +1,36 @@ +package forms + +import java.util.UUID + +import play.api.data.Form +import play.api.data.Forms._ + +/** + * The form which handles the submission of the credentials plus verification code for TOTP-authentication + */ +object TotpRecoveryForm { + /** + * A play framework form. + */ + val form = Form( + mapping( + "userID" -> uuid, + "sharedKey" -> nonEmptyText, + "rememberMe" -> boolean, + "recoveryCode" -> nonEmptyText(minLength = 8, maxLength = 8) + )(Data.apply)(Data.unapply) + ) + + /** + * The form data. + * @param userID The unique identifier of the user. + * @param sharedKey the TOTP shared key + * @param rememberMe Indicates if the user should stay logged in on the next visit. + * @param recoveryCode Verification code for TOTP-authentication + */ + case class Data( + userID: UUID, + sharedKey: String, + rememberMe: Boolean, + recoveryCode: String = "") +} diff --git a/src/main/g8/app/forms/TotpSetupForm.scala b/src/main/g8/app/forms/TotpSetupForm.scala new file mode 100644 index 0000000..183513d --- /dev/null +++ b/src/main/g8/app/forms/TotpSetupForm.scala @@ -0,0 +1,40 @@ +package forms + +import com.mohiva.play.silhouette.api.util.PasswordInfo +import play.api.data.Form +import play.api.data.Forms._ + +/** + * The form which handles the submission of the form with data for TOTP-authentication enabling + */ +object TotpSetupForm { + /** + * A play framework form. + */ + val form = Form( + mapping( + "sharedKey" -> nonEmptyText, + "scratchCodes" -> seq( + mapping( + "hasher" -> nonEmptyText, + "password" -> nonEmptyText, + "salt" -> optional(nonEmptyText) + )(PasswordInfo.apply)(PasswordInfo.unapply) + ), + "scratchCodesPlain" -> seq(nonEmptyText), + "verificationCode" -> nonEmptyText(minLength = 6, maxLength = 6) + )(Data.apply)(Data.unapply) + ) + + /** + * The form data. + * @param sharedKey Shared user key for TOTP authentication. + * @param scratchCodes Scratch or recovery codes used for one time TOTP authentication. + * @param verificationCode Verification code for TOTP-authentication + */ + case class Data( + sharedKey: String, + scratchCodes: Seq[PasswordInfo], + scratchCodesPlain: Seq[String], + verificationCode: String = "") +} diff --git a/src/main/g8/app/jobs/AuthTokenCleaner.scala b/src/main/g8/app/jobs/AuthTokenCleaner.scala new file mode 100644 index 0000000..e34053e --- /dev/null +++ b/src/main/g8/app/jobs/AuthTokenCleaner.scala @@ -0,0 +1,55 @@ +package jobs + +import javax.inject.Inject + +import akka.actor._ +import com.mohiva.play.silhouette.api.util.Clock +import jobs.AuthTokenCleaner.Clean +import models.services.AuthTokenService +import utils.Logger + +import scala.concurrent.ExecutionContext.Implicits.global + +/** + * A job which cleanup invalid auth tokens. + * + * @param service The auth token service implementation. + * @param clock The clock implementation. + */ +class AuthTokenCleaner @Inject() ( + service: AuthTokenService, + clock: Clock) + extends Actor with Logger { + + /** + * Process the received messages. + */ + def receive: Receive = { + case Clean => + val start = clock.now.getMillis + val msg = new StringBuffer("\n") + msg.append("=================================\n") + msg.append("Start to cleanup auth tokens\n") + msg.append("=================================\n") + service.clean.map { deleted => + val seconds = (clock.now.getMillis - start) / 1000 + msg.append("Total of %s auth tokens(s) were deleted in %s seconds".format(deleted.length, seconds)).append("\n") + msg.append("=================================\n") + + msg.append("=================================\n") + logger.info(msg.toString) + }.recover { + case e => + msg.append("Couldn't cleanup auth tokens because of unexpected error\n") + msg.append("=================================\n") + logger.error(msg.toString, e) + } + } +} + +/** + * The companion object. + */ +object AuthTokenCleaner { + case object Clean +} diff --git a/src/main/g8/app/jobs/Scheduler.scala b/src/main/g8/app/jobs/Scheduler.scala new file mode 100644 index 0000000..59c7cd8 --- /dev/null +++ b/src/main/g8/app/jobs/Scheduler.scala @@ -0,0 +1,18 @@ +package jobs + +import akka.actor.{ ActorRef, ActorSystem } +import com.google.inject.Inject +import com.google.inject.name.Named +import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension + +/** + * Schedules the jobs. + */ +class Scheduler @Inject() ( + system: ActorSystem, + @Named("auth-token-cleaner") authTokenCleaner: ActorRef) { + + QuartzSchedulerExtension(system).schedule("AuthTokenCleaner", authTokenCleaner, AuthTokenCleaner.Clean) + + authTokenCleaner ! AuthTokenCleaner.Clean +} diff --git a/src/main/g8/app/models/AuthToken.scala b/src/main/g8/app/models/AuthToken.scala new file mode 100644 index 0000000..2acb397 --- /dev/null +++ b/src/main/g8/app/models/AuthToken.scala @@ -0,0 +1,17 @@ +package models + +import java.util.UUID + +import org.joda.time.DateTime + +/** + * A token to authenticate a user against an endpoint for a short time period. + * + * @param id The unique token ID. + * @param userID The unique ID of the user the token is associated with. + * @param expiry The date-time the token expires. + */ +case class AuthToken( + id: UUID, + userID: UUID, + expiry: DateTime) diff --git a/src/main/g8/app/models/User.scala b/src/main/g8/app/models/User.scala new file mode 100644 index 0000000..00f6618 --- /dev/null +++ b/src/main/g8/app/models/User.scala @@ -0,0 +1,42 @@ +package models + +import java.util.UUID + +import com.mohiva.play.silhouette.api.{ Identity, LoginInfo } + +/** + * The user object. + * + * @param userID The unique ID of the user. + * @param loginInfo The linked login info. + * @param firstName Maybe the first name of the authenticated user. + * @param lastName Maybe the last name of the authenticated user. + * @param fullName Maybe the full name of the authenticated user. + * @param email Maybe the email of the authenticated provider. + * @param avatarURL Maybe the avatar URL of the authenticated provider. + * @param activated Indicates that the user has activated its registration. + */ +case class User( + userID: UUID, + loginInfo: LoginInfo, + firstName: Option[String], + lastName: Option[String], + fullName: Option[String], + email: Option[String], + avatarURL: Option[String], + activated: Boolean) extends Identity { + + /** + * Tries to construct a name. + * + * @return Maybe a name. + */ + def name = fullName.orElse { + firstName -> lastName match { + case (Some(f), Some(l)) => Some(f + " " + l) + case (Some(f), None) => Some(f) + case (None, Some(l)) => Some(l) + case _ => None + } + } +} diff --git a/src/main/g8/app/models/daos/AuthTokenDAO.scala b/src/main/g8/app/models/daos/AuthTokenDAO.scala new file mode 100644 index 0000000..d1cbf8b --- /dev/null +++ b/src/main/g8/app/models/daos/AuthTokenDAO.scala @@ -0,0 +1,45 @@ +package models.daos + +import java.util.UUID + +import models.AuthToken +import org.joda.time.DateTime + +import scala.concurrent.Future + +/** + * Give access to the [[AuthToken]] object. + */ +trait AuthTokenDAO { + + /** + * Finds a token by its ID. + * + * @param id The unique token ID. + * @return The found token or None if no token for the given ID could be found. + */ + def find(id: UUID): Future[Option[AuthToken]] + + /** + * Finds expired tokens. + * + * @param dateTime The current date time. + */ + def findExpired(dateTime: DateTime): Future[Seq[AuthToken]] + + /** + * Saves a token. + * + * @param token The token to save. + * @return The saved token. + */ + def save(token: AuthToken): Future[AuthToken] + + /** + * Removes the token for the given ID. + * + * @param id The ID for which the token should be removed. + * @return A future to wait for the process to be completed. + */ + def remove(id: UUID): Future[Unit] +} diff --git a/src/main/g8/app/models/daos/AuthTokenDAOImpl.scala b/src/main/g8/app/models/daos/AuthTokenDAOImpl.scala new file mode 100644 index 0000000..95b5173 --- /dev/null +++ b/src/main/g8/app/models/daos/AuthTokenDAOImpl.scala @@ -0,0 +1,69 @@ +package models.daos + +import java.util.UUID + +import models.AuthToken +import models.daos.AuthTokenDAOImpl._ +import org.joda.time.DateTime + +import scala.collection.mutable +import scala.concurrent.Future + +/** + * Give access to the [[AuthToken]] object. + */ +class AuthTokenDAOImpl extends AuthTokenDAO { + + /** + * Finds a token by its ID. + * + * @param id The unique token ID. + * @return The found token or None if no token for the given ID could be found. + */ + def find(id: UUID) = Future.successful(tokens.get(id)) + + /** + * Finds expired tokens. + * + * @param dateTime The current date time. + */ + def findExpired(dateTime: DateTime) = Future.successful { + tokens.filter { + case (_, token) => + token.expiry.isBefore(dateTime) + }.values.toSeq + } + + /** + * Saves a token. + * + * @param token The token to save. + * @return The saved token. + */ + def save(token: AuthToken) = { + tokens += (token.id -> token) + Future.successful(token) + } + + /** + * Removes the token for the given ID. + * + * @param id The ID for which the token should be removed. + * @return A future to wait for the process to be completed. + */ + def remove(id: UUID) = { + tokens -= id + Future.successful(()) + } +} + +/** + * The companion object. + */ +object AuthTokenDAOImpl { + + /** + * The list of tokens. + */ + val tokens: mutable.HashMap[UUID, AuthToken] = mutable.HashMap() +} diff --git a/src/main/g8/app/models/daos/UserDAO.scala b/src/main/g8/app/models/daos/UserDAO.scala new file mode 100644 index 0000000..be0ecb0 --- /dev/null +++ b/src/main/g8/app/models/daos/UserDAO.scala @@ -0,0 +1,38 @@ +package models.daos + +import java.util.UUID + +import com.mohiva.play.silhouette.api.LoginInfo +import models.User + +import scala.concurrent.Future + +/** + * Give access to the user object. + */ +trait UserDAO { + + /** + * Finds a user by its login info. + * + * @param loginInfo The login info of the user to find. + * @return The found user or None if no user for the given login info could be found. + */ + def find(loginInfo: LoginInfo): Future[Option[User]] + + /** + * Finds a user by its user ID. + * + * @param userID The ID of the user to find. + * @return The found user or None if no user for the given ID could be found. + */ + def find(userID: UUID): Future[Option[User]] + + /** + * Saves a user. + * + * @param user The user to save. + * @return The saved user. + */ + def save(user: User): Future[User] +} diff --git a/src/main/g8/app/models/daos/UserDAOImpl.scala b/src/main/g8/app/models/daos/UserDAOImpl.scala new file mode 100644 index 0000000..54d6cc0 --- /dev/null +++ b/src/main/g8/app/models/daos/UserDAOImpl.scala @@ -0,0 +1,56 @@ +package models.daos + +import java.util.UUID + +import com.mohiva.play.silhouette.api.LoginInfo +import models.User +import models.daos.UserDAOImpl._ + +import scala.collection.mutable +import scala.concurrent.Future + +/** + * Give access to the user object. + */ +class UserDAOImpl extends UserDAO { + + /** + * Finds a user by its login info. + * + * @param loginInfo The login info of the user to find. + * @return The found user or None if no user for the given login info could be found. + */ + def find(loginInfo: LoginInfo) = Future.successful( + users.find { case (_, user) => user.loginInfo == loginInfo }.map(_._2) + ) + + /** + * Finds a user by its user ID. + * + * @param userID The ID of the user to find. + * @return The found user or None if no user for the given ID could be found. + */ + def find(userID: UUID) = Future.successful(users.get(userID)) + + /** + * Saves a user. + * + * @param user The user to save. + * @return The saved user. + */ + def save(user: User) = { + users += (user.userID -> user) + Future.successful(user) + } +} + +/** + * The companion object. + */ +object UserDAOImpl { + + /** + * The list of users. + */ + val users: mutable.HashMap[UUID, User] = mutable.HashMap() +} diff --git a/src/main/g8/app/models/services/AuthTokenService.scala b/src/main/g8/app/models/services/AuthTokenService.scala new file mode 100644 index 0000000..a26b02a --- /dev/null +++ b/src/main/g8/app/models/services/AuthTokenService.scala @@ -0,0 +1,39 @@ +package models.services + +import java.util.UUID + +import models.AuthToken + +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.language.postfixOps + +/** + * Handles actions to auth tokens. + */ +trait AuthTokenService { + + /** + * Creates a new auth token and saves it in the backing store. + * + * @param userID The user ID for which the token should be created. + * @param expiry The duration a token expires. + * @return The saved auth token. + */ + def create(userID: UUID, expiry: FiniteDuration = 5 minutes): Future[AuthToken] + + /** + * Validates a token ID. + * + * @param id The token ID to validate. + * @return The token if it's valid, None otherwise. + */ + def validate(id: UUID): Future[Option[AuthToken]] + + /** + * Cleans expired tokens. + * + * @return The list of deleted tokens. + */ + def clean: Future[Seq[AuthToken]] +} diff --git a/src/main/g8/app/models/services/AuthTokenServiceImpl.scala b/src/main/g8/app/models/services/AuthTokenServiceImpl.scala new file mode 100644 index 0000000..11ab9fe --- /dev/null +++ b/src/main/g8/app/models/services/AuthTokenServiceImpl.scala @@ -0,0 +1,60 @@ +package models.services + +import java.util.UUID +import javax.inject.Inject + +import com.mohiva.play.silhouette.api.util.Clock +import models.AuthToken +import models.daos.AuthTokenDAO +import org.joda.time.DateTimeZone + +import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.duration._ +import scala.language.postfixOps + +/** + * Handles actions to auth tokens. + * + * @param authTokenDAO The auth token DAO implementation. + * @param clock The clock instance. + * @param ex The execution context. + */ +class AuthTokenServiceImpl @Inject() ( + authTokenDAO: AuthTokenDAO, + clock: Clock +)( + implicit + ex: ExecutionContext +) extends AuthTokenService { + + /** + * Creates a new auth token and saves it in the backing store. + * + * @param userID The user ID for which the token should be created. + * @param expiry The duration a token expires. + * @return The saved auth token. + */ + def create(userID: UUID, expiry: FiniteDuration = 5 minutes) = { + val token = AuthToken(UUID.randomUUID(), userID, clock.now.withZone(DateTimeZone.UTC).plusSeconds(expiry.toSeconds.toInt)) + authTokenDAO.save(token) + } + + /** + * Validates a token ID. + * + * @param id The token ID to validate. + * @return The token if it's valid, None otherwise. + */ + def validate(id: UUID) = authTokenDAO.find(id) + + /** + * Cleans expired tokens. + * + * @return The list of deleted tokens. + */ + def clean = authTokenDAO.findExpired(clock.now.withZone(DateTimeZone.UTC)).flatMap { tokens => + Future.sequence(tokens.map { token => + authTokenDAO.remove(token.id).map(_ => token) + }) + } +} diff --git a/src/main/g8/app/models/services/UserService.scala b/src/main/g8/app/models/services/UserService.scala new file mode 100644 index 0000000..d263012 --- /dev/null +++ b/src/main/g8/app/models/services/UserService.scala @@ -0,0 +1,41 @@ +package models.services + +import java.util.UUID + +import com.mohiva.play.silhouette.api.services.IdentityService +import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile +import models.User + +import scala.concurrent.Future + +/** + * Handles actions to users. + */ +trait UserService extends IdentityService[User] { + + /** + * Retrieves a user that matches the specified ID. + * + * @param id The ID to retrieve a user. + * @return The retrieved user or None if no user could be retrieved for the given ID. + */ + def retrieve(id: UUID): Future[Option[User]] + + /** + * Saves a user. + * + * @param user The user to save. + * @return The saved user. + */ + def save(user: User): Future[User] + + /** + * Saves the social profile for a user. + * + * If a user exists for this profile then update the user, otherwise create a new user with the given profile. + * + * @param profile The social profile to save. + * @return The user for whom the profile was saved. + */ + def save(profile: CommonSocialProfile): Future[User] +} diff --git a/src/main/g8/app/models/services/UserServiceImpl.scala b/src/main/g8/app/models/services/UserServiceImpl.scala new file mode 100644 index 0000000..c914451 --- /dev/null +++ b/src/main/g8/app/models/services/UserServiceImpl.scala @@ -0,0 +1,76 @@ +package models.services + +import java.util.UUID +import javax.inject.Inject + +import com.mohiva.play.silhouette.api.LoginInfo +import com.mohiva.play.silhouette.impl.providers.CommonSocialProfile +import models.User +import models.daos.UserDAO + +import scala.concurrent.{ ExecutionContext, Future } + +/** + * Handles actions to users. + * + * @param userDAO The user DAO implementation. + * @param ex The execution context. + */ +class UserServiceImpl @Inject() (userDAO: UserDAO)(implicit ex: ExecutionContext) extends UserService { + + /** + * Retrieves a user that matches the specified ID. + * + * @param id The ID to retrieve a user. + * @return The retrieved user or None if no user could be retrieved for the given ID. + */ + def retrieve(id: UUID) = userDAO.find(id) + + /** + * Retrieves a user that matches the specified login info. + * + * @param loginInfo The login info to retrieve a user. + * @return The retrieved user or None if no user could be retrieved for the given login info. + */ + def retrieve(loginInfo: LoginInfo): Future[Option[User]] = userDAO.find(loginInfo) + + /** + * Saves a user. + * + * @param user The user to save. + * @return The saved user. + */ + def save(user: User) = userDAO.save(user) + + /** + * Saves the social profile for a user. + * + * If a user exists for this profile then update the user, otherwise create a new user with the given profile. + * + * @param profile The social profile to save. + * @return The user for whom the profile was saved. + */ + def save(profile: CommonSocialProfile) = { + userDAO.find(profile.loginInfo).flatMap { + case Some(user) => // Update user with profile + userDAO.save(user.copy( + firstName = profile.firstName, + lastName = profile.lastName, + fullName = profile.fullName, + email = profile.email, + avatarURL = profile.avatarURL + )) + case None => // Insert a new user + userDAO.save(User( + userID = UUID.randomUUID(), + loginInfo = profile.loginInfo, + firstName = profile.firstName, + lastName = profile.lastName, + fullName = profile.fullName, + email = profile.email, + avatarURL = profile.avatarURL, + activated = true + )) + } + } +} diff --git a/src/main/g8/app/modules/BaseModule.scala b/src/main/g8/app/modules/BaseModule.scala new file mode 100644 index 0000000..2bb22a0 --- /dev/null +++ b/src/main/g8/app/modules/BaseModule.scala @@ -0,0 +1,20 @@ +package modules + +import com.google.inject.AbstractModule +import models.daos.{ AuthTokenDAO, AuthTokenDAOImpl } +import models.services.{ AuthTokenService, AuthTokenServiceImpl } +import net.codingwell.scalaguice.ScalaModule + +/** + * The base Guice module. + */ +class BaseModule extends AbstractModule with ScalaModule { + + /** + * Configures the module. + */ + override def configure(): Unit = { + bind[AuthTokenDAO].to[AuthTokenDAOImpl] + bind[AuthTokenService].to[AuthTokenServiceImpl] + } +} diff --git a/src/main/g8/app/modules/JobModule.scala b/src/main/g8/app/modules/JobModule.scala new file mode 100644 index 0000000..c74e22f --- /dev/null +++ b/src/main/g8/app/modules/JobModule.scala @@ -0,0 +1,19 @@ +package modules + +import jobs.{ AuthTokenCleaner, Scheduler } +import net.codingwell.scalaguice.ScalaModule +import play.api.libs.concurrent.AkkaGuiceSupport + +/** + * The job module. + */ +class JobModule extends ScalaModule with AkkaGuiceSupport { + + /** + * Configures the module. + */ + override def configure() = { + bindActor[AuthTokenCleaner]("auth-token-cleaner") + bind[Scheduler].asEagerSingleton() + } +} diff --git a/src/main/g8/app/modules/SilhouetteModule.scala b/src/main/g8/app/modules/SilhouetteModule.scala new file mode 100644 index 0000000..1431e6c --- /dev/null +++ b/src/main/g8/app/modules/SilhouetteModule.scala @@ -0,0 +1,475 @@ +package modules + +import com.google.inject.name.Named +import com.google.inject.{ AbstractModule, Provides } +import com.mohiva.play.silhouette.api.actions.{ SecuredErrorHandler, UnsecuredErrorHandler } +import com.mohiva.play.silhouette.api.crypto._ +import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository +import com.mohiva.play.silhouette.api.services._ +import com.mohiva.play.silhouette.api.util._ +import com.mohiva.play.silhouette.api.{ Environment, EventBus, Silhouette, SilhouetteProvider } +import com.mohiva.play.silhouette.crypto.{ JcaCrypter, JcaCrypterSettings, JcaSigner, JcaSignerSettings } +import com.mohiva.play.silhouette.impl.authenticators._ +import com.mohiva.play.silhouette.impl.providers._ +import com.mohiva.play.silhouette.impl.providers.oauth1._ +import com.mohiva.play.silhouette.impl.providers.oauth1.secrets.{ CookieSecretProvider, CookieSecretSettings } +import com.mohiva.play.silhouette.impl.providers.oauth1.services.PlayOAuth1Service +import com.mohiva.play.silhouette.impl.providers.oauth2._ +import com.mohiva.play.silhouette.impl.providers.openid.YahooProvider +import com.mohiva.play.silhouette.impl.providers.openid.services.PlayOpenIDService +import com.mohiva.play.silhouette.impl.providers.state.{ CsrfStateItemHandler, CsrfStateSettings } +import com.mohiva.play.silhouette.impl.services._ +import com.mohiva.play.silhouette.impl.util._ +import com.mohiva.play.silhouette.password.{ BCryptPasswordHasher, BCryptSha256PasswordHasher } +import com.mohiva.play.silhouette.persistence.daos.{ DelegableAuthInfoDAO, InMemoryAuthInfoDAO } +import com.mohiva.play.silhouette.persistence.repositories.DelegableAuthInfoRepository +import com.typesafe.config.Config +import models.daos._ +import models.services.{ UserService, UserServiceImpl } +import net.ceedubs.ficus.Ficus._ +import net.ceedubs.ficus.readers.ArbitraryTypeReader._ +import net.ceedubs.ficus.readers.ValueReader +import net.codingwell.scalaguice.ScalaModule +import play.api.Configuration +import play.api.libs.openid.OpenIdClient +import play.api.libs.ws.WSClient +import play.api.mvc.{ Cookie, CookieHeaderEncoding } +import utils.auth.{ CustomSecuredErrorHandler, CustomUnsecuredErrorHandler, DefaultEnv } + +import scala.concurrent.ExecutionContext.Implicits.global + +/** + * The Guice module which wires all Silhouette dependencies. + */ +class SilhouetteModule extends AbstractModule with ScalaModule { + + /** + * A very nested optional reader, to support these cases: + * Not set, set None, will use default ('Lax') + * Set to null, set Some(None), will use 'No Restriction' + * Set to a string value try to match, Some(Option(string)) + */ + implicit val sameSiteReader: ValueReader[Option[Option[Cookie.SameSite]]] = + (config: Config, path: String) => { + if (config.hasPathOrNull(path)) { + if (config.getIsNull(path)) + Some(None) + else { + Some(Cookie.SameSite.parse(config.getString(path))) + } + } else { + None + } + } + + /** + * Configures the module. + */ + override def configure() { + bind[Silhouette[DefaultEnv]].to[SilhouetteProvider[DefaultEnv]] + bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler] + bind[SecuredErrorHandler].to[CustomSecuredErrorHandler] + bind[UserService].to[UserServiceImpl] + bind[UserDAO].to[UserDAOImpl] + bind[CacheLayer].to[PlayCacheLayer] + bind[IDGenerator].toInstance(new SecureRandomIDGenerator()) + bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false)) + bind[EventBus].toInstance(EventBus()) + bind[Clock].toInstance(Clock()) + + // Replace this with the bindings to your concrete DAOs + bind[DelegableAuthInfoDAO[GoogleTotpInfo]].toInstance(new InMemoryAuthInfoDAO[GoogleTotpInfo]) + bind[DelegableAuthInfoDAO[PasswordInfo]].toInstance(new InMemoryAuthInfoDAO[PasswordInfo]) + bind[DelegableAuthInfoDAO[OAuth1Info]].toInstance(new InMemoryAuthInfoDAO[OAuth1Info]) + bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info]) + bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo]) + } + + /** + * Provides the HTTP layer implementation. + * + * @param client Play's WS client. + * @return The HTTP layer implementation. + */ + @Provides + def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client) + + /** + * Provides the Silhouette environment. + * + * @param userService The user service implementation. + * @param authenticatorService The authentication service implementation. + * @param eventBus The event bus instance. + * @return The Silhouette environment. + */ + @Provides + def provideEnvironment( + userService: UserService, + authenticatorService: AuthenticatorService[CookieAuthenticator], + eventBus: EventBus): Environment[DefaultEnv] = { + + Environment[DefaultEnv]( + userService, + authenticatorService, + Seq(), + eventBus + ) + } + + /** + * Provides the social provider registry. + * + * @param facebookProvider The Facebook provider implementation. + * @param googleProvider The Google provider implementation. + * @param vkProvider The VK provider implementation. + * @param twitterProvider The Twitter provider implementation. + * @param xingProvider The Xing provider implementation. + * @param yahooProvider The Yahoo provider implementation. + * @return The Silhouette environment. + */ + @Provides + def provideSocialProviderRegistry( + facebookProvider: FacebookProvider, + googleProvider: GoogleProvider, + vkProvider: VKProvider, + twitterProvider: TwitterProvider, + xingProvider: XingProvider, + yahooProvider: YahooProvider): SocialProviderRegistry = { + + SocialProviderRegistry(Seq( + googleProvider, + facebookProvider, + twitterProvider, + vkProvider, + xingProvider, + yahooProvider + )) + } + + /** + * Provides the signer for the OAuth1 token secret provider. + * + * @param configuration The Play configuration. + * @return The signer for the OAuth1 token secret provider. + */ + @Provides @Named("oauth1-token-secret-signer") + def provideOAuth1TokenSecretSigner(configuration: Configuration): Signer = { + val config = configuration.underlying.as[JcaSignerSettings]("silhouette.oauth1TokenSecretProvider.signer") + + new JcaSigner(config) + } + + /** + * Provides the crypter for the OAuth1 token secret provider. + * + * @param configuration The Play configuration. + * @return The crypter for the OAuth1 token secret provider. + */ + @Provides @Named("oauth1-token-secret-crypter") + def provideOAuth1TokenSecretCrypter(configuration: Configuration): Crypter = { + val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.oauth1TokenSecretProvider.crypter") + + new JcaCrypter(config) + } + + /** + * Provides the signer for the CSRF state item handler. + * + * @param configuration The Play configuration. + * @return The signer for the CSRF state item handler. + */ + @Provides @Named("csrf-state-item-signer") + def provideCSRFStateItemSigner(configuration: Configuration): Signer = { + val config = configuration.underlying.as[JcaSignerSettings]("silhouette.csrfStateItemHandler.signer") + + new JcaSigner(config) + } + + /** + * Provides the signer for the social state handler. + * + * @param configuration The Play configuration. + * @return The signer for the social state handler. + */ + @Provides @Named("social-state-signer") + def provideSocialStateSigner(configuration: Configuration): Signer = { + val config = configuration.underlying.as[JcaSignerSettings]("silhouette.socialStateHandler.signer") + + new JcaSigner(config) + } + + /** + * Provides the signer for the authenticator. + * + * @param configuration The Play configuration. + * @return The signer for the authenticator. + */ + @Provides @Named("authenticator-signer") + def provideAuthenticatorSigner(configuration: Configuration): Signer = { + val config = configuration.underlying.as[JcaSignerSettings]("silhouette.authenticator.signer") + + new JcaSigner(config) + } + + /** + * Provides the crypter for the authenticator. + * + * @param configuration The Play configuration. + * @return The crypter for the authenticator. + */ + @Provides @Named("authenticator-crypter") + def provideAuthenticatorCrypter(configuration: Configuration): Crypter = { + val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.authenticator.crypter") + + new JcaCrypter(config) + } + + /** + * Provides the auth info repository. + * + * @param totpInfoDAO The implementation of the delegable totp auth info DAO. + * @param passwordInfoDAO The implementation of the delegable password auth info DAO. + * @param oauth1InfoDAO The implementation of the delegable OAuth1 auth info DAO. + * @param oauth2InfoDAO The implementation of the delegable OAuth2 auth info DAO. + * @param openIDInfoDAO The implementation of the delegable OpenID auth info DAO. + * @return The auth info repository instance. + */ + @Provides + def provideAuthInfoRepository( + totpInfoDAO: DelegableAuthInfoDAO[GoogleTotpInfo], + passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo], + oauth1InfoDAO: DelegableAuthInfoDAO[OAuth1Info], + oauth2InfoDAO: DelegableAuthInfoDAO[OAuth2Info], + openIDInfoDAO: DelegableAuthInfoDAO[OpenIDInfo]): AuthInfoRepository = { + + new DelegableAuthInfoRepository(totpInfoDAO, passwordInfoDAO, oauth1InfoDAO, oauth2InfoDAO, openIDInfoDAO) + } + + /** + * Provides the authenticator service. + * + * @param signer The signer implementation. + * @param crypter The crypter implementation. + * @param cookieHeaderEncoding Logic for encoding and decoding `Cookie` and `Set-Cookie` headers. + * @param fingerprintGenerator The fingerprint generator implementation. + * @param idGenerator The ID generator implementation. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @return The authenticator service. + */ + @Provides + def provideAuthenticatorService( + @Named("authenticator-signer") signer: Signer, + @Named("authenticator-crypter") crypter: Crypter, + cookieHeaderEncoding: CookieHeaderEncoding, + fingerprintGenerator: FingerprintGenerator, + idGenerator: IDGenerator, + configuration: Configuration, + clock: Clock): AuthenticatorService[CookieAuthenticator] = { + + val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator") + val authenticatorEncoder = new CrypterAuthenticatorEncoder(crypter) + + new CookieAuthenticatorService(config, None, signer, cookieHeaderEncoding, authenticatorEncoder, fingerprintGenerator, idGenerator, clock) + } + + /** + * Provides the avatar service. + * + * @param httpLayer The HTTP layer implementation. + * @return The avatar service implementation. + */ + @Provides + def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer) + + /** + * Provides the OAuth1 token secret provider. + * + * @param signer The signer implementation. + * @param crypter The crypter implementation. + * @param configuration The Play configuration. + * @param clock The clock instance. + * @return The OAuth1 token secret provider implementation. + */ + @Provides + def provideOAuth1TokenSecretProvider( + @Named("oauth1-token-secret-signer") signer: Signer, + @Named("oauth1-token-secret-crypter") crypter: Crypter, + configuration: Configuration, + clock: Clock): OAuth1TokenSecretProvider = { + + val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider") + new CookieSecretProvider(settings, signer, crypter, clock) + } + + /** + * Provides the CSRF state item handler. + * + * @param idGenerator The ID generator implementation. + * @param signer The signer implementation. + * @param configuration The Play configuration. + * @return The CSRF state item implementation. + */ + @Provides + def provideCsrfStateItemHandler( + idGenerator: IDGenerator, + @Named("csrf-state-item-signer") signer: Signer, + configuration: Configuration): CsrfStateItemHandler = { + val settings = configuration.underlying.as[CsrfStateSettings]("silhouette.csrfStateItemHandler") + new CsrfStateItemHandler(settings, idGenerator, signer) + } + + /** + * Provides the social state handler. + * + * @param signer The signer implementation. + * @return The social state handler implementation. + */ + @Provides + def provideSocialStateHandler( + @Named("social-state-signer") signer: Signer, + csrfStateItemHandler: CsrfStateItemHandler): SocialStateHandler = { + + new DefaultSocialStateHandler(Set(csrfStateItemHandler), signer) + } + + /** + * Provides the password hasher registry. + * + * @return The password hasher registry. + */ + @Provides + def providePasswordHasherRegistry(): PasswordHasherRegistry = { + PasswordHasherRegistry(new BCryptSha256PasswordHasher(), Seq(new BCryptPasswordHasher())) + } + + /** + * Provides the credentials provider. + * + * @param authInfoRepository The auth info repository implementation. + * @param passwordHasherRegistry The password hasher registry. + * @return The credentials provider. + */ + @Provides + def provideCredentialsProvider( + authInfoRepository: AuthInfoRepository, + passwordHasherRegistry: PasswordHasherRegistry): CredentialsProvider = { + + new CredentialsProvider(authInfoRepository, passwordHasherRegistry) + } + + /** + * Provides the TOTP provider. + * + * @return The credentials provider. + */ + @Provides + def provideTotpProvider(passwordHasherRegistry: PasswordHasherRegistry): GoogleTotpProvider = { + new GoogleTotpProvider(passwordHasherRegistry) + } + + /** + * Provides the Facebook provider. + * + * @param httpLayer The HTTP layer implementation. + * @param socialStateHandler The social state handler implementation. + * @param configuration The Play configuration. + * @return The Facebook provider. + */ + @Provides + def provideFacebookProvider( + httpLayer: HTTPLayer, + socialStateHandler: SocialStateHandler, + configuration: Configuration): FacebookProvider = { + + new FacebookProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.facebook")) + } + + /** + * Provides the Google provider. + * + * @param httpLayer The HTTP layer implementation. + * @param socialStateHandler The social state handler implementation. + * @param configuration The Play configuration. + * @return The Google provider. + */ + @Provides + def provideGoogleProvider( + httpLayer: HTTPLayer, + socialStateHandler: SocialStateHandler, + configuration: Configuration): GoogleProvider = { + + new GoogleProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.google")) + } + + /** + * Provides the VK provider. + * + * @param httpLayer The HTTP layer implementation. + * @param socialStateHandler The social state handler implementation. + * @param configuration The Play configuration. + * @return The VK provider. + */ + @Provides + def provideVKProvider( + httpLayer: HTTPLayer, + socialStateHandler: SocialStateHandler, + configuration: Configuration): VKProvider = { + + new VKProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.vk")) + } + + /** + * Provides the Twitter provider. + * + * @param httpLayer The HTTP layer implementation. + * @param tokenSecretProvider The token secret provider implementation. + * @param configuration The Play configuration. + * @return The Twitter provider. + */ + @Provides + def provideTwitterProvider( + httpLayer: HTTPLayer, + tokenSecretProvider: OAuth1TokenSecretProvider, + configuration: Configuration): TwitterProvider = { + + val settings = configuration.underlying.as[OAuth1Settings]("silhouette.twitter") + new TwitterProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) + } + + /** + * Provides the Xing provider. + * + * @param httpLayer The HTTP layer implementation. + * @param tokenSecretProvider The token secret provider implementation. + * @param configuration The Play configuration. + * @return The Xing provider. + */ + @Provides + def provideXingProvider( + httpLayer: HTTPLayer, + tokenSecretProvider: OAuth1TokenSecretProvider, + configuration: Configuration): XingProvider = { + + val settings = configuration.underlying.as[OAuth1Settings]("silhouette.xing") + new XingProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings) + } + + /** + * Provides the Yahoo provider. + * + * @param httpLayer The HTTP layer implementation. + * @param client The OpenID client implementation. + * @param configuration The Play configuration. + * @return The Yahoo provider. + */ + @Provides + def provideYahooProvider( + httpLayer: HTTPLayer, + client: OpenIdClient, + configuration: Configuration): YahooProvider = { + + val settings = configuration.underlying.as[OpenIDSettings]("silhouette.yahoo") + new YahooProvider(httpLayer, new PlayOpenIDService(client, settings), settings) + } +} diff --git a/src/main/g8/app/utils/Filters.scala b/src/main/g8/app/utils/Filters.scala new file mode 100644 index 0000000..93d384a --- /dev/null +++ b/src/main/g8/app/utils/Filters.scala @@ -0,0 +1,15 @@ +package utils + +import javax.inject.Inject + +import play.api.http.HttpFilters +import play.api.mvc.EssentialFilter +import play.filters.csrf.CSRFFilter +import play.filters.headers.SecurityHeadersFilter + +/** + * Provides filters. + */ +class Filters @Inject() (csrfFilter: CSRFFilter, securityHeadersFilter: SecurityHeadersFilter) extends HttpFilters { + override def filters: Seq[EssentialFilter] = Seq(csrfFilter, securityHeadersFilter) +} diff --git a/src/main/g8/app/utils/Logger.scala b/src/main/g8/app/utils/Logger.scala new file mode 100644 index 0000000..191c223 --- /dev/null +++ b/src/main/g8/app/utils/Logger.scala @@ -0,0 +1,12 @@ +package utils + +/** + * Implement this to get a named logger in scope. + */ +trait Logger { + + /** + * A named logger instance. + */ + val logger = play.api.Logger(this.getClass) +} diff --git a/src/main/g8/app/utils/auth/CustomSecuredErrorHandler.scala b/src/main/g8/app/utils/auth/CustomSecuredErrorHandler.scala new file mode 100644 index 0000000..89ba1c3 --- /dev/null +++ b/src/main/g8/app/utils/auth/CustomSecuredErrorHandler.scala @@ -0,0 +1,42 @@ +package utils.auth + +import javax.inject.Inject + +import com.mohiva.play.silhouette.api.actions.SecuredErrorHandler +import play.api.i18n.{ MessagesApi, I18nSupport, Messages } +import play.api.mvc.RequestHeader +import play.api.mvc.Results._ + +import scala.concurrent.Future + +/** + * Custom secured error handler. + * + * @param messagesApi The Play messages API. + */ +class CustomSecuredErrorHandler @Inject() (val messagesApi: MessagesApi) extends SecuredErrorHandler with I18nSupport { + + /** + * Called when a user is not authenticated. + * + * As defined by RFC 2616, the status code of the response should be 401 Unauthorized. + * + * @param request The request header. + * @return The result to send to the client. + */ + override def onNotAuthenticated(implicit request: RequestHeader) = { + Future.successful(Redirect(controllers.routes.SignInController.view())) + } + + /** + * Called when a user is authenticated but not authorized. + * + * As defined by RFC 2616, the status code of the response should be 403 Forbidden. + * + * @param request The request header. + * @return The result to send to the client. + */ + override def onNotAuthorized(implicit request: RequestHeader) = { + Future.successful(Redirect(controllers.routes.SignInController.view()).flashing("error" -> Messages("access.denied"))) + } +} diff --git a/src/main/g8/app/utils/auth/CustomUnsecuredErrorHandler.scala b/src/main/g8/app/utils/auth/CustomUnsecuredErrorHandler.scala new file mode 100644 index 0000000..0c27eb3 --- /dev/null +++ b/src/main/g8/app/utils/auth/CustomUnsecuredErrorHandler.scala @@ -0,0 +1,25 @@ +package utils.auth + +import com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandler +import play.api.mvc.RequestHeader +import play.api.mvc.Results._ + +import scala.concurrent.Future + +/** + * Custom unsecured error handler. + */ +class CustomUnsecuredErrorHandler extends UnsecuredErrorHandler { + + /** + * Called when a user is authenticated but not authorized. + * + * As defined by RFC 2616, the status code of the response should be 403 Forbidden. + * + * @param request The request header. + * @return The result to send to the client. + */ + override def onNotAuthorized(implicit request: RequestHeader) = { + Future.successful(Redirect(controllers.routes.ApplicationController.index())) + } +} diff --git a/src/main/g8/app/utils/auth/Env.scala b/src/main/g8/app/utils/auth/Env.scala new file mode 100644 index 0000000..a88c872 --- /dev/null +++ b/src/main/g8/app/utils/auth/Env.scala @@ -0,0 +1,13 @@ +package utils.auth + +import com.mohiva.play.silhouette.api.Env +import com.mohiva.play.silhouette.impl.authenticators.CookieAuthenticator +import models.User + +/** + * The default env. + */ +trait DefaultEnv extends Env { + type I = User + type A = CookieAuthenticator +} diff --git a/src/main/g8/app/utils/auth/WithProvider.scala b/src/main/g8/app/utils/auth/WithProvider.scala new file mode 100644 index 0000000..329626a --- /dev/null +++ b/src/main/g8/app/utils/auth/WithProvider.scala @@ -0,0 +1,32 @@ +package utils.auth + +import com.mohiva.play.silhouette.api.{ Authenticator, Authorization } +import models.User +import play.api.mvc.Request + +import scala.concurrent.Future + +/** + * Grants only access if a user has authenticated with the given provider. + * + * @param provider The provider ID the user must authenticated with. + * @tparam A The type of the authenticator. + */ +case class WithProvider[A <: Authenticator](provider: String) extends Authorization[User, A] { + + /** + * Indicates if a user is authorized to access an action. + * + * @param user The usr object. + * @param authenticator The authenticator instance. + * @param request The current request. + * @tparam B The type of the request body. + * @return True if the user is authorized, false otherwise. + */ + override def isAuthorized[B](user: User, authenticator: A)( + implicit + request: Request[B]): Future[Boolean] = { + + Future.successful(user.loginInfo.providerID == provider) + } +} diff --git a/src/main/g8/app/utils/route/Binders.scala b/src/main/g8/app/utils/route/Binders.scala new file mode 100644 index 0000000..740287a --- /dev/null +++ b/src/main/g8/app/utils/route/Binders.scala @@ -0,0 +1,24 @@ +package utils.route + +import java.util.UUID + +import play.api.mvc.PathBindable + +/** + * Some route binders. + */ +object Binders { + + /** + * A `java.util.UUID` bindable. + */ + implicit object UUIDPathBindable extends PathBindable[UUID] { + def bind(key: String, value: String) = try { + Right(UUID.fromString(value)) + } catch { + case _: Exception => Left("Cannot parse parameter '" + key + "' with value '" + value + "' as UUID") + } + + def unbind(key: String, value: UUID): String = value.toString + } +} diff --git a/src/main/g8/app/views/activateAccount.scala.html b/src/main/g8/app/views/activateAccount.scala.html new file mode 100644 index 0000000..c9db833 --- /dev/null +++ b/src/main/g8/app/views/activateAccount.scala.html @@ -0,0 +1,19 @@ +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import play.twirl.api.Html +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder + +@(email: String)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@main(messages("activate.account.title")) { +
+ @messages("activate.account") +
+

@messages("activate.account.text1")

+

@email

+

@messages("activate.account.text2")

+

@Html(messages("activate.account.text3", controllers.routes.ActivateAccountController.send(helper.urlEncode(email))))

+
+
+} diff --git a/src/main/g8/app/views/changePassword.scala.html b/src/main/g8/app/views/changePassword.scala.html new file mode 100644 index 0000000..2552da9 --- /dev/null +++ b/src/main/g8/app/views/changePassword.scala.html @@ -0,0 +1,27 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import b3.inline.fieldConstructor + +@(changePasswordForm: Form[(String, String)], user: models.User)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +@main(messages("change.password.title"), Some(user)) { +
+ @messages("change.password") + @helper.form(action = controllers.routes.ChangePasswordController.submit, 'autocomplete -> "off") { +

@messages("strong.password.info")

+ @helper.CSRF.formField + @b3.password(changePasswordForm("current-password"), '_hiddenLabel -> messages("current.password"), 'placeholder -> messages("current.password"), 'class -> "form-control input-lg") + @passwordStrength(changePasswordForm("new-password"), '_hiddenLabel -> messages("new.password"), 'placeholder -> messages("new.password"), 'class -> "form-control input-lg") +
+
+ +
+
+ } +
+} diff --git a/src/main/g8/app/views/emails/activateAccount.scala.html b/src/main/g8/app/views/emails/activateAccount.scala.html new file mode 100644 index 0000000..ce46247 --- /dev/null +++ b/src/main/g8/app/views/emails/activateAccount.scala.html @@ -0,0 +1,11 @@ +@import play.api.i18n.Messages +@import play.twirl.api.Html + +@(user: models.User, url: String)(implicit messages: Messages) + + + +

@messages("email.activate.account.hello", user.name.getOrElse("user"))

+

@Html(messages("email.activate.account.html.text", url))

+ + diff --git a/src/main/g8/app/views/emails/activateAccount.scala.txt b/src/main/g8/app/views/emails/activateAccount.scala.txt new file mode 100644 index 0000000..2cd4b2c --- /dev/null +++ b/src/main/g8/app/views/emails/activateAccount.scala.txt @@ -0,0 +1,6 @@ +@import play.api.i18n.Messages + +@(user: models.User, url: String)(implicit messages: Messages) +@messages("email.activate.account.hello", user.name.getOrElse("user")) + +@messages("email.activate.account.txt.text", url) diff --git a/src/main/g8/app/views/emails/alreadySignedUp.scala.html b/src/main/g8/app/views/emails/alreadySignedUp.scala.html new file mode 100644 index 0000000..66a3ed9 --- /dev/null +++ b/src/main/g8/app/views/emails/alreadySignedUp.scala.html @@ -0,0 +1,11 @@ +@import play.api.i18n.Messages +@import play.twirl.api.Html + +@(user: models.User, url: String)(implicit messages: Messages) + + + +

@messages("email.already.signed.up.hello", user.name.getOrElse("user"))

+

@Html(messages("email.already.signed.up.html.text", url))

+ + diff --git a/src/main/g8/app/views/emails/alreadySignedUp.scala.txt b/src/main/g8/app/views/emails/alreadySignedUp.scala.txt new file mode 100644 index 0000000..bd40e9b --- /dev/null +++ b/src/main/g8/app/views/emails/alreadySignedUp.scala.txt @@ -0,0 +1,6 @@ +@import play.api.i18n.Messages + +@(user: models.User, url: String)(implicit messages: Messages) +@messages("email.already.signed.up.hello", user.name.getOrElse("user")) + +@messages("email.already.signed.up.txt.text", url) diff --git a/src/main/g8/app/views/emails/resetPassword.scala.html b/src/main/g8/app/views/emails/resetPassword.scala.html new file mode 100644 index 0000000..126ce68 --- /dev/null +++ b/src/main/g8/app/views/emails/resetPassword.scala.html @@ -0,0 +1,11 @@ +@import play.api.i18n.Messages +@import play.twirl.api.Html + +@(user: models.User, url: String)(implicit messages: Messages) + + + +

@messages("email.reset.password.hello", user.name.getOrElse("user"))

+

@Html(messages("email.reset.password.html.text", url))

+ + diff --git a/src/main/g8/app/views/emails/resetPassword.scala.txt b/src/main/g8/app/views/emails/resetPassword.scala.txt new file mode 100644 index 0000000..fae4c96 --- /dev/null +++ b/src/main/g8/app/views/emails/resetPassword.scala.txt @@ -0,0 +1,6 @@ +@import play.api.i18n.Messages + +@(user: models.User, url: String)(implicit messages: Messages) +@messages("email.reset.password.hello", user.name.getOrElse("user")) + +@messages("email.reset.password.txt.text", url) diff --git a/src/main/g8/app/views/emails/signUp.scala.html b/src/main/g8/app/views/emails/signUp.scala.html new file mode 100644 index 0000000..7663114 --- /dev/null +++ b/src/main/g8/app/views/emails/signUp.scala.html @@ -0,0 +1,11 @@ +@import play.api.i18n.Messages +@import play.twirl.api.Html + +@(user: models.User, url: String)(implicit messages: Messages) + + + +

@messages("email.sign.up.hello", user.name.getOrElse("user"))

+

@Html(messages("email.sign.up.html.text", url))

+ + diff --git a/src/main/g8/app/views/emails/signUp.scala.txt b/src/main/g8/app/views/emails/signUp.scala.txt new file mode 100644 index 0000000..37f119d --- /dev/null +++ b/src/main/g8/app/views/emails/signUp.scala.txt @@ -0,0 +1,6 @@ +@import play.api.i18n.Messages + +@(user: models.User, url: String)(implicit messages: Messages) +@messages("email.sign.up.hello", user.name.getOrElse("user")) + +@messages("email.sign.up.txt.text", url) diff --git a/src/main/g8/app/views/forgotPassword.scala.html b/src/main/g8/app/views/forgotPassword.scala.html new file mode 100644 index 0000000..c2f9359 --- /dev/null +++ b/src/main/g8/app/views/forgotPassword.scala.html @@ -0,0 +1,25 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder + +@(forgotPasswordForm: Form[String])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +@main(messages("forgot.password.title")) { +
+ @messages("forgot.password") + @helper.form(action = controllers.routes.ForgotPasswordController.submit(), 'autocomplete -> "off") { +

@messages("forgot.password.info")

+ @helper.CSRF.formField + @b3.text(forgotPasswordForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") +
+
+ +
+
+ } +
+} diff --git a/src/main/g8/app/views/home.scala.html b/src/main/g8/app/views/home.scala.html new file mode 100644 index 0000000..0954580 --- /dev/null +++ b/src/main/g8/app/views/home.scala.html @@ -0,0 +1,93 @@ +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import play.api.data._ +@import forms.TotpSetupForm.Data +@import com.mohiva.play.silhouette.impl.providers.GoogleTotpCredentials +@import com.mohiva.play.silhouette.impl.providers.GoogleTotpInfo + +@(user: models.User, totpInfoOpt: Option[GoogleTotpInfo], totpDataOpt: Option[(Form[Data], GoogleTotpCredentials)] = None)(implicit request: RequestHeader, + messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ + b3.vertical.fieldConstructor() +} + +@main(messages("home.title"), Some(user)) { +
+
+
+

@messages("welcome.signed.in")

+
+ +
+
+
+
+
+
+

@messages("first.name") + :

@user.firstName.getOrElse("None")

+
+
+

@messages("last.name") + :

@user.lastName.getOrElse("None")

+
+
+

@messages("full.name") + :

@user.fullName.getOrElse("None")

+
+
+

@messages("email") + :

@user.email.getOrElse("None")

+
+
+
+
+ @if(totpInfoOpt.nonEmpty) { +

@messages("totp.enabled.title")

+ + + + } else { + @totpDataOpt match { + case Some((totpForm, credentials)) => { +

@messages("totp.enabling.title")

+

@messages("totp.shared.key.title")

+ +

@messages("totp.recovery.tokens.title")

+
    + @for(scratchCodePlain <- credentials.scratchCodesPlain) { +
  • @{ + scratchCodePlain + }
  • + } +
+ @helper.form(action = controllers.routes.TotpController.enableTotpSubmit()) { + @helper.CSRF.formField + @b3.text(totpForm("verificationCode"), '_hiddenLabel -> messages("totp.verification.code "), 'placeholder -> messages("totp.verification.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") + @b3.hidden(totpForm("sharedKey")) + @helper.repeat(totpForm("scratchCodes"), min = 1) { scratchCodeField => + @b3.hidden(scratchCodeField("hasher")) + @b3.hidden(scratchCodeField("password")) + @b3.hidden(scratchCodeField("salt")) + } +
+
+ +
+
+ } + } + case None => { +

@messages("totp.disabled.title")

+ + + + } + } + } +
+
+} diff --git a/src/main/g8/app/views/main.scala.html b/src/main/g8/app/views/main.scala.html new file mode 100644 index 0000000..c601ae2 --- /dev/null +++ b/src/main/g8/app/views/main.scala.html @@ -0,0 +1,88 @@ +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import play.twirl.api.Html +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder + +@(title: String, user: Option[models.User] = None)(content: Html)(implicit request: RequestHeader, messages: Messages, assets: AssetsFinder, webJarsUtil: WebJarsUtil) + + + + + + + + + @title + + + @webJarsUtil.locate("bootstrap.min.css").css() + @webJarsUtil.locate("bootstrap-theme.min.css").css() + + + + + + +
+
+ @request.flash.get("error").map { msg => +
+ × + @messages("error") @msg +
+ } + @request.flash.get("info").map { msg => +
+ × + @messages("info") @msg +
+ } + @request.flash.get("success").map { msg => +
+ × + @messages("success") @msg +
+ } + @content +
+
+ @webJarsUtil.locate("jquery.min.js").script() + @webJarsUtil.locate("bootstrap.min.js").script() + + + + diff --git a/src/main/g8/app/views/passwordStrength.scala.html b/src/main/g8/app/views/passwordStrength.scala.html new file mode 100644 index 0000000..7748402 --- /dev/null +++ b/src/main/g8/app/views/passwordStrength.scala.html @@ -0,0 +1,13 @@ +@import play.api.data.Field +@import play.api.i18n.MessagesProvider + +@(field: Field, options: (Symbol, Any)*)(implicit messagesProvider: MessagesProvider) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +
+ @b3.password(field, (Symbol("data-pwd"), "true") +: options:_*) + + +

+
diff --git a/src/main/g8/app/views/resetPassword.scala.html b/src/main/g8/app/views/resetPassword.scala.html new file mode 100644 index 0000000..855670c --- /dev/null +++ b/src/main/g8/app/views/resetPassword.scala.html @@ -0,0 +1,24 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import java.util.UUID + +@(form: Form[String], token: UUID)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@main(messages("reset.password.title")) { +
+ @messages("reset.password") + @helper.form(action = controllers.routes.ResetPasswordController.submit(token), 'autocomplete -> "off") { +

@messages("strong.password.info")

+ @helper.CSRF.formField + @passwordStrength(form("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") +
+
+ +
+
+ } +
+} diff --git a/src/main/g8/app/views/signIn.scala.html b/src/main/g8/app/views/signIn.scala.html new file mode 100644 index 0000000..14be42a --- /dev/null +++ b/src/main/g8/app/views/signIn.scala.html @@ -0,0 +1,44 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import com.mohiva.play.silhouette.impl.providers.SocialProviderRegistry +@import forms.SignInForm.Data + +@(signInForm: Form[Data], socialProviders: SocialProviderRegistry)(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +@main(messages("sign.in.title")) { +
+ @messages("sign.in.credentials") + @helper.form(action = controllers.routes.SignInController.submit()) { + @helper.CSRF.formField + @b3.email(signInForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") + @b3.password(signInForm("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") + @b3.checkbox(signInForm("rememberMe"), '_text -> messages("remember.me"), 'checked -> true) +
+
+ +
+
+ } + + + + @if(socialProviders.providers.nonEmpty) { + + } + +
+} diff --git a/src/main/g8/app/views/signUp.scala.html b/src/main/g8/app/views/signUp.scala.html new file mode 100644 index 0000000..c4b6d5f --- /dev/null +++ b/src/main/g8/app/views/signUp.scala.html @@ -0,0 +1,31 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import forms.SignUpForm.Data + +@(signUpForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +@main(messages("sign.up.title")) { +
+ @messages("sign.up.account") + @helper.form(action = controllers.routes.SignUpController.submit()) { + @helper.CSRF.formField + @b3.text(signUpForm("firstName"), '_hiddenLabel -> messages("first.name"), 'placeholder -> messages("first.name"), 'class -> "form-control input-lg") + @b3.text(signUpForm("lastName"), '_hiddenLabel -> messages("last.name"), 'placeholder -> messages("last.name"), 'class -> "form-control input-lg") + @b3.text(signUpForm("email"), '_hiddenLabel -> messages("email"), 'placeholder -> messages("email"), 'class -> "form-control input-lg") + @passwordStrength(signUpForm("password"), '_hiddenLabel -> messages("password"), 'placeholder -> messages("password"), 'class -> "form-control input-lg") +
+
+ +
+
+ + } +
+} diff --git a/src/main/g8/app/views/totp.scala.html b/src/main/g8/app/views/totp.scala.html new file mode 100644 index 0000000..b391b76 --- /dev/null +++ b/src/main/g8/app/views/totp.scala.html @@ -0,0 +1,35 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import forms.TotpForm.Data +@import forms.TotpRecoveryForm +@import java.util.UUID + +@(totpForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + +@implicitFieldConstructor = @{ b3.vertical.fieldConstructor() } + +@main(messages("sign.in.title")) { +
+ @messages("sign.in.totp") + @helper.form(action = controllers.routes.TotpController.submit()) { + @helper.CSRF.formField + @b3.text(totpForm("verificationCode"), '_hiddenLabel -> messages("totp.verification.code"), 'placeholder -> messages("totp.verification.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") + @b3.hidden(totpForm("userID")) + @b3.hidden(totpForm("sharedKey")) + @b3.hidden(totpForm("rememberMe")) +
+
+ +
+
+ } + + @messages("totp.open.the.app.for.2fa") +
+

@messages("totp.dont.have.your.phone") @messages("totp.use.recovery.code")

+
+
+} diff --git a/src/main/g8/app/views/totpRecovery.scala.html b/src/main/g8/app/views/totpRecovery.scala.html new file mode 100644 index 0000000..b898edc --- /dev/null +++ b/src/main/g8/app/views/totpRecovery.scala.html @@ -0,0 +1,34 @@ +@import play.api.data.Form +@import play.api.i18n.Messages +@import play.api.mvc.RequestHeader +@import org.webjars.play.WebJarsUtil +@import controllers.AssetsFinder +@import forms.TotpRecoveryForm.Data + +@(totpRecoveryForm: Form[Data])(implicit request: RequestHeader, messages: Messages, webJarsUtil: WebJarsUtil, assets: AssetsFinder) + + @implicitFieldConstructor = @{ + b3.vertical.fieldConstructor() + } + + @main(messages("sign.in.title")) { +
+ @messages("sign.in.totp.recovery") + @helper.form(action = controllers.routes.TotpRecoveryController.submit()) { + @helper.CSRF.formField + @b3.text(totpRecoveryForm("recoveryCode"), '_hiddenLabel -> messages("totp.recovery.code"), 'placeholder -> messages("totp.recovery.code"), 'autocomplete -> "off", 'class -> "form-control input-lg") + @b3.hidden(totpRecoveryForm("userID")) + @b3.hidden(totpRecoveryForm("sharedKey")) + @b3.hidden(totpRecoveryForm("rememberMe")) +
+
+ +
+
+ } + +
+

@messages("totp.lost.your.recovery.codes") @messages("totp.contact.support")

+
+
+ } diff --git a/src/main/g8/conf/application.conf b/src/main/g8/conf/application.conf new file mode 100644 index 0000000..7fb9279 --- /dev/null +++ b/src/main/g8/conf/application.conf @@ -0,0 +1,69 @@ +# This is the main configuration file for the application. +# ~~~~~ + +# Secret key +# ~~~~~ +# The secret key is used to secure cryptographics functions. +# If you deploy your application to several instances be sure to use the same key! +play.http.secret.key="changeme" + +# The application languages +# ~~~~~ +play.i18n.langs=["en"] + +# Registers the request handler +# ~~~~~ +play.http.requestHandler = "play.api.http.DefaultHttpRequestHandler" + +# Registers the filters +# ~~~~~ +play.http.filters = "utils.Filters" + +# The application DI modules +# ~~~~~ +play.modules.enabled += "modules.BaseModule" +play.modules.enabled += "modules.JobModule" +play.modules.enabled += "modules.SilhouetteModule" +play.modules.enabled += "play.api.libs.mailer.MailerModule" + +play.modules.disabled += "com.mohiva.play.silhouette.api.actions.SecuredErrorHandlerModule" +play.modules.disabled += "com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandlerModule" + +# The asset configuration +# ~~~~~ +play.assets { + path = "/public" + urlPrefix = "/assets" +} + +# Akka config +akka { + loglevel = "INFO" + jvm-exit-on-fatal-error=off + + # Auth token cleaner + quartz.schedules.AuthTokenCleaner { + expression = "0 0 */1 * * ?" + timezone = "UTC" + description = "cleanup the auth tokens on every hour" + } +} + +# Play mailer +play.mailer { + host = "localhost" + port = 25 + mock = true +} + +# Security Filter Configuration - Content Security Policy +play.filters.csp { + CSPFilter = "default-src 'self';" + CSPFilter = ${play.filters.headers.contentSecurityPolicy}" img-src 'self' *.fbcdn.net *.twimg.com *.googleusercontent.com *.xingassets.com vk.com *.yimg.com secure.gravatar.com chart.googleapis.com;" + CSPFilter = ${play.filters.headers.contentSecurityPolicy}" style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com maxcdn.bootstrapcdn.com cdn.jsdelivr.net fonts.googleapis.com;" + CSPFilter = ${play.filters.headers.contentSecurityPolicy}" font-src 'self' fonts.gstatic.com fonts.googleapis.com cdnjs.cloudflare.com;" + CSPFilter = ${play.filters.headers.contentSecurityPolicy}" script-src 'self' cdnjs.cloudflare.com;" + CSPFilter = ${play.filters.headers.contentSecurityPolicy}" connect-src 'self' twitter.com *.xing.com;" +} + +include "silhouette.conf" diff --git a/src/main/g8/conf/application.prod.conf b/src/main/g8/conf/application.prod.conf new file mode 100644 index 0000000..dbe8ebb --- /dev/null +++ b/src/main/g8/conf/application.prod.conf @@ -0,0 +1,53 @@ +include "application.conf" + +play.crypto.secret=${?PLAY_APP_SECRET} + +# Allow all proxies for Heroku so that X-Forwarded headers can be read by Play +# ~~~~~ +play.http.forwarded.trustedProxies=["0.0.0.0/0", "::/0"] + +# Play mailer +play.mailer { + host = "smtp.sendgrid.net" + port = 587 + tls = true + mock = false + user = "" + user = ${?SENDGRID_USERNAME} + password = "" + password = ${?SENDGRID_PASSWORD} +} + +silhouette { + + # Authenticator settings + authenticator.cookieDomain="play-silhouette-seed.herokuapp.com" + authenticator.secureCookie=true + + # OAuth1 token secret provider settings + oauth1TokenSecretProvider.cookieDomain="play-silhouette-seed.herokuapp.com" + oauth1TokenSecretProvider.secureCookie=true + + # OAuth2 state provider settings + oauth2StateProvider.cookieDomain="play-silhouette-seed.herokuapp.com" + oauth2StateProvider.secureCookie=true + + # Facebook provider + facebook.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/facebook" + + # Google provider + google.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/google" + + # VK provider + vk.redirectURL="https://play-silhouette-seed.herokuapp.com/authenticate/vk" + + # Twitter provider + twitter.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/twitter" + + # Xing provider + xing.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/xing" + + # Yahoo provider + yahoo.callbackURL="https://play-silhouette-seed.herokuapp.com/authenticate/yahoo" + yahoo.realm="https://play-silhouette-seed.herokuapp.com" +} diff --git a/src/main/g8/conf/messages b/src/main/g8/conf/messages new file mode 100644 index 0000000..acf08bf --- /dev/null +++ b/src/main/g8/conf/messages @@ -0,0 +1,127 @@ +error.email = Valid email required +error.required = This field is required +invalid.credentials = Invalid credentials! +invalid.unexpected.totp = Unexpected TOTP exception! +invalid.verification.code = Invalid verification code! +invalid.recovery.code = Invalid recovery code! +access.denied = Access denied! +could.not.authenticate = Could not authenticate with social provider! Please try again! + +home.title = Silhouette - Home +sign.up.title = Silhouette - Sign Up +sign.in.title = Silhouette - Sign In +totp.title = Silhouette - TOTP +forgot.password.title = Silhouette - Forgot Password +reset.password.title = Silhouette - Reset Password +change.password.title = Silhouette - Change Password +activate.account.title = Silhouette - Activate Account + +toggle.navigation = Toggle navigation +welcome.signed.in = Welcome, you are now signed in! + +totp.enable = Enable two-factor authentication +totp.disable = Disable two-factor authentication +totp.enabling.title = 2 factor auth enabling +totp.disabled.title = 2 factor auth is not enabled +totp.enabled.title = 2 factor auth is enabled +totp.shared.key.title = Shared key: +totp.recovery.tokens.title = Recovery tokens: +totp.enabling.info = 2 factor auth enabled successfully! +totp.disabling.info = 2 factor auth disabled successfully! +totp.recovery.code = Recovery code +totp.verification.code = Verification Code +totp.verify = Verify +totp.open.the.app.for.2fa = Open the two-factor authentication app on your device to view your authentication code and verify your identity. +totp.dont.have.your.phone = Don't have your phone? +totp.use.recovery.code = Enter a two-factor recovery code +totp.lost.your.recovery.codes = Lost your recovery codes? +totp.contact.support = Contact support + +sign.up.account = Sign up for a new account +sign.in.credentials = Sign in with your credentials +sign.in.totp = Two-factor authentication +sign.in.totp.recovery = Two-factor authentication using recovery code + +error = Error! +info = Info! +success = Success! +home = Home +first.name = First name +last.name = Last name +full.name = Full name +email = Email +password = Password +send = Send +change = Change +reset = Reset +sign.up = Sign up +sign.in = Sign in +sign.out = Sign out +sign.in.now = Sign in now +sign.up.now = Sign up now +already.a.member = Already a member? +not.a.member = Not a member? +forgot.your.password = Forgot your password? +forgot.password = Forgot password +reset.password = Reset password +change.password = Change password +activate.account = Activate Account +current.password = Current password +new.password = New password + +remember.me = Remember my login on this computer +or.use.social = Or use your existing account on one of the following services to sign in: +forgot.password.info = Please enter your email address and we will send you an email with further instructions to reset your password. +strong.password.info = Strong passwords include numbers, letters and punctuation marks. +current.password.invalid = The entered password is invalid. Please enter the correct password! +activate.account.text1 = You can''t log in yet. We previously sent an activation email to you at: +activate.account.text2 = Please follow the instructions in that email to activate your account. +activate.account.text3 = Click here to send the activation email again. + +sign.up.email.sent = You''re almost done! We sent an activation mail to {0}. Please follow the instructions in the email to activate your account. If it doesn''t arrive, check your spam folder, or try to log in again to send another activation mail. +activation.email.sent = We sent another activation email to you at {0}. It might take a few minutes for it to arrive; be sure to check your spam folder. +reset.email.sent = We have sent you an email with further instructions to reset your password, on condition that the address was found in our system. If you do not receive an email within the next 5 minutes, then please recheck your entered email address and try it again. + +invalid.activation.link = The link isn't valid anymore! Please sign in to send the activation email again. +invalid.reset.link = The link isn't valid anymore! Please request a new link to reset your password. + +password.reset = We have reset your password. You can now sign in with your credentials. +account.activated = Your account is now activated! Please sign in to use your new account. +password.changed = Your password has been changed. + +google = Google +facebook = Facebook +twitter = Twitter +vk = VK +xing = Xing +yahoo = Yahoo + +########## +# Emails +########## + +email.from = Silhouette + +# Sign Up +email.sign.up.subject = Welcome +email.sign.up.hello = Hello {0}, +email.sign.up.html.text = Please follow this link to confirm and activate your new account. +email.sign.up.txt.text = Please follow the link to confirm and activate your new account: {0} + +# Already Signed Up +email.already.signed.up.subject = Welcome +email.already.signed.up.hello = Hello {0}, +email.already.signed.up.html.text = You already have an account registered. Please follow this link to sign in into your account. +email.already.signed.up.txt.text = You already have an account registered. Please follow the link to sign in into your account: {0} + +# Reset Password +email.reset.password.subject = Reset password +email.reset.password.hello = Hello {0}, +email.reset.password.html.text = Please follow this link to reset your password. +email.reset.password.txt.text = Please follow the link to reset your password: {0} + +# Activate Account +email.activate.account.subject = Activate account +email.activate.account.hello = Hello {0}, +email.activate.account.html.text = Please follow this link to confirm and activate your new account. +email.activate.account.txt.text = Please follow the link to confirm and activate your new account: {0} diff --git a/src/main/g8/conf/routes b/src/main/g8/conf/routes new file mode 100644 index 0000000..87db0e3 --- /dev/null +++ b/src/main/g8/conf/routes @@ -0,0 +1,37 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +# Home page +GET / controllers.ApplicationController.index +GET /signOut controllers.ApplicationController.signOut +GET /authenticate/:provider controllers.SocialAuthController.authenticate(provider) + +GET /signUp controllers.SignUpController.view +POST /signUp controllers.SignUpController.submit + +GET /signIn controllers.SignInController.view +POST /signIn controllers.SignInController.submit + +GET /totp controllers.TotpController.view(userId: java.util.UUID, sharedKey: String, rememberMe: Boolean) +GET /enableTotp controllers.TotpController.enableTotp +GET /disableTotp controllers.TotpController.disableTotp +POST /totpSubmit controllers.TotpController.submit +POST /enableTotpSubmit controllers.TotpController.enableTotpSubmit + +GET /totpRecovery controllers.TotpRecoveryController.view(userID: java.util.UUID, sharedKey: String, rememberMe: Boolean) +POST /totpRecoverySubmit controllers.TotpRecoveryController.submit + +GET /password/forgot controllers.ForgotPasswordController.view +POST /password/forgot controllers.ForgotPasswordController.submit +GET /password/reset/:token controllers.ResetPasswordController.view(token: java.util.UUID) +POST /password/reset/:token controllers.ResetPasswordController.submit(token: java.util.UUID) +GET /password/change controllers.ChangePasswordController.view +POST /password/change controllers.ChangePasswordController.submit + +GET /account/email/:email controllers.ActivateAccountController.send(email: String) +GET /account/activate/:token controllers.ActivateAccountController.activate(token: java.util.UUID) + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.versioned(file) +-> /webjars webjars.Routes diff --git a/src/main/g8/conf/silhouette.conf b/src/main/g8/conf/silhouette.conf new file mode 100644 index 0000000..9492d97 --- /dev/null +++ b/src/main/g8/conf/silhouette.conf @@ -0,0 +1,103 @@ +silhouette { + + # Authenticator settings + authenticator.cookieName="authenticator" + authenticator.cookiePath="/" + authenticator.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set + authenticator.httpOnlyCookie=true + authenticator.sameSite="Lax" + authenticator.useFingerprinting=true + authenticator.authenticatorIdleTimeout=30 minutes + authenticator.authenticatorExpiry=12 hours + + authenticator.rememberMe.cookieMaxAge=30 days + authenticator.rememberMe.authenticatorIdleTimeout=5 days + authenticator.rememberMe.authenticatorExpiry=30 days + + authenticator.signer.key = "[changeme]" // A unique encryption key + authenticator.crypter.key = "[changeme]" // A unique encryption key + + # OAuth1 token secret provider settings + oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret" + oauth1TokenSecretProvider.cookiePath="/" + oauth1TokenSecretProvider.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set + oauth1TokenSecretProvider.httpOnlyCookie=true + oauth1TokenSecretProvider.sameSite="Lax" + oauth1TokenSecretProvider.expirationTime=5 minutes + + oauth1TokenSecretProvider.signer.key = "[changeme]" // A unique encryption key + oauth1TokenSecretProvider.crypter.key = "[changeme]" // A unique encryption key + + # Social state handler + socialStateHandler.signer.key = "[changeme]" // A unique encryption key + + # CSRF state item handler settings + csrfStateItemHandler.cookieName="OAuth2State" + csrfStateItemHandler.cookiePath="/" + csrfStateItemHandler.secureCookie=false // Disabled for testing on localhost without SSL, otherwise cookie couldn't be set + csrfStateItemHandler.httpOnlyCookie=true + csrfStateItemHandler.sameSite="Lax" + csrfStateItemHandler.expirationTime=5 minutes + + csrfStateItemHandler.signer.key = "[changeme]" // A unique encryption key + + # Facebook provider + facebook.authorizationURL="https://graph.facebook.com/v2.3/oauth/authorize" + facebook.accessTokenURL="https://graph.facebook.com/v2.3/oauth/access_token" + facebook.redirectURL="http://localhost:9000/authenticate/facebook" + facebook.clientID="" + facebook.clientID=${?FACEBOOK_CLIENT_ID} + facebook.clientSecret="" + facebook.clientSecret=${?FACEBOOK_CLIENT_SECRET} + facebook.scope="email" + + # Google provider + google.authorizationURL="https://accounts.google.com/o/oauth2/auth" + google.accessTokenURL="https://accounts.google.com/o/oauth2/token" + google.redirectURL="http://localhost:9000/authenticate/google" + google.clientID="" + google.clientID=${?GOOGLE_CLIENT_ID} + google.clientSecret="" + google.clientSecret=${?GOOGLE_CLIENT_SECRET} + google.scope="profile email" + + # VK provider + vk.authorizationURL="http://oauth.vk.com/authorize" + vk.accessTokenURL="https://oauth.vk.com/access_token" + vk.redirectURL="http://localhost:9000/authenticate/vk" + vk.clientID="" + vk.clientID=${?VK_CLIENT_ID} + vk.clientSecret="" + vk.clientSecret=${?VK_CLIENT_SECRET} + vk.scope="email" + + # Twitter provider + twitter.requestTokenURL="https://twitter.com/oauth/request_token" + twitter.accessTokenURL="https://twitter.com/oauth/access_token" + twitter.authorizationURL="https://twitter.com/oauth/authenticate" + twitter.callbackURL="http://localhost:9000/authenticate/twitter" + twitter.consumerKey="" + twitter.consumerKey=${?TWITTER_CONSUMER_KEY} + twitter.consumerSecret="" + twitter.consumerSecret=${?TWITTER_CONSUMER_SECRET} + + # Xing provider + xing.requestTokenURL="https://api.xing.com/v1/request_token" + xing.accessTokenURL="https://api.xing.com/v1/access_token" + xing.authorizationURL="https://api.xing.com/v1/authorize" + xing.callbackURL="http://localhost:9000/authenticate/xing" + xing.consumerKey="" + xing.consumerKey=${?XING_CONSUMER_KEY} + xing.consumerSecret="" + xing.consumerSecret=${?XING_CONSUMER_SECRET} + + # Yahoo provider + yahoo.providerURL="https://me.yahoo.com/" + yahoo.callbackURL="http://localhost:9000/authenticate/yahoo" + yahoo.axRequired={ + "fullname": "http://axschema.org/namePerson", + "email": "http://axschema.org/contact/email", + "image": "http://axschema.org/media/image/default" + } + yahoo.realm="http://localhost:9000" +} diff --git a/src/main/g8/public/images/favicon.png b/src/main/g8/public/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d92d2ae47434d9a61c90bc205e099b673b9dd5 GIT binary patch literal 687 zcmV;g0#N;lP)ezT{T_ZJ?}AL z5NC{NW(ESID=>(O3&Eg8 zmA9J&6c`h4_f6L;=bU>_H8aNG`kfvCj9zomNt)?O;rzWqZs0LEt%1WB218%1fo9uB zsW^yhBR7C(mqN%GEK9&msg0~ zWY?#bf4q8G-~2KttQZ($odJvy&_-~f?9*ThK@fwR$U^1)p*8=_+^3BXx0$i1BC8XC zr21u6D5nVK&^!dOAw&|1E;qC3uFNj3*Jj#&%Oje@0D-nhfmM*o%^5f}-pxQ07(95H z3|LoV>V19w#rLgmRmtVy9!T3M3FUE3><0T8&b3yEsWcLW`0(=1+qsqc(k(ymBLK0h zK!6(6$7MX~M`-QA2$wk7n(7hhkJ}4Rwi-Vd(_ZFX1Yk7TXuB0IJYpo@kLb2G8m)E{ z`9v=!hi}fOytKckfN^C@6+Z*+MVI9-W_p@_3yyR#UYc0FTpD}i#k>c!wYCS)4v@E$ zchZCo=zV@)`v^$;V18ixdjFMY#q^2$wEX%{f(XD8POnsn$bpbClpC@hPxjzyO>pY|*pF3UU2tYcCN?rUk{Sskej70Mmu9vPwMYhO1m{AxAt(zqDT|0jP7FaX=6 V`?~}E4H^Id002ovPDHLkV1hC)G==~G literal 0 HcmV?d00001 diff --git a/src/main/g8/public/images/providers/facebook.png b/src/main/g8/public/images/providers/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..06dbaa0d71c6f29021d21814015f31ff894bf2a0 GIT binary patch literal 449 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVC?X8aSW-r_4d|YFJ?!PwucTL zJhwciMMOq$OmpK_bX7>mT)ejF0Y96IzJu>`(JBpn4&u#N0hQCFfejBFfa);Ft8{zM7oB!HY$e(7Dh1mmb5xu zDPAHFyLeh|gV8SMb^GKlboe$f*jn>^Xj02z{vylP5zxypk8#i9>rZ~IWPQN&b`h&d ztJ>t(JhR+??_+YwFgSgQ(K(0B`(-Mv?MtSmH)J+f z1|6?tTanICuuAqpknsVHs!M;Q1s3%0xzF0iHt9Xn{rk6{S~jRVLVN`E+yi?CrE~&QInIC5lt7M z3(y7V0(1eo09}ACKo_73&;{rMbOCd00lW?@MPiU^#<;bB^{9j&0ra5JcZ0=2XitYQ#HkyR9v^=H|O2AxqcP#^Ho3UO| zeuz&HB{xFpXoE}}jv(3Q-I0ykVtlFUTVXFs&fIAz<<6307v?rR@5WXTmP5ZTQeS0nQI#5BPv3puXt=qTTj z@>zTX+vOQY)tJTacg6@E4_4VYNwOtJETD6K#7U8DRXcuE=_uYmvQd)pibBTX2D2sU zV!uDdycYa)R=Z?@_~j0TgvAZtuDt)*wb^prX7 z>u%NYge5S}+|*DPk{-ESJZ_T1@+)=;JU2IW{I=-qvR`wNSHDtur(l+g-ps=+>mJHP z2^a?)U^_WCfnVh_lVc}SRH2-80kfyalg=x;7v^nquWxhpNQqe0FxSJ$Dt1zR)8h$# zlT#Sm6=wQtOts_R!LyQQXOY4s_RcN4N|Z_j_zl>lJCoOHv%Wbl>GDy)TcbotZ!YJx zLuqLOMQ;PP_4nJ zu)P6cM!8Z8Q=p(^iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$QVa~t7M?DS zAr*7p&OGlO>L_yDKFfY$U4WpNZp(^)4NK%(c4W;AoVFw5a=)wIAr%ddLmE*cNgWrI zUOr!>uqY~`TO*3)(V_*ul9Anu?kTN!;W%rSZ}(3BGw-UupV?D>pK}gB&-cpbbBv$Y z-Jff2e4Hzp$9SgKTb3=HOd0A6d<7h=8FQN$ZgFM^vt00Vn61!I#&nB?af|Q;HI4;y z8_XOTz)GiN&iQ&k_5u3`<7KIh;ttU>Kh0cnXrtN;rP?y(sgS?VYTqQ zx0}5^$o8XR>>xdA9{LwKS#|Jso!guVwq-(9?nzT(UNodi}_JK<;q)5$!8WkzjRIV=#BK8#-puz*JCcP;!JCFdoa8G{q7f4k~f(D z6n)6;pEbjzdy(@2u?M}2~9WCyQx z{j9rgfl$Pp<^PShv0c>nU2%u$PkBpyZzl1{5twq%_J}{=zrXyo(ao)EK-rYR)78&q Iol`;+0Qg0gLjV8( literal 0 HcmV?d00001 diff --git a/src/main/g8/public/images/providers/xing.png b/src/main/g8/public/images/providers/xing.png new file mode 100644 index 0000000000000000000000000000000000000000..29131e58f5f78e4947e600fb825109e36c94f11c GIT binary patch literal 684 zcmV;d0#p5oP)#E`yOiN{QMpGZ0Gm5-_PUT-?`_WymUH!pfLd^zyz28 z6JP>NfC(@GCcte9K$$>Nvw$(M6vHnts8v8akj5G~rcpoM{fIzw%JHf_&U0oCA-5aM-)+DWGE`LG{whX`_MTg^qX?N#|8pb~5d zVP56X_HO&KBQ!{axe%G($+kTo1e^j}LYU`~`Q>1VZF{Z@$OltGkY!LAsku*tp8|mo z0Ai}g{+4g>eDjx(igH#qv;1aU! zb@}jNBn0n-Ad8?PQgf0Bp91wh2)IFnxxT}j<0w%#=B@yK-TxLG+M%`vtPv8YufkN`z76bkhv;2U!bxhP;i7f%b|uvP&fcuzy% zDdnA025#WOn?b60Vco%m;e)yGyec$;Qeam Sw|->+0000aSW-r_4cN{$CX5x;~&pi zn|Pk}nf%YkS5TzTn2Bg<{P~Sx7@c7E6V4c_sEwe|Kf9}28(%zx5&(LdRO17q5Wuqc7Aj92gYT#+&jNB zZ90{G=I|EQGfLf(OJb_|ELu`$lr24E9A(H}tID(BTs!+hm1_cbtR^Xbmpd;0;`E~x z`Tk9N4RrG>1-uq=)bb0M$5abF-qFAMg{!%cA4|ZygIhEYZ?%nJO|I$np0@eC@{T#d zj8igalsWHSCO`Yucd?6tMei(Gz8^Vi-!^Nab3y&Z*2EXp{f9;8REr&qEzxV3bi}IZ zH*;;KNmxai`Ip=W9g`WSB;J^I?^$$=_?NT4KU6xfEz^~nFY-3%!uJd94A&EGxwLFP z=Mdk|&&ZZ|E2U+#)z;tFFT8VZsJmT!&}{CtclFOtr#ICaymg(rz9BH4iRZu;pO($H z3dQ>iPfcfVHpneguhF^JQ8?opC+~{zvc+Pi(>~`Z_8QJ9Fg&1~V$j^Lad4aHln=tL ze-2D|T4S5GJeyG};DDOhhOT&x1kTDQCY3cuH+|y0UwHGAao+0cX$1$iG8ArED5`s7 z_1+`d6CG-}IS>5PUT|e0r{T}*GjPW+P)778`>gANEeGO7rvOtSgQu&X%Q~loCIF}f BEkytT literal 0 HcmV?d00001 diff --git a/src/main/g8/public/images/silhouette.png b/src/main/g8/public/images/silhouette.png new file mode 100644 index 0000000000000000000000000000000000000000..bc2e3656d1238eff6f2e5f461fbf97228ee64e2a GIT binary patch literal 10664 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 literal 0 HcmV?d00001 diff --git a/src/main/g8/public/javascripts/zxcvbnShim.js b/src/main/g8/public/javascripts/zxcvbnShim.js new file mode 100644 index 0000000..3335e90 --- /dev/null +++ b/src/main/g8/public/javascripts/zxcvbnShim.js @@ -0,0 +1,31 @@ +$(function() { + var strength = { + 0: "Worst", + 1: "Bad", + 2: "Weak", + 3: "Good", + 4: "Strong" + }; + + var password = $('[data-pwd="true"]'); + var meter = $('#password-strength-meter'); + var msg = $('#password-strength-text'); + + function showFeedback() { + var val = this.value; + var result = zxcvbn(val); + + // Update the password strength meter + meter.val(result.score); + + // Update the text indicator + if (val !== "") { + msg.text("Strength: " + strength[result.score]); + } else { + msg.text(""); + } + } + + password.change(showFeedback); + password.keyup(showFeedback); +}); diff --git a/src/main/g8/public/styles/main.css b/src/main/g8/public/styles/main.css new file mode 100644 index 0000000..683225e --- /dev/null +++ b/src/main/g8/public/styles/main.css @@ -0,0 +1,135 @@ +body { + padding-top: 50px; + font-size: 16px; + background: #f5f7f9; +} + +h1 { + text-align: center; + font-size: 30px; +} +.starter-template { + padding: 40px 15px; +} + +input, button { + margin: 5px 0; +} + +input:hover, input:focus { + outline: 0; + transition: all .5s linear; + box-shadow: inset 0 0 10px #ccc; +} + +fieldset { + margin-top: 100px; +} +legend { + font-family: 'Montserrat', sans-serif; + text-align: center; + font-size: 20px; + padding: 15px; +} +a { + cursor: pointer; +} + +.provider { + display: inline-block; + width: 64px; + height: 64px; + border-radius: 4px; + outline: none; +} +.facebook { background: #3B5998; } +.google { background: #D14836; } +.twitter { background: #00ACED; } +.yahoo { background: #731A8B; } +.xing { background: #006567; } +.vk { background: #567ca4; } + +.social-providers, +.sign-in-now, +.already-member, +.not-a-member { + text-align: center; + margin-top: 20px; +} + +.user { + margin-top: 50px; +} +.user .data { + margin-top: 10px; +} + +.form-control { + border-radius: 0; +} + +[class^='ion-'] { + font-size: 1.2em; +} + +.has-feedback .form-control-feedback { + top: 0; + left: 0; + width: 46px; + height: 46px; + line-height: 46px; + color: #555; +} + +.has-feedback .form-control { + padding-left: 42.5px; +} + +.btn { + font-weight: bold; + border-radius: 2px; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26); +} + +.btn-lg { + font-size: 18px; +} + +meter { + /* Reset the default appearance */ + -moz-appearance: none; + appearance: none; + + margin: 0 auto 1em; + width: 100%; + height: .5em; + + /* Applicable only to Firefox */ + background: none; + background-color: rgba(0,0,0,0.1); +} + +meter::-webkit-meter-bar { + background: none; + background-color: rgba(0,0,0,0.1); +} + +meter[value="0"]::-webkit-meter-optimum-value, +meter[value="1"]::-webkit-meter-optimum-value { background: red; } +meter[value="2"]::-webkit-meter-optimum-value { background: orange; } +meter[value="3"]::-webkit-meter-optimum-value { background: yellow; } +meter[value="4"]::-webkit-meter-optimum-value { background: green; } + +meter::-webkit-meter-even-less-good-value { background: red; } +meter::-webkit-meter-suboptimum-value { background: orange; } +meter::-webkit-meter-optimum-value { background: green; } + +meter[value="1"]::-moz-meter-bar, +meter[value="1"]::-moz-meter-bar { background: red; } +meter[value="2"]::-moz-meter-bar { background: orange; } +meter[value="3"]::-moz-meter-bar { background: yellow; } +meter[value="4"]::-moz-meter-bar { background: green; } + +meter::-webkit-meter-optimum-value { + transition: width .4s ease-out; +} diff --git a/src/main/g8/scripts/reformat b/src/main/g8/scripts/reformat new file mode 100755 index 0000000..87e60b6 --- /dev/null +++ b/src/main/g8/scripts/reformat @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Reformats source code. +# +# Copyright 2015 Mohiva Organisation (license at mohiva dot com) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -o nounset -o errexit + +scripts/sbt scalariformFormat test:scalariformFormat diff --git a/src/main/g8/scripts/sbt b/src/main/g8/scripts/sbt new file mode 100755 index 0000000..fc7e522 --- /dev/null +++ b/src/main/g8/scripts/sbt @@ -0,0 +1,621 @@ +#!/usr/bin/env bash +# +# A more capable sbt runner, coincidentally also called sbt. +# Author: Paul Phillips +# https://github.com/paulp/sbt-extras + +set -o pipefail + +declare -r sbt_release_version="1.3.2" +declare -r sbt_unreleased_version="1.3.2" + +declare -r latest_213="2.13.1" +declare -r latest_212="2.12.10" +declare -r latest_211="2.11.12" +declare -r latest_210="2.10.7" +declare -r latest_29="2.9.3" +declare -r latest_28="2.8.2" + +declare -r buildProps="project/build.properties" + +declare -r sbt_launch_ivy_release_repo="https://repo.typesafe.com/typesafe/ivy-releases" +declare -r sbt_launch_ivy_snapshot_repo="https://repo.scala-sbt.org/scalasbt/ivy-snapshots" +declare -r sbt_launch_mvn_release_repo="https://repo.scala-sbt.org/scalasbt/maven-releases" +declare -r sbt_launch_mvn_snapshot_repo="https://repo.scala-sbt.org/scalasbt/maven-snapshots" + +declare -r default_jvm_opts_common="-Xms512m -Xss2m" +declare -r noshare_opts="-Dsbt.global.base=project/.sbtboot -Dsbt.boot.directory=project/.boot -Dsbt.ivy.home=project/.ivy" + +declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new +declare sbt_explicit_version +declare verbose noshare batch trace_level +declare debugUs + +declare java_cmd="java" +declare sbt_launch_dir="$HOME/.sbt/launchers" +declare sbt_launch_repo + +# pull -J and -D options to give to java. +declare -a java_args scalac_args sbt_commands residual_args + +# args to jvm/sbt via files or environment variables +declare -a extra_jvm_opts extra_sbt_opts + +echoerr () { echo >&2 "$@"; } +vlog () { [[ -n "$verbose" ]] && echoerr "$@"; } +die () { echo "Aborting: $*" ; exit 1; } + +setTrapExit () { + # save stty and trap exit, to ensure echo is re-enabled if we are interrupted. + SBT_STTY="$(stty -g 2>/dev/null)" + export SBT_STTY + + # restore stty settings (echo in particular) + onSbtRunnerExit() { + [ -t 0 ] || return + vlog "" + vlog "restoring stty: $SBT_STTY" + stty "$SBT_STTY" + } + + vlog "saving stty: $SBT_STTY" + trap onSbtRunnerExit EXIT +} + +# this seems to cover the bases on OSX, and someone will +# have to tell me about the others. +get_script_path () { + local path="$1" + [[ -L "$path" ]] || { echo "$path" ; return; } + + local -r target="$(readlink "$path")" + if [[ "${target:0:1}" == "/" ]]; then + echo "$target" + else + echo "${path%/*}/$target" + fi +} + +script_path="$(get_script_path "${BASH_SOURCE[0]}")" +declare -r script_path +script_name="${script_path##*/}" +declare -r script_name + +init_default_option_file () { + local overriding_var="${!1}" + local default_file="$2" + if [[ ! -r "$default_file" && "$overriding_var" =~ ^@(.*)$ ]]; then + local envvar_file="${BASH_REMATCH[1]}" + if [[ -r "$envvar_file" ]]; then + default_file="$envvar_file" + fi + fi + echo "$default_file" +} + +sbt_opts_file="$(init_default_option_file SBT_OPTS .sbtopts)" +jvm_opts_file="$(init_default_option_file JVM_OPTS .jvmopts)" + +build_props_sbt () { + [[ -r "$buildProps" ]] && \ + grep '^sbt\.version' "$buildProps" | tr '=\r' ' ' | awk '{ print $2; }' +} + +set_sbt_version () { + sbt_version="${sbt_explicit_version:-$(build_props_sbt)}" + [[ -n "$sbt_version" ]] || sbt_version=$sbt_release_version + export sbt_version +} + +url_base () { + local version="$1" + + case "$version" in + 0.7.*) echo "https://simple-build-tool.googlecode.com" ;; + 0.10.* ) echo "$sbt_launch_ivy_release_repo" ;; + 0.11.[12]) echo "$sbt_launch_ivy_release_repo" ;; + 0.*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" + echo "$sbt_launch_ivy_snapshot_repo" ;; + 0.*) echo "$sbt_launch_ivy_release_repo" ;; + *-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) # ie "*-yyyymmdd-hhMMss" + echo "$sbt_launch_mvn_snapshot_repo" ;; + *) echo "$sbt_launch_mvn_release_repo" ;; + esac +} + +make_url () { + local version="$1" + + local base="${sbt_launch_repo:-$(url_base "$version")}" + + case "$version" in + 0.7.*) echo "$base/files/sbt-launch-0.7.7.jar" ;; + 0.10.* ) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + 0.11.[12]) echo "$base/org.scala-tools.sbt/sbt-launch/$version/sbt-launch.jar" ;; + 0.*) echo "$base/org.scala-sbt/sbt-launch/$version/sbt-launch.jar" ;; + *) echo "$base/org/scala-sbt/sbt-launch/$version/sbt-launch-${version}.jar" ;; + esac +} + +addJava () { vlog "[addJava] arg = '$1'" ; java_args+=("$1"); } +addSbt () { vlog "[addSbt] arg = '$1'" ; sbt_commands+=("$1"); } +addScalac () { vlog "[addScalac] arg = '$1'" ; scalac_args+=("$1"); } +addResidual () { vlog "[residual] arg = '$1'" ; residual_args+=("$1"); } + +addResolver () { addSbt "set resolvers += $1"; } +addDebugger () { addJava "-Xdebug" ; addJava "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1"; } +setThisBuild () { + vlog "[addBuild] args = '$*'" + local key="$1" && shift + addSbt "set $key in ThisBuild := $*" +} +setScalaVersion () { + [[ "$1" == *"-SNAPSHOT" ]] && addResolver 'Resolver.sonatypeRepo("snapshots")' + addSbt "++ $1" +} +setJavaHome () { + java_cmd="$1/bin/java" + setThisBuild javaHome "_root_.scala.Some(file(\"$1\"))" + export JAVA_HOME="$1" + export JDK_HOME="$1" + export PATH="$JAVA_HOME/bin:$PATH" +} + +getJavaVersion() { + local -r str=$("$1" -version 2>&1 | grep -E -e '(java|openjdk) version' | awk '{ print $3 }' | tr -d '"') + + # java -version on java8 says 1.8.x + # but on 9 and 10 it's 9.x.y and 10.x.y. + if [[ "$str" =~ ^1\.([0-9]+)\..*$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ "$str" =~ ^([0-9]+)\..*$ ]]; then + echo "${BASH_REMATCH[1]}" + elif [[ -n "$str" ]]; then + echoerr "Can't parse java version from: $str" + fi +} + +checkJava() { + # Warn if there is a Java version mismatch between PATH and JAVA_HOME/JDK_HOME + + [[ -n "$JAVA_HOME" && -e "$JAVA_HOME/bin/java" ]] && java="$JAVA_HOME/bin/java" + [[ -n "$JDK_HOME" && -e "$JDK_HOME/lib/tools.jar" ]] && java="$JDK_HOME/bin/java" + + if [[ -n "$java" ]]; then + pathJavaVersion=$(getJavaVersion java) + homeJavaVersion=$(getJavaVersion "$java") + if [[ "$pathJavaVersion" != "$homeJavaVersion" ]]; then + echoerr "Warning: Java version mismatch between PATH and JAVA_HOME/JDK_HOME, sbt will use the one in PATH" + echoerr " Either: fix your PATH, remove JAVA_HOME/JDK_HOME or use -java-home" + echoerr " java version from PATH: $pathJavaVersion" + echoerr " java version from JAVA_HOME/JDK_HOME: $homeJavaVersion" + fi + fi +} + +java_version () { + local -r version=$(getJavaVersion "$java_cmd") + vlog "Detected Java version: $version" + echo "$version" +} + +# MaxPermSize critical on pre-8 JVMs but incurs noisy warning on 8+ +default_jvm_opts () { + local -r v="$(java_version)" + if [[ $v -ge 8 ]]; then + echo "$default_jvm_opts_common" + else + echo "-XX:MaxPermSize=384m $default_jvm_opts_common" + fi +} + +build_props_scala () { + if [[ -r "$buildProps" ]]; then + versionLine="$(grep '^build.scala.versions' "$buildProps")" + versionString="${versionLine##build.scala.versions=}" + echo "${versionString%% .*}" + fi +} + +execRunner () { + # print the arguments one to a line, quoting any containing spaces + vlog "# Executing command line:" && { + for arg; do + if [[ -n "$arg" ]]; then + if printf "%s\n" "$arg" | grep -q ' '; then + printf >&2 "\"%s\"\n" "$arg" + else + printf >&2 "%s\n" "$arg" + fi + fi + done + vlog "" + } + + setTrapExit + + if [[ -n "$batch" ]]; then + "$@" < /dev/null + else + "$@" + fi +} + +jar_url () { make_url "$1"; } + +is_cygwin () { [[ "$(uname -a)" == "CYGWIN"* ]]; } + +jar_file () { + is_cygwin \ + && cygpath -w "$sbt_launch_dir/$1/sbt-launch.jar" \ + || echo "$sbt_launch_dir/$1/sbt-launch.jar" +} + +download_url () { + local url="$1" + local jar="$2" + + mkdir -p "${jar%/*}" && { + if command -v curl > /dev/null 2>&1; then + curl --fail --silent --location "$url" --output "$jar" + elif command -v wget > /dev/null 2>&1; then + wget -q -O "$jar" "$url" + fi + } && [[ -r "$jar" ]] +} + +acquire_sbt_jar () { + { + sbt_jar="$(jar_file "$sbt_version")" + [[ -r "$sbt_jar" ]] + } || { + sbt_jar="$HOME/.ivy2/local/org.scala-sbt/sbt-launch/$sbt_version/jars/sbt-launch.jar" + [[ -r "$sbt_jar" ]] + } || { + sbt_jar="$(jar_file "$sbt_version")" + jar_url="$(make_url "$sbt_version")" + + echoerr "Downloading sbt launcher for ${sbt_version}:" + echoerr " From ${jar_url}" + echoerr " To ${sbt_jar}" + + download_url "${jar_url}" "${sbt_jar}" + + case "${sbt_version}" in + 0.*) vlog "SBT versions < 1.0 do not have published MD5 checksums, skipping check"; echo "" ;; + *) verify_sbt_jar "${sbt_jar}" ;; + esac + } +} + +verify_sbt_jar() { + local jar="${1}" + local md5="${jar}.md5" + + download_url "$(make_url "${sbt_version}").md5" "${md5}" > /dev/null 2>&1 + + if command -v md5sum > /dev/null 2>&1; then + if echo "$(cat "${md5}") ${jar}" | md5sum -c -; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v md5 > /dev/null 2>&1; then + if [ "$(md5 -q "${jar}")" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + elif command -v openssl > /dev/null 2>&1; then + if [ "$(openssl md5 -r "${jar}" | awk '{print $1}')" == "$(cat "${md5}")" ]; then + rm -rf "${md5}" + return 0 + else + echoerr "Checksum does not match" + return 1 + fi + else + echoerr "Could not find an MD5 command" + return 1 + fi +} + +usage () { + set_sbt_version + cat < display stack traces with a max of frames (default: -1, traces suppressed) + -debug-inc enable debugging log for the incremental compiler + -no-colors disable ANSI color codes + -sbt-create start sbt even if current directory contains no sbt project + -sbt-dir path to global settings/plugins directory (default: ~/.sbt/) + -sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11+) + -ivy path to local Ivy repository (default: ~/.ivy2) + -no-share use all local caches; no sharing + -offline put sbt in offline mode + -jvm-debug Turn on JVM debugging, open at the given port. + -batch Disable interactive mode + -prompt Set the sbt prompt; in expr, 's' is the State and 'e' is Extracted + -script Run the specified file as a scala script + + # sbt version (default: sbt.version from $buildProps if present, otherwise $sbt_release_version) + -sbt-force-latest force the use of the latest release of sbt: $sbt_release_version + -sbt-version use the specified version of sbt (default: $sbt_release_version) + -sbt-dev use the latest pre-release version of sbt: $sbt_unreleased_version + -sbt-jar use the specified jar as the sbt launcher + -sbt-launch-dir directory to hold sbt launchers (default: $sbt_launch_dir) + -sbt-launch-repo repo url for downloading sbt launcher jar (default: $(url_base "$sbt_version")) + + # scala version (default: as chosen by sbt) + -28 use $latest_28 + -29 use $latest_29 + -210 use $latest_210 + -211 use $latest_211 + -212 use $latest_212 + -213 use $latest_213 + -scala-home use the scala build at the specified directory + -scala-version use the specified version of scala + -binary-version use the specified scala version when searching for dependencies + + # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) + -java-home alternate JAVA_HOME + + # passing options to the jvm - note it does NOT use JAVA_OPTS due to pollution + # The default set is used if JVM_OPTS is unset and no -jvm-opts file is found + $(default_jvm_opts) + JVM_OPTS environment variable holding either the jvm args directly, or + the reference to a file containing jvm args if given path is prepended by '@' (e.g. '@/etc/jvmopts') + Note: "@"-file is overridden by local '.jvmopts' or '-jvm-opts' argument. + -jvm-opts file containing jvm args (if not given, .jvmopts in project root is used if present) + -Dkey=val pass -Dkey=val directly to the jvm + -J-X pass option -X directly to the jvm (-J is stripped) + + # passing options to sbt, OR to this runner + SBT_OPTS environment variable holding either the sbt args directly, or + the reference to a file containing sbt args if given path is prepended by '@' (e.g. '@/etc/sbtopts') + Note: "@"-file is overridden by local '.sbtopts' or '-sbt-opts' argument. + -sbt-opts file containing sbt args (if not given, .sbtopts in project root is used if present) + -S-X add -X to sbt's scalacOptions (-S is stripped) +EOM +} + +process_args () { + require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + die "$opt requires <$type> argument" + fi + } + while [[ $# -gt 0 ]]; do + case "$1" in + -h|-help) usage; exit 0 ;; + -v) verbose=true && shift ;; + -d) addSbt "--debug" && shift ;; + -w) addSbt "--warn" && shift ;; + -q) addSbt "--error" && shift ;; + -x) debugUs=true && shift ;; + -trace) require_arg integer "$1" "$2" && trace_level="$2" && shift 2 ;; + -ivy) require_arg path "$1" "$2" && addJava "-Dsbt.ivy.home=$2" && shift 2 ;; + -no-colors) addJava "-Dsbt.log.noformat=true" && shift ;; + -no-share) noshare=true && shift ;; + -sbt-boot) require_arg path "$1" "$2" && addJava "-Dsbt.boot.directory=$2" && shift 2 ;; + -sbt-dir) require_arg path "$1" "$2" && sbt_dir="$2" && shift 2 ;; + -debug-inc) addJava "-Dxsbt.inc.debug=true" && shift ;; + -offline) addSbt "set offline in Global := true" && shift ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger "$2" && shift 2 ;; + -batch) batch=true && shift ;; + -prompt) require_arg "expr" "$1" "$2" && setThisBuild shellPrompt "(s => { val e = Project.extract(s) ; $2 })" && shift 2 ;; + -script) require_arg file "$1" "$2" && sbt_script="$2" && addJava "-Dsbt.main.class=sbt.ScriptMain" && shift 2 ;; + + -sbt-create) sbt_create=true && shift ;; + -sbt-jar) require_arg path "$1" "$2" && sbt_jar="$2" && shift 2 ;; + -sbt-version) require_arg version "$1" "$2" && sbt_explicit_version="$2" && shift 2 ;; + -sbt-force-latest) sbt_explicit_version="$sbt_release_version" && shift ;; + -sbt-dev) sbt_explicit_version="$sbt_unreleased_version" && shift ;; + -sbt-launch-dir) require_arg path "$1" "$2" && sbt_launch_dir="$2" && shift 2 ;; + -sbt-launch-repo) require_arg path "$1" "$2" && sbt_launch_repo="$2" && shift 2 ;; + -scala-version) require_arg version "$1" "$2" && setScalaVersion "$2" && shift 2 ;; + -binary-version) require_arg version "$1" "$2" && setThisBuild scalaBinaryVersion "\"$2\"" && shift 2 ;; + -scala-home) require_arg path "$1" "$2" && setThisBuild scalaHome "_root_.scala.Some(file(\"$2\"))" && shift 2 ;; + -java-home) require_arg path "$1" "$2" && setJavaHome "$2" && shift 2 ;; + -sbt-opts) require_arg path "$1" "$2" && sbt_opts_file="$2" && shift 2 ;; + -jvm-opts) require_arg path "$1" "$2" && jvm_opts_file="$2" && shift 2 ;; + + -D*) addJava "$1" && shift ;; + -J*) addJava "${1:2}" && shift ;; + -S*) addScalac "${1:2}" && shift ;; + -28) setScalaVersion "$latest_28" && shift ;; + -29) setScalaVersion "$latest_29" && shift ;; + -210) setScalaVersion "$latest_210" && shift ;; + -211) setScalaVersion "$latest_211" && shift ;; + -212) setScalaVersion "$latest_212" && shift ;; + -213) setScalaVersion "$latest_213" && shift ;; + new) sbt_new=true && : ${sbt_explicit_version:=$sbt_release_version} && addResidual "$1" && shift ;; + *) addResidual "$1" && shift ;; + esac + done +} + +# process the direct command line arguments +process_args "$@" + +# skip #-styled comments and blank lines +readConfigFile() { + local end=false + until $end; do + read -r || end=true + [[ $REPLY =~ ^# ]] || [[ -z $REPLY ]] || echo "$REPLY" + done < "$1" +} + +# if there are file/environment sbt_opts, process again so we +# can supply args to this runner +if [[ -r "$sbt_opts_file" ]]; then + vlog "Using sbt options defined in file $sbt_opts_file" + while read -r opt; do extra_sbt_opts+=("$opt"); done < <(readConfigFile "$sbt_opts_file") +elif [[ -n "$SBT_OPTS" && ! ("$SBT_OPTS" =~ ^@.*) ]]; then + vlog "Using sbt options defined in variable \$SBT_OPTS" + IFS=" " read -r -a extra_sbt_opts <<< "$SBT_OPTS" +else + vlog "No extra sbt options have been defined" +fi + +[[ -n "${extra_sbt_opts[*]}" ]] && process_args "${extra_sbt_opts[@]}" + +# reset "$@" to the residual args +set -- "${residual_args[@]}" +argumentCount=$# + +# set sbt version +set_sbt_version + +checkJava + +# only exists in 0.12+ +setTraceLevel() { + case "$sbt_version" in + "0.7."* | "0.10."* | "0.11."* ) echoerr "Cannot set trace level in sbt version $sbt_version" ;; + *) setThisBuild traceLevel "$trace_level" ;; + esac +} + +# set scalacOptions if we were given any -S opts +[[ ${#scalac_args[@]} -eq 0 ]] || addSbt "set scalacOptions in ThisBuild += \"${scalac_args[*]}\"" + +[[ -n "$sbt_explicit_version" && -z "$sbt_new" ]] && addJava "-Dsbt.version=$sbt_explicit_version" +vlog "Detected sbt version $sbt_version" + +if [[ -n "$sbt_script" ]]; then + residual_args=( "$sbt_script" "${residual_args[@]}" ) +else + # no args - alert them there's stuff in here + (( argumentCount > 0 )) || { + vlog "Starting $script_name: invoke with -help for other options" + residual_args=( shell ) + } +fi + +# verify this is an sbt dir, -create was given or user attempts to run a scala script +[[ -r ./build.sbt || -d ./project || -n "$sbt_create" || -n "$sbt_script" || -n "$sbt_new" ]] || { + cat < identity)) + + /** + * The application. + */ + lazy val application = new GuiceApplicationBuilder() + .overrides(new FakeModule) + .build() + } +} diff --git a/src/main/g8/tutorial/index.html b/src/main/g8/tutorial/index.html new file mode 100644 index 0000000..e7500e1 --- /dev/null +++ b/src/main/g8/tutorial/index.html @@ -0,0 +1,137 @@ + +
+

Introduction

+

What is the purpose of this tutorial ?

+

+ This tutorial is not really meant as a manual to use this seed, but more as a first step to begin to understand how Silhouette works. You should refer to the documentation for detailed explanations and you can ask questions on the Silhouette Forum. +

+ +

Overview

+ +
+
+

Run Your Application

+

+ You can already run your app through the activator ui or with the activator run command and visit http://localhost:9000. But you will not be able to sign up because the application tries to send an email through SendGrid, which has to be configured. And the same goes for the other authentication providers (Google, Facebook, ...) : you have to register your application and set the provider key and secret in silhouette.conf for each of them to work. +
+ Therefore, if you just want to experiment with this project, you can make two small changes allowing you to signup. First you have to set the Play mailer into mock mode by adding the following line to your application.conf file : +

play.mailer.mock = true
+ This tells the Play mailer to log the email instead of trying to send it. But now you will not be able to activate your account after signing up, so you have to initialize new accounts as already activated by slightly modifiying the SignUpController : +
val user = User(
+  userID = UUID.randomUUID(),
+  loginInfo = loginInfo,
+  firstName = Some(data.firstName),
+  lastName = Some(data.lastName),
+  fullName = Some(data.firstName + " " + data.lastName),
+  email = Some(data.email),
+  avatarURL = None,
+  //activated = false
+  activated = true // TODO delete to avoid activating all users by default
+)
+ You should now be able to sign up and then sign in. +

+
+
+

Endpoints

+

+ As explained in the documentation, the endpoints are the Actions and the WebSockets that are managed by Silhouette. This means, for example, that to make sure that only registered users can access to an endpoint of your application, you simply have to use a silhouette.SecuredAction instead of a standard Action, like for index in the ApplicationController. +

+

+ These endpoints are provided by the silhouette: Silhouette[DefaultEnv] object which is injected in each controller using dependency injection. To see where it comes from you have to examine the modules (next section). +

+
+
+

Modules

+

+ Silhouette uses dependency injection to separate the API definition from its implementation. The modules to customize the bindings from the traits to their implementation can be found in the modules package. We are in particular interested by the SilhouetteModule. +
+ This file contains all the bindings related to Silhouette and is therefore rather long. We are going to look at the part concerning the Silhouette[DefaultEnv] object, which provides the endpoints. After that, the structure of the file should be clear enough for you to find what you need. +

+

+ We see in the first line of the configure method that the Silhouette[DefaultEnv] we came across in the controller is bound to the class SilhouetteProvider[DefaultEnv]. To discover from where this one comes from you have to look into the Silhouette library itself. At the bottom of the Silhouette.scala file, you can observe that SilhouetteProvider[E <: Env] depends on Environment[E], SecuredAction, UnsecuredAction and UserAwareAction. The three Actions already have a default implementation bound (in SecuredAction for example), so the only one left to be bound is the Environment. We can now go back to the SilhouetteModule to find the provideEnvironment method. +
+ The @Provides annotation means that every time an intance of Environment[DefaultEnv] has to be injected, this method will provide it. The dependencies of provideEnvironment are themselves bound in the configure method. The Environment provided here has the type parameter DefaultEnv, an arbitrary name chosen for the environment type of this application, defined in Env.scala. This trait defines which Identity (i.e. structure of a user, doc) and which Authenticator (i.e. mean of authentication, doc) are used in this project. See the documentation about the environment for more information. +

+

+ In summary, a module has a configure method where all the bindings are declared and other methods annotated with @Provides to provide instances with specific arguments. +

+
+ +
+

Handle errors in endpoints

+

+ If a user tries to access a secured enpoint without being authenticated, the default behaviour of Silhouette is to send a simple "not authenticated" message. The same goes if an authenticated user requests a page he is not authorized to access. To customize this behaviour, this seed defines two custom error handlers, CustomSecuredErrorHandler and CustomUnsecuredErrorHandler. +
+ To enable them, the defauld handlers have first to be disabled. This is done in the application.conf file with the following lines : +

play.modules.disabled += "com.mohiva.play.silhouette.api.actions.SecuredErrorHandlerModule"
+ play.modules.disabled += "com.mohiva.play.silhouette.api.actions.UnsecuredErrorHandlerModule"
+ The new handlers can then be bound to the error handler traits in the SilhouetteModule : +
bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler]
+ bind[SecuredErrorHandler].to[CustomSecuredErrorHandler]
+

+
+ +
+

Identity/IdentityService

+

+ Identity is a trait in Silhouette representing a user (documentation). Its implementation in this seed is the User class. This class has to be specified in the environment type, here DefaultEnv (see the documentation about the environment). +

+

+ Silhouette also needs an IdentityService, extended here by UserService and then implemented by UserServiceImpl. You can find the binding concerning these in the SilhouetteModule. The IdentityService is needed in the authentication process to get a user given his identity provider and his identifier, bundled in a LoginInfo object. Here a UserService also allows to save a user, making it an additional layer of abstraction above the user data access object (UserDAO) to simplify the saving process. +

+

+ The identity of a user is accessible in every secured or user aware endpoint via its Request object. In the case of a SecuredAction you directly get the Identity from request.identity, and for an UserAwareAction it is an Option[Identity] since this type of endpoint also accepts unauthentified users. +

+
+ +
+

DAOs

+

+ As you can see in the daos package, this seed contains data access objects (DAOs) for Users and for AuthTokens (tokens to activate a user via email or change the password). They are implemented to store the data in an in-memory HashMap for the example. To keep the data over a restart of your application you have to give these DAOs an other implementation storing the values in a database. You can then replace the implementation bound in the SilhouetteModule (or BaseModule for the AuthToken). +

+

+ There are other DAOs in this application, but their implementation comes from Silhouette. You can find the bindings in the SilhouetteModule : +

// Replace this with the bindings to your concrete DAOs
+bind[DelegableAuthInfoDAO[PasswordInfo]].toInstance(new InMemoryAuthInfoDAO[PasswordInfo])
+bind[DelegableAuthInfoDAO[OAuth1Info]].toInstance(new InMemoryAuthInfoDAO[OAuth1Info])
+bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info])
+bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo])
+

+ These are all AuthInfoDAOs which store the information concerning authentication, like the password for users signing in with credentials. You may wonder why not directly store the password in the User object, but remember that we want users to be able to sign in via other authentication providers like Google or Facebook. Our application will not have to store a password for these users, but other authentication information depending on the protocol (OAuth1, OAuth2 or OpenID). Therefore it makes more sense to split the auth info from the rest of the user and store it in a different DAO. +
+ Like for the UserDAO, you will have to provide other implementations to persist the data. +

+
+ +
+

Authorization

+

+ To specify which endpoints a user can access in your application, you can implement an Authorization (documentation). WithProvider is an example allowing only users which are authenticated via a specified provider. You can implement your own logic and combine authorizations with logical operators like described in the documentation. +

+
+ +
+

Conclusion

+

+ If you are planning to use Silhouette in your project, you may think that this seed contains quite a lot of features and maybe you want to take only what is necessary. So here is a list of the classes and files you absolutely need to make Silhouette work : +

+
    +
  • A module binding the necessary implementations, or providing the instances when you need to specify the constructor arguments
  • +
  • An implementation of Identity
  • +
  • An implementation of IdentityService
  • +
  • A trait extending Env, like DefaultEnv
  • +
+

+ But if you are planning to use your application in production, this project contains a few things that come in handy (AuthTokenCleaner, security headers, ...) and it may therefore be easier to use it as seed from the beginning. +

+
+ From 14681e21ccd0d14b1a49faaab43d5ced10e45f25 Mon Sep 17 00:00:00 2001 From: Alexander Myltsev Date: Sat, 28 Dec 2019 10:30:05 +0300 Subject: [PATCH 3/3] Ignore `$` --- default.properties => src/main/g8/default.properties | 1 + 1 file changed, 1 insertion(+) rename default.properties => src/main/g8/default.properties (50%) diff --git a/default.properties b/src/main/g8/default.properties similarity index 50% rename from default.properties rename to src/main/g8/default.properties index f8194fb..4a077d2 100644 --- a/default.properties +++ b/src/main/g8/default.properties @@ -1,2 +1,3 @@ name=My Something Project description=Say something about this template. +verbatim = scripts/sbt Procfile *.conf *.scala *.scala.html *.js *.png