Permalink
Browse files

Added flash sessions!

Prety cool and show an example of showing it for login that fails
  • Loading branch information...
1 parent 1a41c4f commit 7eb054bfb96d4dac3c62fea51a82a8a0932c0051 @fzakaria committed Oct 27, 2014
@@ -1,5 +1,5 @@
akka {
- loglevel = DEBUG
+ loglevel = INFO
}
spray.can.server {
@@ -17,7 +17,7 @@
<!-- Off these ones as they are annoying, and anyway we manage configuration ourself -->
- <root level="DEBUG">
+ <root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
@@ -17,6 +17,7 @@ import com.github.fzakaria.addressme.factories.{ UserRepositoryFactory, UserRepo
import spray.util.LoggingContext
import spray.http.StatusCodes._
import spray.httpx.PlayTwirlSupport._
+import spray.routing.SessionDirectives._
trait ApiActor extends HttpServiceActor with ActorLogging {
me: ApiRouterFactory with StaticRouterFactory with UserRepositoryFactory =>
@@ -29,7 +30,9 @@ trait ApiActor extends HttpServiceActor with ActorLogging {
//log the request/response when Akkas log level is debug or lower
logRequestResponse("[API]") {
authenticate(SessionLoginAuth(findUser)) { potentialUser =>
- apiRouter.route(RequestSession(potentialUser))
+ getAndClearFlash { flash =>
+ apiRouter.route(RequestSession(potentialUser, flash))
+ }
}
}
)
@@ -0,0 +1,23 @@
+package com.github.fzakaria.addressme.authentication
+
+import com.github.fzakaria.addressme.models.User
+import scala.concurrent.Future
+import com.github.fzakaria.addressme.factories.UserRepositoryFactory
+import com.roundeights.hasher.Implicits._
+
+trait UserPasswordProvider {
+
+ def name: String = "addressme"
+
+ def login(username: String, hashpwd: String): Option[User]
+
+}
+
+trait UserPasswordProviderImpl extends UserPasswordProvider {
+ me: UserRepositoryFactory =>
+
+ override def login(username: String, pwd: String): Option[User] = {
+ userRepo.findByProviderAndUserId(name, username).filter(_.password.map(p => pwd.bcrypt hash = p) getOrElse false)
+ }
+
+}
@@ -7,5 +7,5 @@ trait LoginRouterFactory {
}
trait LoginRouterFactoryImpl extends LoginRouterFactory {
- override def loginRouter: LoginRouter = new LoginRouter() with OAuth2RouterFactoryImpl
+ override def loginRouter: LoginRouter = new LoginRouter() with OAuth2RouterFactoryImpl with UserPasswordProviderFactoryImpl
}
@@ -0,0 +1,15 @@
+package com.github.fzakaria.addressme.factories
+
+import com.github.fzakaria.addressme.authentication.{ UserPasswordProvider, UserPasswordProviderImpl }
+
+trait UserPasswordProviderFactory {
+
+ val userPasswordProvider: UserPasswordProvider
+
+}
+
+trait UserPasswordProviderFactoryImpl extends UserPasswordProviderFactory {
+
+ val userPasswordProvider: UserPasswordProvider = new UserPasswordProviderImpl() with UserRepositoryFactoryImpl
+
+}
@@ -33,15 +33,6 @@ case class OAuth1Info(token: Option[String] = None, secret: Option[String] = Non
case class OAuth2Info(accessToken: Option[String] = None, tokenType: Option[String] = None,
expiresIn: Option[Int] = None, refreshToken: Option[String] = None)
-/**
- * The password details
- *
- * @param hasher the id of the hasher used to hash this password
- * @param password the hashed password
- * @param salt the optional salt used when hashing
- */
-case class PasswordInfo(hasher: Option[String] = None, password: Option[String] = None, salt: Option[String] = None)
-
case class User(
id: Option[Long] = None,
userId: Option[String] = None,
@@ -56,7 +47,7 @@ case class User(
//OAuth2
oAuth2Info: OAuth2Info = OAuth2Info(),
//PasswordInfo
- passwordInfo: PasswordInfo = PasswordInfo())
+ password: Option[String] = None)
trait UsersComponent {
me: DriverComponent =>
@@ -94,11 +85,7 @@ trait UsersComponent {
def oAuth2Info = (accessToken, tokenType, expiresIn, refreshToken) <> (OAuth2Info.tupled, OAuth2Info.unapply)
// password login
- def hasher = column[Option[String]]("hasher")
def password = column[Option[String]]("password")
- def salt = column[Option[String]]("salt")
-
- def passwordInfo = (hasher, password, salt) <> (PasswordInfo.tupled, PasswordInfo.unapply)
def * = (
id.?,
@@ -111,7 +98,7 @@ trait UsersComponent {
authMethod,
oAuth1Info,
oAuth2Info,
- passwordInfo
+ password
) <> (User.tupled, User.unapply)
}
val users = TableQuery[Users]
@@ -8,15 +8,34 @@ import spray.httpx.PlayTwirlSupport._
import com.github.fzakaria.addressme.factories.OAuth2RouterFactory
import com.typesafe.scalalogging.LazyLogging
import spray.routing.SessionDirectives._
+import com.github.fzakaria.addressme.factories.UserPasswordProviderFactory
+
+case class LoginForm(email: String, password: String, rememberMe: Boolean)
trait LoginRouter extends Routable with LazyLogging {
- me: OAuth2RouterFactory =>
+ me: OAuth2RouterFactory with UserPasswordProviderFactory =>
+
+ val incorrectLoginHandler = RejectionHandler {
+ case ValidationRejection(message, cause) :: _ => {
+ addFlash(("danger", message)) {
+ redirect("/api/login", spray.http.StatusCodes.SeeOther)
+ }
+ }
+ }
override def route(rs: RequestSession): Route = {
pathPrefix("login") {
(pathEnd & get) {
complete { html.login.render(rs) }
} ~
+ (pathEnd & post & formFields('email, 'password, 'rememberMe.as[Boolean] ? false).as(LoginForm)) { form =>
+ val user = userPasswordProvider.login(form.email, form.password)
+ handleRejections(incorrectLoginHandler) {
+ validate(user.isDefined, "We cannot find a user with those credentials") {
+ redirect("/api/home", spray.http.StatusCodes.TemporaryRedirect)
+ }
+ }
+ } ~
oauth2Router.route(rs)
} ~
path("logout" ~ Slash.?) {
@@ -0,0 +1,69 @@
+package spray.routing
+
+import com.github.fzakaria.addressme.factories.{ CryptoProviderFactoryImpl, ConfigServiceFactoryImpl }
+/**
+ * HTTP Flash scope.
+ *
+ * Flash data are encoded into an HTTP cookie, and can only contain simple `String` values.
+ */
+case class Flash(data: Map[String, String] = Map.empty[String, String]) {
+
+ /**
+ * Optionally returns the flash value associated with a key.
+ */
+ def get(key: String) = data.get(key)
+
+ /**
+ * Returns `true` if this flash scope is empty.
+ */
+ def isEmpty: Boolean = data.isEmpty
+
+ /**
+ * Adds a value to the flash scope, and returns a new flash scope.
+ *
+ * For example:
+ * {{{
+ * flash + ("success" -> "Done!")
+ * }}}
+ *
+ * @param kv the key-value pair to add
+ * @return the modified flash scope
+ */
+ def +(kv: (String, String)) = {
+ require(kv._2 != null, "Cookie values cannot be null")
+ copy(data + kv)
+ }
+
+ /**
+ * Removes a value from the flash scope.
+ *
+ * For example:
+ * {{{
+ * flash - "success"
+ * }}}
+ *
+ * @param key the key to remove
+ * @return the modified flash scope
+ */
+ def -(key: String) = copy(data - key)
+
+ /**
+ * Retrieves the flash value that is associated with the given key.
+ */
+ def apply(key: String) = data(key)
+}
+
+/**
+ * Helper utilities to manage the Session cookie.
+ */
+object Flash extends CookieBaker[Flash] with CryptoProviderFactoryImpl with ConfigServiceFactoryImpl {
+
+ val emptyCookie = new Flash
+ override val isSigned = false
+
+ override def cookieName: String = "flash"
+
+ def deserialize(data: Map[String, String]) = new Flash(data)
+
+ def serialize(session: Flash) = session.data
+}
@@ -2,7 +2,7 @@ package spray.routing
import com.github.fzakaria.addressme.models.User
-case class RequestSession(user: Option[User] = None) {
+case class RequestSession(user: Option[User] = None, flash: Flash = Flash.emptyCookie) {
def withUser(user: Option[User]): RequestSession = this.copy(user = user)
@@ -21,6 +21,13 @@ trait SessionDirectives {
import RouteDirectives._
/**
+ * Extracts either the baked cookie or the empty cookie object
+ */
+ def bakedCookieOrDefault[T <: AnyRef](baker: CookieBaker[T]): Directive[T :: HNil] = {
+ optionalBakedCookie(baker).map(optionalCookie => optionalCookie getOrElse baker.emptyCookie)
+ }
+
+ /**
* Extracts and passes a baked cookie object from the cookie
* @param baker CookieBaker that can decode the cookie
*/
@@ -55,6 +62,8 @@ trait SessionDirectives {
*/
def session: Directive[Session :: HNil] = bakedCookie(Session)
+ def flashOrDefault: Directive[Flash :: HNil] = bakedCookieOrDefault(Flash)
+
/**
* Extracts a Session value with the given name. Rejects MissingSessionRejection
* if the session value was not found
@@ -95,10 +104,20 @@ trait SessionDirectives {
*/
def setSession(mapData: (String, String)*): Directive0 = setSession(Session(Map(mapData: _*)))
+ def addFlash(flashValues: (String, String)): Directive0 = flashOrDefault.flatMap {
+ case c => setBakedCookie(Flash, c + flashValues)
+ }
+
/**
* Same as clearBakedCookie, using Session as the CookieBaker
*/
def clearSession: Directive0 = clearBakedCookie(Session)
+
+ def clearFlash: Directive0 = clearBakedCookie(Flash)
+
+ def getAndClearFlash: Directive[Flash :: HNil] = {
+ (flashOrDefault & clearFlash)
+ }
}
object SessionDirectives extends SessionDirectives
@@ -5,12 +5,12 @@
<link href="/static/css/signin.css" rel="stylesheet">
- <form class="form-signin" role="form">
+ <form class="form-signin" role="form" method="post">
<h2 class="form-signin-heading">Please sign in</h2>
- <input type="email" class="form-control" placeholder="Email address" required="" autofocus="">
- <input type="password" class="form-control" placeholder="Password" required="">
+ <input name="email" type="email" class="form-control" placeholder="Email address" required="" autofocus="">
+ <input name="password" type="password" class="form-control" placeholder="Password" required="">
<label class="checkbox">
- <input type="checkbox" value="remember-me"> Remember me
+ <input name="rememberMe" type="checkbox" value="remember-me"> Remember me
</label>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
@@ -19,10 +19,19 @@
<body>
@navbar(rs)
-
<!-- Begin page content -->
<div class="container">
+ @for((key,value) <- rs.flash.data){
+ <div class="alert alert-@key alert-dismissible" role="alert">
+ <button type="button" class="close" data-dismiss="alert">
+ <span aria-hidden="true">&times;</span>
+ <span class="sr-only">Close</span>
+ </button>
+ @value</div>
+ }
+ <div class="row">
@content
+ </div>
</div>
<div class="footer">
@@ -13,7 +13,7 @@
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
- <li class="active"><a href="/api/home">Home</a></li>
+ <li><a href="/api/home">Home</a></li>
<li><a href="/api/about">About</a></li>
<li><a href="/api/contact">Contact</a></li>
</ul>
@@ -31,6 +31,7 @@
<li><a href="/api/logout">Logout</a></li>
}.getOrElse {
<li><a href="/api/login">Login</a></li>
+ <li><a href="/api/register">Register</a></li>
}
}

0 comments on commit 7eb054b

Please sign in to comment.