Authentication
authorTomas Zeman <tzeman@volny.cz>
Fri, 20 Apr 2012 08:26:24 +0200
changeset 70 3a53fa30e03e
parent 69 b1dc0efd1303
child 71 b177060246a9
Authentication
src/main/scala/bootstrap/liftweb/Boot.scala
src/main/scala/fis/aaa/model/Authenticator.scala
src/main/scala/fis/aaa/model/UserCrud.scala
src/main/scala/fis/aaa/ui/AuthnSnippet.scala
src/main/webapp/css/base.css
src/main/webapp/login.html
src/main/webapp/templates-hidden/_resources.html
src/main/webapp/templates-hidden/_resources_cs.html
src/main/webapp/templates-hidden/default.html
--- a/src/main/scala/bootstrap/liftweb/Boot.scala	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/scala/bootstrap/liftweb/Boot.scala	Fri Apr 20 08:26:24 2012 +0200
@@ -17,7 +17,8 @@
 
 import fis.base.model._
 import fis.base.ui._
-import fis.aaa.ui.UserSnippet
+import fis.aaa.model._
+import fis.aaa.ui._
 import fis.crm.ui._
 import fis.geo.ui.{CitySnippet, CountrySnippet}
 import fis.db.SquerylTxMgr
@@ -39,12 +40,20 @@
 
     import Loc._
 
+    AuthnSnippet.init()
     SecNav.init()
 
+    /* Authn wiring */
+    UserVendors.cur.default.set(Vendor(() => AuthnSnippet.cur))
+    AuthnSnippet.
+      registerAuthenticator(FetchUserAuthenticator).
+      registerAuthenticator(PasswordAuthenticator)
+
     val menus = List(Menu("/", "FIS Main page") / "index" >> Hidden,
       Menu.i("Home") / "" , ContactSnippet.menu,
       CompanySnippet.menu,
       UserSnippet.menu,
+      AuthnSnippet.menu,
       CountrySnippet.menu,
       CitySnippet.menu)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/aaa/model/Authenticator.scala	Fri Apr 20 08:26:24 2012 +0200
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.
+ */
+package fis.aaa.model
+
+import net.liftweb.common._
+import net.liftweb.http._
+import net.liftweb.util._
+import net.liftweb.util.Helpers._
+import net.tz.lift.model._
+import scala.collection.mutable.ArrayStack
+
+trait Authenticator {
+  def authenticate(u: User, p: String): Box[User]
+}
+
+trait StackableAuthenticator extends Authenticator {
+  private val stack = new ArrayStack[Authenticator]
+
+  def registerAuthenticator(auth: Authenticator): StackableAuthenticator = {
+    stack += auth
+    this
+  }
+
+  def authenticate(in: User, p: String): Box[User] =
+    authn(stack.iterator, Full(in), p)
+
+  private def authn(it: Iterator[Authenticator], u: Box[User], p: String):
+    Box[User] =
+    if (!it.hasNext || u.isEmpty) {
+      u
+    } else {
+      val n = u flatMap { v => it.next.authenticate(v, p) }
+      authn(it, n, p)
+    }
+}
+
+object FetchUserAuthenticator extends Authenticator with UserCrud {
+  def authenticate(u: User, p: String): Box[User] = byLogin(u.login.get) $ {
+    _ match {
+      case Failure(_, _, _) => S error l10n("error.user-fetch")
+      case Empty => S error l10n("error.user-not-exist")
+      case _ => // empty
+    }}
+}
+
+object PasswordAuthenticator extends Authenticator with UserCrud with Loggable {
+  def authenticate(u: User, p: String): Box[User] = byLogin(u.login.get) flatMap { u =>
+    logger.debug("Pass: '%s' '%s' '%s'".format(p, md5(p), u.password.get))
+    ((p.length > 0) && (u.password.get == md5(p))).box(u) $ { _ match {
+    case Full(_) => // ok
+    case _ => S error l10n("error.invalid-password")
+  }}
+  }
+}
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/aaa/model/UserCrud.scala	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/scala/fis/aaa/model/UserCrud.scala	Fri Apr 20 08:26:24 2012 +0200
@@ -16,9 +16,13 @@
 package fis.aaa.model
 
 import fis.base.model.RecordCrud
+import net.liftweb.common._
+import net.liftweb.squerylrecord.RecordTypeMode._
 
 trait UserCrud extends RecordCrud[User] {
   val table = AaaSchema.userT
+  def byLogin(l: String): Box[User] = from(table)(u =>
+    where(u.deleted === false and u.login === l) select(u)) headOption
 }
 
 object UserCrud extends UserCrud
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/fis/aaa/ui/AuthnSnippet.scala	Fri Apr 20 08:26:24 2012 +0200
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
+ *
+ * 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.
+ */
+package fis.aaa.ui
+
+import fis.aaa.model._
+import fis.base.ui.EntityLink
+import net.liftweb.common._
+import net.liftweb.http._
+import net.liftweb.sitemap._
+import net.liftweb.sitemap.Loc._
+import net.liftweb.squerylrecord.RecordTypeMode._
+import net.liftweb.util._
+import net.liftweb.util.Helpers._
+import net.tz.lift.model._
+import net.tz.lift.snippet._
+import scala.xml.{NodeSeq, Text}
+
+/**
+ * Login/logout snippet.
+ */
+object AuthnSnippet extends UserCrud with Loggable with
+  StackableAuthenticator {
+
+  object cur extends SessionVar[Box[User]](Empty)
+  object whence extends RequestVar[String](S.referer openOr "/")
+
+  val loginOp = Menu("login.title", l10n("login.title")) / "login" >>
+    locTpl("login") >> Snippet("form", render) >> Hidden
+
+  val logoutOp = Menu("logout.title", l10n("logout.title")) / "logout" >>
+    EarlyResponse({() =>
+      cur(None)
+      Full(RedirectWithState("/", RedirectState(Empty,
+        l10n("logout.success") -> NoticeType.Notice)))
+    }) >> Hidden
+
+  val menu = loginOp submenus (logoutOp)
+
+  def init() {
+    LiftRules.snippets.append {
+      case List("user-label") => userLabel
+    }
+  }
+
+  private def render: CssTr = {
+    for {
+      r <- S.request if r.post_?
+      un <- S.param("login") $ {
+        case Full("") | Empty => S error l10n("error.login.required")
+        case _ =>
+      } if un.length > 0
+      pass <- S.param("pass") $ {
+        case Full("") | Empty => S error l10n("error.password.required")
+        case _ =>
+      } if pass.length > 0
+    } {
+      cur(authenticate(User.createRecord.login(un), pass))
+      cur map { _ =>
+        S notice l10n("login.welcome")
+        S.redirectTo(whence)
+      }
+    }
+    ("#login [value]" #> S.param("login") &
+     "#whence" #> SHtml.hidden(whence(_), whence))
+  }
+
+  private val lpars = "rel" -> "nofollow,noindex"
+
+  private def userLabel: CssTr = "*" #> (User.get match {
+    case Full(u) =>
+      val l = logoutOp.loc
+      i18n("logged.as") ++ EntityLink(u).map(_.asHtml) ++ Text(" [") ++
+      (a(l.calcDefaultHref)(l.linkText openOr NodeSeq.Empty) % lpars) ++
+      Text("]")
+    case _ =>
+      val l = loginOp.loc
+      a(l.calcDefaultHref)(l.linkText openOr NodeSeq.Empty) % lpars
+  })
+}
+
+// vim: set ts=2 sw=2 et:
--- a/src/main/webapp/css/base.css	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/webapp/css/base.css	Fri Apr 20 08:26:24 2012 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright 2011 Tomas Zeman <tzeman@volny.cz>
+ * Copyright 2011-2012 Tomas Zeman <tzeman@volny.cz>
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -73,3 +73,7 @@
 .sf-menu span {
   padding: 0.75em 1em;
 }
+
+#user-label {
+  font-size: 12px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/login.html	Fri Apr 20 08:26:24 2012 +0200
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Fis login</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="span12">
+        <form method="post" class="lift:form form-horizontal">
+          <fieldset>
+            <legend><span class="lift:loc?locid=login.form-title"/></legend>
+            <div class="control-group">
+              <label class="control-label" for="login">
+                <span class="lift:loc?locid=user.login"></span></label>
+              <div class="controls">
+                <input type="text" class="input-xlarge" id="login" name="login">
+              </div>
+            </div>
+            <div class="control-group">
+              <label class="control-label" for="pass">
+                <span class="lift:loc?locid=user.password"></span></label>
+              <div class="controls">
+                <input type="password" class="input-xlarge" id="pass" name="pass">
+              </div>
+            </div>
+            <div class="form-actions">
+              <button class="btn btn-primary" type="submit">
+                <span class="lift:loc?locid=btn.login"></span>
+              </button>
+            </div>
+          </fieldset>
+          <span id="whence"/>
+        </form>
+      </div>
+    </div>
+  </body>
+</html>
--- a/src/main/webapp/templates-hidden/_resources.html	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources.html	Fri Apr 20 08:26:24 2012 +0200
@@ -9,6 +9,19 @@
   <res name="Add" lang="en" default="true">Add</res>
   <res name="linkName" lang="en" default="true">Name</res>
 
+  <!-- authn -->
+  <res name="login.title" lang="en" default="true">Log In</res>
+  <res name="logout.title" lang="en" default="true">Log Out</res>
+  <res name="login.welcome" lang="en" default="true">Welcome!</res>
+  <res name="logout.success" lang="en" default="true">You were successfully logged out.</res>
+  <res name="logged.as" lang="en" default="true">Logged as </res>
+  <res name="btn.login" lang="en" default="true">Log In</res>
+  <res name="error.login.required" lang="en" default="true">Login value required.</res>
+  <res name="error.password.required" lang="en" default="true">Password required.</res>
+  <res name="error.user-fetch" lang="en" default="true">Error while retrieving user from database.</res>
+  <res name="error.user-not-exist" lang="en" default="true">User does not exist.</res>
+  <res name="error.invalid-password" lang="en" default="true">Invalid password.</res>
+
 
   <-- contact -->
   <res name="Contact" lang="en" default="true">Contact</res>
--- a/src/main/webapp/templates-hidden/_resources_cs.html	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources_cs.html	Fri Apr 20 08:26:24 2012 +0200
@@ -10,6 +10,20 @@
   <res name="linkName" lang="cs">Název</res>
 
 
+  <!-- authn -->
+  <res name="login.title" lang="cs">Log In</res>
+  <res name="logout.title" lang="cs">Log Out</res>
+  <res name="login.welcome" lang="cs">Vítejte!</res>
+  <res name="logout.success" lang="cs">Byli jste úspěšně odhlášeni.</res>
+  <res name="logged.as" lang="cs">Přihlášen jako </res>
+  <res name="btn.login" lang="cs">Log In</res>
+  <res name="error.login.required" lang="cs">Login je povinný.</res>
+  <res name="error.password.required" lang="cs">Heslo je povinné.</res>
+  <res name="error.user-fetch" lang="cs">Chyba při načítání uživatele.</res>
+  <res name="error.user-not-exist" lang="cs">Uživatel neexistuje.</res>
+  <res name="error.invalid-password" lang="cs">Nesprávné heslo.</res>
+
+
   <-- contact -->
   <res name="Contact" lang="cs">Kontakt</res>
   <res name="Contact %s" lang="cs">Kontakt %s</res>
--- a/src/main/webapp/templates-hidden/default.html	Fri Apr 20 08:26:24 2012 +0200
+++ b/src/main/webapp/templates-hidden/default.html	Fri Apr 20 08:26:24 2012 +0200
@@ -25,6 +25,9 @@
       <div class="hero-unit">
         <h1>Functional Information System</h1>
         <p>Project management, CRM, ...</p>
+        <p id="user-label" class="pull-right">
+          <span class="lift:user-label"></span>
+        </p>
       </div>
       <div id="topnav" class="navbar">
         <div class="navbar-inner">