doortts doortts 2013-04-24
new feature: SignUp, pending to use and account locking
- new to hive sign-up user, pending to user until site admin confirm.
- site admin can lock user account at sites/settings menu
if you want to use sign-up confirm, uncomment below property in application.conf
#signup.require.confirm = true

see, HIVE-138
@b1651ea4714150214f35e5c3b725cf5eedd4355f
app/controllers/SiteApp.java
--- app/controllers/SiteApp.java
+++ app/controllers/SiteApp.java
@@ -93,4 +93,19 @@
     public static Result softwareMap() {
         return TODO;
     }
+
+    public static Result toggleAccountLock(String loginId){
+        if( User.findByLoginId(session().get("loginId")).isSiteManager() ){
+            User targetUser = User.findByLoginId(loginId);
+            if (targetUser.isAnonymous()){
+                flash(Constants.WARNING, "user.notExists.name");
+                return redirect(routes.SiteApp.userList(0, null));
+            }
+            targetUser.isLocked = !targetUser.isLocked;
+            targetUser.save();
+            return ok(userList.render("title.siteSetting", User.findUsers(0, null)));
+        }
+        flash(Constants.WARNING, "auth.unauthorized.title");
+        return redirect(routes.Application.index());
+    }
 }
app/controllers/UserApp.java
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
@@ -5,21 +5,12 @@
 import models.enumeration.ResourceType;
 
 import org.apache.commons.lang.StringUtils;
-import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.IncorrectCredentialsException;
-import org.apache.shiro.authc.LockedAccountException;
-import org.apache.shiro.authc.UnknownAccountException;
-import org.apache.shiro.authc.UsernamePasswordToken;
-import org.apache.shiro.config.IniSecurityManagerFactory;
 import org.apache.shiro.crypto.RandomNumberGenerator;
 import org.apache.shiro.crypto.SecureRandomNumberGenerator;
 import org.apache.shiro.crypto.hash.Sha256Hash;
-import org.apache.shiro.mgt.SecurityManager;
-import org.apache.shiro.subject.Subject;
 import org.apache.shiro.util.ByteSource;
-import org.apache.shiro.util.Factory;
 
+import play.Configuration;
 import play.Logger;
 import play.data.Form;
 import play.mvc.*;
@@ -81,15 +72,31 @@
 		return redirect(routes.Application.index());
 	}
 
+    private static boolean isUseSignUpConfirm(){
+        Configuration config = play.Play.application().configuration();
+        String useSignUpConfirm = config.getString("signup.require.confirm");
+        if (useSignUpConfirm != null && useSignUpConfirm.equals("true")) {
+            return true;
+        } else {
+            return false;
+        }
+    }
 	public static Result login() {
 		Form<User> userForm = form(User.class).bindFromRequest();
 		if(userForm.hasErrors()) {
             return badRequest(login.render("title.login", userForm));
         }
         User sourceUser = form(User.class).bindFromRequest().get();
-		User authenticate = authenticateWithPlainPassword(sourceUser.loginId, sourceUser.password);
 
-		if(authenticate!=null) {
+        if (isUseSignUpConfirm()) {
+            if( User.findByLoginId(sourceUser.loginId).isLocked == true ){
+                flash(Constants.WARNING, "user.locked");
+                return redirect(routes.UserApp.loginForm());
+            }
+        }
+        User authenticate = authenticateWithPlainPassword(sourceUser.loginId, sourceUser.password);
+
+		if(authenticate != null) {
 			addUserInfoToSession(authenticate);
 			if (sourceUser.rememberMe) {
 				setupRememberMe(authenticate);
@@ -162,13 +169,25 @@
 		else {
 			User user = newUserForm.get();
 			user.avatarUrl = DEFAULT_AVATAR_URL;
-			User.create(hashedPassword(user));
-
-			addUserInfoToSession(user);
+            lockAccountIfSignUpConfirmModeIsUsed(user);
+            User.create(hashedPassword(user));
+            if(user.isLocked){
+                flash(Constants.INFO, "user.signup.requested");
+            } else {
+                addUserInfoToSession(user);
+            }
 			return redirect(routes.Application.index());
 		}
 	}
 
+    private static void lockAccountIfSignUpConfirmModeIsUsed(User user) {
+        Configuration config = play.Play.application().configuration();
+        String useSignUpConfirm = config.getString("signup.require.confirm");
+        if (useSignUpConfirm != null && useSignUpConfirm.equals("true")) {
+            user.isLocked = true;
+        }
+    }
+
     //Fixme user.password가 plain text 였다가 다시 덮여쓰여지는 식으로 동작한다. 혹시라도 패스워드 reset을 위해 이 메소드를 잘못 사용했다가는 자칫 로그인을 할 수 없게 되는 상황이 발생할 수 있다.
 	public static User hashedPassword(User user) {
 		RandomNumberGenerator rng = new SecureRandomNumberGenerator();
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -6,12 +6,7 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.persistence.CascadeType;
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.OneToMany;
-import javax.persistence.Table;
-import javax.persistence.Transient;
+import javax.persistence.*;
 
 import models.enumeration.Direction;
 import models.enumeration.Matching;
@@ -53,10 +48,9 @@
 
     @Email(message = "user.wrongEmail.alert")
     public String email;
-
     public String avatarUrl;
-
     public boolean rememberMe;
+    public boolean isLocked = false;
 
     @Formats.DateTime(pattern = "yyyy-MM-dd")
     public Date createdDate;
app/views/site/userList.scala.html
--- app/views/site/userList.scala.html
+++ app/views/site/userList.scala.html
@@ -1,70 +1,100 @@
 @(message: String, currentPage: com.avaje.ebean.Page[User])
 
 @siteMngMain(message) {
+  <h2>@Messages("site.sidebar.userList")</h2>
   <div class="row-fluid">
     <form class="form-search" action="@routes.SiteApp.searchUser()">
       <input type="text" class="input-medium search-query input-xlarge" name="loginId" placeholder="@Messages("site.userList.search")">
       <button type="submit" class="btn">검색</button>
     </form>
   </div>
-  
+
+  <h3> @Messages("site.userlist.locked")</h3>
   <div class="row-fluid">
     <table class="table table-striped table-condensed">
       <thead>
-        <tr>
-          <th>@Messages("user.loginId")</th>
-          <th>@Messages("user.name")</th>
-          <th>@Messages("user.email")</th>
-          <th></th>
-        </tr>
+          @userlistingHead
       </thead>
       <tbody>
         @for(user <- currentPage.getList()) {
-          <tr>
-            <td>@user.loginId</td>
-            <td>@user.name</td>
-            <td>@user.email</td>
-            <td>
-              <a class="btn"data-toggle="modal" href="@routes.UserApp.userInfo(user.loginId)">@Messages("button.detail")</a>
-              <a class="btn btn-danger" data-toggle="modal" href="#alertDeletion@user.loginId">@Messages("button.delete")</a>
-            </td> 
-          </tr>
-          
-          <div class="modal hide" id="alertDeletion@user.loginId">
-              <div class="modal-header">
-                  <button type="button" class="close" data-dismiss="modal">×</button>
-                  <h3>@Messages("site.user.delete")</h3>
-              </div>
-              <div class="modal-body">
-                  <p>@Messages("site.user.deleteConfirm")</p>
-              </div>
-              <div class="modal-footer">
-                  <a href="#" class="btn" data-dismiss="modal">@Messages("button.no")</a>
-                  <a href="@routes.SiteApp.deleteUser(user.id)" class="btn btn-danger">@Messages("button.yes")</a>
-              </div>
-          </div>
-          <div class="modal hide" id="alertEdit@user.loginId">
-              <div class="modal-header">
-                  <button type="button" class="close" data-dismiss="modal">×</button>
-                  <h3>@Messages("site.user.emailEdit")</h3>
-              </div>
-              <form method="post" action="#">
-                  <div class="modal-body">
-                      <p>@Messages("site.user.editConfirm")</p>
-                        <input/>
-                  </div>
-                  <div class="modal-footer">
-                      <a href="#" class="btn" data-dismiss="modal">@Messages("button.cancel")</a>
-                      <input type="submit" " class="btn btn-danger" value="@Messages("button.save")"/>
-                  </div>
-              </form>
-          </div>
+            @if( user.isLocked == true ){
+            @userlisting(user)
+            }
         }
       </tbody>
     </table>
   </div>
-  
+
+  <h3>@Messages("site.userlist.unlocked")</h3>
+  <div class="row-fluid">
+    <table class="table table-striped table-condensed">
+      <thead>
+          @userlistingHead
+      </thead>
+      <tbody>
+        @for(user <- currentPage.getList()) {
+            @if(user.isLocked == false){
+                @userlisting(user)
+            }
+        }
+      </tbody>
+    </table>
+  </div>
+
   <div class="row-fluid">
     <center>@paginationForUserList(currentPage, 5, "pagination")</center>
   </div>
+}
+@userlistingHead = {
+    <tr>
+        <th>@Messages("user.loginId")</th>
+        <th>@Messages("user.name")</th>
+        <th>@Messages("user.email")</th>
+        <th>@Messages("user.isLocked")</th>
+        <th></th>
+    </tr>
+}
+
+@userlisting(user: models.User) = {
+    <tr>
+        <td>@user.loginId</td>
+        <td>@user.name</td>
+        <td>@user.email</td>
+        <td>@user.isLocked</td>
+        <td>
+            <a class="btn"data-toggle="modal" href="@routes.UserApp.userInfo(user.loginId)">@Messages("button.detail")</a>
+            <a class="btn"data-toggle="modal" href="@routes.SiteApp.toggleAccountLock(user.loginId)">@Messages("button.user.makeAccountUnlock."+ user.isLocked)</a>
+            <a class="btn btn-danger" data-toggle="modal" href="#alertDeletion@user.loginId">@Messages("button.delete")</a>
+        </td>
+    </tr>
+
+    <div class="modal hide" id="alertDeletion@user.loginId">
+        <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal">×</button>
+            <h3>@Messages("site.user.delete")</h3>
+        </div>
+        <div class="modal-body">
+            <p>@Messages("site.user.deleteConfirm")</p>
+        </div>
+        <div class="modal-footer">
+            <a href="#" class="btn" data-dismiss="modal">@Messages("button.no")</a>
+            <a href="@routes.SiteApp.deleteUser(user.id)" class="btn btn-danger">@Messages("button.yes")</a>
+        </div>
+    </div>
+    <div class="modal hide" id="alertEdit@user.loginId">
+        <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal">×</button>
+            <h3>@Messages("site.user.emailEdit")</h3>
+        </div>
+        <form method="post" action="#">
+            <div class="modal-body">
+                <p>@Messages("site.user.editConfirm")</p>
+                <input/>
+            </div>
+            <div class="modal-footer">
+                <a href="#" class="btn" data-dismiss="modal">@Messages("button.cancel")</a>
+                <input type="submit" " class="btn btn-danger" value="@Messages("button.save")"/>
+            </div>
+        </form>
+    </div>
 }
(No newline at end of file)
conf/application.conf
--- conf/application.conf
+++ conf/application.conf
@@ -111,3 +111,6 @@
 %prod.http.port=80
 %prod.application.log=INFO
 %prod.application.mode=prod
+
+#if you want to use sign-up confirm, uncomment below
+#signup.require.confirm = true
 
conf/evolutions/default/10.sql (added)
+++ conf/evolutions/default/10.sql
@@ -0,0 +1,6 @@
+# --- !Ups
+ALTER TABLE N4USER ADD COLUMN is_locked boolean default false;
+
+
+# --- !Downs
+ALTER TABLE N4USER DROP COLUMN is_locked;
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -88,6 +88,8 @@
 button.apply = Apply
 button.back = Back
 button.upload = Upload
+button.user.makeAccountUnlock.true = Unlock
+button.user.makeAccountUnlock.false = Lock
 
 checkbox.commented = Comment
 checkbox.fileAttached = File Attached
@@ -277,6 +279,8 @@
 site.user.deleteConfirm = Are you sure that his user leaves this site?
 site.project.delete = Delete an project
 site.project.deleteConfirm = Do you want to delete this project?
+site.userlist.unlocked = Active User List
+site.userlist.locked = Account Locked User List
 
 #User
 user.loginId = Login ID
@@ -299,6 +303,9 @@
 user.signupBtn = Sign Up
 user.loginWithNewPassword = Please, login with new password!
 user.notExists.name = User Doesn't Exists
+user.locked = User account is locked
+user.isLocked = is locked?
+user.signup.requested = Sign-up for hive is requested. Site admin will review your request. Thanks
 
 #Role
 role.manager = Manager
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -91,6 +91,8 @@
 button.apply = 적용
 button.back = 돌아가기
 button.upload = 파일 올리기
+button.user.makeAccountUnlock.true = 잠김해제
+button.user.makeAccountUnlock.false = 계정잠그기
 
 checkbox.commented = 댓글
 checkbox.fileAttached = 첨부파일
@@ -283,6 +285,8 @@
 site.user.deleteConfirm = 정말로 해당 유저를 사이트에서 탈퇴시키겠습니까?
 site.project.delete = 프로젝트 삭제
 site.project.deleteConfirm = 정말로 해당 프로젝트를 사이트에서 삭제하겠습니까?
+site.userlist.unlocked = 활성화된 유저 목록
+site.userlist.locked = 계정이 잠긴 유저 목록
 
 #User
 user.loginId = 아이디
@@ -305,6 +309,9 @@
 user.signupBtn = 참여하기
 user.loginWithNewPassword = 새로 설정한 비밀번호로 로그인 하세요
 user.notExists.name = 존재하지 않는 유저입니다.
+user.locked = 잠긴 사용자 계정입니다.
+user.isLocked = 잠김여부
+user.signup.requested = 하이브 가입이 요청되었습니다. 사이트 관리자가 검토/승인 후 사용가능합니다. 감사합니다.
 
 #Role
 role.manager = 관리자
conf/routes
--- conf/routes
+++ conf/routes
@@ -39,6 +39,7 @@
 GET     /sites/projectList                              controllers.SiteApp.projectList(filter:String ?= "")
 GET     /sites/project/delete/:projectId                controllers.SiteApp.deleteProject(projectId:Long)
 GET     /sites/softwareMap                              controllers.SiteApp.softwareMap()
+GET     /sites/toggleAccountLock/:loginId               controllers.SiteApp.toggleAccountLock(loginId: String)
 GET     /lostPassword                                   controllers.PasswordResetApp.lostPassword
 POST    /lostPassword                                   controllers.PasswordResetApp.requestResetPasswordEmail()
 GET     /resetPassword                                  controllers.PasswordResetApp.resetPasswordForm(s:String)
 
test/controllers/SiteAppTest.java (added)
+++ test/controllers/SiteAppTest.java
@@ -0,0 +1,66 @@
+package controllers;
+
+import models.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import play.test.FakeApplication;
+import play.test.Helpers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static play.test.Helpers.callAction;
+import static play.test.Helpers.fakeRequest;
+
+public class SiteAppTest {
+    protected static FakeApplication app;
+    private User admin;
+    private User notAdmin;
+
+    @BeforeClass
+    public static void beforeClass() {
+        callAction(
+                routes.ref.Application.init()
+        );
+    }
+
+    @Before
+    public void before() {
+        app = Helpers.fakeApplication(Helpers.inMemoryDatabase());
+        Helpers.start(app);
+
+        admin = User.findByLoginId("admin");
+        notAdmin = User.findByLoginId("doortts");
+    }
+
+    @After
+    public void after() {
+        Helpers.stop(app);
+    }
+
+    @Test
+    public void testToggleUserAccountLock() {
+        //Given
+
+        //Given
+        Map<String,String> data = new HashMap<String,String>();
+        final String loginId= "doortts";
+        data.put("loginId", loginId);
+
+        User targetUser = User.findByLoginId(loginId);
+        boolean currentIsLocked = targetUser.isLocked;
+
+        //When
+        return callAction(
+                controllers.routes.ref.SiteApp.toggleAccountLock(),
+                fakeRequest()
+                        .withFormUrlEncodedBody(data)
+                        .withSession(UserApp.SESSION_USERID, targetUser.id)
+        );
+        //Then
+        assertThat(User.findByLoginId(loginId).isLocked).isNotEqualTo(currentIsLocked);
+    }
+}
test/controllers/UserAppTest.java
--- test/controllers/UserAppTest.java
+++ test/controllers/UserAppTest.java
@@ -1,11 +1,16 @@
 package controllers;
 
+import models.User;
 import org.junit.*;
 
 import java.util.*;
 
+import play.Configuration;
+import play.GlobalSettings;
+import play.i18n.Messages;
 import play.mvc.*;
 import play.test.Helpers;
+import utils.JodaDateUtil;
 
 import static play.test.Helpers.*;
 import static org.fest.assertions.Assertions.*;
@@ -76,4 +81,61 @@
             }
         });
     }
+
+    @Test
+    public void login_notComfirmedUser() {
+        Map<String, String> fakeConf = Helpers.inMemoryDatabase();
+
+        running(fakeApplication(fakeConf), new Runnable() {
+            public void run() {
+                //Given
+                User user = new User(-31l);
+                user.loginId = "fakeUser";
+                user.email = "fakeuser@fake.com";
+                user.name = "racoon";
+                user.createdDate = JodaDateUtil.now();
+                user.isLocked = true;
+                user.save();
+
+                Map<String, String> data = new HashMap<String,String>();
+                data.put("loginId", user.loginId);
+                data.put("password", user.password);
+
+                //When
+                Result result = callAction(
+                        controllers.routes.ref.UserApp.login(),
+                        fakeRequest().withFormUrlEncodedBody(data)
+                );
+
+                //Then
+                assertThat(status(result)).describedAs("result status should '303 see other'").isEqualTo(303);
+            }
+        });
+    }
+
+    @Test
+    public void newUser_confirmSignUpMode() {
+        Map<String, String> fakeConf = Helpers.inMemoryDatabase();
+
+        running(fakeApplication(fakeConf), new Runnable() {
+            public void run() {
+                //Given
+                final String loginId = "somefakeuserid";
+                Map<String, String> data = new HashMap<String,String>();
+                data.put("loginId", loginId);
+                data.put("password", "somefakepassword");
+                data.put("email", "fakeuser@fake.com");
+                data.put("name", "racoon");
+
+                //When
+                Result result = callAction(
+                        controllers.routes.ref.UserApp.newUser(),
+                        fakeRequest().withFormUrlEncodedBody(data)
+                );
+
+                //Then
+                assertThat(status(result)).describedAs("result status should '303 see other'").isEqualTo(303);
+            }
+        });
+    }
 }
Add a comment
List