From 4ea1d7ef73f904547625a2b337959781530e3c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E6=B3=BD=E5=86=9B?= <5654792+tcubic21@user.noreply.gitee.com> Date: Tue, 17 Jun 2025 18:23:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/TodoHeader.tsx | 4 +- client/src/main.tsx | 2 + client/src/pages/ForgotPassword.tsx | 7 +- client/src/pages/Login.tsx | 6 +- client/src/pages/Profile.tsx | 191 +++++++++++++++++++++++ client/src/pages/Register.tsx | 14 +- server/src/controllers/authController.ts | 135 ++++++++++++++-- server/src/controllers/todoController.ts | 67 ++++---- server/src/middleware/authMiddleware.ts | 22 +++ server/src/models/User.ts | 9 ++ server/src/routes/authRoutes.ts | 7 +- server/src/routes/todoRoutes.ts | 2 +- 12 files changed, 407 insertions(+), 59 deletions(-) create mode 100644 client/src/pages/Profile.tsx create mode 100644 server/src/middleware/authMiddleware.ts diff --git a/client/src/components/TodoHeader.tsx b/client/src/components/TodoHeader.tsx index fc1793b..6f91c00 100644 --- a/client/src/components/TodoHeader.tsx +++ b/client/src/components/TodoHeader.tsx @@ -55,12 +55,12 @@ const TodoHeader: React.FC = () => {
Todo List
- + navigate('/profile')}> {userName.charAt(0).toUpperCase()} {userName} - diff --git a/client/src/main.tsx b/client/src/main.tsx index 6432203..b576ef2 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -5,6 +5,7 @@ import App from './App.tsx'; import Login from './pages/Login'; import Register from './pages/Register'; import ForgotPassword from './pages/ForgotPassword'; +import Profile from './pages/Profile'; import 'antd/dist/reset.css'; import '@ant-design/v5-patch-for-react-19'; @@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( } /> } /> } /> + } /> } /> diff --git a/client/src/pages/ForgotPassword.tsx b/client/src/pages/ForgotPassword.tsx index 05d0bd8..8325ac1 100644 --- a/client/src/pages/ForgotPassword.tsx +++ b/client/src/pages/ForgotPassword.tsx @@ -19,11 +19,12 @@ function ForgotPassword() { try { // 模拟API调用,实际情况会调用后端接口发送重置邮件 console.log('Reset password request for:', values.email); - await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟 + // await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟 - setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。'); + // setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。'); // 实际情况下,这里可能会调用后端接口,例如: - // await api.post('/auth/forgot-password', { email: values.email }); + await api.post('/auth/forgot-password', { email: values.email }); + setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。'); } catch (err: any) { const errorMessage = err.response?.data?.message || '请求失败,请稍后再试。'; diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 22410e4..ce13bf3 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -20,7 +20,7 @@ function Login() { password: values.password, }); localStorage.setItem('token', response.data.token); - localStorage.setItem('username', values.username); + localStorage.setItem('username', response.data.username); navigate('/'); } catch (err: any) { const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。'; @@ -72,11 +72,11 @@ function Login() { > } - placeholder="账户" + placeholder="账户/邮箱" size="large" /> diff --git a/client/src/pages/Profile.tsx b/client/src/pages/Profile.tsx new file mode 100644 index 0000000..aabcdf6 --- /dev/null +++ b/client/src/pages/Profile.tsx @@ -0,0 +1,191 @@ +import React, { useState, useEffect } from 'react'; +import { Form, Input, Button, Card, Space, Typography, Avatar, notification, Tabs } from 'antd'; +import { UserOutlined, MailOutlined, LockOutlined, ArrowLeftOutlined } from '@ant-design/icons'; +import { api } from '../api/axios'; +import { useNavigate } from 'react-router-dom'; + +const { Title, Text } = Typography; + +interface UserProfile { + username: string; + email: string; +} + +const Profile: React.FC = () => { + const [loading, setLoading] = useState(false); + const [profile, setProfile] = useState(null); + const [form] = Form.useForm(); + const [passwordForm] = Form.useForm(); + const navigate = useNavigate(); + + useEffect(() => { + fetchUserProfile(); + }, []); + + const fetchUserProfile = async () => { + try { + const response = await api.get('/auth/profile'); + setProfile(response.data); + form.setFieldsValue(response.data); + } catch (error: any) { + notification.error({ + message: '获取用户信息失败', + description: error.response?.data?.message || '请稍后再试。', + }); + } + }; + + const onUpdateProfile = async (values: any) => { + setLoading(true); + try { + const response = await api.put('/auth/profile', { email: values.email }); + setProfile(response.data); + notification.success({ + message: response.data.message || '用户信息更新成功', + }); + fetchUserProfile(); + } catch (error: any) { + notification.error({ + message: '更新用户信息失败', + description: error.response?.data?.message || '请稍后再试。', + }); + } finally { + setLoading(false); + } + }; + + const onChangePassword = async (values: any) => { + setLoading(true); + try { + await api.put('/auth/change-password', { oldPassword: values.oldPassword, newPassword: values.newPassword }); + notification.success({ + message: '密码修改成功', + }); + passwordForm.resetFields(); + } catch (error: any) { + notification.error({ + message: '密码修改失败', + description: error.response?.data?.message || '请稍后再试。', + }); + } finally { + setLoading(false); + } + }; + + const tabItems = [ + { + key: '1', + label: '基本信息', + children: ( +
+ + } disabled /> + + + } disabled={!!(profile && profile.email)} /> + + + + +
+ ), + }, + { + key: '2', + label: '修改密码', + children: ( +
+ + } /> + + + } /> + + ({ + validator(_, value) { + if (!value || getFieldValue('newPassword') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致!')); + }, + }), + ]} + > + } /> + + + + +
+ ), + }, + ]; + + return ( +
+ +
+ ); +}; + +export default Profile; \ No newline at end of file diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx index 749e846..dc77c9f 100644 --- a/client/src/pages/Register.tsx +++ b/client/src/pages/Register.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd'; -import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; +import { UserOutlined, LockOutlined, CheckSquareOutlined, MailOutlined } from '@ant-design/icons'; import { api } from '../api/axios'; const { Title, Text, Link } = Typography; @@ -17,6 +17,7 @@ function Register() { try { await api.post('/auth/register', { username: values.username, + email: values.email, password: values.password, }); navigate('/login'); @@ -79,6 +80,17 @@ function Register() { /> + + } + placeholder="邮箱" + size="large" + /> + + { - const { username, password } = req.body; +export const register = async (req: Request, res: Response): Promise => { + const { username, email, password } = req.body; try { - const existingUser = await User.findOne({ username }); + const existingUser = await User.findOne({ $or: [{ username }, { email }] }); if (existingUser) { - return res.status(400).json({ message: '用户名已存在' }); + res.status(400).json({ message: '用户名或邮箱已存在' }); + return; } const hashedPassword = await bcrypt.hash(password, 10); - const user = new User({ username, password: hashedPassword }); + const user = new User({ username, email, password: hashedPassword }); await user.save(); res.status(201).json({ message: '注册成功' }); } catch (error) { @@ -23,20 +30,130 @@ export const register = async (req: Request, res: Response) => { }; // 用户登录 -export const login = async (req: Request, res: Response) => { +export const login = async (req: Request, res: Response): Promise => { const { username, password } = req.body; try { - const user = await User.findOne({ username }); + const user = await User.findOne({ $or: [{ username }, { email: username }] }); + if (!user) { - return res.status(400).json({ message: '用户不存在' }); + res.status(400).json({ message: '用户不存在' }); + return; } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { - return res.status(400).json({ message: '密码错误' }); + res.status(400).json({ message: '密码错误' }); + return; } const token = jwt.sign({ userId: user._id, username: user.username }, JWT_SECRET, { expiresIn: '7d' }); res.status(200).json({ token, username: user.username }); } catch (error) { res.status(500).json({ message: '登录失败', error }); } +}; + +// 用户忘记密码 +export const forgotPassword = async (req: Request, res: Response): Promise => { + const { email } = req.body; + + try { + const user = await User.findOne({ username: email }); + + if (!user) { + res.status(404).json({ message: '用户不存在' }); + return; + } + + const resetToken = crypto.randomBytes(32).toString('hex'); + user.resetPasswordToken = resetToken; + user.resetPasswordExpires = new Date(Date.now() + 3600000); // 1 小时后过期 + await user.save(); + + res.status(200).json({ message: '密码重置链接已发送到您的邮箱' }); + } catch (error) { + console.error('Forgot password error:', error); + res.status(500).json({ message: '重置密码失败' }); + } +}; + +// 获取用户个人信息 +export const getUserProfile = async (req: Request & { user?: any }, res: Response): Promise => { + try { + const user = await User.findById(req.user.userId).select('-password'); // 不返回密码 + if (!user) { + res.status(404).json({ message: '用户未找到' }); + return; + } + res.status(200).json({ username: user.username, email: user.email }); + } catch (error) { + console.error('Get user profile error:', error); + res.status(500).json({ message: '获取用户信息失败' }); + } +}; + +// 修改用户密码 +export const changePassword = async (req: Request & { user?: any }, res: Response): Promise => { + const { oldPassword, newPassword } = req.body; + + try { + const user = await User.findById(req.user.userId); + if (!user) { + res.status(404).json({ message: '用户未找到' }); + return; + } + + const isMatch = await bcrypt.compare(oldPassword, user.password); + if (!isMatch) { + res.status(400).json({ message: '旧密码不正确' }); + return; + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + user.password = hashedPassword; + user.resetPasswordToken = undefined; // 清除重置令牌 + user.resetPasswordExpires = undefined; // 清除重置令牌过期时间 + await user.save(); + + res.status(200).json({ message: '密码修改成功' }); + } catch (error) { + console.error('Change password error:', error); + res.status(500).json({ message: '密码修改失败' }); + } +}; + +// 更新用户个人信息(包括邮箱绑定/修改) +export const updateUserProfile = async (req: Request & { user?: any }, res: Response): Promise => { + const { email } = req.body; + + try { + const user = await User.findById(req.user.userId); + if (!user) { + res.status(404).json({ message: '用户未找到' }); + return; + } + + // 如果用户尝试绑定邮箱 + if (email && !user.email) { + const existingEmailUser = await User.findOne({ email }); + if (existingEmailUser) { + res.status(400).json({ message: '该邮箱已被其他用户绑定' }); + return; + } + user.email = email; + await user.save(); + res.status(200).json({ message: '邮箱绑定成功', username: user.username, email: user.email }); + } else if (email && user.email && user.email !== email) { + // 如果用户已绑定邮箱并尝试修改为不同邮箱 + res.status(400).json({ message: '邮箱已绑定,不支持修改。如需修改,请联系管理员。' }); + return; + } else if (user.email === email) { + // 如果用户已绑定邮箱并尝试修改为相同的邮箱,视为无操作 + res.status(200).json({ message: '邮箱未更改', username: user.username, email: user.email }); + return; + } + + res.status(200).json({ message: '用户信息更新成功', username: user.username, email: user.email }); + } catch (error) { + console.error('Update user profile error:', error); + res.status(500).json({ message: '更新用户信息失败' }); + } }; \ No newline at end of file diff --git a/server/src/controllers/todoController.ts b/server/src/controllers/todoController.ts index 93f96ba..70ad676 100644 --- a/server/src/controllers/todoController.ts +++ b/server/src/controllers/todoController.ts @@ -1,40 +1,23 @@ import { Request, Response, NextFunction } from 'express'; import Todo, { ITodo } from '../models/Todo'; -import jwt from 'jsonwebtoken'; import mongoose from 'mongoose'; // 导入 mongoose +import { authMiddleware } from '../middleware/authMiddleware'; // Import authMiddleware const JWT_SECRET = process.env.JWT_SECRET || 'secret_key'; -// 鉴权中间件,解析 token 并注入 req.user -export const authMiddleware = (req: any, res: Response, next: NextFunction): void => { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - res.status(401).json({ message: '未授权' }); - return; - } - const token = authHeader.split(' ')[1]; - try { - const decoded = jwt.verify(token, JWT_SECRET) as any; - req.user = decoded; - next(); - } catch (err) { - res.status(401).json({ message: '无效的 token' }); - } -}; - // 获取所有待办事项(只返回当前用户的) -export const getAllTodos = async (req: Request & { user?: any }, res: Response): Promise => { +export const getAllTodos = async (req: Request & { user?: any }, res: Response): Promise => { try { const todos = await Todo.find({ user: req.user.userId }).sort({ createdAt: -1 }); - return res.status(200).json(todos); + res.status(200).json(todos); } catch (error) { console.error("Error in getAllTodos:", error); - return res.status(500).json({ message: '获取待办事项失败', error }); + res.status(500).json({ message: '获取待办事项失败', error }); } }; // 创建新的待办事项(自动绑定 user) -export const createTodo = async (req: Request & { user?: any }, res: Response): Promise => { +export const createTodo = async (req: Request & { user?: any }, res: Response): Promise => { try { const { title, description = '', priority = 'medium' } = req.body; const todo = new Todo({ @@ -44,24 +27,26 @@ export const createTodo = async (req: Request & { user?: any }, res: Response): user: req.user.userId }); const savedTodo = await todo.save(); - return res.status(201).json(savedTodo); + res.status(201).json(savedTodo); } catch (error) { console.error("Error in createTodo:", error); - return res.status(400).json({ message: '创建待办事项失败', error }); + res.status(400).json({ message: '创建待办事项失败', error }); } }; // 更新待办事项 -export const updateTodo = async (req: Request & { user?: any }, res: Response): Promise => { +export const updateTodo = async (req: Request & { user?: any }, res: Response): Promise => { try { const { id } = req.params; // 使用 id 作为参数名,便于匹配 if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ message: '无效的待办事项 ID' }); + res.status(400).json({ message: '无效的待办事项 ID' }); + return; } const todo = await Todo.findOne({ _id: id, user: req.user.userId }); if (!todo) { - return res.status(404).json({ message: '待办事项不存在或无权访问' }); + res.status(404).json({ message: '待办事项不存在或无权访问' }); + return; } const updatedTodo = await Todo.findByIdAndUpdate( @@ -70,53 +55,57 @@ export const updateTodo = async (req: Request & { user?: any }, res: Response): { new: true, runValidators: true } ); - return res.status(200).json(updatedTodo); + res.status(200).json(updatedTodo); } catch (error) { console.error("Error in updateTodo:", error); - return res.status(400).json({ message: '更新待办事项失败', error }); + res.status(400).json({ message: '更新待办事项失败', error }); } }; // 切换待办事项完成状态 -export const toggleTodoStatus = async (req: Request & { user?: any }, res: Response): Promise => { +export const toggleTodoStatus = async (req: Request & { user?: any }, res: Response): Promise => { try { const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ message: '无效的待办事项 ID' }); + res.status(400).json({ message: '无效的待办事项 ID' }); + return; } const todo = await Todo.findOne({ _id: id, user: req.user.userId }); if (!todo) { - return res.status(404).json({ message: '待办事项不存在或无权访问' }); + res.status(404).json({ message: '待办事项不存在或无权访问' }); + return; } todo.completed = !todo.completed; // 切换完成状态 const updatedTodo = await todo.save(); - return res.status(200).json(updatedTodo); + res.status(200).json(updatedTodo); } catch (error) { console.error("Error in toggleTodoStatus:", error); - return res.status(400).json({ message: '切换状态失败', error }); + res.status(400).json({ message: '切换状态失败', error }); } }; // 删除待办事项 -export const deleteTodo = async (req: Request & { user?: any }, res: Response): Promise => { +export const deleteTodo = async (req: Request & { user?: any }, res: Response): Promise => { try { const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ message: '无效的待办事项 ID' }); + res.status(400).json({ message: '无效的待办事项 ID' }); + return; } const todo = await Todo.findOne({ _id: id, user: req.user.userId }); if (!todo) { - return res.status(404).json({ message: '待办事项不存在或无权访问' }); + res.status(404).json({ message: '待办事项不存在或无权访问' }); + return; } await Todo.findByIdAndDelete(id); - return res.status(200).json({ message: '待办事项已删除' }); + res.status(200).json({ message: '待办事项已删除' }); } catch (error) { console.error("Error in deleteTodo:", error); - return res.status(400).json({ message: '删除待办事项失败', error }); + res.status(400).json({ message: '删除待办事项失败', error }); } }; \ No newline at end of file diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..14bf3e5 --- /dev/null +++ b/server/src/middleware/authMiddleware.ts @@ -0,0 +1,22 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'secret_key'; + +const authMiddleware = (req: any, res: Response, next: NextFunction): void => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ message: '未授权' }); + return; + } + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + req.user = decoded; + next(); + } catch (err) { + res.status(401).json({ message: '无效的 token' }); + } +}; + +export default authMiddleware; \ No newline at end of file diff --git a/server/src/models/User.ts b/server/src/models/User.ts index 1e4abb8..0c2ca8d 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -2,12 +2,21 @@ import mongoose, { Schema, Document } from 'mongoose'; export interface IUser extends Document { username: string; + email: string; password: string; + resetPasswordToken?: string; + resetPasswordExpires?: Date; } const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, + email: { type: String, required: true, unique: true }, password: { type: String, required: true } }); +UserSchema.add({ + resetPasswordToken: String, + resetPasswordExpires: Date, +}); + export default mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts index 47bcf62..0c0da17 100644 --- a/server/src/routes/authRoutes.ts +++ b/server/src/routes/authRoutes.ts @@ -1,9 +1,14 @@ import { Router } from 'express'; -import { register, login } from '../controllers/authController'; +import { register, login, forgotPassword, getUserProfile, changePassword, updateUserProfile } from '../controllers/authController'; +import authMiddleware from '../middleware/authMiddleware'; const router = Router(); router.post('/register', register); router.post('/login', login); +router.post('/forgot-password', forgotPassword); +router.get('/profile', authMiddleware, getUserProfile); +router.put('/profile', authMiddleware, updateUserProfile); +router.put('/change-password', authMiddleware, changePassword); export default router; \ No newline at end of file diff --git a/server/src/routes/todoRoutes.ts b/server/src/routes/todoRoutes.ts index 6d333f7..465f0e7 100644 --- a/server/src/routes/todoRoutes.ts +++ b/server/src/routes/todoRoutes.ts @@ -5,8 +5,8 @@ import { updateTodo, deleteTodo, toggleTodoStatus, - authMiddleware } from '../controllers/todoController'; +import authMiddleware from '../middleware/authMiddleware'; const router = Router();