From 3c864a12ed5ed13dcaba40caa065fdd5fc6b02b9 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 19 May 2024 11:15:24 +0000 Subject: [PATCH] implemented resetting passwords Signed-off-by: Stephan Richter --- .../de/srsoftware/widerhall/Constants.java | 2 + .../java/de/srsoftware/widerhall/Util.java | 24 +- .../srsoftware/widerhall/data/Database.java | 24 +- .../widerhall/data/MailingList.java | 21 +- .../de/srsoftware/widerhall/data/User.java | 47 +++- .../java/de/srsoftware/widerhall/web/Web.java | 246 +++++++++++------- .../de/srsoftware/widerhall/UtilTest.java | 65 +++-- .../widerhall/data/ListMemberTest.java | 4 +- static/templates/login.st | 1 + static/templates/new_password_form.st | 33 +++ static/templates/reset-pw.st | 21 ++ static/templates/reset_link_sent.st | 17 ++ static/templates/subscribe.st | 4 +- 13 files changed, 371 insertions(+), 138 deletions(-) create mode 100644 static/templates/new_password_form.st create mode 100644 static/templates/reset-pw.st create mode 100644 static/templates/reset_link_sent.st diff --git a/src/main/java/de/srsoftware/widerhall/Constants.java b/src/main/java/de/srsoftware/widerhall/Constants.java index cd57c4a..3f54df1 100644 --- a/src/main/java/de/srsoftware/widerhall/Constants.java +++ b/src/main/java/de/srsoftware/widerhall/Constants.java @@ -21,8 +21,10 @@ public class Constants { public static final String MESSAGE_ID = "message_id"; public static final String MODERATOR = "moderator"; public static final String MONTH = "month"; + public static final String NEW_PASSWORD_FORM = "new_password_form"; public static final String NOTES = "notes"; public static final String PASSWORD = "password"; + public static final String PASSWORD_REPEAT = "password-repeat"; public static final String PERMISSIONS = "permissions"; public static final Object PORT = "port"; public static final String PREFIX = "prefix"; diff --git a/src/main/java/de/srsoftware/widerhall/Util.java b/src/main/java/de/srsoftware/widerhall/Util.java index c49ce75..edff6fa 100644 --- a/src/main/java/de/srsoftware/widerhall/Util.java +++ b/src/main/java/de/srsoftware/widerhall/Util.java @@ -4,12 +4,9 @@ import de.srsoftware.tools.translations.Translation; import de.srsoftware.widerhall.data.MailingList; import de.srsoftware.widerhall.data.User; -import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.net.URLEncoder; @@ -19,6 +16,7 @@ import java.security.NoSuchAlgorithmException; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; +import java.util.Random; import java.util.stream.Collectors; import static de.srsoftware.widerhall.Constants.*; @@ -28,6 +26,14 @@ public class Util { private static final MessageDigest SHA256 = getSha256(); private static final String EMAIL_PATTERN = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+(?:\\.[a-zA-Z0-9_!#$%&'*+/=?`{|}~^-]+)*@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"; + public static char boundedChar(int i) { + i = (i<0 ? -i : i) % 62; + i += '0'; + if (i>57) i+=7; + if (i>90) i+=6; + return (char) i; + } + public static String dropEmail(String tx) { return tx.replaceAll( "[.\\-\\w]+@[.\\-\\w]+", "[email_removed]"); } @@ -119,6 +125,18 @@ public class Util { return email.matches(EMAIL_PATTERN); } + public static String randomString(int length) { + Random rand = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i=0; i listActive() { + try { + var list = new ArrayList(); + var rs = Database.open().select(TABLE_NAME).where(STATE+" % 2",1).compile().exec(); + while (rs.next()) list.add(MailingList.from(rs)); + return list; + } catch (SQLException e){ + LOG.debug("Failed to load active MailingLists"); + } + return List.of(); + } + /** * Load a ML object by it's identifying email address. * This is a cached method: if the ML has been loaded before, the already-loaded object will be returned. @@ -308,6 +320,7 @@ public class MailingList implements MessageHandler, ProblemListener { return ml; } + /** * Load a ML object by it's identifying email address. * This is a cached method: if the ML has been loaded before, the already-loaded object will be returned. @@ -679,6 +692,10 @@ public class MailingList implements MessageHandler, ProblemListener { } } + public void sendPasswordReset(String email, String subject,String text) throws MessagingException, UnsupportedEncodingException { + smtp.send(email(),name(),email,subject,text); + } + protected SmtpClient smtp(){ return smtp; } diff --git a/src/main/java/de/srsoftware/widerhall/data/User.java b/src/main/java/de/srsoftware/widerhall/data/User.java index ce1bbaa..ba4efe6 100644 --- a/src/main/java/de/srsoftware/widerhall/data/User.java +++ b/src/main/java/de/srsoftware/widerhall/data/User.java @@ -6,6 +6,7 @@ import java.security.InvalidKeyException; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.*; import static de.srsoftware.widerhall.Constants.*; @@ -20,10 +21,11 @@ public class User { public static final int PERMISSION_ADMIN = 1; public static final int PERMISSION_CREATE_LISTS = 2; public static final String HASHED_PASS = "hashedPassword"; + public static final String RESET_TOKEN = "resetToken"; public static final String SALT = "salt"; private static final HashMap users = new HashMap<>(); - private String email, salt, hashedPass, name; + private String email, salt, hashedPass, name, token; private int permissions; /** @@ -34,11 +36,12 @@ public class User { * @param hashedPass * @param permissions */ - public User(String email, String name, String salt, String hashedPass, int permissions) { + public User(String email, String name, String salt, String hashedPass, String token, int permissions) { this.email = email.toLowerCase(); this.name = name; this.salt = salt; this.hashedPass = hashedPass; + this.token = token; this.permissions = permissions; } @@ -81,6 +84,21 @@ public class User { .run(); } + public static void addTokenColumn() throws SQLException { + String sql = "ALTER TABLE %s ADD COLUMN %s %s;".formatted(TABLE_NAME,RESET_TOKEN,VARCHAR); + Database.open().query(sql).compile().run(); + } + + public static User byToken(String token) throws SQLException { + if (token == null || token.isBlank()) return null; + var rs = Database.open().select(TABLE_NAME).where(RESET_TOKEN,token).compile().exec(); + try { + if (rs.next()) return User.from(rs); + return null; + } finally { + rs.close(); + } + } /** * Create a new user object by hashing it's password and storing user data, salt and hashed password to the db. @@ -96,10 +114,10 @@ public class User { String salt = null; String hashedPass = null; if (password != null) { - salt = Util.sha256(email + name + LocalDate.now()); + salt = Util.sha256(email + LocalDateTime.now() + name); hashedPass = Util.sha256(password + salt); } - return new User(email,name,salt,hashedPass,0).save(); + return new User(email,name,salt,hashedPass,null,0).save(); } /** @@ -115,7 +133,6 @@ public class User { .append(PERMISSIONS).append(" ").append(INT).append(", ") .append(SALT).append(" ").append(VARCHAR).append(", ") .append(HASHED_PASS).append(" ").append(VARCHAR) - .append(");"); Database.open().query(sql).compile().run(); } @@ -195,10 +212,16 @@ public class User { rs.getString(NAME), rs.getString(SALT), rs.getString(HASHED_PASS), + rs.getString(RESET_TOKEN), rs.getInt(PERMISSIONS))); return user; } + public String generateToken() throws SQLException { + token = Util.randomString(64); + Database.open().update(TABLE_NAME).set(RESET_TOKEN,token).where(EMAIL,this.email).compile().run(); + return token; + } /** * Loads the user identified by it's email, but only if the provided password matches. @@ -284,4 +307,18 @@ public class User { req.compile().run(); return this; } + + public void setPassword(String newPassword) throws SQLException { + if (newPassword != null) { + String newSalt = Util.sha256(email + LocalDateTime.now() + name); + String newHashedPass = Util.sha256(newPassword + newSalt); + Database.open().update(TABLE_NAME).set(HASHED_PASS,newHashedPass).set(SALT,newSalt).where(EMAIL,email).compile().run(); + hashedPass = newHashedPass; + salt = newSalt; + } + } + + public String token() { + return token; + } } diff --git a/src/main/java/de/srsoftware/widerhall/web/Web.java b/src/main/java/de/srsoftware/widerhall/web/Web.java index 573bb0e..b6644a5 100644 --- a/src/main/java/de/srsoftware/widerhall/web/Web.java +++ b/src/main/java/de/srsoftware/widerhall/web/Web.java @@ -15,8 +15,7 @@ import java.io.IOException; import java.nio.file.Files; import java.security.InvalidKeyException; import java.sql.SQLException; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import static de.srsoftware.widerhall.Constants.*; import static de.srsoftware.widerhall.Util.t; @@ -37,6 +36,7 @@ public class Web extends TemplateServlet { private static final String POST = "post"; private static final String REGISTER = "register"; private static final String RELOAD = "reload"; + private static final String RESET_PASSWORD = "reset-pw"; private static final String SUBSCRIBE = "subscribe"; private static final String UNSUBSCRIBE = "unsubscribe"; private static final String IMAP_HOST = "imap_host"; @@ -49,7 +49,7 @@ public class Web extends TemplateServlet { private static final String SMTP_PASS = "smtp_pass"; private static final int PRIMARY_KEY_CONSTRAINT = 19; - private String addList(HttpServletRequest req, HttpServletResponse resp) { + private String addList(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { var user = Util.getUser(req); if (user == null) return redirectTo(LOGIN,resp); var data = new HashMap(); @@ -139,15 +139,16 @@ public class Web extends TemplateServlet { var allowed = list.hasPublicArchive() || list.mayBeAlteredBy(user); if (!allowed) return t("You are not allowed to access the archive of this list"); - var map = new HashMap(); - map.put(LIST,list.email()); + var data = new HashMap(); + if (user != null) data.put(USER,user); + data.put(LIST,list.email()); var month = req.getParameter(MONTH); if (month != null && !month.isBlank()){ - map.put(MONTH,month); - map.put(MODERATOR,list.mayBeAlteredBy(user)); + data.put(MONTH,month); + data.put(MODERATOR,list.mayBeAlteredBy(user)); } - return loadTemplate(ARCHIVE,map,resp); + return loadTemplate(ARCHIVE,data,resp); } private String confirm(HttpServletRequest req, HttpServletResponse resp) { @@ -169,17 +170,17 @@ public class Web extends TemplateServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - String error = handleGet(req, resp); + String error = handleRequest(req, resp, true); if (error != null) resp.sendError(400,error); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { - String error = handlePost(req, resp); + String error = handleRequest(req, resp, false); if (error != null) resp.sendError(400,error); } - public String editList(HttpServletRequest req, HttpServletResponse resp) { + public String editList(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { var user = Util.getUser(req); if (user == null) return redirectTo(LOGIN,resp); @@ -188,6 +189,7 @@ public class Web extends TemplateServlet { var list = Util.getMailingList(req); data.put(LIST,list.safeMap()); + if (isGet) return loadTemplate(EDIT_LIST,data,resp); try { var allowed = user.hashPermission(PERMISSION_ADMIN) || ListMember.load(list,user).isOwner(); if (!allowed) return loadTemplate(ADMIN,data,resp); @@ -273,11 +275,56 @@ public class Web extends TemplateServlet { return sqle; } - private User getSessionUser(HttpServletRequest req) { - return req.getSession().getAttribute(USER) instanceof User user ? user : null; + private String handleLogin(HttpServletRequest req, HttpServletResponse resp,boolean isGet) { + if (isGet) return loadTemplate(LOGIN,null,resp); + var email = req.getParameter("email"); + var pass = req.getParameter("pass"); + if (email == null || pass == null) return loadTemplate(LOGIN, Map.of("error",t("Missing username or password!")), resp); + if (!Util.isEmail(email)) return loadTemplate(LOGIN, Map.of("error",t("'{}' is not a valid email address!",email)), resp); + try { + var user = User.loadUser(email,pass); + req.getSession().setAttribute("user",user); + // loading user successfull: goto index + resp.sendRedirect(String.join("/",WEB_ROOT,"admin")); + } catch (Exception e) { + try { + LOG.warn("Static.handleLogin failed:",e); + Thread.sleep(10000); + } finally { + return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp); + } + } + return null; } - private String handleGet(HttpServletRequest req, HttpServletResponse resp) { + private String handleNewPassword(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { + var user = Util.getUser(req); + if (user == null) return redirectTo(LOGIN,resp); + var data = new HashMap(); + data.put(USER,user.safeMap()); + if (!isGet) { + var pass = req.getParameter(PASSWORD); + var repeat = req.getParameter(PASSWORD_REPEAT); + if (pass == null || pass.isBlank()) { + data.put(ERROR, "Please set a password!"); + } else if (!pass.equals(repeat)) { + data.put(ERROR, "Passwords do not match"); + } else if (Util.simplePassword(pass)) { + data.put(ERROR, "Your password is to simple"); + } else { + try { + user.setPassword(pass); + data.put(NOTES,"Your password has been updated!"); + return loadTemplate(ADMIN,data,resp); + } catch (SQLException e) { + data.put(ERROR,t("Failed to update password in database: {}",e.getMessage())); + } + } + } + return loadTemplate(NEW_PASSWORD_FORM,data,resp); + } + + private String handleRequest(HttpServletRequest req, HttpServletResponse resp, boolean isGet){ var path = Util.getPath(req); var user = Util.getUser(req); var data = new HashMap(); @@ -285,39 +332,75 @@ public class Web extends TemplateServlet { if (user != null) data.put(USER,user.safeMap()); if (list != null) data.put(LIST,list.minimalMap()); + var interesting = !Set.of("js","jquery","css","OpenSans-Regular.woff","Bhineka.ttf").contains(path); + LOG.debug("{}",interesting?"interesting":"boring"); + if (user != null){ + switch (path){ + case ADD_LIST: + return addList(req,resp,isGet); + case ADMIN: + return loadTemplate(path,data,resp); + case EDIT_LIST: + return editList(req,resp,isGet); + case INSPECT: + return inspect(req,resp,isGet); + case NEW_PASSWORD_FORM: + return handleNewPassword(req,resp,isGet); + } + } switch (path){ case ARCHIVE: return archive(list,user,req,resp); + case CSS: + case INDEX: + return loadTemplate(path,data,resp); case CONFIRM: return confirm(req,resp); + case LOGIN: + return handleLogin(req,resp,isGet); + case LOGOUT: + req.getSession().invalidate(); + return redirectTo(INDEX,resp); case POST: return post(req,resp); + case REGISTER: + return registerUser(req,resp,isGet); case RELOAD: loadTemplates(); data.put(NOTES,t("Templates have been reloaded!")); - path = INDEX; - case CSS: - case INDEX: - return loadTemplate(path,data,resp); + return loadTemplate(INDEX,data,resp); + case RESET_PASSWORD: + if (!isGet) return resetPassword(req,resp); + // TODO: move following code into resetPassword method + try { + user = User.byToken(req.getParameter(TOKEN)); + if (user != null) { + req.getSession().setAttribute("user",user); + return redirectTo(NEW_PASSWORD_FORM,resp); + } + } catch (SQLException sqle){ + return loadTemplate(path,Map.of(ERROR,"Failed to add user for token!"),resp); + } + var email = req.getParameter(EMAIL); + return loadTemplate(path,email == null ? null : Map.of(EMAIL,email),resp); + case SUBSCRIBE: + if (!isGet) { + return subscribe(req, resp); + } // TODO: Code für GET-Request mit in subscribe-Methode verschieben if (list.isOpenFor(user)) { data.put(LIST,list.email()); return loadTemplate(path, data, resp); } return t("You are not allowed to subscribe to '{}'!",list.email()); + case UNSUBSCRIBE: + return unsubscribe(req,resp,isGet); + } + /* uninteresting paths */ + switch (path){ case "js": resp.setContentType("text/javascript"); return loadTemplate(path,data,resp); - case LOGIN: - try { - if (User.noUsers()) return loadTemplate(REGISTER, Map.of(NOTES,t("User database is empty. Create admin user first:")), resp); - return loadTemplate(path,null,resp); - } catch (SQLException e) { - return "Error reading user database!"; - } - case LOGOUT: - req.getSession().invalidate(); - return redirectTo(INDEX,resp); case "jquery": resp.setContentType("text/javascript"); return loadFile("jquery-3.6.0.min.js",resp); @@ -329,69 +412,16 @@ public class Web extends TemplateServlet { return loadFile("Bhineka.ttf",resp); case UNSUBSCRIBE: data.put(LIST,list.email()); return loadTemplate(path,data,resp); - - } - - if (user != null){ - if (list != null) data.put(LIST,req.getParameter(LIST)); - switch (path){ - case EDIT_LIST: - return editList(req,resp); - } - return loadTemplate(path,data,resp); } return redirectTo(LOGIN,resp); } - private String handleLogin(HttpServletRequest req, HttpServletResponse resp) { - var email = req.getParameter("email"); - var pass = req.getParameter("pass"); - if (email == null || pass == null) return loadTemplate("login", Map.of("error",t("Missing username or password!")), resp); - if (!Util.isEmail(email)) return loadTemplate("login", Map.of("error",t("'{}' is not a valid email address!",email)), resp); - try { - var user = User.loadUser(email,pass); - req.getSession().setAttribute("user",user); - // loading user successfull: goto index - resp.sendRedirect(String.join("/",WEB_ROOT,"admin")); - } catch (Exception e) { - try { - LOG.warn("Static.handleLogin failed:",e); - Thread.sleep(10000); - } finally { - return loadTemplate("login", Map.of(ERROR,t("Invalid username/password"),EMAIL,email), resp); - } - } - return null; - } - - private String handlePost(HttpServletRequest req, HttpServletResponse resp) { - final var path = Util.getPath(req); - - switch (path){ - case ADD_LIST: - return addList(req,resp); - case EDIT_LIST: - return editList(req,resp); - case INSPECT: - return inspect(req,resp); - case LOGIN: - return handleLogin(req,resp); - case REGISTER: - return registerUser(req,resp); - case SUBSCRIBE: - return subscribe(req,resp); - case UNSUBSCRIBE: - return unsubscribe(req,resp); - } - - return t("No handler for path {}!",path); - } - - private String inspect(HttpServletRequest req, HttpServletResponse resp) { + private String inspect(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { var user = Util.getUser(req); if (user == null) return redirectTo(LOGIN,resp); var data = new HashMap(); + data.put(USER,user); var error = false; var list = Util.getMailingList(req); @@ -399,6 +429,7 @@ public class Web extends TemplateServlet { error = true; data.put(ERROR, t("No valid mailing list provided!")); } else data.put(LIST, list.email()); + if (isGet) return loadTemplate(INSPECT,data,resp); if (!error && !list.mayBeAlteredBy(user)) { error = true; @@ -451,10 +482,8 @@ public class Web extends TemplateServlet { } } - - - private String registerUser(HttpServletRequest req, HttpServletResponse resp) { - + private String registerUser(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { + if (isGet) return loadTemplate(REGISTER,null,resp); var email = req.getParameter("email"); var pass = req.getParameter("pass"); var pass_repeat = req.getParameter("pass_repeat"); @@ -463,9 +492,9 @@ public class Web extends TemplateServlet { if (email == null || email.isBlank() || name == null || name.isBlank() || pass == null || pass.isBlank() || - pass_repeat == null || pass_repeat.isBlank()) return loadTemplate(REGISTER,Map.of(ERROR,t("Fill all fields, please!"),NAME,name,EMAIL,email),resp); - if (!pass.equals(pass_repeat)) return loadTemplate(REGISTER,Map.of(ERROR,t("Passwords do not match!"),NAME,name,EMAIL,email),resp); - if (Util.simplePassword(pass)) return loadTemplate(REGISTER,Map.of(ERROR,t("Password to short or to simple!"),NAME,name,EMAIL,email),resp); + pass_repeat == null || pass_repeat.isBlank()) return loadTemplate(REGISTER,Map.of(ERROR,t("Fill all fields, please!"),NAME,name,EMAIL,email),resp); + if (!pass.equals(pass_repeat)) return loadTemplate(REGISTER,Map.of(ERROR,t("Passwords do not match!"),NAME,name,EMAIL,email),resp); + if (Util.simplePassword(pass)) return loadTemplate(REGISTER,Map.of(ERROR,t("Password to short or to simple!"),NAME,name,EMAIL,email),resp); var firstUser = false; try { @@ -474,7 +503,6 @@ public class Web extends TemplateServlet { return t("Failed to access user database: {}",e.getMessage()); } - try { var user = User.create(email, name, pass); if (firstUser) user.addPermission(PERMISSION_ADMIN|User.PERMISSION_CREATE_LISTS); @@ -486,6 +514,33 @@ public class Web extends TemplateServlet { } } + private String resetPassword(HttpServletRequest req, HttpServletResponse resp) { + var email = req.getParameter("email"); + if (email == null) return loadTemplate("login", Map.of("error",t("Missing email address!")), resp); + if (!Util.isEmail(email)) return loadTemplate("login", Map.of("error",t("'{}' is not a valid email address!",email)), resp); + + try { + var user = User.load(email); + if (user != null) { + MailingList list = ListMember.listsOf(user).stream().map(ListMember::list).filter(ml -> ml.hasState(STATE_ENABLED)).findAny().orElse(null); + if (list == null) list = MailingList.listActive().stream().findAny().orElse(null); + if (list == null) throw new NullPointerException("no active List found!"); + String token = user.generateToken(); + var host = Arrays.stream(req.getRequestURL().toString().split("/")).filter(s -> !s.isEmpty() && !s.startsWith("http")).findFirst().orElse("unknwon host"); + + var link = req.getRequestURL().toString()+"?token="+token; + var subject = t("Reset your password at {}",host); + var text = t("Somebody asked to reset your account password at {}.\n\nIf that was you, please open the following link in your web browser:\n\n{}\n\nIf you did not expect this email, please ignore it.",host,link); + + list.sendPasswordReset(email,subject,text); + return loadTemplate("reset_link_sent",null,resp); + } + } catch (Exception e){ + return loadTemplate(Util.getPath(req),Map.of(EMAIL,email,ERROR,"Failed to send reset email: "+e.getMessage()),resp); + } + return null; + } + private String subscribe(HttpServletRequest req, HttpServletResponse resp) { var name = req.getParameter(NAME); var email = req.getParameter(EMAIL); @@ -517,6 +572,7 @@ public class Web extends TemplateServlet { int code = cause.getErrorCode(); if (code == PRIMARY_KEY_CONSTRAINT) try {// user already exists user = User.loadUser(email,pass); + req.getSession().setAttribute("user",user); skipConfirmation = pass != null; // subscription with email address already known to database // success → subscribe } catch (InvalidKeyException | SQLException e) { @@ -555,11 +611,9 @@ public class Web extends TemplateServlet { } } - - - private String unsubscribe(HttpServletRequest req, HttpServletResponse resp) { + private String unsubscribe(HttpServletRequest req, HttpServletResponse resp, boolean isGet) { var data = new HashMap(); - var user = getSessionUser(req); + var user = Util.getUser(req); var email = req.getParameter(EMAIL); var list = Util.getMailingList(req); data.put(EMAIL,email); @@ -568,6 +622,7 @@ public class Web extends TemplateServlet { data.put(ERROR,t("No list provided by form data!")); return loadTemplate(UNSUBSCRIBE,data,resp); } else data.put(LIST,list.email()); + if (isGet) return loadTemplate(UNSUBSCRIBE,data,resp); if (user == null) { if (email == null || email.isBlank()) { data.put(ERROR, t("Email is required for list un-subscription!")); @@ -607,7 +662,6 @@ public class Web extends TemplateServlet { LOG.warn("Problem during unscubsription of {} from {}:",user.email(),list.email(),e); data.put(ERROR,"Failed to unsubscribe!"); return loadTemplate(UNSUBSCRIBE,data,resp); - } } } diff --git a/src/test/java/de/srsoftware/widerhall/UtilTest.java b/src/test/java/de/srsoftware/widerhall/UtilTest.java index 412e2c4..a7e353f 100644 --- a/src/test/java/de/srsoftware/widerhall/UtilTest.java +++ b/src/test/java/de/srsoftware/widerhall/UtilTest.java @@ -4,6 +4,7 @@ import junit.framework.TestCase; import java.util.Map; import java.util.TreeMap; +import static de.srsoftware.widerhall.Util.*; public class UtilTest extends TestCase { @@ -17,17 +18,17 @@ public class UtilTest extends TestCase { } public void testHex(){ - assertEquals("00",Util.hex(0)); - assertEquals("09",Util.hex(9)); - assertEquals("0A",Util.hex(10)); - assertEquals("0F",Util.hex(15)); - assertEquals("10",Util.hex(16)); - assertEquals("19",Util.hex(25)); - assertEquals("FF",Util.hex(255)); + assertEquals("00",hex(0)); + assertEquals("09",hex(9)); + assertEquals("0A",hex(10)); + assertEquals("0F",hex(15)); + assertEquals("10",hex(16)); + assertEquals("19",hex(25)); + assertEquals("FF",hex(255)); } public void testSha256() { - assertEquals("9F722959A023C02A3BA0FAFDBA81ADED642D6610EFF5DCA32DCE35132E16B6C5",Util.sha256("Dies ist ein Test")); + assertEquals("9F722959A023C02A3BA0FAFDBA81ADED642D6610EFF5DCA32DCE35132E16B6C5",sha256("Dies ist ein Test")); } public void testTranslate() { @@ -35,30 +36,40 @@ public class UtilTest extends TestCase { } public void testIsEmail() { - assertFalse(Util.isEmail("Test")); - assertFalse(Util.isEmail("Test@")); - assertFalse(Util.isEmail("@Test")); - assertTrue(Util.isEmail("Test@Domain")); - assertFalse(Util.isEmail("Test@Domain@Test")); + assertFalse(isEmail("Test")); + assertFalse(isEmail("Test@")); + assertFalse(isEmail("@Test")); + assertTrue(isEmail("Test@Domain")); + assertFalse(isEmail("Test@Domain@Test")); } public void testSimplePassword() { - assertTrue(Util.simplePassword("$@%€#")); // too short - assertTrue(Util.simplePassword("test123")); // no special chars - assertFalse(Util.simplePassword("test$23")); // contains special chars - assertTrue(Util.simplePassword("skgjafdsg")); // chars only - assertTrue(Util.simplePassword("986535465")); // digits only - assertFalse(Util.simplePassword("test9523")); // mixed digits and chars - assertFalse(Util.simplePassword("8986546054")); // digits only, but long enough - assertFalse(Util.simplePassword("salgksdjbw")); // chars only, but long enough + assertTrue(simplePassword("$@%€#")); // too short + assertTrue(simplePassword("test123")); // no special chars + assertFalse(simplePassword("test$23")); // contains special chars + assertTrue(simplePassword("skgjafdsg")); // chars only + assertTrue(simplePassword("986535465")); // digits only + assertFalse(simplePassword("test9523")); // mixed digits and chars + assertFalse(simplePassword("8986546054")); // digits only, but long enough + assertFalse(simplePassword("salgksdjbw")); // chars only, but long enough } public void testUnsetFlags(){ - assertEquals(0,Util.unset(31,1,2,4,8,16)); - assertEquals(1,Util.unset(31,2,4,8,16)); - assertEquals(2,Util.unset(31,1,4,8,16)); - assertEquals(4,Util.unset(31,1,2,8,16)); - assertEquals(8,Util.unset(31,1,2,4,16)); - assertEquals(16,Util.unset(31,1,2,4,8)); + assertEquals(0,unset(31,1,2,4,8,16)); + assertEquals(1,unset(31,2,4,8,16)); + assertEquals(2,unset(31,1,4,8,16)); + assertEquals(4,unset(31,1,2,8,16)); + assertEquals(8,unset(31,1,2,4,16)); + assertEquals(16,unset(31,1,2,4,8)); + } + + public void testBoundedChar(){ + assertEquals('0',boundedChar(0)); + assertEquals('9',boundedChar(9)); + assertEquals('A',boundedChar(10)); + assertEquals('Z',boundedChar(35)); + assertEquals('a',boundedChar(36)); + assertEquals('z',boundedChar(61)); + assertEquals('0',boundedChar(62)); } } \ No newline at end of file diff --git a/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java b/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java index 43f0361..b404ab1 100644 --- a/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java +++ b/src/test/java/de/srsoftware/widerhall/data/ListMemberTest.java @@ -110,7 +110,7 @@ public class ListMemberTest extends TestCase { }*/ public void testSafeMap() { - var user = new User("email","name","salt","hash",0); + var user = new User("email","name","salt","hash",null,0); var lm = new ListMember(null,user,ListMember.STATE_AWAITING_CONFIRMATION,"token"); assertEquals(Map.of(EMAIL,"email",NAME,"name",STATE,"awaiting confirmation"),lm.safeMap()); } @@ -137,7 +137,7 @@ public class ListMemberTest extends TestCase { }*/ public void testUser() { - var user = new User("email","name","salt","hash",0); + var user = new User("email","name","salt","hash",null,0); var lm = new ListMember(null,user,ListMember.STATE_AWAITING_CONFIRMATION,"token"); assertEquals(user,lm.user()); } diff --git a/static/templates/login.st b/static/templates/login.st index 04512ef..d83124f 100644 --- a/static/templates/login.st +++ b/static/templates/login.st @@ -19,5 +19,6 @@ + Forgot password? \ No newline at end of file diff --git a/static/templates/new_password_form.st b/static/templates/new_password_form.st new file mode 100644 index 0000000..c2696d4 --- /dev/null +++ b/static/templates/new_password_form.st @@ -0,0 +1,33 @@ + + + «head()» + «userinfo()» + + + «navigation()» + «messages()» +

Set new account password

+
+
+ + Dear «if(data.user.name)»«data.user.name»«else»«data.user.email»«endif», you may now set a new password for your account «data.email»: + Login credentials + + + + + +
+
+ Forgot password? + + \ No newline at end of file diff --git a/static/templates/reset-pw.st b/static/templates/reset-pw.st new file mode 100644 index 0000000..ff84a16 --- /dev/null +++ b/static/templates/reset-pw.st @@ -0,0 +1,21 @@ + + + «head()» + + «navigation()» + «messages()» +

Widerhall password recovery

+
+
+ Forgot password? + If you have lost or forgot your password, please enter your email below. + Upon pressing the "Reset password" button, you will be sent an email with a link allowing you to set a new password. + + +
+
+ + \ No newline at end of file diff --git a/static/templates/reset_link_sent.st b/static/templates/reset_link_sent.st new file mode 100644 index 0000000..ff8e9d6 --- /dev/null +++ b/static/templates/reset_link_sent.st @@ -0,0 +1,17 @@ + + + «head()» + + «navigation()» + «messages()» + +

Confirmation

+
+ + Link sent + + An email was sent to you:
+ This mail contains a link, that proves you are in control about the resprective mail address. Open the received link in a web browser and you will be able to set a new password. +
+ + \ No newline at end of file diff --git a/static/templates/subscribe.st b/static/templates/subscribe.st index a90376d..4c768c8 100644 --- a/static/templates/subscribe.st +++ b/static/templates/subscribe.st @@ -11,11 +11,11 @@
Suscribe to "«data.list»"