From 0c784dc5cc78420cb97e30c6ed084c5cbe1fbc31 Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:02:02 +0800 Subject: [PATCH] feat: add password recovery --- open-isle-cli/src/router/index.js | 6 + .../src/views/ForgotPasswordPageView.vue | 176 ++++++++++++++++++ open-isle-cli/src/views/LoginPageView.vue | 3 +- .../openisle/controller/AuthController.java | 54 ++++++ src/main/java/com/openisle/model/User.java | 2 + .../java/com/openisle/service/JwtService.java | 23 +++ .../com/openisle/service/UserService.java | 28 +++ src/main/resources/application.properties | 1 + 8 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 open-isle-cli/src/views/ForgotPasswordPageView.vue diff --git a/open-isle-cli/src/router/index.js b/open-isle-cli/src/router/index.js index 2239d8a1a..7d02d50c5 100644 --- a/open-isle-cli/src/router/index.js +++ b/open-isle-cli/src/router/index.js @@ -15,6 +15,7 @@ import NotFoundPageView from '../views/NotFoundPageView.vue' import GithubCallbackPageView from '../views/GithubCallbackPageView.vue' import DiscordCallbackPageView from '../views/DiscordCallbackPageView.vue' import TwitterCallbackPageView from '../views/TwitterCallbackPageView.vue' +import ForgotPasswordPageView from '../views/ForgotPasswordPageView.vue' const routes = [ { @@ -57,6 +58,11 @@ const routes = [ name: 'login', component: LoginPageView }, + { + path: '/forgot-password', + name: 'forgot-password', + component: ForgotPasswordPageView + }, { path: '/signup', name: 'signup', diff --git a/open-isle-cli/src/views/ForgotPasswordPageView.vue b/open-isle-cli/src/views/ForgotPasswordPageView.vue new file mode 100644 index 000000000..a3d76cbde --- /dev/null +++ b/open-isle-cli/src/views/ForgotPasswordPageView.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/open-isle-cli/src/views/LoginPageView.vue b/open-isle-cli/src/views/LoginPageView.vue index 172a19da1..b14a48f60 100644 --- a/open-isle-cli/src/views/LoginPageView.vue +++ b/open-isle-cli/src/views/LoginPageView.vue @@ -24,7 +24,8 @@ -
没有账号? 注册 +
没有账号? | +
diff --git a/src/main/java/com/openisle/controller/AuthController.java b/src/main/java/com/openisle/controller/AuthController.java index e7204e50b..e824758ba 100644 --- a/src/main/java/com/openisle/controller/AuthController.java +++ b/src/main/java/com/openisle/controller/AuthController.java @@ -13,6 +13,7 @@ import com.openisle.service.RegisterModeService; import com.openisle.service.NotificationService; import com.openisle.model.RegisterMode; import com.openisle.repository.UserRepository; +import com.openisle.exception.FieldException; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -38,6 +39,7 @@ public class AuthController { private final NotificationService notificationService; private final UserRepository userRepository; + @Value("${app.captcha.enabled:false}") private boolean captchaEnabled; @@ -270,6 +272,41 @@ public class AuthController { return ResponseEntity.ok(Map.of("valid", true)); } + @PostMapping("/forgot/send") + public ResponseEntity sendReset(@RequestBody ForgotPasswordRequest req) { + Optional userOpt = userService.findByEmail(req.getEmail()); + if (userOpt.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("error", "User not found")); + } + String code = userService.generatePasswordResetCode(req.getEmail()); + emailService.sendEmail(req.getEmail(), "Password Reset Code", "Your verification code is " + code); + return ResponseEntity.ok(Map.of("message", "Verification code sent")); + } + + @PostMapping("/forgot/verify") + public ResponseEntity verifyReset(@RequestBody VerifyForgotRequest req) { + boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode()); + if (ok) { + String username = userService.findByEmail(req.getEmail()).get().getUsername(); + return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username))); + } + return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code")); + } + + @PostMapping("/forgot/reset") + public ResponseEntity resetPassword(@RequestBody ResetPasswordRequest req) { + String username = jwtService.validateAndGetSubjectForReset(req.getToken()); + try { + userService.updatePassword(username, req.getPassword()); + return ResponseEntity.ok(Map.of("message", "Password updated")); + } catch (FieldException e) { + return ResponseEntity.badRequest().body(Map.of( + "field", e.getField(), + "error", e.getMessage() + )); + } + } + @Data private static class RegisterRequest { private String username; @@ -320,4 +357,21 @@ public class AuthController { private String token; private String reason; } + + @Data + private static class ForgotPasswordRequest { + private String email; + } + + @Data + private static class VerifyForgotRequest { + private String email; + private String code; + } + + @Data + private static class ResetPasswordRequest { + private String token; + private String password; + } } diff --git a/src/main/java/com/openisle/model/User.java b/src/main/java/com/openisle/model/User.java index dcc8516bb..6bd6abdc8 100644 --- a/src/main/java/com/openisle/model/User.java +++ b/src/main/java/com/openisle/model/User.java @@ -37,6 +37,8 @@ public class User { private String verificationCode; + private String passwordResetCode; + private String avatar; @Column(length = 1000) diff --git a/src/main/java/com/openisle/service/JwtService.java b/src/main/java/com/openisle/service/JwtService.java index bd68b665c..07d9df4c8 100644 --- a/src/main/java/com/openisle/service/JwtService.java +++ b/src/main/java/com/openisle/service/JwtService.java @@ -21,6 +21,9 @@ public class JwtService { @Value("${app.jwt.reason-secret}") private String reasonSecret; + @Value("${app.jwt.reset-secret}") + private String resetSecret; + @Value("${app.jwt.expiration}") private long expiration; @@ -56,6 +59,17 @@ public class JwtService { .compact(); } + public String generateResetToken(String subject) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKeyForSecret(resetSecret)) + .compact(); + } + public String validateAndGetSubject(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKeyForSecret(secret)) @@ -73,4 +87,13 @@ public class JwtService { .getBody(); return claims.getSubject(); } + + public String validateAndGetSubjectForReset(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(getSigningKeyForSecret(resetSecret)) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } } diff --git a/src/main/java/com/openisle/service/UserService.java b/src/main/java/com/openisle/service/UserService.java index 1f37fb8e7..05d2029e4 100644 --- a/src/main/java/com/openisle/service/UserService.java +++ b/src/main/java/com/openisle/service/UserService.java @@ -155,4 +155,32 @@ public class UserService { } return userRepository.save(user); } + + public String generatePasswordResetCode(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + String code = genCode(); + user.setPasswordResetCode(code); + userRepository.save(user); + return code; + } + + public boolean verifyPasswordResetCode(String email, String code) { + Optional userOpt = userRepository.findByEmail(email); + if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) { + User user = userOpt.get(); + user.setPasswordResetCode(null); + userRepository.save(user); + return true; + } + return false; + } + + public User updatePassword(String username, String newPassword) { + passwordValidator.validate(newPassword); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); + user.setPassword(passwordEncoder.encode(newPassword)); + return userRepository.save(user); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 706e9c44e..7d74a929c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,6 +9,7 @@ spring.jpa.hibernate.ddl-auto=update # for jwt app.jwt.secret=${JWT_SECRET:jwt_sec} app.jwt.reason-secret=${JWT_REASON_SECRET:jwt_reason_sec} +app.jwt.reset-secret=${JWT_RESET_SECRET:jwt_reset_sec} app.jwt.expiration=${JWT_EXPIRATION:86400000} # Password strength: LOW, MEDIUM or HIGH app.password.strength=${PASSWORD_STRENGTH:LOW}