recover password (wip)

This commit is contained in:
Athou
2013-05-20 21:53:13 +02:00
parent 17ac0432b2
commit 515cc4c0dd
16 changed files with 409 additions and 89 deletions

View File

@@ -44,4 +44,19 @@ public class UserDAO extends GenericDAO<User> {
return user;
}
public User findByEmail(String email) {
CriteriaQuery<User> query = builder.createQuery(getType());
Root<User> root = query.from(getType());
query.where(builder.equal(root.get(User_.email), email));
TypedQuery<User> q = em.createQuery(query);
User user = null;
try {
user = q.getSingleResult();
} catch (NoResultException e) {
user = null;
}
return user;
}
}

View File

@@ -43,6 +43,12 @@ public class User extends AbstractModel {
@Temporal(TemporalType.TIMESTAMP)
private Date lastLogin;
@Column(length = 40)
private String recoverPasswordToken;
@Temporal(TemporalType.TIMESTAMP)
private Date recoverPasswordTokenDate;
@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
private Set<UserRole> roles = Sets.newHashSet();
@@ -110,4 +116,20 @@ public class User extends AbstractModel {
this.apiKey = apiKey;
}
public String getRecoverPasswordToken() {
return recoverPasswordToken;
}
public void setRecoverPasswordToken(String recoverPasswordToken) {
this.recoverPasswordToken = recoverPasswordToken;
}
public Date getRecoverPasswordTokenDate() {
return recoverPasswordTokenDate;
}
public void setRecoverPasswordTokenDate(Date recoverPasswordTokenDate) {
this.recoverPasswordTokenDate = recoverPasswordTokenDate;
}
}

View File

@@ -20,8 +20,10 @@ import com.google.api.client.util.Lists;
@Stateless
public class FeedSubscriptionService {
@SuppressWarnings("serial")
@ApplicationException
public class FeedSubscriptionException extends RuntimeException {
public static class FeedSubscriptionException extends RuntimeException {
public FeedSubscriptionException(String msg) {
super(msg);
}
@@ -51,10 +53,10 @@ public class FeedSubscriptionService {
final String pubUrl = applicationSettingsService.get().getPublicUrl();
if (pubUrl == null) {
throw new FeedSubscriptionException(
"Public URL of this CommaFeed is unset");
"Public URL of this CommaFeed is unset");
}
if (url.startsWith(pubUrl)) {
throw new RuntimeException(
throw new FeedSubscriptionException(
"Could not subscribe to a feed from this CommaFeed instance");
}

View File

@@ -1,8 +1,10 @@
package com.commafeed.backend.services;
import java.io.Serializable;
import java.util.Properties;
import javax.inject.Inject;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
@@ -10,35 +12,47 @@ import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.User;
public class MailService {
@SuppressWarnings("serial")
public class MailService implements Serializable {
private static Logger log = LoggerFactory.getLogger(MailService.class);
@Inject
ApplicationSettingsService applicationSettingsService;
public void sendMail(User user, String subject, String content) throws Exception {
public void sendMail(User user, String subject, String content)
throws Exception {
ApplicationSettings settings = applicationSettingsService.get();
final String username = settings.getSmtpUserName();
final String password = settings.getSmtpPassword();
log.info(username);
log.info(password);
log.info("" + settings.isSmtpTls());
log.info(settings.getSmtpHost());
log.info("" + settings.getSmtpPort());
String dest = user.getEmail();
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", settings.isSmtpTls());
props.put("mail.smtp.starttls.enable", "" + settings.isSmtpTls());
props.put("mail.smtp.host", settings.getSmtpHost());
props.put("mail.smtp.port", settings.getSmtpPort());
props.put("mail.smtp.port", "" + settings.getSmtpPort());
Session session = Session.getInstance(props,
new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(username, "CommaFeed"));

View File

@@ -2,10 +2,13 @@ package com.commafeed.backend.services;
import java.util.Calendar;
import java.util.Collection;
import java.util.UUID;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.apache.commons.codec.digest.DigestUtils;
import com.commafeed.backend.dao.FeedCategoryDAO;
import com.commafeed.backend.dao.FeedEntryStatusDAO;
import com.commafeed.backend.dao.FeedSubscriptionDAO;
@@ -94,4 +97,10 @@ public class UserService {
userRoleDAO.delete(userRoleDAO.findAll(user));
userDAO.delete(user);
}
public String generateApiKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID()
.toString(), user.getSalt());
return DigestUtils.sha1Hex(key);
}
}

View File

@@ -44,6 +44,8 @@ import com.commafeed.frontend.pages.GoogleImportRedirectPage;
import com.commafeed.frontend.pages.HomePage;
import com.commafeed.frontend.pages.LogoutPage;
import com.commafeed.frontend.pages.NextUnreadRedirectPage;
import com.commafeed.frontend.pages.PasswordRecoveryCallbackPage;
import com.commafeed.frontend.pages.PasswordRecoveryPage;
import com.commafeed.frontend.pages.WelcomePage;
import com.commafeed.frontend.utils.exception.DisplayExceptionPage;
@@ -66,8 +68,13 @@ public class CommaFeedApplication extends AuthenticatedWebApplication {
mountPage("welcome", WelcomePage.class);
mountPage("demo", DemoLoginPage.class);
mountPage("recover", PasswordRecoveryPage.class);
mountPage("recover2", PasswordRecoveryCallbackPage.class);
mountPage("logout", LogoutPage.class);
mountPage("error", DisplayExceptionPage.class);
mountPage("google/import/redirect", GoogleImportRedirectPage.class);
mountPage(GoogleImportCallbackPage.PAGE_PATH,
GoogleImportCallbackPage.class);

View File

@@ -28,6 +28,7 @@ import com.commafeed.backend.model.ApplicationSettings;
import com.commafeed.backend.model.User;
import com.commafeed.backend.model.UserSettings;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.backend.services.MailService;
import com.commafeed.frontend.CommaFeedSession;
import com.commafeed.frontend.utils.WicketUtils;
import com.google.api.client.util.Maps;
@@ -61,6 +62,9 @@ public abstract class BasePage extends WebPage {
@Inject
protected UserRoleDAO userRoleDAO;
@Inject
MailService mailService;
@Inject
ApplicationSettingsService applicationSettingsService;

View File

@@ -0,0 +1,22 @@
<html xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<div class="container">
<div class="text-center">
<img src="images/logo_2.png" />
<div wicket:id="feedback"></div>
<form wicket:id="form">
Password:
<input type="password" wicket:id="password" />
<br />
Password:
<input type="password" wicket:id="confirm" />
<br />
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn" wicket:id="cancel" value="Home page" />
</form>
</div>
</div>
</wicket:extend>
</body>
</html>

View File

@@ -0,0 +1,80 @@
package com.commafeed.frontend.pages;
import java.util.Calendar;
import javax.inject.Inject;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.PasswordTextField;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.validation.validator.StringValidator;
import com.commafeed.backend.model.User;
import com.commafeed.backend.services.PasswordEncryptionService;
import com.commafeed.backend.services.UserService;
import com.commafeed.frontend.pages.components.BootstrapFeedbackPanel;
import com.commafeed.frontend.utils.exception.DisplayException;
@SuppressWarnings("serial")
public class PasswordRecoveryCallbackPage extends BasePage {
public static final String PARAM_EMAIL = "email";
public static final String PARAM_TOKEN = "token";
@Inject
PasswordEncryptionService encryptionService;
@Inject
UserService userService;
public PasswordRecoveryCallbackPage(PageParameters params) {
String email = params.get(PARAM_EMAIL).toString();
String token = params.get(PARAM_TOKEN).toString();
final User user = userDAO.findByEmail(email);
if (user == null) {
throw new DisplayException("email not found");
}
if (user.getRecoverPasswordToken() == null
|| !user.getRecoverPasswordToken().equals(token)) {
throw new DisplayException("invalid token");
}
if (user.getRecoverPasswordTokenDate().before(
DateUtils.addDays(Calendar.getInstance().getTime(), -2))) {
throw new DisplayException("token expired");
}
final IModel<String> password = new Model<String>();
final IModel<String> confirm = new Model<String>();
add(new BootstrapFeedbackPanel("feedback"));
Form<Void> form = new Form<Void>("form") {
@Override
protected void onSubmit() {
String passwd = password.getObject();
if (StringUtils.equals(passwd, confirm.getObject())) {
byte[] password = encryptionService.getEncryptedPassword(
passwd, user.getSalt());
user.setPassword(password);
user.setApiKey(userService.generateApiKey(user));
userDAO.update(user);
info("Password saved.");
} else {
error("Password do not match");
}
}
};
add(form);
form.add(new PasswordTextField("password", password).setResetPassword(
true).add(StringValidator.minimumLength(6)));
form.add(new PasswordTextField("confirm", confirm).setResetPassword(
true).add(StringValidator.minimumLength(6)));
form.add(new BookmarkablePageLink<Void>("cancel", HomePage.class));
}
}

View File

@@ -0,0 +1,19 @@
<html xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<div class="container">
<div class="text-center">
<img src="images/logo_2.png" />
<div wicket:id="feedback"></div>
<form wicket:id="form">
Email:
<input type="email" wicket:id="email" />
<br />
<input type="submit" class="btn btn-primary" value="Submit" />
<input type="button" class="btn" wicket:id="cancel" value="Cancel" />
</form>
</div>
</div>
</wicket:extend>
</body>
</html>

View File

@@ -0,0 +1,71 @@
package com.commafeed.frontend.pages;
import java.util.Calendar;
import java.util.UUID;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.wicket.extensions.validation.validator.RfcCompliantEmailAddressValidator;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.commafeed.backend.model.User;
import com.commafeed.frontend.pages.components.BootstrapFeedbackPanel;
@SuppressWarnings("serial")
public class PasswordRecoveryPage extends BasePage {
private static Logger log = LoggerFactory
.getLogger(PasswordRecoveryPage.class);
public PasswordRecoveryPage() {
IModel<String> email = new Model<String>();
add(new BootstrapFeedbackPanel("feedback"));
Form<String> form = new Form<String>("form", email) {
@Override
protected void onSubmit() {
super.onSubmit();
User user = userDAO.findByEmail(getModelObject());
if (user == null) {
error("Email not found.");
} else {
try {
user.setRecoverPasswordToken(DigestUtils.sha1Hex(UUID
.randomUUID().toString()));
user.setRecoverPasswordTokenDate(Calendar.getInstance()
.getTime());
userDAO.update(user);
mailService.sendMail(user,
"CommaFeed - Password recovery",
buildEmailContent(user));
info("Email sent.");
} catch (Exception e) {
log.error(e.getMessage(), e);
error("Cannot send email, please contact the staff.");
}
}
}
};
add(form);
form.add(new RequiredTextField<String>("email", email) {
@Override
protected String getInputType() {
return "email";
}
}.add(RfcCompliantEmailAddressValidator.getInstance()));
form.add(new BookmarkablePageLink<Void>("cancel", HomePage.class));
}
private String buildEmailContent(User user) {
return "cc";
}
}

View File

@@ -24,6 +24,7 @@
</p>
<div>
<input type="submit" class="btn btn-primary" value="Log in" />
<a wicket:id="recover" class="pull-right">Forgot password?</a>
</div>
</form>
</wicket:panel>

View File

@@ -1,15 +1,36 @@
package com.commafeed.frontend.pages.components;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.authroles.authentication.panel.SignInPanel;
import org.apache.wicket.feedback.ContainerFeedbackMessageFilter;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import com.commafeed.backend.services.ApplicationSettingsService;
import com.commafeed.frontend.pages.PasswordRecoveryPage;
@SuppressWarnings("serial")
public class LoginPanel extends SignInPanel {
@Inject
ApplicationSettingsService applicationSettingsService;
public LoginPanel(String id) {
super(id);
replace(new BootstrapFeedbackPanel("feedback",
new ContainerFeedbackMessageFilter(this)));
Form<?> form = (Form<?>) get("signInForm");
form.add(new BookmarkablePageLink<Void>("recover",
PasswordRecoveryPage.class){
@Override
protected void onConfigure() {
super.onConfigure();
String smtpHost = applicationSettingsService.get().getSmtpHost();
setVisibilityAllowed(StringUtils.isNotBlank(smtpHost));
}
});
}
}

View File

@@ -226,12 +226,13 @@ public class FeedREST extends AbstractResourceREST {
FeedInfo info = (FeedInfo) fetchFeed(url).getEntity();
try {
feedSubscriptionService.subscribe(getUser(), info.getUrl(),
req.getTitle(), category);
req.getTitle(), category);
} catch (Exception e) {
log.info("Failed to subscribe to URL {}: {}", url, e.getMessage());
return Response.status(Status.SERVICE_UNAVAILABLE).entity(
"Failed to subscribe to URL " + url + ": " + e.getMessage()
).build();
return Response
.status(Status.SERVICE_UNAVAILABLE)
.entity("Failed to subscribe to URL " + url + ": "
+ e.getMessage()).build();
}
return Response.ok(Status.OK).build();
}

View File

@@ -1,14 +1,11 @@
package com.commafeed.frontend.rest.resources;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import com.commafeed.backend.StartupBean;
@@ -120,10 +117,10 @@ public class UserREST extends AbstractResourceREST {
byte[] password = encryptionService.getEncryptedPassword(
request.getPassword(), user.getSalt());
user.setPassword(password);
user.setApiKey(generateKey(user));
user.setApiKey(userService.generateApiKey(user));
}
if (request.isNewApiKey()) {
user.setApiKey(generateKey(user));
user.setApiKey(userService.generateApiKey(user));
}
userDAO.update(user);
return Response.ok().build();
@@ -140,10 +137,4 @@ public class UserREST extends AbstractResourceREST {
userService.unregister(getUser());
return Response.ok().build();
}
private String generateKey(User user) {
byte[] key = encryptionService.getEncryptedPassword(UUID.randomUUID()
.toString(), user.getSalt());
return DigestUtils.sha1Hex(key);
}
}