diff --git a/README.ja.md b/README.ja.md index 749947f..5e1889b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -10,7 +10,7 @@ Play2.x module for Authentication and Authorization [![Build Status](https://sec Java版には [Deadbolt 2](https://github.com/schaloner/deadbolt-2) というモジュールがありますので こちらも参考にして下さい。 -Play2.0.4 及び Play2.1.0 で動作確認をしています。 +Play2.1.0 で動作確認をしています。 動機 --------------------------------------- @@ -35,27 +35,35 @@ Play2.0.4 及び Play2.1.0 で動作確認をしています。 柔軟に他の操作を組み合わせて使用することができます。 -導入 +以前のバージョン --------------------------------------- -`Build.scala` もしくは `build.sbt` にライブラリ依存性定義を追加します。 +Play2.0.x 向けの使用方法は [こちら](https://github.com/t2v/play20-auth/blob/release0.7/README.ja.md)をご参照ください。 + +0.8以前をお使いの方へ +--------------------------------------- + +version 0.8以降、アーティファクトIDとパッケージ名が変更になっています。 -* __Play2.0.4版__ +0.7以前からバージョンアップを行う方はご注意ください。 - "jp.t2v" %% "play20.auth" % "0.5" +導入 +--------------------------------------- -* __Play2.1-RC1版__ +`Build.scala` もしくは `build.sbt` にライブラリ依存性定義を追加します。 - "jp.t2v" %% "play21.auth" % "0.7" + "jp.t2v" %% "play2.auth" % "0.8" + "jp.t2v" %% "play2.auth.test" % "0.8" % "test" For example: `Build.scala` ```scala val appDependencies = Seq( - "jp.t2v" %% "play21.auth" % "0.7" + "jp.t2v" %% "play2.auth" % "0.8", + "jp.t2v" %% "play2.auth.test" % "0.8" % "test" ) - val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA) + val main = play.Project(appName, appVersion, appDependencies) ``` このモジュールはシンプルな Scala ライブラリとして作成されています。 `play.plugins` ファイルは作成する必要ありません。 @@ -64,7 +72,7 @@ For example: `Build.scala` 使い方 --------------------------------------- -1. `app/controllers` 以下に `jp.t2v.lab.play20.auth.AuthConfig` を実装した `trait` を作成します。 +1. `app/controllers` 以下に `jp.t2v.lab.play2.auth.AuthConfig` を実装した `trait` を作成します。 ```scala // (例) @@ -153,7 +161,7 @@ For example: `Build.scala` 1. 次にログイン、ログアウトを行う `Controller` を作成します。 この `Controller` に、先ほど作成した `AuthConfigImpl` トレイトと、 - `jp.t2v.lab.play20.auth.LoginLogout` トレイトを mixin します。 + `jp.t2v.lab.play2.auth.LoginLogout` トレイトを mixin します。 ```scala object Application extends Controller with LoginLogout with AuthConfigImpl { @@ -174,7 +182,7 @@ For example: `Build.scala` * gotoLogoutSucceeded メソッドを呼び出した結果を返して下さい。 * * gotoLogoutSucceeded メソッドは Result を返します。 - * jp.t2v.lab.play20.auth._ を import していた場合、 + * jp.t2v.lab.play2.auth._ を import していた場合、 * 以下のようにflashingなどを追加することもできます。 * * gotoLogoutSucceeded.flashing( @@ -191,7 +199,7 @@ For example: `Build.scala` * gotoLoginSucceeded メソッドを呼び出した結果を返して下さい。 * * gotoLoginSucceeded メソッドも gotoLogoutSucceeded と同じく Result を返します。 - * jp.t2v.lab.play20.auth._ を import して、任意の処理を追加することも可能です。 + * jp.t2v.lab.play2.auth._ を import して、任意の処理を追加することも可能です。 */ def authenticate = Action { implicit request => loginForm.bindFromRequest.fold( @@ -204,32 +212,38 @@ For example: `Build.scala` ``` 1. 最後は、好きな `Controller` に 先ほど作成した `AuthConfigImpl` トレイトと - `jp.t2v.lab.play20.auth.Auth` トレイト を mixin すれば、認証/認可の仕組みを導入することができます。 + `jp.t2v.lab.play2.auth.AuthElement` トレイト を mixin すれば、認証/認可の仕組みを導入することができます。 ```scala - object Message extends Controller with Auth with AuthConfigImpl { + object Message extends Controller with AuthElement with AuthConfigImpl { - // authorizedAction は 第一引数に権限チェック用の Authority を取り、 - // 第二引数に User => Request[AnyContent] => Result な関数を取り、 - // Action を返します。 + // StackAction の 引数に権限チェック用の (AuthorityKey, Authority) 型のオブジェクトを指定します。 + // 第二引数に RequestWithAttribute[AnyContent] => Result な関数を渡します。 - def main = authorizedAction(NormalUser) { user => implicit request => + // AuthElement は loggedIn[A](implicit RequestWithAttribute[A]): User というメソッドをもっています。 + // このメソッドから認証/認可済みのユーザを取得することができます。 + + def main = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "message main" Ok(html.message.main(title)) } - def list = authorizedAction(NormalUser) { user => implicit request => + def list = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "all messages" Ok(html.message.list(title)) } - def detail(id: Int) = authorizedAction(NormalUser) { user => implicit request => + def detail(id: Int) = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "messages detail " Ok(html.message.detail(title + id)) } // このActionだけ、Administrator でなければ実行できなくなります。 - def write = authorizedAction(Administrator) { user => implicit request => + def write = aStackAction(AuthorityKey -> Administrator) { implicit request => + val user = loggedIn val title = "write message" Ok(html.message.write(title)) } @@ -237,6 +251,50 @@ For example: `Build.scala` } ``` +テスト +--------------------------------------- + +play2.auth では、version 0.8 からテスト用のサポートを提供しています。 + +`FakeRequest` を使って `Controller` のテストを行う際に、 +ログイン状態のユーザを指定することができます。 + +```scala +package test + +import org.specs2.mutable._ + +import play.api.test._ +import play.api.test.Helpers._ +import controllers.{AuthConfigImpl, Messages} +import jp.t2v.lab.play2.auth.test.Helpers._ + +class ApplicationSpec extends Specification { + + object config extends AuthConfigImpl + + "Messages" should { + "return list when user is authorized" in new WithApplication { + val res = Messages.list(FakeRequest().withLoggedIn(config)(1)) + contentType(res) must equalTo("text/html") + } + } + +} +``` + +1. まず `jp.t2v.lab.play2.auth.test.Helpers._` を import します。 +1. 次にテスト対象に mixin されているものと同じ `AuthConfigImpl` のインスタンスを生成します。 + + object config extends AuthConfigImpl + +1. `FakeRequest` の `withLoggedIn` メソッドを呼び出します。 + * 第一引数には、先ほど定義した `AuthConfigImpl` インスタンス + * 第二引数には、このリクエストがログインしている事にする、対象のユーザIDを指定します。 + + +以上で play2.auth を使用したコントローラのテストを行うことができます。 + 高度な使い方 @@ -264,12 +322,12 @@ trait AuthConfigImpl extends AuthConfig { ``` ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with AuthElement with AuthConfigImpl { private def sameAuthor(messageId: Int)(account: Account): Boolean = Message.getAuther(messageId) == account - def edit(messageId: Int) = authorizedAction(sameAuthor(messageId)) { user => request => + def edit(messageId: Int) = StackAction(AuthorityKey -> sameAuthor(messageId)) { request => val target = Message.findById(messageId) Ok(html.message.edit(messageForm.fill(target))) } @@ -307,13 +365,16 @@ trait AuthConfigImpl extends AuthConfig { トップページなどにおいて、未ログイン状態でも画面を正常に表示し、 ログイン状態であればユーザ名などを表示する、といったことがしたい場合、 -以下のように `optionalUserAction` を使用することで実現することができます。 +以下のように `AuthElement` の代わりに `OptionalAuthElement` を使用することで実現することができます。 + +`OptionalAuthElement` を使用する場合、`Authority` は必要ありません。 ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with OptionalAuthElement with AuthConfigImpl { // maybeUser is an Option[User] instance. - def index = optionalUserAction { maybeUser => request => + def index = StackAction { implicit request => + val maybeUser: Option[User] = loggedIn val user: User = maybeUser.getOrElse(GuestUser) Ok(html.index(user)) } @@ -322,6 +383,25 @@ object Application extends Controller with Auth with AuthConfigImpl { ``` +### 認証だけ行って認可は行わない。 + +認証だけ行うこともできます。 + +`AuthElement` の代わりに `AuthenticationElement` を使うだけです。 +この場合、 `AuthorityKey` の指定は必要ありません。 + +```scala +object Application extends Controller with AuthenticationElement with AuthConfigImpl { + + def index = StackAction { implicit request => + val user: User = loggedIn + Ok(html.index(user)) + } + +} +``` + + ### Ajaxリクエスト時の認証失敗で401を返す 通常のアクセスで認証が失敗した場合にはログイン画面にリダイレクトさせたいけれども、 @@ -342,70 +422,47 @@ def authenticationFailed(request: RequestHeader) = { ### 他のAction操作と合成する +[stackable-controller](https://github.com/t2v/stackable-controller) の仕組みを使用します。 + 例えば、CSRF対策で各Actionでトークンのチェックをしたい、としましょう。 -全てのActionで毎回チェックロジックを書くのは大変なので、普通はこんなActionの拡張をすると思います。 +全てのActionで毎回チェックロジックを書くのは大変なので、以下のようなトレイトを作成します。 ```scala -object Application extends Controller { +trait TokenValidateElement extends StackableController { + self: Controller => // Token の発行処理は省略 - val tokenForm = Form("token" -> text) + private val tokenForm = Form("token" -> text) private def validateToken(request: Request[AnyContent]): Boolean = (for { tokenInForm <- tokenForm.bindFromRequest(request).value tokenInSession <- request.session.get("token") } yield tokenInForm == tokenInSession).getOrElse(false) - private def validAction(f: Request[AnyContent] => Result) = Action { request => - if (validateToken(request)) f(request) + abstract override proceed[A](reqest: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Result): Result = { + if (validateToken(request)) super.proceed(request)(f) else BadRequest } - def page1 = validAction { request => - // do something - Ok(html.page1("result")) - } - - def page2 = validAction { request => - // do something - Ok(html.page2("result")) - } - } ``` -この validateToken に認証/認可の仕組みを組み込むにはどうすればいいでしょうか? - -`authorizedAction` メソッドの代わりに `authorized` メソッドを使うことで簡単に実現ができます。 +この `TokenValidateElement` トレイトと `AuthElement` トレイトを両方mixinすることで、 +CSRFトークンチェックと認証/認可を両方行うことができます。 ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl { // Token の発行処理は省略 - val tokenForm = Form("token" -> text) - - private def validateToken(implicit request: Request[AnyContent]): Boolean = (for { - tokenInForm <- tokenForm.bindFromRequest(request).value - tokenInSession <- request.session.get("token") - } yield tokenInForm == tokenInSession).getOrElse(false) - - private authAndValidAction(authority: Authority)(f: User => Request[AnyContent] => Result) = - Action { implicit request => - (for { - user <- authorized(authority).right - _ <- Either.cond(validateToken, (), BadRequest).right - } yield f(user)(request)).merge - } - - def page1 = authAndValidAction { user => request => + def page1 = StackAction(AuthorityKey -> NormalUser) { implicit request => // do something Ok(html.page1("result")) } - def page2 = authAndValidAction { user => request => + def page2 = StackAction(AuthorityKey -> NormalUser) { implicit request => // do something Ok(html.page2("result")) } @@ -413,46 +470,6 @@ object Application extends Controller with Auth with AuthConfigImpl { } ``` -この例だけでは簡単さが実感できないかもしれません。 -ではこれに更に pjax によって動的に Template を切り替えたいといったらどうでしょう? - -その場合でも柔軟に取込むことができます。 - -```scala - - private type Template = String => Html - private def pjax(implicit request: Request[AnyContent]): Template = { - if (request.headers.keys("X-PJAX")) { - html.pjaxTemplate.apply - } else { - val displayValues = DomainLogic.getDisplayValues() - html.fullTemplate.apply(displayValues) - } - } - - private complexAction(authority: Authority)(f: User => Template => Request[AnyContent] => Result) = - Action { implicit request => - (for { - user <- authorized(authority).right - _ <- Either.cond(validateToken, (), BadRequest).right - template <- Right(pjax).right - } yield f(user)(template)(request)).merge - } - - def page1 = complexAction { user => template => request => - // do something - Ok(template("result")) - } - - def page2 = complexAction { user => template => request => - // do something - Ok(template("result")) - } -``` - -このようにどんどん Action に対して操作の合成を行っていくことができます。 - - ### Stateless このモジュールの標準実装はステートフルな実装になっています。 @@ -489,8 +506,7 @@ trait AuthConfigImpl extends AuthConfig { 1. `git clone https://github.com/t2v/play20-auth.git` 1. `cd play20-auth` -1. `play` -1. `run` +1. `play "project sample" play run` 1. ブラウザで `http://localhost:9000/` にアクセス 1. 「Database 'default' needs evolution!」と聞かれるので `Apply this script now!` を押して実行します 1. 適当にログインします diff --git a/README.md b/README.md index 8b563a0..f5dd36f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This module targets the __Scala__ version of __Play2.x__. For the Java version of Play2.x, there is an authorization module called [Deadbolt 2](https://github.com/schaloner/deadbolt-2). -This module has been tested on Play2.0.4 and Play2.1.0 +Play2.1.0 Motivation --------------------------------------- @@ -36,27 +36,39 @@ Play2x-Auth provides an interface that returns an [`Either[PlainResult, User]`]( making writing complicated action methods easier. [`Either`](http://www.scala-lang.org/api/current/scala/Either.html) is a wrapper similar to `Option` -Installation +Previous Version --------------------------------------- -Add a dependency declaration into your `Build.scala` or `build.sbt` file: +for Play2.0.x, Please see [previous version README](https://github.com/t2v/play20-auth/tree/release0.7) + + +Attention +--------------------------------------- + +The artifact ID and package name was changed at version 0.8 -* __for Play2.0.x__ +you should be careful to version up from 0.7 - "jp.t2v" %% "play20.auth" % "0.5" + +Installation +--------------------------------------- + +Add a dependency declaration into your `Build.scala` or `build.sbt` file: * __for Play2.1.0__ - "jp.t2v" %% "play21.auth" % "0.7" + "jp.t2v" %% "play2.auth" % "0.8" + "jp.t2v" %% "play2.auth.test" % "0.8" For example your `Build.scala` might look like this: ```scala val appDependencies = Seq( - "jp.t2v" %% "play21.auth" % "0.7" + "jp.t2v" %% "play2.auth" % "0.8", + "jp.t2v" %% "play2.auth.test" % "0.8" ) - val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA) + val main = play.Project(appName, appVersion, appDependencies) ``` You don't need to create a `play.plugins` file. @@ -64,7 +76,7 @@ You don't need to create a `play.plugins` file. Usage --------------------------------------- -1. First create a trait that extends `jp.t2v.lab.play20.auth.AuthConfig` in `app/controllers`. +1. First create a trait that extends `jp.t2v.lab.play2.auth.AuthConfig` in `app/controllers`. ```scala // Example @@ -97,8 +109,6 @@ Usage * Use something like this: */ val idTag: ClassTag[Id] = classTag[Id] - // for version 0.5 as follows - // val idManifest: ClassManifest[Id] = classManifest[Id] /** * The session timeout in seconds @@ -152,7 +162,7 @@ Usage ``` 1. Next create a `Controller` that defines both login and logout actions. - This `Controller` mixes in the `jp.t2v.lab.play20.auth.LoginLogout` trait and + This `Controller` mixes in the `jp.t2v.lab.play2.auth.LoginLogout` trait and the trait that you created in first step. ```scala @@ -173,7 +183,7 @@ Usage * Return the `gotoLogoutSucceeded` method's result in the logout action. * * Since the `gotoLogoutSucceeded` returns `Result`, - * If you import `jp.t2v.lab.play20.auth._`, you can add a procedure like the following. + * If you import `jp.t2v.lab.play2.auth._`, you can add a procedure like the following. * * gotoLogoutSucceeded.flashing( * "success" -> "You've been logged out" @@ -188,7 +198,7 @@ Usage * Return the `gotoLoginSucceeded` method's result in the login action. * * Since the `gotoLoginSucceeded` returns `Result`, - * If you import `jp.t2v.lab.play20.auth._`, you can add a procedure like the `gotoLogoutSucceeded`. + * If you import `jp.t2v.lab.play2.auth._`, you can add a procedure like the `gotoLogoutSucceeded`. */ def authenticate = Action { implicit request => loginForm.bindFromRequest.fold( @@ -200,34 +210,41 @@ Usage } ``` -1. Lastly, mix `jp.t2v.lab.play20.auth.Auth` trait and the trait that was created in the first step +1. Lastly, mix `jp.t2v.lab.play2.auth.AuthElement` trait and the trait that was created in the first step into your Controllers: ```scala - object Message extends Controller with Auth with AuthConfigImpl { + object Message extends Controller with AuthElement with AuthConfigImpl { - // The `authorizedAction` method - // takes `Authority` as the first argument and - // a function signature `User => Request[AnyContent] => Result` as the second argument and + // The `StackAction` method + // takes `(AuthorityKey, Authority)` as the first argument and + // a function signature `RequestWithAttributes[AnyContent] => Result` as the second argument and // returns an `Action` - def main = authorizedAction(NormalUser) { user => implicit request => + // thw `loggedIn` method + // returns current logged in user + + def main = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "message main" Ok(html.message.main(title)) } - def list = authorizedAction(NormalUser) { user => implicit request => + def list = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "all messages" Ok(html.message.list(title)) } - def detail(id: Int) = authorizedAction(NormalUser) { user => implicit request => + def detail(id: Int) = StackAction(AuthorityKey -> NormalUser) { implicit request => + val user = loggedIn val title = "messages detail " Ok(html.message.detail(title + id)) } // Only Administrator can execute this action. - def write = authorizedAction(Administrator) { user => implicit request => + def write = StackAction(AuthorityKey -> Administrator) { implicit request => + val user = loggedIn val title = "write message" Ok(html.message.write(title)) } @@ -236,6 +253,50 @@ Usage ``` +Test +--------------------------------------- + +play2.auth provides test module at version 0.8 + +You can use `FakeRequest` with logged-in status. + +```scala +package test + +import org.specs2.mutable._ + +import play.api.test._ +import play.api.test.Helpers._ +import controllers.{AuthConfigImpl, Messages} +import jp.t2v.lab.play2.auth.test.Helpers._ + +class ApplicationSpec extends Specification { + + object config extends AuthConfigImpl + + "Messages" should { + "return list when user is authorized" in new WithApplication { + val res = Messages.list(FakeRequest().withLoggedIn(config)(1)) + contentType(res) must equalTo("text/html") + } + } + +} +``` + +1. Import `jp.t2v.lab.play2.auth.test.Helpers._` +1. Define instance what is mixed-in `AuthConfigImpl` + + object config extends AuthConfigImpl + +1. Call `withLoggedIn` method on `FakeRequest` + * first argument: `AuthConfigImpl` instance. + * second argument: user ID of the user who is logged-in at this request + + +It makes enable to test controllers with play2.auth + + Advanced usage --------------------------------------- @@ -260,12 +321,13 @@ trait AuthConfigImpl extends AuthConfig { ``` ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with AuthElement with AuthConfigImpl { private def sameAuthor(messageId: Int)(account: Account): Boolean = Message.getAuther(messageId) == account - def edit(messageId: Int) = authorizedAction(sameAuthor(messageId)) { user => request => + def edit(messageId: Int) = StackAction(AuthorityKey -> sameAuthor(messageId)) { implicit request => + val user = loggedIn val target = Message.findById(messageId) Ok(html.message.edit(messageForm.fill(target))) } @@ -301,13 +363,15 @@ trait AuthConfigImpl extends AuthConfig { ### Changing the display depending on whether the user is logged in If you want to display the application's index differently to non-logged-in users -and logged-in users, you can use `optionalUserAction`: +and logged-in users, you can use `OptionalAuthElement` insted of `AuthElement`: ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with OptionalAuthElement with AuthConfigImpl { // maybeUser is an instance of `Option[User]`. - def index = optionalUserAction { maybeUser => request => + // `OptionalAuthElement` dont need `AuthorityKey` + def index = StackAction { implicit request => + val maybeUser: Option[User] = loggedIn val user: User = maybeUser.getOrElse(GuestUser) Ok(html.index(user)) } @@ -316,6 +380,22 @@ object Application extends Controller with Auth with AuthConfigImpl { ``` +### For action that doesn't require authorization + +you can `AuthenticationElement` insted of `AuthElement` for authentication without authorization. + +```scala +object Application extends Controller with AuthenticationElement with AuthConfigImpl { + + def index = StackAction { implicit request => + val user: User = loggedIn + Ok(html.index(user)) + } + +} +``` + + ### return 401 when a request is sent by Ajax Normally, you want to return a login page redirection at a authentication failed. @@ -335,70 +415,42 @@ def authenticationFailed(request: RequestHeader) = { ### Action composition +play2.auth use [stackable-controller](https://github.com/t2v/stackable-controller) + Suppose you want to validate a token at every action in order to defeat a [Cross Site Request Forgery](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) attack. -Since it is impractical to perform the validation in all actions, usually you would define a method like this: +Since it is impractical to perform the validation in all actions, you would define a trait like this: ```scala -object Application extends Controller { +trait TokenValidateElement extends StackableController { + self: Controller => - // Other settings are omitted. - - val tokenForm = Form("token" -> text) + private val tokenForm = Form("token" -> text) private def validateToken(request: Request[AnyContent]): Boolean = (for { tokenInForm <- tokenForm.bindFromRequest(request).value tokenInSession <- request.session.get("token") } yield tokenInForm == tokenInSession).getOrElse(false) - private def validAction(f: Request[AnyContent] => Result) = Action { request => - if (validateToken(request)) f(request) + abstract override proceed[A](reqest: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Result): Result = { + if (validateToken(request)) super.proceed(request)(f) else BadRequest } - def page1 = validAction { request => - // do something - Ok(html.page1("result")) - } - - def page2 = validAction { request => - // do something - Ok(html.page2("result")) - } - } ``` -Authenticating and authorizing a user using a `validateToken` - -You need to use the `authorized` method instead of the `authorizedAction` method. +You can use `TokenValidateElement` trait with `AuthElement` trait. ```scala -object Application extends Controller with Auth with AuthConfigImpl { +object Application extends Controller with TokenValidateElement with AuthElement with AuthConfigImpl { - // The token publication is omitted. - - val tokenForm = Form("token" -> text) - - private def validateToken(implicit request: Request[AnyContent]): Boolean = (for { - tokenInForm <- tokenForm.bindFromRequest(request).value - tokenInSession <- request.session.get("token") - } yield tokenInForm == tokenInSession).getOrElse(false) - - private authAndValidAction(authority: Authority)(f: User => Request[AnyContent] => Result) = - Action { implicit request => - (for { - user <- authorized(authority).right - _ <- Either.cond(validateToken, (), BadRequest).right - } yield f(user)(request)).merge - } - - def page1 = authAndValidAction(NormalUser) { user => request => + def page1 = StackAction(AuthorityKey -> NormalUser) { implicit request => // do something Ok(html.page1("result")) } - def page2 = authAndValidAction(NormalUser) { user => request => + def page2 = StackAction(AuthorityKey -> NormalUser) { implicit request => // do something Ok(html.page2("result")) } @@ -406,43 +458,6 @@ object Application extends Controller with Auth with AuthConfigImpl { } ``` -A complex example: Changing templates dynamically using [pjax](http://pjax.heroku.com/dinosaurs.html) - - -```scala - - private type Template = String => Html - private def pjax(implicit request: Request[AnyContent]): Template = { - if (request.headers.keys("X-PJAX")) { - html.pjaxTemplate.apply - } else { - val displayValues = DomainLogic.getDisplayValues() - html.fullTemplate.apply(displayValues) - } - } - - private complexAction(authority: Authority)(f: User => Template => Request[AnyContent] => Result) = - Action { implicit request => - (for { - user <- authorized(authority).right - _ <- Either.cond(validateToken, (), BadRequest).right - template <- Right(pjax).right - } yield f(user)(template)(request)).merge - } - - def page1 = complexAction(NormalUser) { user => template => request => - // do something - Ok(template("result")) - } - - def page2 = complexAction(NormalUser) { user => template => request => - // do something - Ok(template("result")) - } -``` - -Note that you can _combine functions_ for action methods. - ### Stateless vs Stateful implementation. @@ -475,8 +490,7 @@ Running The Sample Application 1. `git clone https://github.com/t2v/play20-auth.git` 1. `cd play20-auth` -1. `play` -1. `run` +1. `play "project sample" play run` 1. access to `http://localhost:9000/` on your browser. 1. click `Apply this script now!` 1. login diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/Auth.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/Auth.scala similarity index 98% rename from module/src/main/scala/jp/t2v/lab/play20/auth/Auth.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/Auth.scala index 5f7f34a..d3fc474 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/Auth.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/Auth.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.mvc._ import play.api.libs.iteratee.{Input, Done} diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/AuthConfig.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/AuthConfig.scala similarity index 96% rename from module/src/main/scala/jp/t2v/lab/play20/auth/AuthConfig.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/AuthConfig.scala index 24016bd..9912664 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/AuthConfig.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/AuthConfig.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.mvc._ import scala.reflect.ClassTag diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/AuthElement.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/AuthElement.scala similarity index 98% rename from module/src/main/scala/jp/t2v/lab/play20/auth/AuthElement.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/AuthElement.scala index 79284e1..aeb587e 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/AuthElement.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/AuthElement.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.mvc.{Result, Controller} import jp.t2v.lab.play2.stackc.{RequestWithAttributes, RequestAttributeKey, StackableController} diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/CacheIdContainer.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/CacheIdContainer.scala similarity index 98% rename from module/src/main/scala/jp/t2v/lab/play20/auth/CacheIdContainer.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/CacheIdContainer.scala index 8e6330e..590afb7 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/CacheIdContainer.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/CacheIdContainer.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.cache.Cache import play.api.Play._ diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/CookieIdContainer.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/CookieIdContainer.scala similarity index 97% rename from module/src/main/scala/jp/t2v/lab/play20/auth/CookieIdContainer.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/CookieIdContainer.scala index 97017f3..3f0a54b 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/CookieIdContainer.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/CookieIdContainer.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import scala.util.control.Exception._ diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/CookieUtil.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/CookieUtil.scala similarity index 90% rename from module/src/main/scala/jp/t2v/lab/play20/auth/CookieUtil.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/CookieUtil.scala index 39b1cb2..1d03ec2 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/CookieUtil.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/CookieUtil.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.libs.Crypto import play.api.mvc.Cookie diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/IdContainer.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/IdContainer.scala similarity index 89% rename from module/src/main/scala/jp/t2v/lab/play20/auth/IdContainer.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/IdContainer.scala index b2e702f..02af7be 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/IdContainer.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/IdContainer.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth trait IdContainer[Id] { diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/LoginLogout.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/LoginLogout.scala similarity index 96% rename from module/src/main/scala/jp/t2v/lab/play20/auth/LoginLogout.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/LoginLogout.scala index f11aea2..2399c97 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/LoginLogout.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/LoginLogout.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20.auth +package jp.t2v.lab.play2.auth import play.api.mvc._ import play.api.mvc.Cookie diff --git a/module/src/main/scala/jp/t2v/lab/play20/auth/package.scala b/module/src/main/scala/jp/t2v/lab/play2/auth/package.scala similarity index 70% rename from module/src/main/scala/jp/t2v/lab/play20/auth/package.scala rename to module/src/main/scala/jp/t2v/lab/play2/auth/package.scala index d5d7249..fdf12ce 100644 --- a/module/src/main/scala/jp/t2v/lab/play20/auth/package.scala +++ b/module/src/main/scala/jp/t2v/lab/play2/auth/package.scala @@ -1,4 +1,4 @@ -package jp.t2v.lab.play20 +package jp.t2v.lab.play2 package object auth { diff --git a/project/Build.scala b/project/Build.scala index c4cdd33..6a54c1e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -4,10 +4,10 @@ import play.Project._ object ApplicationBuild extends Build { - val appName = "play21.auth" + val appName = "play2.auth" lazy val baseSettings = Seq( - version := "0.8-SNAPSHOT", + version := "0.8", scalaVersion := "2.10.0", scalaBinaryVersion := "2.10", crossScalaVersions := Seq("2.10.0"), @@ -16,23 +16,17 @@ object ApplicationBuild extends Build { resolvers += "Sonatype Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" ) - lazy val core = Project("core", base = file("module")) - .settings(baseSettings: _*) - .settings( - name := appName, - libraryDependencies += "play" %% "play" % "2.1.0", - libraryDependencies += "jp.t2v" %% "stackable-controller" % "0.1-SNAPSHOT", - publishMavenStyle := true, - publishArtifact in Test := false, - pomIncludeRepository := { _ => false }, - publishTo <<= version { (v: String) => - val nexus = "https://oss.sonatype.org/" - if (v.trim.endsWith("SNAPSHOT")) - Some("snapshots" at nexus + "content/repositories/snapshots") - else - Some("releases" at nexus + "service/local/staging/deploy/maven2") - }, - pomExtra := ( + lazy val appPublishMavenStyle = true + lazy val appPublishArtifactInTest = false + lazy val appPomIncludeRepository = { _: MavenRepository => false } + lazy val appPublishTo = { (v: String) => + val nexus = "https://oss.sonatype.org/" + if (v.trim.endsWith("SNAPSHOT")) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + } + lazy val appPomExtra = { https://github.com/t2v/play20-auth @@ -52,14 +46,32 @@ object ApplicationBuild extends Build { https://github.com/gakuzzzz - ) + } + + + lazy val core = Project("core", base = file("module")) + .settings(baseSettings: _*) + .settings( + libraryDependencies += "play" %% "play" % "2.1.0", + libraryDependencies += "jp.t2v" %% "stackable-controller" % "0.1", + name := appName, + publishMavenStyle := appPublishMavenStyle, + publishArtifact in Test := appPublishArtifactInTest, + pomIncludeRepository := appPomIncludeRepository, + publishTo <<=(version)(appPublishTo), + pomExtra := appPomExtra ) lazy val test = Project("test", base = file("test")) .settings(baseSettings: _*) .settings( - name := appName + ".test", - libraryDependencies += "play" %% "play-test" % "2.1.0" + libraryDependencies += "play" %% "play-test" % "2.1.0", + name := appName + ".test", + publishMavenStyle := appPublishMavenStyle, + publishArtifact in Test := appPublishArtifactInTest, + pomIncludeRepository := appPomIncludeRepository, + publishTo <<=(version)(appPublishTo), + pomExtra := appPomExtra ).dependsOn(core) lazy val sample = play.Project("sample", path = file("sample")) diff --git a/sample/app/Global.scala b/sample/app/Global.scala index 1d3d17a..9f30e33 100644 --- a/sample/app/Global.scala +++ b/sample/app/Global.scala @@ -8,9 +8,9 @@ object Global extends GlobalSettings { if (Account.findAll.isEmpty) { Seq( - Account("aaaaaa", "alice@example.com", "secret", "Alice", Administrator), - Account("bbbbbb", "bob@example.com", "secret", "Bob", NormalUser), - Account("cccccc", "chris@example.com", "secret", "Chris", NormalUser) + Account(1, "alice@example.com", "secret", "Alice", Administrator), + Account(2, "bob@example.com", "secret", "Bob", NormalUser), + Account(3, "chris@example.com", "secret", "Chris", NormalUser) ) foreach Account.create } diff --git a/sample/app/controllers/Application.scala b/sample/app/controllers/Application.scala index f207193..329b3ba 100644 --- a/sample/app/controllers/Application.scala +++ b/sample/app/controllers/Application.scala @@ -7,10 +7,10 @@ import models._ import views._ import play.api.mvc._ import play.api.mvc.Results._ -import jp.t2v.lab.play20.auth._ +import jp.t2v.lab.play2.auth._ import play.api.Play._ import play.api.cache.Cache -import reflect.{ClassTag, classTag} +import reflect.classTag import jp.t2v.lab.play2.stackc.{RequestWithAttributes, RequestAttributeKey, StackableController} object Application extends Controller with LoginLogout with AuthConfigImpl { @@ -38,16 +38,15 @@ object Application extends Controller with LoginLogout with AuthConfigImpl { } } -object Message extends Controller with Pjax with AuthElement with AuthConfigImpl { +trait Messages extends Controller with Pjax with AuthElement with AuthConfigImpl { def main = StackAction(AuthorityKey -> NormalUser) { implicit request => val title = "message main" - Cache.set("hoge", "testtttttt") Ok(html.message.main(title)) } def list = StackAction(AuthorityKey -> NormalUser) { implicit request => - val title = Cache.getAs[String]("hoge").getOrElse("all messages") + val title = "all messages" Ok(html.message.list(title)) } @@ -62,9 +61,10 @@ object Message extends Controller with Pjax with AuthElement with AuthConfigImpl } } +object Messages extends Messages trait AuthConfigImpl extends AuthConfig { - type Id = String + type Id = Int type User = Account @@ -76,7 +76,7 @@ trait AuthConfigImpl extends AuthConfig { def resolveUser(id: Id) = Account.findById(id) - def loginSucceeded(request: RequestHeader) = Redirect(routes.Message.main) + def loginSucceeded(request: RequestHeader) = Redirect(routes.Messages.main) def logoutSucceeded(request: RequestHeader) = Redirect(routes.Application.login) diff --git a/sample/app/models/Account.scala b/sample/app/models/Account.scala index 6b3e287..d737cea 100644 --- a/sample/app/models/Account.scala +++ b/sample/app/models/Account.scala @@ -4,13 +4,13 @@ import org.mindrot.jbcrypt.BCrypt import scalikejdbc._ import scalikejdbc.SQLInterpolation._ -case class Account(id: String, email: String, password: String, name: String, permission: Permission) +case class Account(id: Int, email: String, password: String, name: String, permission: Permission) object Account { val * = { rs: WrappedResultSet => Account( - id = rs.string("id"), + id = rs.int("id"), email = rs.string("email"), password = rs.string("password"), name = rs.string("name"), @@ -28,7 +28,7 @@ object Account { } } - def findById(id: String): Option[Account] = { + def findById(id: Int): Option[Account] = { DB localTx { implicit s => sql"SELECT * FROM account WHERE id = ${id}".map(*).single.apply() } diff --git a/sample/conf/application.conf b/sample/conf/application.conf index adb7ba9..600f9bd 100644 --- a/sample/conf/application.conf +++ b/sample/conf/application.conf @@ -30,7 +30,7 @@ db.default.password="" # Evolutions # ~~~~~ # You can disable evolutions if needed -#evolutionplugin=disabled +evolutionplugin=disabled # Logger # ~~~~~ diff --git a/sample/conf/routes b/sample/conf/routes index f3daf3a..9dd6dc0 100644 --- a/sample/conf/routes +++ b/sample/conf/routes @@ -10,10 +10,10 @@ POST /login controllers.Application.authenticate GET /logout controllers.Application.logout # Message -GET /message/main controllers.Message.main -GET /message/list controllers.Message.list -GET /message/detail/:id controllers.Message.detail(id: Int) -GET /message/write controllers.Message.write +GET /message/main controllers.Messages.main +GET /message/list controllers.Messages.list +GET /message/detail/:id controllers.Messages.detail(id: Int) +GET /message/write controllers.Messages.write # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file) diff --git a/sample/test/ApplicationSpec.scala b/sample/test/ApplicationSpec.scala new file mode 100644 index 0000000..5427ed3 --- /dev/null +++ b/sample/test/ApplicationSpec.scala @@ -0,0 +1,21 @@ +package test + +import org.specs2.mutable._ + +import play.api.test._ +import play.api.test.Helpers._ +import controllers.{AuthConfigImpl, Messages} +import jp.t2v.lab.play2.auth.test.Helpers._ + +class ApplicationSpec extends Specification { + + object config extends AuthConfigImpl + + "Messages" should { + "return list when user is authorized" in new WithApplication { + val res = Messages.list(FakeRequest().withLoggedIn(config)(1)) + contentType(res) must equalTo("text/html") + } + } + +} diff --git a/test/src/main/scala/jp/t2v/lab/play2/auth/test/Helpers.scala b/test/src/main/scala/jp/t2v/lab/play2/auth/test/Helpers.scala new file mode 100644 index 0000000..0685216 --- /dev/null +++ b/test/src/main/scala/jp/t2v/lab/play2/auth/test/Helpers.scala @@ -0,0 +1,22 @@ +package jp.t2v.lab.play2.auth.test + +import play.api.test._ +import play.api.mvc.{Cookie, Request} +import jp.t2v.lab.play2.auth.AuthConfig +import play.api.libs.Crypto + +trait Helpers { + + implicit class AuthFakeRequest[A](fakeRequest: FakeRequest[A]) { + + def withLoggedIn(implicit config: AuthConfig): config.Id => Request[A] = { id => + val token = config.idContainer.startNewSession(id, config.sessionTimeoutInSeconds) + val value = Crypto.sign(token) + token + import config._ + fakeRequest.withCookies(Cookie(cookieName, value, None, cookiePathOption, cookieDomainOption, cookieSecureOption, cookieHttpOnlyOption)) + } + + } + +} +object Helpers extends Helpers \ No newline at end of file