JiHan Kim 2014-03-31
Login: added ajax dialog form
@eee6252c2221305217ed9a6e26c72de21204328f
app/assets/stylesheets/less/_mixins.less
--- app/assets/stylesheets/less/_mixins.less
+++ app/assets/stylesheets/less/_mixins.less
@@ -19,9 +19,9 @@
 
 // Drop shadows
 .box-shadow(@shadow: 0 1px 2px rgba(0, 0, 0, .25)){
-    -webkit-box-shadow: @shadow;
-    -moz-box-shadow: @shadow;
-    box-shadow: @shadow;
+    -webkit-box-shadow: @shadow !important;
+    -moz-box-shadow: @shadow !important;
+    box-shadow: @shadow !important;
 }
 
 // Transitions
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -2957,6 +2957,8 @@
         }
         .disabled {
             resize:none;
+            height:80px;
+            background:#f3f3f3;
         }
         input[type="radio"]{
           display: none;
@@ -2996,9 +2998,6 @@
             margin-right: 5px;
         }
     }
-    /*a {
-        margin-left: 5px;
-    }*/
 }
 .board-actrow {
     margin:20px 0;
@@ -5665,3 +5664,21 @@
     }
 }
 
+.loginDialog {
+    width:460px; margin-left:-230px;
+    input { .box-shadow(none); }
+    .login-form-wrap { margin:20px auto !important; }
+    .error {
+        display:none;
+        margin-bottom:20px;
+        color:#E74C3C;
+        .yobicon-error { vertical-align: middle; margin-right:5px; font-size:13px; }
+    }
+    .act-row {
+        margin-bottom:20px;
+        input.checkbox { margin-top:4px !important; }
+        label.bg-checkbox { display:inline-block; }
+    }
+    .btns-row { width:auto; }
+    .fullsize { width:100%; }
+}
app/controllers/UserApp.java
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
@@ -149,42 +149,66 @@
 
     /**
      * 로그인 처리
-     * 시스템 설정에서 가입승인 기능이 활성화 되어 있고 사용자 상태가 잠금상태(미승인?)라면 계정이 잠겼다는 메시지를 노출하고 로그인 폼으로 돌아감
-     * 시스템 설정에서 가입승인 기능이 활성화 되어 있지 않다면, 사용자 상태가 잠금상태라도 로그인이 가능하다 (스펙확인 필요)
-     * 요청의 정보로 사용자 인증에 성공하면 로그인쿠키를 생성하고 로그인유지하기가 선택되었다면, 로그인유지를 위한 쿠키를 별도로 생성한다
-     * 인증에 실패하면 관련된 메시지를 노출하고 로그인 폼으로 돌아간다
      *
      * @return
      */
+
     public static Result login() {
+        if (HttpUtil.isJSONPreferred(request())) {
+            return loginByAjaxRequest();
+        } else {
+            return loginByFormRequest();
+        }
+    }
+
+    /**
+     * Process login in general case of request.
+     *
+     * Returns:
+     * - If "signup.require.confirm = true" has enabled in application.conf,
+     *   the user in state of locked(or unconfirmed) cannot be logged in.
+     *   and page will be redirected to login form with message "user.locked".
+     *
+     * - If "signup.require.confirm" is disabled(as default),
+     *   the user in state of locked can be logged in. (TODO: check this in feature specification).
+     *
+     * - If failed to authentication, redirect to login form with error message.
+     *
+     * Cookie for login will be created
+     * if success to authenticate with request.
+     *
+     * If "rememberMe" included in request,
+     * Cookie for "rememberMe" (which means "Stay logged in") will be create
+     * separate from login cookie.
+     *
+     * @return
+     */
+    private static Result loginByFormRequest(){
         Form<User> userForm = form(User.class).bindFromRequest();
+
         if(userForm.hasErrors()) {
             return badRequest(login.render("title.login", userForm, null));
         }
+
         User sourceUser = form(User.class).bindFromRequest().get();
-
-        Map<String, String[]> params = request().body().asFormUrlEncoded();
-        String redirectUrl = HttpUtil.getFirstValueFromQuery(params, "redirectUrl");
-
-        String loginFormUrl = routes.UserApp.loginForm().url();
-        loginFormUrl += "?redirectUrl=" + redirectUrl;
 
         if (isUseSignUpConfirm()) {
             if (User.findByLoginId(sourceUser.loginId).state == UserState.LOCKED) {
                 flash(Constants.WARNING, "user.locked");
-                return redirect(loginFormUrl);
+                return redirect(getLoginFormURLWithRedirectURL());
             }
         }
 
         if (User.findByLoginId(sourceUser.loginId).state == UserState.DELETED) {
             flash(Constants.WARNING, "user.deleted");
-            return redirect(loginFormUrl);
+            return redirect(getLoginFormURLWithRedirectURL());
         }
 
         User authenticate = authenticateWithPlainPassword(sourceUser.loginId, sourceUser.password);
 
         if (!authenticate.isAnonymous()) {
             addUserInfoToSession(authenticate);
+
             if (sourceUser.rememberMe) {
                 setupRememberMe(authenticate);
             }
@@ -192,7 +216,9 @@
             authenticate.lang = play.mvc.Http.Context.current().lang().code();
             authenticate.update();
 
-            if (StringUtils.isEmpty(redirectUrl)) {
+            String redirectUrl = getRedirectURLFromParams();
+
+            if(StringUtils.isEmpty(redirectUrl)){
                 return redirect(routes.Application.index());
             } else {
                 return redirect(redirectUrl);
@@ -204,6 +230,89 @@
     }
 
     /**
+     * Process login request by AJAX
+     *
+     * Almost same with loginByFormRequest
+     * except part of handle with "redirectUrl" has excluded.
+     *
+     * Returns:
+     * - In case of success: empty JSON string {}
+     * - In case of failed: error message as JSON string in form of {"message":"cause"}.
+     *
+     * @return
+     */
+    private static Result loginByAjaxRequest(){
+        Form<User> userForm = form(User.class).bindFromRequest();
+
+        if(userForm.hasErrors()) {
+            return badRequest(getObjectNodeWithMessage("validation.required"));
+        }
+
+        User sourceUser = form(User.class).bindFromRequest().get();
+
+        if (isUseSignUpConfirm()) {
+            if (User.findByLoginId(sourceUser.loginId).state == UserState.LOCKED) {
+                return forbidden(getObjectNodeWithMessage("user.locked"));
+            }
+        }
+
+        if (User.findByLoginId(sourceUser.loginId).state == UserState.DELETED) {
+            return notFound(getObjectNodeWithMessage("user.deleted"));
+        }
+
+        User authenticate = authenticateWithPlainPassword(sourceUser.loginId, sourceUser.password);
+
+        if (!authenticate.isAnonymous()) {
+            addUserInfoToSession(authenticate);
+
+            if (sourceUser.rememberMe) {
+                setupRememberMe(authenticate);
+            }
+
+            authenticate.lang = play.mvc.Http.Context.current().lang().code();
+            authenticate.update();
+
+            return ok("{}");
+        }
+
+        return forbidden(getObjectNodeWithMessage("user.login.failed"));
+    }
+
+    /**
+     * Get value of "redirectUrl" from query
+     * @return
+     */
+    private static String getRedirectURLFromParams(){
+        Map<String, String[]> params = request().body().asFormUrlEncoded();
+        return HttpUtil.getFirstValueFromQuery(params, "redirectUrl");
+    }
+
+    /**
+     * Get login form URL string with "redirectUrl" parameter in query
+     * @return
+     */
+    private static String getLoginFormURLWithRedirectURL(){
+        String redirectUrl = getRedirectURLFromParams();
+        String loginFormUrl = routes.UserApp.loginForm().url();
+        loginFormUrl = loginFormUrl + "?redirectUrl=" + redirectUrl;
+
+        return loginFormUrl;
+    }
+
+    /**
+     * Returns ObjectNode which has "message" node filled with {@code message}
+     * loginByAjaxRequest() uses this to return result as JSON string
+     *
+     * @param message
+     * @return
+     */
+    private static ObjectNode getObjectNodeWithMessage(String message){
+        ObjectNode result = Json.newObject();
+        result.put("message", message);
+        return result;
+    }
+
+    /**
      * loginId 와 hash 값을 이용해서 사용자 인증.
      * 인증에 성공하면 DB 에서 조회된 사용자 정보를 리턴
      * 인증에 실패하면 {@code User.anonymous} 리턴
 
app/views/common/loginDialog.scala.html (added)
+++ app/views/common/loginDialog.scala.html
@@ -0,0 +1,68 @@
+@**
+* Yobi, Project Hosting SW
+*
+* Copyright 2014 NAVER Corp.
+* http://yobi.io
+*
+* @Author Jihan Kim
+*
+* 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.
+**@
+@()
+
+<div id="loginDialog" class="modal hide loginDialog" tabindex="-1" role="dialog">
+  <div class="modal-body">
+    <div class="pull-right">
+      <button type="button" class="close" data-dismiss="modal" aria-hidden="true" class="mr10">&times;</button>
+    </div>
+    <form action="@routes.UserApp.login()" method="post" class="frm-wrap login-form-wrap">
+      <dl>
+        <dt>
+          <label for="loginIdD">@Messages("user.loginId")</label>
+        </dt>
+        <dd>
+          <input id="loginIdD" name="loginId" type="text" class="text email" autocomplete="off">
+        </dd>
+
+        <dt>
+          <label for="passwordD">@Messages("user.password")</label>
+        </dt>
+        <dd>
+          <input id="passwordD" name="password" type="password" class="text password" autocomplete="off">
+        </dd>
+      </dl>
+
+      <div class="error">
+        <i class="yobicon-error"></i>
+        <span class="error-message">@Messages("user.login.failed")</span>
+      </div>
+
+      <div class="btns-row nm">
+        <button type="submit" class="ybtn ybtn-primary fullsize">@Messages("button.login")</button>
+      </div>
+
+      <div class="act-row right-txt mt20">
+        <div class="pull-left">
+          <input id="remember-meD" type="checkbox" name="rememberMe" class="checkbox">
+          <label for="remember-meD" class="bg-checkbox">@Messages("title.rememberMe")</label>
+        </div>
+
+        <a href="@routes.PasswordResetApp.lostPassword()">@Messages("title.resetPassword")</a>
+        <span class="gray-txt ml10 mr10">|</span>
+        <a href="@routes.UserApp.signupForm">@Messages("title.signup")</a>
+      </div>
+    </form>
+  </div>
+</div>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery-ui-1.10.4.custom.min.js")"></script>
+<script type="text/javascript" src="@routes.Assets.at("javascripts/common/yobi.LoginDialog.js")"></script>
app/views/common/scripts.scala.html
--- app/views/common/scripts.scala.html
+++ app/views/common/scripts.scala.html
@@ -55,6 +55,8 @@
 </div>
 @**<!-- //yobi.ui.Spinner -->**@
 
+@common.loginDialog()
+
 <script type="text/javascript" src="@routes.Application.jsMessages()"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery.tmpl.js")"></script>
 <script type="text/javascript" src="@routes.Assets.at("javascripts/lib/jquery/jquery.form.js")"></script>
app/views/common/usermenu.scala.html
--- app/views/common/usermenu.scala.html
+++ app/views/common/usermenu.scala.html
@@ -126,7 +126,7 @@
         </a>
     </li>
     <li class="gnb-usermenu-item">
-        <a href="@routes.UserApp.loginForm()" class="user-item-btn" >
+        <a href="@routes.UserApp.loginForm()" class="user-item-btn" login-required>
             @Messages("title.login")
         </a>
     </li>
 
public/javascripts/common/yobi.LoginDialog.js (added)
+++ public/javascripts/common/yobi.LoginDialog.js
@@ -0,0 +1,98 @@
+/**
+ * Yobi, Project Hosting SW
+ *
+ * Copyright 2014 NAVER Corp.
+ * http://yobi.io
+ *
+ * @author Jihan Kim
+ *
+ * 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.
+ */
+$(function(){
+    "use strict";
+
+    var htElement = {};
+
+    function _init(){
+        // do this except in loginForm, signUpForm
+        if(location.pathname.substr(1).split('/')[0] === "users"){
+            return;
+        }
+
+        _initElement();
+        _attachEvent();
+    }
+
+    function _initElement(){
+        htElement.welDialog = $("#loginDialog");
+        htElement.welForm = htElement.welDialog.find("form");
+        htElement.welInputId = htElement.welDialog.find("input[name='loginId']");
+        htElement.welInputPw = htElement.welDialog.find("input[name='password']");
+        htElement.welLoginError = htElement.welDialog.find(".error");
+        htElement.welLoginErrorMsg = htElement.welLoginError.find(".error-message");
+    }
+
+    function _attachEvent(){
+        $(document.body).on('click', '[login-required]', _showDialog);
+        htElement.welForm.on('submit', _onSubmitForm);
+    }
+
+    function _showDialog(weEvt){
+        if(_isInputElement(weEvt.target)){
+            $(weEvt.target).blur();
+        }
+
+        htElement.welLoginError.hide();
+        htElement.welInputPw.val("");
+        htElement.welInputId.val("");
+
+        htElement.welDialog.modal("show");
+        htElement.welInputId.focus();
+
+        weEvt.preventDefault();
+        weEvt.stopPropagation();
+        return false;
+    }
+
+    function _isInputElement(el){
+        return (["INPUT", "TEXTAREA"].indexOf(el.tagName.toUpperCase()) > -1);
+    }
+
+    function _onSubmitForm(weEvt){
+        $.ajax(htElement.welForm.attr("action"), {
+            "type": "post",
+            "dataType": "json",
+            "data": {
+                "loginId" : htElement.welInputId.val(),
+                "password": htElement.welInputPw.val()
+            }
+        }).done(function(){
+            document.location.reload();
+        }).fail(function(htResult){
+            _showDialogError(Messages(htResult.message));
+        });
+
+        weEvt.preventDefault();
+        weEvt.stopPropagation();
+        return false;
+    }
+
+    function _showDialogError(sMessage){
+        htElement.welLoginErrorMsg.html(sMessage);
+        htElement.welLoginError.show();
+        htElement.welDialog.effect("shake", {"distance": 2}, 200);
+        htElement.welInputId.focus();
+    }
+
+    _init();
+});
Add a comment
List