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 @@
+
+
+
+
找回密码
+
+
+
{{ emailError }}
+
发送验证码
+
发送中...
+
+
+
+
+
{{ passwordError }}
+
重置密码
+
提交中...
+
+
+
+
+
+
+
+
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}