mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
re-implement password recovery
This commit is contained in:
@@ -17,11 +17,11 @@ app:
|
||||
databaseUpdateThreads: 1
|
||||
|
||||
# settings for sending emails (password recovery)
|
||||
smtpHost:
|
||||
smtpPort:
|
||||
smtpHost: localhost
|
||||
smtpPort: 25
|
||||
smtpTls: false
|
||||
smtpUserName:
|
||||
smtpPassword:
|
||||
smtpUserName: user
|
||||
smtpPassword: pass
|
||||
|
||||
# wether this commafeed instance has a lot of feeds to refresh
|
||||
# leave this to false in almost all cases
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 52 KiB |
@@ -1460,9 +1460,16 @@ module.controller('MetricsCtrl', ['$scope', 'AdminMetricsService', function($sco
|
||||
$scope.metrics = AdminMetricsService.get();
|
||||
}]);
|
||||
|
||||
module.controller('LoginCtrl', ['$scope', '$location', 'SessionService', function($scope, $location, SessionService) {
|
||||
module.controller('LoginCtrl', ['$scope', '$location', 'SessionService', 'ServerService', function($scope, $location, SessionService, ServerService) {
|
||||
$scope.model = {};
|
||||
|
||||
$scope.recovery_model = {};
|
||||
$scope.recovery = false;
|
||||
$scope.recovery_enabled = false;
|
||||
|
||||
ServerService.get(function(data) {
|
||||
$scope.recovery_enabled = data.smtpEnabled;
|
||||
});
|
||||
|
||||
var login = function(model) {
|
||||
var success = function(data) {
|
||||
window.location.href = window.location.href.substring(0, window.location.href.lastIndexOf('#'));
|
||||
@@ -1485,6 +1492,22 @@ module.controller('LoginCtrl', ['$scope', '$location', 'SessionService', functio
|
||||
$scope.login = function() {
|
||||
login($scope.model);
|
||||
};
|
||||
|
||||
$scope.toggleRecovery = function() {
|
||||
$scope.recovery = !$scope.recovery;
|
||||
};
|
||||
|
||||
var recovery_success = function(data) {
|
||||
$scope.recovery_message = "Email has ben sent. Check your inbox.";
|
||||
};
|
||||
var recovery_error = function(data) {
|
||||
$scope.recovery_message = data.data;
|
||||
};
|
||||
$scope.recover = function() {
|
||||
SessionService.passwordReset({
|
||||
email : $scope.recovery_model.email
|
||||
}, recovery_success, recovery_error);
|
||||
}
|
||||
}]);
|
||||
|
||||
module.controller('RegisterCtrl', ['$scope', '$location', 'SessionService', 'ServerService',
|
||||
|
||||
@@ -36,6 +36,7 @@ module.factory('SessionService', ['$resource', function($resource) {
|
||||
var res = {};
|
||||
res.login = $resource('rest/user/login').save;
|
||||
res.register = $resource('rest/user/register').save;
|
||||
res.passwordReset = $resource('rest/user/passwordReset').save;
|
||||
return res;
|
||||
}]);
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ label {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.clear-both {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -63,7 +67,11 @@ label {
|
||||
}
|
||||
|
||||
.welcome .header {
|
||||
margin: 20px 0 40px 0;
|
||||
margin: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.welcome h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.welcome .tagline {
|
||||
@@ -73,13 +81,14 @@ label {
|
||||
}
|
||||
|
||||
.welcome .preview {
|
||||
margin: 20px 0 20px 0;
|
||||
max-width: 100%;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.welcome .demo {
|
||||
font-size: 24px;
|
||||
color: #B3B3B3;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.main .spinner {
|
||||
|
||||
@@ -6,21 +6,18 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pull-right tagline">Bloat-free feed reader</div>
|
||||
<div class="text-center">
|
||||
<a href="#" ng-click="demoLogin()">
|
||||
<img src="images/preview.jpg" class="preview" />
|
||||
<div class="text-center clear-both">
|
||||
<img src="images/preview.jpg" class="preview" />
|
||||
<a href="#" ng-click="demoLogin()" class="demo">
|
||||
Try out the demo
|
||||
<br />
|
||||
<span class="demo">
|
||||
Try out the demo
|
||||
<br />
|
||||
<small>(some features are disabled)</small>
|
||||
</span>
|
||||
<small>(some features are disabled)</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6" ng-controller="LoginCtrl">
|
||||
<div class="well" id="login-panel">
|
||||
<div class="well" id="login-panel" ng-if="!recovery">
|
||||
<h3>Login</h3>
|
||||
<span class="feedback">{{message}}</span>
|
||||
<form ng-submit="login()">
|
||||
@@ -34,7 +31,21 @@
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="btn btn-primary" value="Log in" />
|
||||
<a class="pull-right">Forgot password?</a>
|
||||
<a href="" class="pull-right" ng-click="toggleRecovery()" ng-if="recovery_enabled">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="well" id="recovery-panel" ng-if="recovery">
|
||||
<h3>Password Recovery</h3>
|
||||
<span class="feedback">{{recovery_message}}</span>
|
||||
<form ng-submit="recover()">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" name="email" class="form-control" ng-model="recovery_model.email" focus="true"></input>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="btn btn-primary" value="Recover" />
|
||||
<input type="button" class="btn" value="Cancel" ng-click="toggleRecovery()" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@ import com.commafeed.backend.service.FeedEntryTagService;
|
||||
import com.commafeed.backend.service.FeedService;
|
||||
import com.commafeed.backend.service.FeedSubscriptionService;
|
||||
import com.commafeed.backend.service.FeedUpdateService;
|
||||
import com.commafeed.backend.service.MailService;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.PubSubService;
|
||||
import com.commafeed.backend.service.StartupService;
|
||||
@@ -160,6 +161,7 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
FeedSubscriptionService feedSubscriptionService = new FeedSubscriptionService(feedEntryStatusDAO, feedSubscriptionDAO, feedService,
|
||||
queues, cacheService, config);
|
||||
FeedUpdateService feedUpdateService = new FeedUpdateService(feedEntryDAO, feedEntryContentService);
|
||||
MailService mailService = new MailService(config);
|
||||
PasswordEncryptionService encryptionService = new PasswordEncryptionService();
|
||||
PubSubService pubSubService = new PubSubService(config, queues);
|
||||
UserService userService = new UserService(feedCategoryDAO, userDAO, userSettingsDAO, feedSubscriptionService, encryptionService,
|
||||
@@ -198,7 +200,8 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
feedSubscriptionService, queues, opmlImporter, opmlExporter, cacheService, config));
|
||||
environment.jersey().register(new PubSubHubbubCallbackREST(feedDAO, feedParser, queues, config, metrics));
|
||||
environment.jersey().register(new ServerREST(httpGetter, config, applicationPropertiesService));
|
||||
environment.jersey().register(new UserREST(userDAO, userRoleDAO, userSettingsDAO, userService, encryptionService));
|
||||
environment.jersey().register(
|
||||
new UserREST(userDAO, userRoleDAO, userSettingsDAO, userService, encryptionService, mailService, config));
|
||||
|
||||
// Servlets
|
||||
NextUnreadServlet nextUnreadServlet = new NextUnreadServlet(sessionFactory, feedSubscriptionDAO, feedEntryStatusDAO,
|
||||
@@ -230,8 +233,6 @@ public class CommaFeedApplication extends Application<CommaFeedConfiguration> {
|
||||
SwaggerConfig swaggerConfig = ConfigFactory.config();
|
||||
swaggerConfig.setApiVersion("1");
|
||||
swaggerConfig.setBasePath("/rest");
|
||||
|
||||
// TODO password recovery
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.commafeed.backend.service;
|
||||
import java.io.Serializable;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.mail.Authenticator;
|
||||
import javax.mail.Message;
|
||||
import javax.mail.PasswordAuthentication;
|
||||
@@ -12,6 +11,8 @@ import javax.mail.Transport;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.CommaFeedConfiguration.ApplicationSettings;
|
||||
import com.commafeed.backend.model.User;
|
||||
@@ -21,10 +22,10 @@ import com.commafeed.backend.model.User;
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@RequiredArgsConstructor
|
||||
public class MailService implements Serializable {
|
||||
|
||||
@Inject
|
||||
CommaFeedConfiguration config;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
public void sendMail(User user, String subject, String content) throws Exception {
|
||||
|
||||
|
||||
@@ -15,5 +15,7 @@ public class ServerInfo implements Serializable {
|
||||
private String version;
|
||||
private String gitCommit;
|
||||
private boolean allowRegistrations;
|
||||
private String googleAnalyticsCode;
|
||||
private boolean smtpEnabled;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.commafeed.frontend.model.request;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.hibernate.validator.constraints.Email;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import com.wordnik.swagger.annotations.ApiModel;
|
||||
import com.wordnik.swagger.annotations.ApiModelProperty;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
@Data
|
||||
@ApiModel
|
||||
public class PasswordResetRequest implements Serializable {
|
||||
|
||||
@ApiModelProperty(value = "email address for password recovery", required = true)
|
||||
@Email
|
||||
@NotEmpty
|
||||
private String email;
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.HttpGetter;
|
||||
import com.commafeed.backend.HttpGetter.HttpResult;
|
||||
@@ -45,6 +47,8 @@ public class ServerREST {
|
||||
infos.setVersion(applicationPropertiesService.getVersion());
|
||||
infos.setGitCommit(applicationPropertiesService.getGitCommit());
|
||||
infos.setAllowRegistrations(config.getApplicationSettings().isAllowRegistrations());
|
||||
infos.setGoogleAnalyticsCode(config.getApplicationSettings().getGoogleAnalyticsTrackingCode());
|
||||
infos.setSmtpEnabled(StringUtils.isNotBlank(config.getApplicationSettings().getSmtpHost()));
|
||||
return Response.ok(infos).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import io.dropwizard.jersey.validation.ValidationErrorMessage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.validation.ConstraintViolation;
|
||||
@@ -15,18 +17,26 @@ import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
|
||||
import com.commafeed.CommaFeedApplication;
|
||||
import com.commafeed.CommaFeedConfiguration;
|
||||
import com.commafeed.backend.dao.UserDAO;
|
||||
import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.dao.UserSettingsDAO;
|
||||
import com.commafeed.backend.feed.FeedUtils;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
@@ -34,12 +44,14 @@ import com.commafeed.backend.model.UserSettings;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.model.UserSettings.ViewMode;
|
||||
import com.commafeed.backend.service.MailService;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.auth.SecurityCheck;
|
||||
import com.commafeed.frontend.model.Settings;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.LoginRequest;
|
||||
import com.commafeed.frontend.model.request.PasswordResetRequest;
|
||||
import com.commafeed.frontend.model.request.ProfileModificationRequest;
|
||||
import com.commafeed.frontend.model.request.RegistrationRequest;
|
||||
import com.google.common.base.Optional;
|
||||
@@ -53,6 +65,7 @@ import com.wordnik.swagger.annotations.ApiParam;
|
||||
@Api(value = "/user", description = "Operations about the user")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class UserREST {
|
||||
|
||||
@@ -61,6 +74,8 @@ public class UserREST {
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final MailService mailService;
|
||||
private final CommaFeedConfiguration config;
|
||||
|
||||
@Path("/settings")
|
||||
@GET
|
||||
@@ -233,6 +248,75 @@ public class UserREST {
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/passwordReset")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
@ApiOperation(value = "send a password reset email")
|
||||
public Response sendPasswordReset(@Valid PasswordResetRequest req) {
|
||||
User user = userDAO.findByEmail(req.getEmail());
|
||||
if (user == null) {
|
||||
return Response.status(Status.PRECONDITION_FAILED).entity("Email not found.").build();
|
||||
}
|
||||
try {
|
||||
user.setRecoverPasswordToken(DigestUtils.sha1Hex(UUID.randomUUID().toString()));
|
||||
user.setRecoverPasswordTokenDate(new Date());
|
||||
userDAO.saveOrUpdate(user);
|
||||
mailService.sendMail(user, "Password recovery", buildEmailContent(user));
|
||||
return Response.ok().build();
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Response.status(Status.INTERNAL_SERVER_ERROR).entity("could not send email: " + e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String buildEmailContent(User user) throws Exception {
|
||||
String publicUrl = FeedUtils.removeTrailingSlash(config.getApplicationSettings().getPublicUrl());
|
||||
publicUrl += "/rest/user/passwordResetCallback";
|
||||
return String
|
||||
.format("You asked for password recovery for account '%s', <a href='%s'>follow this link</a> to change your password. Ignore this if you didn't request a password recovery.",
|
||||
user.getName(), callbackUrl(user, publicUrl));
|
||||
}
|
||||
|
||||
private String callbackUrl(User user, String publicUrl) throws Exception {
|
||||
return new URIBuilder(publicUrl).addParameter("email", user.getEmail()).addParameter("token", user.getRecoverPasswordToken())
|
||||
.build().toURL().toString();
|
||||
}
|
||||
|
||||
@Path("/passwordResetCallback")
|
||||
@GET
|
||||
@UnitOfWork
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public Response passwordRecoveryCallback(@QueryParam("email") String email, @QueryParam("token") String token) {
|
||||
Preconditions.checkNotNull(email);
|
||||
Preconditions.checkNotNull(token);
|
||||
|
||||
User user = userDAO.findByEmail(email);
|
||||
if (user == null) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Email not found.").build();
|
||||
}
|
||||
if (user.getRecoverPasswordToken() == null || !user.getRecoverPasswordToken().equals(token)) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Invalid token.").build();
|
||||
}
|
||||
if (user.getRecoverPasswordTokenDate().before(DateUtils.addDays(new Date(), -2))) {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("token expired.").build();
|
||||
}
|
||||
|
||||
String passwd = RandomStringUtils.randomAlphanumeric(10);
|
||||
byte[] password = encryptionService.getEncryptedPassword(passwd, user.getSalt());
|
||||
user.setPassword(password);
|
||||
if (StringUtils.isNotBlank(user.getApiKey())) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
user.setRecoverPasswordToken(null);
|
||||
user.setRecoverPasswordTokenDate(null);
|
||||
userDAO.saveOrUpdate(user);
|
||||
|
||||
String message = "Your new password is: " + passwd;
|
||||
message += "<br />";
|
||||
message += String.format("<a href=\"%s\">Back to Homepage</a>", config.getApplicationSettings().getPublicUrl());
|
||||
return Response.ok(message).build();
|
||||
}
|
||||
|
||||
@Path("/profile/deleteAccount")
|
||||
@POST
|
||||
@UnitOfWork
|
||||
|
||||
Reference in New Issue
Block a user