Add password reset route. Closes #29
This commit is contained in:
parent
4901e63599
commit
fd1ec2b475
@ -298,6 +298,7 @@ public final class APIv3 extends AbstractVerticle {
|
||||
http.post("/users/:id/leave").handler(new POSTUserLeave());
|
||||
http.post("/users/:id/login").blockingHandler(new POSTUserLogin());
|
||||
http.post("/users/:id/notify").blockingHandler(new POSTUserNotify(), false);
|
||||
http.post("/users/:id/passwordReset").blockingHandler(new POSTUserPasswordReset(), false);
|
||||
http.post("/users/:id/register").blockingHandler(new POSTUserRegister(), false);
|
||||
http.post("/users/:id/setupTotp").blockingHandler(new POSTUserSetupTotp(), false);
|
||||
http.post("/users/:id/verifyTotp").blockingHandler(new POSTUserVerifyTotp(), false);
|
||||
|
@ -43,6 +43,8 @@ public final class User {
|
||||
@Getter @ExcludeFromReplies private Map<String, Instant> aliases = new HashMap<>();
|
||||
@Getter @ExcludeFromReplies @Setter private String totpSecret;
|
||||
@Getter @ExcludeFromReplies private String password;
|
||||
@Getter @ExcludeFromReplies private String passwordResetToken;
|
||||
@Getter @ExcludeFromReplies private Instant passwordResetTokenSetAt;
|
||||
@Getter private String email;
|
||||
@Getter private Instant registeredAt;
|
||||
@Getter @ExcludeFromReplies private String pendingEmail;
|
||||
@ -317,10 +319,17 @@ public final class User {
|
||||
this.online = false;
|
||||
}
|
||||
|
||||
public void setPassword(String input) {
|
||||
public void startPasswordReset() {
|
||||
this.passwordResetToken = UUID.randomUUID().toString().replaceAll("-", "");
|
||||
this.passwordResetTokenSetAt = Instant.now();
|
||||
}
|
||||
|
||||
public void updatePassword(String password) {
|
||||
this.passwordResetToken = null;
|
||||
this.passwordResetTokenSetAt = null;
|
||||
this.password = Hashing
|
||||
.sha256()
|
||||
.hashString(input + "$" + id.toString(), Charsets.UTF_8)
|
||||
.hashString(password + "$" + id.toString(), Charsets.UTF_8)
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ public final class POSTEmailTokensConfirm implements Handler<RoutingContext> {
|
||||
}
|
||||
|
||||
if ((System.currentTimeMillis() - user.getPendingEmailTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) {
|
||||
ErrorUtils.respondGeneric(ctx, 200, "Email token is expired");
|
||||
ErrorUtils.respondInvalidInput(ctx, "Email token is expired");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -40,12 +40,12 @@ public final class POSTEmailTokensConfirm implements Handler<RoutingContext> {
|
||||
String password = requestBody.getString("password");
|
||||
|
||||
if (password.length() < 8) {
|
||||
ErrorUtils.respondGeneric(ctx, 200, "Your password is too short.");
|
||||
ErrorUtils.respondInvalidInput(ctx, "Your password is too short.");
|
||||
return;
|
||||
}
|
||||
|
||||
user.completeRegistration(user.getPendingEmail());
|
||||
user.setPassword(password);
|
||||
user.updateUsername(password);
|
||||
BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
|
||||
user.save(callback);
|
||||
callback.get();
|
||||
|
@ -11,6 +11,8 @@ import net.frozenorb.apiv3.unsorted.BlockingCallback;
|
||||
import net.frozenorb.apiv3.unsorted.RequiresTotpResult;
|
||||
import net.frozenorb.apiv3.util.ErrorUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class POSTUserChangePassword implements Handler<RoutingContext> {
|
||||
|
||||
public void handle(RoutingContext ctx) {
|
||||
@ -30,19 +32,30 @@ public final class POSTUserChangePassword implements Handler<RoutingContext> {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean authorized = user.checkPassword(requestBody.getString("currentPassword"));
|
||||
boolean authorized = false;
|
||||
|
||||
if (!authorized) {
|
||||
ErrorUtils.respondInvalidInput(ctx, "Current password is not correct.");
|
||||
return;
|
||||
if (user.checkPassword(requestBody.getString("currentPassword"))) {
|
||||
BlockingCallback<RequiresTotpResult> totpRequiredCallback = new BlockingCallback<>();
|
||||
user.requiresTotpAuthorization(null, totpRequiredCallback);
|
||||
RequiresTotpResult requiresTotp = totpRequiredCallback.get();
|
||||
|
||||
if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
authorized = true;
|
||||
} else if (user.getPasswordResetToken() != null && user.getPasswordResetToken().equals(requestBody.getString("passwordResetToken"))) {
|
||||
if ((System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) > TimeUnit.DAYS.toMillis(2)) {
|
||||
ErrorUtils.respondGeneric(ctx, 200, "Password reset token is expired");
|
||||
return;
|
||||
}
|
||||
|
||||
authorized = true;
|
||||
}
|
||||
|
||||
BlockingCallback<RequiresTotpResult> totpRequiredCallback = new BlockingCallback<>();
|
||||
user.requiresTotpAuthorization(null, totpRequiredCallback);
|
||||
RequiresTotpResult requiresTotp = totpRequiredCallback.get();
|
||||
|
||||
if (requiresTotp == RequiresTotpResult.REQUIRED_NO_EXEMPTIONS) {
|
||||
// TODO
|
||||
if (!authorized) {
|
||||
ErrorUtils.respondInvalidInput(ctx, "Could not authorize password change.");
|
||||
return;
|
||||
}
|
||||
|
||||
String newPassword = requestBody.getString("newPassword");
|
||||
@ -52,7 +65,7 @@ public final class POSTUserChangePassword implements Handler<RoutingContext> {
|
||||
return;
|
||||
}
|
||||
|
||||
user.setPassword(newPassword);
|
||||
user.updatePassword(newPassword);
|
||||
BlockingCallback<UpdateResult> saveCallback = new BlockingCallback<>();
|
||||
user.save(saveCallback);
|
||||
saveCallback.get();
|
||||
|
@ -0,0 +1,60 @@
|
||||
package net.frozenorb.apiv3.route.users;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.mongodb.client.result.UpdateResult;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import net.frozenorb.apiv3.APIv3;
|
||||
import net.frozenorb.apiv3.model.NotificationTemplate;
|
||||
import net.frozenorb.apiv3.model.User;
|
||||
import net.frozenorb.apiv3.unsorted.BlockingCallback;
|
||||
import net.frozenorb.apiv3.unsorted.Notification;
|
||||
import net.frozenorb.apiv3.util.ErrorUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class POSTUserPasswordReset implements Handler<RoutingContext> {
|
||||
|
||||
public void handle(RoutingContext ctx) {
|
||||
BlockingCallback<User> userCallback = new BlockingCallback<>();
|
||||
User.findById(ctx.request().getParam("id"), userCallback);
|
||||
User user = userCallback.get();
|
||||
|
||||
if (user == null) {
|
||||
ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.getPasswordResetToken() != null && (System.currentTimeMillis() - user.getPasswordResetTokenSetAt().toEpochMilli()) < TimeUnit.DAYS.toMillis(2)) {
|
||||
ErrorUtils.respondInvalidInput(ctx, "User provided already has password reset token set.");
|
||||
return;
|
||||
}
|
||||
|
||||
user.startPasswordReset();
|
||||
BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
|
||||
user.save(callback);
|
||||
callback.get();
|
||||
|
||||
Map<String, Object> replacements = ImmutableMap.of(
|
||||
"username", user.getLastUsername(),
|
||||
"passwordResetToken", user.getPasswordResetToken()
|
||||
);
|
||||
|
||||
BlockingCallback<NotificationTemplate> notificationTemplateCallback = new BlockingCallback<>();
|
||||
NotificationTemplate.findById("password-reset", notificationTemplateCallback);
|
||||
Notification notification = new Notification(notificationTemplateCallback.get(), replacements, replacements);
|
||||
|
||||
notification.sendAsEmail(user.getEmail(), (ignored, error) -> {
|
||||
if (error != null) {
|
||||
ErrorUtils.respondInternalError(ctx, error);
|
||||
} else {
|
||||
APIv3.respondJson(ctx, ImmutableMap.of(
|
||||
"success", true
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -50,7 +50,7 @@ public final class POSTUserVerifyTotp implements Handler<RoutingContext> {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean authorized = TotpUtils.authorizeUser(user, providedCode);
|
||||
boolean authorized = TotpUtils.authorizeUser(user.getTotpSecret(), providedCode);
|
||||
|
||||
if (authorized) {
|
||||
BlockingCallback<Void> markPreAuthorizedCallback = new BlockingCallback<>();
|
||||
|
Loading…
Reference in New Issue
Block a user