diff --git a/api/server.go b/api/server.go
index 58b8211f..c1767e6f 100644
--- a/api/server.go
+++ b/api/server.go
@@ -79,6 +79,7 @@ func (s *Server) setupRoutes() {
api.POST("/login", s.handleLogin)
api.POST("/verify-otp", s.handleVerifyOTP)
api.POST("/complete-registration", s.handleCompleteRegistration)
+ api.POST("/reset-password", s.handleResetPassword)
// 系统支持的模型和交易所(无需认证)
api.GET("/supported-models", s.handleGetSupportedModels)
@@ -1728,6 +1729,50 @@ func (s *Server) handleVerifyOTP(c *gin.Context) {
})
}
+// handleResetPassword 重置密码(通过邮箱 + OTP 验证)
+func (s *Server) handleResetPassword(c *gin.Context) {
+ var req struct {
+ Email string `json:"email" binding:"required,email"`
+ NewPassword string `json:"new_password" binding:"required,min=6"`
+ OTPCode string `json:"otp_code" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 查询用户
+ user, err := s.database.GetUserByEmail(req.Email)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "邮箱不存在"})
+ return
+ }
+
+ // 验证 OTP
+ if !auth.VerifyOTP(user.OTPSecret, req.OTPCode) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Google Authenticator 验证码错误"})
+ return
+ }
+
+ // 生成新密码哈希
+ newPasswordHash, err := auth.HashPassword(req.NewPassword)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
+ return
+ }
+
+ // 更新密码
+ err = s.database.UpdateUserPassword(user.ID, newPasswordHash)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"})
+ return
+ }
+
+ log.Printf("✓ 用户 %s 密码已重置", user.Email)
+ c.JSON(http.StatusOK, gin.H{"message": "密码重置成功,请使用新密码登录"})
+}
+
// initUserDefaultConfigs 为新用户初始化默认的模型和交易所配置
func (s *Server) initUserDefaultConfigs(userID string) error {
// 注释掉自动创建默认配置,让用户手动添加
diff --git a/config/database.go b/config/database.go
index c3aa171d..a2fd5732 100644
--- a/config/database.go
+++ b/config/database.go
@@ -546,6 +546,16 @@ func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
return err
}
+// UpdateUserPassword 更新用户密码
+func (d *Database) UpdateUserPassword(userID, passwordHash string) error {
+ _, err := d.db.Exec(`
+ UPDATE users
+ SET password_hash = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `, passwordHash, userID)
+ return err
+}
+
// GetAIModels 获取用户的AI模型配置
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
rows, err := d.db.Query(`
diff --git a/web/src/App.tsx b/web/src/App.tsx
index a7e6d82b..147eca2f 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -5,6 +5,7 @@ import { EquityChart } from './components/EquityChart'
import { AITradersPage } from './components/AITradersPage'
import { LoginPage } from './components/LoginPage'
import { RegisterPage } from './components/RegisterPage'
+import { ResetPasswordPage } from './components/ResetPasswordPage'
import { CompetitionPage } from './components/CompetitionPage'
import { LandingPage } from './pages/LandingPage'
import HeaderBar from './components/landing/HeaderBar'
@@ -230,6 +231,9 @@ function App() {
if (route === '/register') {
return
}
+ if (route === '/reset-password') {
+ return
+ }
if (route === '/competition') {
return (
+
+
+
{error && (
diff --git a/web/src/components/ResetPasswordPage.tsx b/web/src/components/ResetPasswordPage.tsx
new file mode 100644
index 00000000..4bf233d7
--- /dev/null
+++ b/web/src/components/ResetPasswordPage.tsx
@@ -0,0 +1,204 @@
+import React, { useState } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { useLanguage } from '../contexts/LanguageContext';
+import { t } from '../i18n/translations';
+import { Header } from './Header';
+import { ArrowLeft, KeyRound, Eye, EyeOff } from 'lucide-react';
+
+export function ResetPasswordPage() {
+ const { language } = useLanguage();
+ const { resetPassword } = useAuth();
+ const [email, setEmail] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [otpCode, setOtpCode] = useState('');
+ const [error, setError] = useState('');
+ const [success, setSuccess] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+
+ const handleResetPassword = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setSuccess(false);
+
+ // 验证两次密码是否一致
+ if (newPassword !== confirmPassword) {
+ setError(t('passwordMismatch', language));
+ return;
+ }
+
+ setLoading(true);
+
+ const result = await resetPassword(email, newPassword, otpCode);
+
+ if (result.success) {
+ setSuccess(true);
+ // 3秒后跳转到登录页面
+ setTimeout(() => {
+ window.history.pushState({}, '', '/login');
+ window.dispatchEvent(new PopStateEvent('popstate'));
+ }, 3000);
+ } else {
+ setError(result.message || t('resetPasswordFailed', language));
+ }
+
+ setLoading(false);
+ };
+
+ return (
+
+
+
+
+
+ {/* Back to Login */}
+
+
+ {/* Logo */}
+
+
+
+
+
+ {t('resetPasswordTitle', language)}
+
+
+ 使用邮箱和 Google Authenticator 重置密码
+
+
+
+ {/* Reset Password Form */}
+
+ {success ? (
+
+
✅
+
+ {t('resetPasswordSuccess', language)}
+
+
+ 3秒后将自动跳转到登录页面...
+
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx
index 96f0dc72..5e87d331 100644
--- a/web/src/contexts/AuthContext.tsx
+++ b/web/src/contexts/AuthContext.tsx
@@ -37,6 +37,11 @@ interface AuthContextType {
userID: string,
otpCode: string
) => Promise<{ success: boolean; message?: string }>
+ resetPassword: (
+ email: string,
+ newPassword: string,
+ otpCode: string
+ ) => Promise<{ success: boolean; message?: string }>
logout: () => void
isLoading: boolean
}
@@ -220,6 +225,36 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}
+ const resetPassword = async (
+ email: string,
+ newPassword: string,
+ otpCode: string
+ ) => {
+ try {
+ const response = await fetch('/api/reset-password', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email,
+ new_password: newPassword,
+ otp_code: otpCode,
+ }),
+ })
+
+ const data = await response.json()
+
+ if (response.ok) {
+ return { success: true, message: data.message }
+ } else {
+ return { success: false, message: data.error }
+ }
+ } catch (error) {
+ return { success: false, message: '密码重置失败,请重试' }
+ }
+ }
+
const logout = () => {
setUser(null)
setToken(null)
@@ -236,6 +271,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
register,
verifyOTP,
completeRegistration,
+ resetPassword,
logout,
isLoading,
}}
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 6916544a..233adff4 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -336,6 +336,14 @@ export const translations = {
forgotPassword: 'Forgot password?',
rememberMe: 'Remember me',
otpCode: 'OTP Code',
+ resetPassword: 'Reset Password',
+ resetPasswordTitle: 'Reset your password',
+ newPassword: 'New Password',
+ newPasswordPlaceholder: 'Enter new password (at least 6 characters)',
+ resetPasswordButton: 'Reset Password',
+ resetPasswordSuccess: 'Password reset successful! Please login with your new password',
+ resetPasswordFailed: 'Password reset failed',
+ backToLogin: 'Back to Login',
scanQRCode: 'Scan QR Code',
enterOTPCode: 'Enter 6-digit OTP code',
verifyOTP: 'Verify OTP',
@@ -811,6 +819,14 @@ export const translations = {
loginNow: '立即登录',
forgotPassword: '忘记密码?',
rememberMe: '记住我',
+ resetPassword: '重置密码',
+ resetPasswordTitle: '重置您的密码',
+ newPassword: '新密码',
+ newPasswordPlaceholder: '请输入新密码(至少6位)',
+ resetPasswordButton: '重置密码',
+ resetPasswordSuccess: '密码重置成功!请使用新密码登录',
+ resetPasswordFailed: '密码重置失败',
+ backToLogin: '返回登录',
otpCode: 'OTP验证码',
scanQRCode: '扫描二维码',
enterOTPCode: '输入6位OTP验证码',
diff --git a/web/tsconfig.json b/web/tsconfig.json
index a7fc6fbf..6d9748fa 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -21,5 +21,6 @@
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
+ "exclude": ["src/**/*.test.tsx", "src/**/*.test.ts", "src/test"],
"references": [{ "path": "./tsconfig.node.json" }]
}