[Notice] Announcing the End of Demo Server [Read me]
new feature: Reset password by email
how to test --------- 1. add suitable settings to application.conf - your email settings - change smtp.mock to false - add your hostname & port settings # application.hostname="www.projecthive.com" # application.port="80" 2. go to login page and select forget password link or use direct access url: /lostPassword 3. follow instructions of the page 4. check your email and follow the instruction, again. Conflicts: conf/messages.en conf/messages.ko
@5dc3385d60f0173cc0e14ad7de1dd5d820207544
+++ app/controllers/PasswordResetApp.java
... | ... | @@ -0,0 +1,104 @@ |
1 | +package controllers; | |
2 | + | |
3 | +import info.schleichardt.play2.mailplugin.Mailer; | |
4 | +import models.PasswordReset; | |
5 | +import models.User; | |
6 | +import org.apache.commons.mail.EmailException; | |
7 | +import org.apache.commons.mail.SimpleEmail; | |
8 | +import play.Configuration; | |
9 | +import play.Logger; | |
10 | +import play.data.DynamicForm; | |
11 | +import play.mvc.*; | |
12 | +import play.i18n.Messages; | |
13 | +import utils.Constants; | |
14 | +import views.html.login; | |
15 | +import views.html.site.lostPassword; | |
16 | +import views.html.user.resetPassword; | |
17 | + | |
18 | +import static play.data.Form.form; | |
19 | + | |
20 | +public class PasswordResetApp extends Controller { | |
21 | + | |
22 | + public static final String LOCAL_HOST = "127.0.0.1"; | |
23 | + public static final String DEV_MODE = "9000"; | |
24 | + | |
25 | + public static Result lostPassword(){ | |
26 | + return ok(lostPassword.render("admin.resetPasswordEmail.title", null, null, false)); | |
27 | + } | |
28 | + | |
29 | + public static Result requestResetPasswordEmail(){ | |
30 | + DynamicForm requestData = form().bindFromRequest(); | |
31 | + String loginId = requestData.get("loginId"); | |
32 | + String emailAddress = requestData.get("emailAddress"); | |
33 | + | |
34 | + Logger.debug("request reset password email by [" + loginId + ":" + emailAddress + "]"); | |
35 | + | |
36 | + User targetUser = User.findByLoginId(loginId); | |
37 | + | |
38 | + boolean isMailSent = false; | |
39 | + String errorMessage = null; | |
40 | + if(!targetUser.isAnonymous() && targetUser.email.equals(emailAddress)) { | |
41 | + String hashString = PasswordReset.generateResetHash(targetUser.loginId); | |
42 | + PasswordReset.addHashToResetTable(targetUser.loginId, hashString); | |
43 | + isMailSent = sendPasswordResetMail(targetUser, hashString); | |
44 | + } else { | |
45 | + Logger.debug("wrong user: " + loginId); | |
46 | + errorMessage = Messages.get("admin.resetPasswordEmail.invalidRequest"); | |
47 | + } | |
48 | + return ok(lostPassword.render("admin.resetPasswordEmail.title", emailAddress, errorMessage, isMailSent)); | |
49 | + } | |
50 | + | |
51 | + //ToDo SiteApp.sendMail()과 통합할 것 | |
52 | + //ToDo site email setting check 하는 부분이 빠져 있음 | |
53 | + private static boolean sendPasswordResetMail(User user, String hashString) { | |
54 | + Configuration config = play.Play.application().configuration(); | |
55 | + String sender = config.getString("smtp.user") + "@" + config.getString("smtp.domain"); | |
56 | + String resetPasswordUrl = getResetPasswordUrl(hashString); | |
57 | + | |
58 | + try { | |
59 | + SimpleEmail email = new SimpleEmail(); | |
60 | + email.setFrom(sender) | |
61 | + .setSubject(Messages.get("admin.resetPasswordEmail.title")) | |
62 | + .addTo(user.email) | |
63 | + .setMsg(Messages.get("admin.resetPasswordEmail.mailcontents") + "\n\n" + resetPasswordUrl) | |
64 | + .setCharset("utf-8"); | |
65 | + | |
66 | + Mailer.send(email); | |
67 | + return true; | |
68 | + } catch (EmailException e) { | |
69 | + e.printStackTrace(); | |
70 | + return false; | |
71 | + } | |
72 | + } | |
73 | + | |
74 | + private static String getResetPasswordUrl(String hashString) { | |
75 | + Configuration config = play.Play.application().configuration(); | |
76 | + String hostname = config.getString("application.hostname"); | |
77 | + String port = config.getString("application.port"); | |
78 | + | |
79 | + if(hostname == null) hostname = LOCAL_HOST; | |
80 | + if(port == null) port = DEV_MODE; | |
81 | + | |
82 | + return "http://" + hostname + ":" + port + "/resetPassword?s=" + hashString; | |
83 | + } | |
84 | + | |
85 | + public static Result resetPasswordForm(String s){ | |
86 | + String hashString = s; | |
87 | + return ok(resetPassword.render("title.resetPassword", form(User.class), hashString)); | |
88 | + } | |
89 | + | |
90 | + public static Result resetPassword(){ | |
91 | + DynamicForm requestData = form().bindFromRequest(); | |
92 | + String hashString = requestData.get("hashString"); | |
93 | + String newPassword = requestData.get("password"); | |
94 | + | |
95 | + if(PasswordReset.isValidResetHash(hashString)){ | |
96 | + PasswordReset.resetPassword(hashString, newPassword); | |
97 | + Logger.debug("Password was reset"); | |
98 | + } else { | |
99 | + Logger.debug("Not a valid request!"); | |
100 | + } | |
101 | + flash(Constants.WARNING, "user.loginWithNewPassword"); | |
102 | + return ok(login.render("title.login", form(User.class))); | |
103 | + } | |
104 | +} |
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
... | ... | @@ -206,6 +206,12 @@ |
206 | 206 |
return user; |
207 | 207 |
} |
208 | 208 |
|
209 |
+ public static void resetPassword(User user, String newPassword) { |
|
210 |
+ user.password = new Sha256Hash(newPassword, |
|
211 |
+ ByteSource.Util.bytes(user.passwordSalt), 1024).toBase64(); |
|
212 |
+ user.save(); |
|
213 |
+ } |
|
214 |
+ |
|
209 | 215 |
public static void addUserInfoToSession(User user) { |
210 | 216 |
session(SESSION_USERID, String.valueOf(user.id)); |
211 | 217 |
session(SESSION_LOGINID, user.loginId); |
--- app/models/NullUser.java
+++ app/models/NullUser.java
... | ... | @@ -19,10 +19,7 @@ |
19 | 19 |
|
20 | 20 |
import javax.persistence.*; |
21 | 21 |
import java.text.SimpleDateFormat; |
22 |
-import java.util.Date; |
|
23 |
-import java.util.LinkedHashMap; |
|
24 |
-import java.util.List; |
|
25 |
-import java.util.Map; |
|
22 |
+import java.util.*; |
|
26 | 23 |
|
27 | 24 |
public class NullUser extends User { |
28 | 25 |
private static final long serialVersionUID = -1L; |
... | ... | @@ -37,7 +34,7 @@ |
37 | 34 |
public String avatarUrl = UserApp.DEFAULT_AVATAR_URL; |
38 | 35 |
|
39 | 36 |
public List<Project> myProjects(){ |
40 |
- return null; |
|
37 |
+ return new ArrayList<Project>(); |
|
41 | 38 |
} |
42 | 39 |
|
43 | 40 |
public boolean isAnonymous() { |
+++ app/models/PasswordReset.java
... | ... | @@ -0,0 +1,99 @@ |
1 | +package models; | |
2 | + | |
3 | +import org.apache.shiro.crypto.SecureRandomNumberGenerator; | |
4 | +import org.apache.shiro.crypto.hash.Sha1Hash; | |
5 | +import org.joda.time.DateTime; | |
6 | +import play.Logger; | |
7 | + | |
8 | +import java.util.HashMap; | |
9 | +import java.util.Map; | |
10 | + | |
11 | +public class PasswordReset { | |
12 | + private static Map<String, String> resetHashMap = new HashMap<String, String>(); | |
13 | + private static Map<String, DateTime> resetHashTimetable = new HashMap<String, DateTime>(); | |
14 | + public static final int HASH_EXPIRE_TIME_SEC = 3600; | |
15 | + | |
16 | + public static String generateResetHash(String userId) { | |
17 | + return new Sha1Hash(userId, new SecureRandomNumberGenerator().nextBytes(), 1).toHex(); | |
18 | + } | |
19 | + | |
20 | + public static void addHashToResetTable(String userId, String hashString) { | |
21 | + Logger.debug(">> add to HashTable " + userId + ":" + hashString); | |
22 | + PasswordReset.resetHashMap.put(userId, hashString); | |
23 | + resetHashTimetable.put(hashString, new DateTime()); | |
24 | + } | |
25 | + | |
26 | + public static String getResetHash(String userId) { | |
27 | + return PasswordReset.resetHashMap.get(userId); | |
28 | + } | |
29 | + | |
30 | + public static boolean invalidateResetHash(String userId) { | |
31 | + String targetIdToReset = PasswordReset.resetHashMap.get(userId); | |
32 | + if (targetIdToReset == null){ | |
33 | + return false; | |
34 | + } | |
35 | + | |
36 | + PasswordReset.resetHashMap.remove(userId); | |
37 | + return true; | |
38 | + } | |
39 | + | |
40 | + public static boolean isHashExist(String userId) { | |
41 | + return PasswordReset.resetHashMap.get(userId) != null; | |
42 | + } | |
43 | + | |
44 | + public static void resetHashTable() { | |
45 | + resetHashMap.clear(); | |
46 | + } | |
47 | + | |
48 | + public static boolean isValidResetHash(String hashString) { | |
49 | + Logger.debug("Reset hash entry size:" + resetHashMap.size()); | |
50 | + for(Map.Entry<String, String> entry: resetHashMap.entrySet()){ | |
51 | + Logger.debug(">> " + entry.getKey()); | |
52 | + } | |
53 | + if( !resetHashMap.containsValue(hashString) ) { | |
54 | + Logger.debug("HashString doesn't exists in resetHashMap: " + hashString); | |
55 | + return false; | |
56 | + } | |
57 | + | |
58 | + if(isExpiredHashString(hashString)) { | |
59 | + Logger.debug("HashString was expired: " + hashString); | |
60 | + return false; | |
61 | + } | |
62 | + | |
63 | + return true; | |
64 | + } | |
65 | + | |
66 | + private static boolean isExpiredHashString(String hashString) { | |
67 | + return resetHashTimetable.get(hashString).getMillis() < new DateTime().minusSeconds(PasswordReset.HASH_EXPIRE_TIME_SEC).getMillis(); | |
68 | + } | |
69 | + | |
70 | + public static void removeResetHash(String hashString) { | |
71 | + String key = getKeyByValue(resetHashMap, hashString); | |
72 | + resetHashMap.remove(key); | |
73 | + resetHashTimetable.remove(hashString); | |
74 | + } | |
75 | + | |
76 | + static Map<String, DateTime> getResetHashTimetable(){ | |
77 | + return resetHashTimetable; | |
78 | + } | |
79 | + | |
80 | + private static <T, E> T getKeyByValue(Map<T, E> map, E value) { | |
81 | + for (Map.Entry<T, E> entry : map.entrySet()) { | |
82 | + if (value.equals(entry.getValue())) { | |
83 | + return entry.getKey(); | |
84 | + } | |
85 | + } | |
86 | + return null; | |
87 | + } | |
88 | + | |
89 | + public static boolean resetPassword(String hashString, String newPassword) { | |
90 | + if( !isValidResetHash(hashString) ) { | |
91 | + return false; | |
92 | + } | |
93 | + | |
94 | + String loginId = getKeyByValue(resetHashMap, hashString); | |
95 | + User.resetPassword(loginId, newPassword); | |
96 | + removeResetHash(hashString); | |
97 | + return true; | |
98 | + } | |
99 | +} |
--- app/models/User.java
+++ app/models/User.java
... | ... | @@ -21,6 +21,8 @@ |
21 | 21 |
import models.support.FinderTemplate; |
22 | 22 |
import models.support.OrderParams; |
23 | 23 |
import models.support.SearchParams; |
24 |
+import org.apache.shiro.crypto.hash.Sha256Hash; |
|
25 |
+import org.apache.shiro.util.ByteSource; |
|
24 | 26 |
import play.data.format.Formats; |
25 | 27 |
import play.data.validation.Constraints.*; |
26 | 28 |
import play.db.ebean.Model; |
... | ... | @@ -173,6 +175,14 @@ |
173 | 175 |
return this == UserApp.anonymous; |
174 | 176 |
} |
175 | 177 |
|
178 |
+ public static void resetPassword(String loginId, String newPassword) { |
|
179 |
+ User user = findByLoginId(loginId); |
|
180 |
+ user.password = new Sha256Hash(newPassword, |
|
181 |
+ ByteSource.Util.bytes(user.passwordSalt), 1024).toBase64(); |
|
182 |
+ user.save(); |
|
183 |
+ } |
|
184 |
+ |
|
185 |
+ |
|
176 | 186 |
public Resource asResource() { |
177 | 187 |
return new Resource() { |
178 | 188 |
@Override |
+++ app/models/exceptions/InvalidResetHash.java
... | ... | @@ -0,0 +1,16 @@ |
1 | +package models.exceptions; | |
2 | + | |
3 | +/** | |
4 | + * User: doortts | |
5 | + * Date: 4/5/13 | |
6 | + * Time: 10:37 AM | |
7 | + */ | |
8 | +public class InvalidResetHash extends RuntimeException { | |
9 | + public static final String NOT_EXISTS = "Not Existed ResetHash"; | |
10 | + private static final long serialVersionUID = -1L; | |
11 | + public static final String EXIRED = "Reset Password hash is expired"; | |
12 | + | |
13 | + public InvalidResetHash(String message) { | |
14 | + super(message); | |
15 | + } | |
16 | +} |
+++ app/views/site/lostPassword.scala.html
... | ... | @@ -0,0 +1,46 @@ |
1 | +@(message: String, sender: String, errorMessage: String, isSent: Boolean) | |
2 | + | |
3 | +@home(message, utils.MenuType.NONE) { | |
4 | + | |
5 | +<form method="POST" action="@routes.PasswordResetApp.requestResetPasswordEmail()" id="mailForm" class="span7 form-horizontal"> | |
6 | + <div class="page-header control-group controls"> | |
7 | + <h3>@Messages(message)</h3> | |
8 | + </div> | |
9 | + | |
10 | +@if(isSent) { | |
11 | + <div class="control-group controls"> | |
12 | + @Messages("mail.sended") | |
13 | + </div> | |
14 | +} | |
15 | +@if(errorMessage) { | |
16 | + <div class="control-group controls"> | |
17 | + @Messages("mail.fail")<br/> | |
18 | + @Html(Messages(errorMessage)) | |
19 | + </div> | |
20 | +} | |
21 | + <div class="control-group"> | |
22 | + <label name="from" class="control-label">@Messages("user.loginId")</label> | |
23 | + <div class="controls"> | |
24 | + <div class="input-prepend"> | |
25 | + <span class="add-on"><i class="icon-user"></i></span> | |
26 | + <input type="text" name="loginId" required="" placeholder="@Messages("user.yourLoginId")"> | |
27 | + </div> | |
28 | + </div> | |
29 | + </div> | |
30 | + <div class="control-group"> | |
31 | + <label name="to" class="control-label">@Messages("user.email")</label> | |
32 | + <div class="controls"> | |
33 | + <div class="input-prepend"> | |
34 | + <span class="add-on"><i class="icon-envelope"></i></span> | |
35 | + <input type="text" name="emailAddress" required="" placeholder="@Messages("user.yourEmail")"> | |
36 | + </div> | |
37 | + </div> | |
38 | + </div> | |
39 | + <div class="controls"> | |
40 | + <button type="submit" class="btn-transparent n-btn orange med"> | |
41 | + <strong>@Messages("button.confirm")</strong> | |
42 | + </button> | |
43 | + </div> | |
44 | +</form> | |
45 | + | |
46 | +} |
+++ app/views/user/resetPassword.scala.html
... | ... | @@ -0,0 +1,42 @@ |
1 | +@(message: String, newUserForm: Form[User], hashString: String) | |
2 | +@import utils.TemplateHelper._ | |
3 | + | |
4 | +@home(message, utils.MenuType.NONE) { | |
5 | + <div class="page full"> | |
6 | + <div class="center-wrap tag-line-wrap signup"> | |
7 | + <h1 class="title"><span class="high-light">Password Reset </span></h1> | |
8 | + </div> | |
9 | + <div class="signup-form-wrap frm-wrap"> | |
10 | + <form action="@routes.PasswordResetApp.resetPassword()" method="POST" name="passwordReset"> | |
11 | + <dl> | |
12 | + <dt> | |
13 | + <label for="password">@Messages("user.password")</label> | |
14 | + </dt> | |
15 | + <dd> | |
16 | + <input type="password" class="text password" id="password" placeholder="" autocomplete="off" name="password"> | |
17 | + </dd> | |
18 | + | |
19 | + <dt> | |
20 | + <label for="retypedPassword">@Messages("validation.retypePassword")</label> | |
21 | + </dt> | |
22 | + <dd> | |
23 | + <input type="password" class="text password" id="retypedPassword" placeholder="" autocomplete="off" name="retypedPassword"> | |
24 | + </dd> | |
25 | + </dl> | |
26 | + | |
27 | + <input type="hidden" name="hashString" value="@hashString"> | |
28 | + <div class="btns-row"> | |
29 | + <button type="submit" class="n-btn orange sign-up">@Messages("button.confirm")</button> | |
30 | + </div> | |
31 | + </form> | |
32 | + </div> | |
33 | + </div> | |
34 | + | |
35 | + <script type="text/javascript" src="@getJSLink("lib/validate")"></script> | |
36 | + <script type="text/javascript"> | |
37 | + $(document).ready(function(){ | |
38 | + $hive.loadModule("resetPassword"); | |
39 | + }); | |
40 | + </script> | |
41 | +} | |
42 | + |
--- conf/application.conf
+++ conf/application.conf
... | ... | @@ -76,8 +76,14 @@ |
76 | 76 |
|
77 | 77 |
# Components used to construct the URL to this application. |
78 | 78 |
application.scheme="http" |
79 |
-# application.hostname="www.nforge.com" |
|
80 |
-# application.port="8080" |
|
79 |
+ |
|
80 |
+# if you want to use password reset email feature, you should set below with yours |
|
81 |
+# application.hostname="www.projecthive.com" |
|
82 |
+# application.port="80" |
|
83 |
+# |
|
84 |
+# below setting is used for default |
|
85 |
+# application.hostname="127.0.0.1" |
|
86 |
+# application.port="9000" |
|
81 | 87 |
|
82 | 88 |
# Mailer |
83 | 89 |
# ~~~~~~ |
... | ... | @@ -86,7 +92,8 @@ |
86 | 92 |
smtp.host = smtp.gmail.com |
87 | 93 |
smtp.port = 465 |
88 | 94 |
smtp.ssl = true |
89 |
-smtp.user = yourGmailId |
|
95 |
+#smtp.user = yourGmailId |
|
96 |
+smtp.user = hiveproject.mail |
|
90 | 97 |
# Be careful!!! |
91 | 98 |
smtp.password = yourGmailPassword |
92 | 99 |
smtp.domain = gmail.com |
--- conf/messages.en
+++ conf/messages.en
... | ... | @@ -25,6 +25,7 @@ |
25 | 25 |
title.projectList = Project List |
26 | 26 |
title.help = HELP |
27 | 27 |
title.search = SEARCH |
28 |
+title.resetPassword = Reset Password |
|
28 | 29 |
|
29 | 30 |
hive.name = HIVE |
30 | 31 |
hive.title = Collaborative Software Development Platform |
... | ... | @@ -270,6 +271,7 @@ |
270 | 271 |
|
271 | 272 |
#User |
272 | 273 |
user.loginId = Login ID |
274 |
+user.yourLoginId = Your login id |
|
273 | 275 |
user.wrongloginId.alert = Valid ID required |
274 | 276 |
user.loginId.duplicate = Already existing ID |
275 | 277 |
user.login.alert = Please, login. |
... | ... | @@ -279,12 +281,14 @@ |
279 | 281 |
user.wrongPassword.alert = Wrong password! |
280 | 282 |
user.name = Name |
281 | 283 |
user.email = E-mail |
284 |
+user.yourEmail = Your email address |
|
282 | 285 |
user.wrongEmail.alert = Wrong email address. |
283 | 286 |
user.login.failed = Your login ID or password is not valid. |
284 | 287 |
user.logout.success = Logout successfully |
285 | 288 |
user.isAlreadySignupUser = HAVE AN ACCOUNT ALREADY? |
286 | 289 |
user.signupId = ID (lower case) |
287 | 290 |
user.signupBtn = Sign Up |
291 |
+user.loginWithNewPassword = Please, login with new password! |
|
288 | 292 |
|
289 | 293 |
#Time |
290 | 294 |
time.just = just now |
... | ... | @@ -346,7 +350,7 @@ |
346 | 350 |
mail.body = Body |
347 | 351 |
mail.send = Send |
348 | 352 |
mail.fail = Failed to send a mail. |
349 |
-mail.sended = Mail is sended. |
|
353 |
+mail.sended = Mail is sent. |
|
350 | 354 |
|
351 | 355 |
author = Author |
352 | 356 |
assignee = Assignee |
... | ... | @@ -370,3 +374,8 @@ |
370 | 374 |
#userinfo |
371 | 375 |
userinfo.myProjects = My Projects |
372 | 376 |
userinfo.starredProjects = Starred |
377 |
+ |
|
378 |
+#Admin |
|
379 |
+admin.resetPasswordEmail.title = [HIVE] Password reset mail request |
|
380 |
+admin.resetPasswordEmail.mailcontents = copy the below url and paste it to browser url bar |
|
381 |
+admin.resetPasswordEmail.invalidRequest = Invalid Password Reset Request |
--- conf/messages.ko
+++ conf/messages.ko
... | ... | @@ -25,6 +25,7 @@ |
25 | 25 |
title.projectList = 프로젝트 목록 |
26 | 26 |
title.help = 도움말 |
27 | 27 |
title.search = 검색 |
28 |
+title.resetPassword = 패스워드 재설정 |
|
28 | 29 |
|
29 | 30 |
hive.name = HIVE |
30 | 31 |
hive.title = 협업개발 플랫폼 |
... | ... | @@ -269,6 +270,7 @@ |
269 | 270 |
|
270 | 271 |
#User |
271 | 272 |
user.loginId = 아이디 |
273 |
+user.yourLoginId = yourLoginId |
|
272 | 274 |
user.wrongloginId.alert = 올바른 아이디를 입력하세요. |
273 | 275 |
user.loginId.duplicate = 이미 존재하는 아이디입니다. |
274 | 276 |
user.login.alert = 로그인이 필요합니다. |
... | ... | @@ -278,12 +280,14 @@ |
278 | 280 |
user.wrongPassword.alert = 잘못된 비밀번호입니다! |
279 | 281 |
user.name = 이름 |
280 | 282 |
user.email = 이메일 |
283 |
+user.yourEmail = yourEmail@address.com |
|
281 | 284 |
user.wrongEmail.alert = 이메일이 잘못되었습니다. |
282 | 285 |
user.login.failed = 아이디 또는 비밀번호가 잘못되었습니다. |
283 | 286 |
user.logout.success = 로그아웃했습니다 |
284 | 287 |
user.isAlreadySignupUser = 이미 가입하셨나요? |
285 | 288 |
user.signupId = 아이디 |
286 | 289 |
user.signupBtn = 참여하기 |
290 |
+user.loginWithNewPassword = 새로 설정한 비밀번호로 로그인 하세요 |
|
287 | 291 |
|
288 | 292 |
#Time |
289 | 293 |
time.just = 방금 전 |
... | ... | @@ -369,4 +373,9 @@ |
369 | 373 |
|
370 | 374 |
#userinfo |
371 | 375 |
userinfo.myProjects = 내 프로젝트 |
372 |
-userinfo.starredProjects = 관심 프로젝트 |
|
376 |
+userinfo.starredProjects = 관심 프로젝트 |
|
377 |
+ |
|
378 |
+#Admin |
|
379 |
+admin.resetPasswordEmail.title = [HIVE] 패스워드 재 설정 |
|
380 |
+admin.resetPasswordEmail.mailcontents = 아래 URL을 브라우저 주소창에 붙여 넣으세요 |
|
381 |
+admin.resetPasswordEmail.invalidRequest = 잘못된 패스워드 재 설정 요청입니다. |
--- conf/routes
+++ conf/routes
... | ... | @@ -39,6 +39,10 @@ |
39 | 39 |
GET /sites/projectList controllers.SiteApp.projectList(filter:String ?= "") |
40 | 40 |
GET /sites/project/delete/:projectId controllers.SiteApp.deleteProject(projectId:Long) |
41 | 41 |
GET /sites/softwareMap controllers.SiteApp.softwareMap() |
42 |
+GET /lostPassword controllers.PasswordResetApp.lostPassword |
|
43 |
+POST /lostPassword controllers.PasswordResetApp.requestResetPasswordEmail() |
|
44 |
+GET /resetPassword controllers.PasswordResetApp.resetPasswordForm(s:String) |
|
45 |
+POST /resetPassword controllers.PasswordResetApp.resetPassword() |
|
42 | 46 |
|
43 | 47 |
# Attachments |
44 | 48 |
GET /files controllers.AttachmentApp.getFileList() |
+++ public/javascripts/service/hive.resetPassword.js
... | ... | @@ -0,0 +1,133 @@ |
1 | +/** | |
2 | + * @(#)hive.user.SignUp.js 2013.04.02 | |
3 | + * | |
4 | + * Copyright NHN Corporation. | |
5 | + * Released under the MIT license | |
6 | + * | |
7 | + * http://hive.dev.naver.com/license | |
8 | + */ | |
9 | + | |
10 | +(function(ns){ | |
11 | + | |
12 | + var oNS = $hive.createNamespace(ns); | |
13 | + oNS.container[oNS.name] = function(){ | |
14 | + | |
15 | + var htVar = {}; | |
16 | + var htElement = {}; | |
17 | + | |
18 | + /** | |
19 | + * initialize | |
20 | + */ | |
21 | + function _init(){ | |
22 | + _initElement(); | |
23 | + _initFormValidator(); | |
24 | + _attachEvent(); | |
25 | + } | |
26 | + | |
27 | + /** | |
28 | + * initialize elements | |
29 | + */ | |
30 | + function _initElement(){ | |
31 | + htElement.welInputPassword = $('#password'); | |
32 | + htElement.welInputPassword2 = $('#retypedPassword'); | |
33 | + | |
34 | + htElement.welForm = $("form[name=passwordReset]"); | |
35 | + } | |
36 | + | |
37 | + /** | |
38 | + * attach event | |
39 | + */ | |
40 | + function _attachEvent(){ | |
41 | + htElement.welInputPassword.focusout(_onBlurInputPassword); | |
42 | + htElement.welInputPassword2.focusout(_onBlurInputPassword); | |
43 | + } | |
44 | + | |
45 | + | |
46 | + /** | |
47 | + * 비밀번호 확인 입력란 벗어날 때 이벤트 핸들러 | |
48 | + * 마지막 입력란이므로 전체 폼 유효성 검사 | |
49 | + */ | |
50 | + function _onBlurInputPassword(){ | |
51 | + htVar.oValidator._validateForm(); | |
52 | + } | |
53 | + | |
54 | + /** | |
55 | + * initialize FormValidator | |
56 | + * @require validate.js | |
57 | + */ | |
58 | + function _initFormValidator(){ | |
59 | + var aRules = [ | |
60 | + {"name": 'password', "rules": 'required|min_length[4]'}, | |
61 | + {"name": 'retypedPassword', "rules": 'required|matches[password]'} | |
62 | + ]; | |
63 | + | |
64 | + htVar.oValidator = new FormValidator('passwordReset', aRules, _onFormValidate); | |
65 | + | |
66 | + // set error message | |
67 | + htVar.oValidator.setMessage('required', Messages("validation.required")); | |
68 | + htVar.oValidator.setMessage('min_length', Messages("validation.tooShortPassword")); | |
69 | + htVar.oValidator.setMessage('matches', Messages("validation.passwordMismatch")); | |
70 | + } | |
71 | + | |
72 | + | |
73 | + /** | |
74 | + * on validate form | |
75 | + * @param {Array} aErrors | |
76 | + */ | |
77 | + function _onFormValidate(aErrors){ | |
78 | + _clearTooltips(); | |
79 | + // to avoid bootstrap bug | |
80 | + if (aErrors.length <= 0) { | |
81 | + return _clearTooltips(); | |
82 | + } | |
83 | + | |
84 | + var welTarget; | |
85 | + aErrors.forEach(function(htError){ | |
86 | + welTarget = htElement.welForm.find("input[name=" + htError.name + "]"); | |
87 | + if(welTarget){ | |
88 | + showErrorMessage(welTarget, htError.message); | |
89 | + } | |
90 | + }); | |
91 | + } | |
92 | + | |
93 | + /** | |
94 | + * 폼 영역에 있는 jquery.tooltip 모두 제거하는 함수 | |
95 | + */ | |
96 | + function _clearTooltips(){ | |
97 | + try { | |
98 | + htElement.welForm.find("input").each(function(i, v){ | |
99 | + $(v).tooltip("destroy"); | |
100 | + }); | |
101 | + } catch(e){} | |
102 | + } | |
103 | + | |
104 | + /** | |
105 | + * Bootstrap toolTip function has some limitation. | |
106 | + * In this case, toolTip doesn't provide easy way to change title and contents. | |
107 | + * So, unfortunately I had to change data value in directly. | |
108 | + * @param {Wrapped Element} welInput | |
109 | + * @param {String} sMessage | |
110 | + */ | |
111 | + function showErrorMessage(welInput, sMessage){ | |
112 | + welInput.tooltip({"trigger": "manual", "placement": "left"}); | |
113 | + | |
114 | + var oToolTip = welInput.data('tooltip'); | |
115 | + oToolTip.options.placement = 'left'; | |
116 | + oToolTip.options.trigger = 'manual'; | |
117 | + oToolTip.options.title = sMessage; | |
118 | + oToolTip.options.content = sMessage; | |
119 | + | |
120 | + welInput.tooltip('show'); | |
121 | + } | |
122 | + | |
123 | + function hideErrorMessage(welInput){ | |
124 | + welInput.tooltip("hide"); | |
125 | + | |
126 | + try{ | |
127 | + welInput.tooltip("destroy"); | |
128 | + } catch(e){} // to avoid bootstrap bug | |
129 | + } | |
130 | + | |
131 | + _init(); | |
132 | + }; | |
133 | +})("hive.resetPassword");(No newline at end of file) |
+++ test/controllers/PasswordResetAppTest.java
... | ... | @@ -0,0 +1,43 @@ |
1 | +package controllers; | |
2 | + | |
3 | +import org.junit.BeforeClass; | |
4 | +import org.junit.Test; | |
5 | +import play.mvc.Result; | |
6 | +import play.test.Helpers; | |
7 | + | |
8 | +import java.util.HashMap; | |
9 | +import java.util.Map; | |
10 | + | |
11 | +import static org.fest.assertions.Assertions.assertThat; | |
12 | +import static play.test.Helpers.*; | |
13 | + | |
14 | +public class PasswordResetAppTest { | |
15 | + @BeforeClass | |
16 | + public static void beforeClass() { | |
17 | + callAction( | |
18 | + routes.ref.Application.init() | |
19 | + ); | |
20 | + } | |
21 | + | |
22 | + @Test | |
23 | + public void testRequestResetPassword_validLoginIdAndEmailAddress() { | |
24 | + running(fakeApplication(Helpers.inMemoryDatabase()), new Runnable() { | |
25 | + public void run() { | |
26 | + //Given | |
27 | + Map<String,String> data = new HashMap<String,String>(); | |
28 | + data.put("loginId", "doortts"); | |
29 | + data.put("emailAddress", "doortts@gmail.com"); | |
30 | + | |
31 | + //When | |
32 | + Result result = callAction( | |
33 | + routes.ref.PasswordResetApp.requestResetPasswordEmail(), | |
34 | + fakeRequest().withFormUrlEncodedBody(data) | |
35 | + ); | |
36 | + | |
37 | + //Then | |
38 | + assertThat(status(result)).isEqualTo(OK); | |
39 | + assertThat(contentAsString(result)).contains("메일을 발송하였습니다."); | |
40 | + } | |
41 | + }); | |
42 | + } | |
43 | +} |
+++ test/models/PasswordResetTest.java
... | ... | @@ -0,0 +1,176 @@ |
1 | +package models; | |
2 | + | |
3 | +import static org.fest.assertions.Assertions.assertThat; | |
4 | +import static org.fest.assertions.Fail.fail; | |
5 | + | |
6 | +import controllers.UserApp; | |
7 | +import org.joda.time.DateTime; | |
8 | +import org.junit.*; | |
9 | + | |
10 | +import java.util.Map; | |
11 | + | |
12 | +/** | |
13 | + * User: doortts | |
14 | + * Date: 4/4/13 | |
15 | + * Time: 5:11 PM | |
16 | + */ | |
17 | +public class PasswordResetTest extends ModelTest<PasswordReset> { | |
18 | + | |
19 | + @Before | |
20 | + public void setUp() throws Exception { | |
21 | + PasswordReset.resetHashTable(); | |
22 | + } | |
23 | + | |
24 | + @Test | |
25 | + public void testGenerateResetHash() throws Exception { | |
26 | + //Given | |
27 | + String userId = "doortts"; | |
28 | + | |
29 | + //When | |
30 | + String hashString = PasswordReset.generateResetHash(userId); | |
31 | + | |
32 | + //Then | |
33 | + assertThat(hashString).describedAs("SHA1 character size").hasSize(40); | |
34 | + } | |
35 | + | |
36 | + @Test | |
37 | + public void testAddHashToResetTable() throws Exception { | |
38 | + //Given | |
39 | + String userId = "doortts"; | |
40 | + String hashString = PasswordReset.generateResetHash(userId); | |
41 | + | |
42 | + //When | |
43 | + PasswordReset.addHashToResetTable(userId, hashString); | |
44 | + | |
45 | + //Then | |
46 | + assertThat(PasswordReset.getResetHash(userId)).isEqualTo(hashString); | |
47 | + } | |
48 | + | |
49 | + @Test | |
50 | + public void testInvalidateResetHash() throws Exception { | |
51 | + //Given | |
52 | + String userId = "doortts"; | |
53 | + String hashString = PasswordReset.generateResetHash(userId); | |
54 | + PasswordReset.addHashToResetTable(userId, hashString); | |
55 | + | |
56 | + //When | |
57 | + boolean result = PasswordReset.invalidateResetHash(userId); | |
58 | + | |
59 | + //Then | |
60 | + assertThat(result).isTrue(); | |
61 | + assertThat(PasswordReset.getResetHash(userId)).isNull(); | |
62 | + } | |
63 | + | |
64 | + @Test | |
65 | + public void testIsHashExists() { | |
66 | + //Given | |
67 | + String userId = "doortts"; | |
68 | + String hashString = PasswordReset.generateResetHash(userId); | |
69 | + PasswordReset.addHashToResetTable(userId, hashString); | |
70 | + | |
71 | + //When | |
72 | + boolean result = PasswordReset.isHashExist(userId); | |
73 | + | |
74 | + //Then | |
75 | + assertThat(result).isTrue(); | |
76 | + } | |
77 | + | |
78 | + @Test | |
79 | + public void testIsHashExists_not() { | |
80 | + //Given | |
81 | + String userId = "doortts"; | |
82 | + | |
83 | + //When | |
84 | + boolean result = PasswordReset.isHashExist(userId); | |
85 | + | |
86 | + //Then | |
87 | + assertThat(result).isFalse(); | |
88 | + } | |
89 | + | |
90 | + @Test | |
91 | + public void testIsValidResetHash_notExists() { | |
92 | + //Given | |
93 | + String userId = "doortts"; | |
94 | + | |
95 | + // When // Then | |
96 | + assertThat(PasswordReset.isValidResetHash("sfalkjsd")).isFalse(); | |
97 | + } | |
98 | + | |
99 | + @Test | |
100 | + public void testIsValidResetHash_expiredHash() { | |
101 | + //Given | |
102 | + String userId = "doortts"; | |
103 | + String hashString = PasswordReset.generateResetHash(userId); | |
104 | + PasswordReset.addHashToResetTable(userId, hashString); | |
105 | + Map<String, DateTime> timetable = PasswordReset.getResetHashTimetable(); | |
106 | + DateTime current = new DateTime(); | |
107 | + | |
108 | + //When //forced assume that time has passed | |
109 | + timetable.remove(hashString); | |
110 | + timetable.put(hashString, current.minusSeconds(PasswordReset.HASH_EXPIRE_TIME_SEC+1)); | |
111 | + | |
112 | + //Then | |
113 | + assertThat(PasswordReset.isValidResetHash(hashString)).isFalse(); | |
114 | + } | |
115 | + | |
116 | + @Test | |
117 | + public void testIsValidResetHash_evertythingOK() { | |
118 | + //Given | |
119 | + String userId = "doortts"; | |
120 | + String hashString = PasswordReset.generateResetHash(userId); | |
121 | + PasswordReset.addHashToResetTable(userId, hashString); | |
122 | + | |
123 | + //When | |
124 | + boolean result = PasswordReset.isValidResetHash(hashString); | |
125 | + | |
126 | + //Then | |
127 | + assertThat(result).isTrue(); | |
128 | + } | |
129 | + | |
130 | + @Test | |
131 | + public void testRemoveResetHash() { | |
132 | + //Given | |
133 | + String userId = "doortts"; | |
134 | + String hashString = PasswordReset.generateResetHash(userId); | |
135 | + PasswordReset.addHashToResetTable(userId, hashString); | |
136 | + | |
137 | + //When | |
138 | + PasswordReset.removeResetHash(hashString); | |
139 | + | |
140 | + //Then | |
141 | + assertThat(PasswordReset.isValidResetHash(hashString)).isFalse(); | |
142 | + } | |
143 | + | |
144 | + @Test | |
145 | + public void testResetPassword() { | |
146 | + //Given | |
147 | + String userId = "doortts"; | |
148 | + String newPassword = "whffudy"; | |
149 | + String hashString = PasswordReset.generateResetHash(userId); | |
150 | + PasswordReset.addHashToResetTable(userId, hashString); | |
151 | + | |
152 | + //When | |
153 | + boolean result = PasswordReset.resetPassword(hashString, newPassword); | |
154 | + | |
155 | + //Then | |
156 | + assertThat(result).isTrue(); | |
157 | + assertThat(UserApp.authenticateWithPlainPassword(userId, newPassword)).isNotNull(); | |
158 | + } | |
159 | + | |
160 | + @Test | |
161 | + public void testResetPassword_wrongHash() { | |
162 | + //Given | |
163 | + String userId = "doortts"; | |
164 | + String newPassword = "whffudy"; | |
165 | + String hashString = PasswordReset.generateResetHash(userId); | |
166 | + PasswordReset.addHashToResetTable(userId, hashString); | |
167 | + | |
168 | + //When | |
169 | + String wrongHash = "sfdlkjafslfjsda"; | |
170 | + boolean result = PasswordReset.resetPassword(wrongHash, newPassword); | |
171 | + | |
172 | + //Then | |
173 | + assertThat(result).isFalse(); | |
174 | + assertThat(UserApp.authenticateWithPlainPassword(userId, newPassword)).isNull(); | |
175 | + } | |
176 | +} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?