Permalink
Browse files

Added flash sessions!

Prety cool and show an example of showing it for login that fails
  • Loading branch information...
fzakaria committed Oct 27, 2014
1 parent 1a41c4f commit 7eb054bfb96d4dac3c62fea51a82a8a0932c0051
@@ -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)
@@ -20,6 +20,13 @@ trait SessionDirectives {
import CookieDirectives._
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.