--- 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">