diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index b428a86..3228568 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -68,6 +68,13 @@ export default function LoginPage() { {/* 使用统一的认证表单组件 */} + + {/* 忘记密码链接 */} +
+ + 忘记密码? + +
{/* 分割线 */}
diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx new file mode 100644 index 0000000..deb00e1 --- /dev/null +++ b/src/app/forgot-password/page.tsx @@ -0,0 +1,96 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import ForgotPasswordForm from '@/components/auth/ForgotPasswordForm'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; + +const ForgotPasswordPage: React.FC = () => { + return ( +
+ {/* 背景装饰元素 - 渐变模糊效果 */} +
+
+
+
+
+ + {/* 主要内容区域 */} +
+
+ {/* 装饰性头部 */} +
+
+
+
+ Logo +
+
+
+

+ 找回您的 + HITWH + 社区账户 +

+

+ 按照步骤重置您的密码,继续您的Minecraft皮肤创作之旅 +

+
+ + {/* 找回密码卡片 */} +
+ {/* 卡片阴影装饰 */} +
+
+ + +
+ + + + 🔑 密码重置 + +
+
+ + {/* 使用统一的忘记密码表单组件 */} + + +
+
+ + {/* 底部装饰 */} +
+
+ 🔐 安全验证 + + 📧 邮箱验证 + + ⚡ 快速重置 + + 🔄 重新开始 +
+
+
+
+ + {/* 浮动装饰元素 */} +
+
🔓
+
+
+
📧
+
+
+ ); +}; + +export default ForgotPasswordPage; \ No newline at end of file diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 0000000..d07eeb5 --- /dev/null +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,301 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import { Input } from '@/components/ui/input'; +import { getVerificationCode, verifyCode as verifyCodeApi, resetPassword } from '../../lib/api/actions'; +import { Button } from '@/components/ui/button'; +import { Label } from '@radix-ui/react-label'; + +interface ForgotPasswordFormProps { + onBack?: () => void; +} + +const ForgotPasswordForm: React.FC = ({ onBack }) => { + // 表单状态 + const [step, setStep] = useState<1 | 2>(1); + const [email, setEmail] = useState(''); + const [username, setUsername] = useState(''); + const [verificationCode, setVerificationCode] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + // UI状态 + const [isLoading, setIsLoading] = useState(false); + const [isSendingCode, setIsSendingCode] = useState(false); + const [error, setError] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [countdown, setCountdown] = useState(0); + + // 处理验证码倒计时 + useEffect(() => { + let timer: NodeJS.Timeout; + if (countdown > 0) { + timer = setTimeout(() => setCountdown(countdown - 1), 1000); + } + return () => clearTimeout(timer); + }, [countdown]); + + // 处理获取验证码 + const handleSendCode = async () => { + if (!email?.trim()) { + setError('请输入邮箱'); + return; + } + if (!username?.trim()) { + setError('请输入用户名'); + return; + } + + // 邮箱格式检查 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setError('请输入有效的邮箱地址'); + return; + } + + setIsSendingCode(true); + setError(''); + + try { + const result = await getVerificationCode(email); + if (result.success) { + setSuccessMessage('验证码已发送到您的邮箱'); + setCountdown(60); // 60秒倒计时 + // 清除成功消息 + setTimeout(() => setSuccessMessage(''), 3000); + } else { + setError(result.error || '发送验证码失败'); + } + } catch (err) { + setError('发送验证码失败,请稍后再试'); + } finally { + setIsSendingCode(false); + } + }; + + // 处理验证验证码 + const handleVerifyCode = async () => { + if (!email?.trim() || !username?.trim() || !verificationCode?.trim()) { + setError('请填写所有必填项'); + return; + } + + setIsLoading(true); + setError(''); + + try { + const result = await verifyCodeApi(email, verificationCode); + if (result.success) { + // 验证成功,进入第二步 + setStep(2); + setError(''); + } else { + setError(result.error || '验证码错误'); + } + } catch (err) { + setError('验证失败,请稍后再试'); + } finally { + setIsLoading(false); + } + }; + + // 处理重置密码 + const handleResetPassword = async () => { + if (!newPassword?.trim() || !confirmPassword?.trim()) { + setError('请填写新密码和确认密码'); + return; + } + + if (newPassword !== confirmPassword) { + setError('两次输入的密码不一致'); + return; + } + + if (newPassword.length < 6) { + setError('密码长度至少为6位'); + return; + } + + setIsLoading(true); + setError(''); + + try { + const result = await resetPassword(email, username, newPassword, verificationCode); + if (result.success) { + setSuccessMessage('密码重置成功,即将跳转到登录页面...'); + // 3秒后跳转到登录页面 + setTimeout(() => { + window.location.href = '/login'; + }, 3000); + } else { + setError(result.error || '重置密码失败'); + } + } catch (err) { + setError('重置密码失败,请稍后再试'); + } finally { + setIsLoading(false); + } + }; + + // 返回上一步 + const handleBack = () => { + if (step === 2) { + setStep(1); + setError(''); + } else if (onBack) { + onBack(); + } else { + window.history.back(); + } + }; + + // 处理表单提交 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (step === 1) { + handleVerifyCode(); + } else { + handleResetPassword(); + } + }; + + return ( +
+

忘记密码

+ + {error && ( +
+ {error} +
+ )} + + {successMessage && ( +
+ {successMessage} +
+ )} + +
+ {step === 1 ? ( + <> + {/* 邮箱输入 */} +
+ + setEmail(e.target.value)} + placeholder="请输入您的邮箱" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={isLoading} + /> +
+ + {/* 用户名输入 */} +
+ + setUsername(e.target.value)} + placeholder="请输入您的用户名" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={isLoading} + /> +
+ + {/* 验证码输入 */} +
+ +
+ setVerificationCode(e.target.value)} + placeholder="请输入验证码" + className="flex-1" + /> + +
+
+ + ) : ( + <> + {/* 新密码输入 */} +
+ + setNewPassword(e.target.value)} + placeholder="请输入新密码" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={isLoading} + /> +

密码长度至少为6位

+
+ + {/* 确认新密码输入 */} +
+ + setConfirmPassword(e.target.value)} + placeholder="请再次输入新密码" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" + disabled={isLoading} + /> +
+ + )} + + {/* 按钮区域 */} +
+
+ + + +
+
+
+
+ ); +}; + +export default ForgotPasswordForm; \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a2df8dc..3e5d7a9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 disabled:bg-gray-300 disabled:text-gray-500 dark:disabled:bg-gray-700 dark:disabled:text-gray-400 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { diff --git a/src/lib/api/actions.ts b/src/lib/api/actions.ts index edb3873..8d94637 100644 --- a/src/lib/api/actions.ts +++ b/src/lib/api/actions.ts @@ -188,6 +188,14 @@ export const getVerificationCode = async (email: string) => { // 导出验证验证码函数 export const verifyCode = async (email: string, code: string) => { + // 对于测试环境,可以直接验证测试账号的验证码 + const TEST_EMAIL = process.env.TEST_EMAIL || 'test@test.com'; + const TEST_VERIFICATION_CODE = process.env.TEST_VERIFICATION_CODE || '123456'; + + // 测试环境下的特殊处理 + if (process.env.NODE_ENV !== 'production' && email === TEST_EMAIL && code === TEST_VERIFICATION_CODE) { + return { success: true }; + } try { // 验证输入 if (!email?.trim()) { @@ -214,6 +222,61 @@ export const verifyCode = async (email: string, code: string) => { } }; +// 导出重置密码函数 +export const resetPassword = async (email: string, username: string, newPassword: string, verificationCode: string) => { + try { + // 基本验证 + if (!email?.trim()) { + return { success: false, error: '邮箱不能为空' }; + } + if (!username?.trim()) { + return { success: false, error: '用户名不能为空' }; + } + if (!newPassword?.trim()) { + return { success: false, error: '新密码不能为空' }; + } + if (!verificationCode?.trim()) { + return { success: false, error: '验证码不能为空' }; + } + + // 邮箱格式检查 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { success: false, error: '请输入有效的邮箱地址' }; + } + + // 密码强度检查 + if (newPassword.length < 6) { + return { success: false, error: '密码长度至少为6位' }; + } + + // 验证码验证 + const verifyResult = await verifyCode(email, verificationCode); + if (!verifyResult.success) { + return verifyResult; + } + + // 调用API重置密码 + const response = await apiClient.post('/auth/reset-password', { + email, + username, + newPassword + }); + return { success: true, ...response.data }; + } catch (error) { + console.error('重置密码失败:', error); + if (error instanceof axios.AxiosError) { + if (error.response?.status === 400) { + return { success: false, error: error.response.data?.error || '重置密码失败,请检查信息' }; + } + if (error.request) { + return { success: false, error: '网络连接失败,请检查您的网络设置' }; + } + } + return { success: false, error: '重置密码失败,请稍后再试' }; + } +}; + // 导出注册函数 export const register = async (userData: { username: string;