完善
This commit is contained in:
parent
e935bd3987
commit
4ea1d7ef73
@ -55,12 +55,12 @@ const TodoHeader: React.FC<TodoHeaderProps> = () => {
|
|||||||
<div style={{ padding: '0' }}>
|
<div style={{ padding: '0' }}>
|
||||||
<Text strong style={{ fontSize: '24px' }}>Todo List</Text>
|
<Text strong style={{ fontSize: '24px' }}>Todo List</Text>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space style={{ cursor: 'pointer' }} onClick={() => navigate('/profile')}>
|
||||||
<Avatar style={{ backgroundColor: avatarBgColor, verticalAlign: 'middle' }} size="large">
|
<Avatar style={{ backgroundColor: avatarBgColor, verticalAlign: 'middle' }} size="large">
|
||||||
{userName.charAt(0).toUpperCase()}
|
{userName.charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Text>{userName}</Text>
|
<Text>{userName}</Text>
|
||||||
<Button type="text" danger icon={<LogoutOutlined />} onClick={showConfirmLogout}>
|
<Button type="text" danger icon={<LogoutOutlined />} onClick={(e) => { e.stopPropagation(); showConfirmLogout(); }}>
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import App from './App.tsx';
|
|||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Register from './pages/Register';
|
import Register from './pages/Register';
|
||||||
import ForgotPassword from './pages/ForgotPassword';
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
|
import Profile from './pages/Profile';
|
||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
import '@ant-design/v5-patch-for-react-19';
|
import '@ant-design/v5-patch-for-react-19';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/*" element={<App />} />
|
<Route path="/*" element={<App />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@ -19,11 +19,12 @@ function ForgotPassword() {
|
|||||||
try {
|
try {
|
||||||
// 模拟API调用,实际情况会调用后端接口发送重置邮件
|
// 模拟API调用,实际情况会调用后端接口发送重置邮件
|
||||||
console.log('Reset password request for:', values.email);
|
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) {
|
} catch (err: any) {
|
||||||
const errorMessage = err.response?.data?.message || '请求失败,请稍后再试。';
|
const errorMessage = err.response?.data?.message || '请求失败,请稍后再试。';
|
||||||
|
|||||||
@ -20,7 +20,7 @@ function Login() {
|
|||||||
password: values.password,
|
password: values.password,
|
||||||
});
|
});
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem('token', response.data.token);
|
||||||
localStorage.setItem('username', values.username);
|
localStorage.setItem('username', response.data.username);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。';
|
const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。';
|
||||||
@ -72,11 +72,11 @@ function Login() {
|
|||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="username"
|
name="username"
|
||||||
rules={[{ required: true, message: '请输入用户名!' }]}
|
rules={[{ required: true, message: '请输入用户名/邮箱!' }]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||||
placeholder="账户"
|
placeholder="账户/邮箱"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
191
client/src/pages/Profile.tsx
Normal file
191
client/src/pages/Profile.tsx
Normal file
@ -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<UserProfile | null>(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: (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="profile_info"
|
||||||
|
onFinish={onUpdateProfile}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Form.Item label="用户名" name="username">
|
||||||
|
<Input prefix={<UserOutlined />} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="邮箱" name="email" rules={profile && profile.email ? [] : [{ required: true, message: '请输入您的邮箱!' }, { type: 'email', message: '请输入有效的邮箱地址!' }]}>
|
||||||
|
<Input prefix={<MailOutlined />} disabled={!!(profile && profile.email)} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" className="w-full" size="large" loading={loading}>
|
||||||
|
保存信息
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
label: '修改密码',
|
||||||
|
children: (
|
||||||
|
<Form
|
||||||
|
form={passwordForm}
|
||||||
|
name="change_password"
|
||||||
|
onFinish={onChangePassword}
|
||||||
|
autoComplete="off"
|
||||||
|
layout="vertical"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="oldPassword"
|
||||||
|
label="旧密码"
|
||||||
|
rules={[{ required: true, message: '请输入您的旧密码!' }]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
rules={[{ required: true, message: '请输入您的新密码!' }, { min: 6, message: '密码至少需要6位字符!' }]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="confirmNewPassword"
|
||||||
|
label="确认新密码"
|
||||||
|
dependencies={['newPassword']}
|
||||||
|
hasFeedback
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请确认您的新密码!' },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('newPassword') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致!'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password prefix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" className="w-full" size="large" loading={loading}>
|
||||||
|
修改密码
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen bg-gray-100" style={{ background: '#f0f2f5', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Card
|
||||||
|
className="w-full max-w-lg"
|
||||||
|
variant="borderless"
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '24px 24px 24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '600px',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined style={{ fontSize: '20px' }} />}
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
style={{ position: 'absolute', top: 16, left: 16, zIndex: 1 }}
|
||||||
|
/>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<Avatar size={64} icon={<UserOutlined />} style={{ backgroundColor: '#87d068', marginBottom: 16 }} />
|
||||||
|
<Title level={2} style={{ margin: '8px 0 0' }}>个人信息</Title>
|
||||||
|
<Text type="secondary">管理您的账户设置</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultActiveKey="1" items={tabItems} centered style={{ width: '100%' }} />
|
||||||
|
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd';
|
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';
|
import { api } from '../api/axios';
|
||||||
|
|
||||||
const { Title, Text, Link } = Typography;
|
const { Title, Text, Link } = Typography;
|
||||||
@ -17,6 +17,7 @@ function Register() {
|
|||||||
try {
|
try {
|
||||||
await api.post('/auth/register', {
|
await api.post('/auth/register', {
|
||||||
username: values.username,
|
username: values.username,
|
||||||
|
email: values.email,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
});
|
});
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
@ -79,6 +80,17 @@ function Register() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[{ required: true, message: '请输入邮箱!' }, { type: 'email', message: '请输入有效的邮箱地址!' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MailOutlined className="site-form-item-icon" />}
|
||||||
|
placeholder="邮箱"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true, message: '请输入密码!' }]}
|
rules={[{ required: true, message: '请输入密码!' }]}
|
||||||
|
|||||||
@ -2,19 +2,26 @@ import { Request, Response } from 'express';
|
|||||||
import User, { IUser } from '../models/User';
|
import User, { IUser } from '../models/User';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import authMiddleware from '../middleware/authMiddleware';
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';
|
||||||
|
const NODE_ENV = process.env.NODE_ENV || 'development';
|
||||||
|
const EMAIL_USER = process.env.EMAIL_USER;
|
||||||
|
const EMAIL_PASS = process.env.EMAIL_PASS;
|
||||||
|
|
||||||
// 用户注册
|
// 用户注册
|
||||||
export const register = async (req: Request, res: Response) => {
|
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||||
const { username, password } = req.body;
|
const { username, email, password } = req.body;
|
||||||
try {
|
try {
|
||||||
const existingUser = await User.findOne({ username });
|
const existingUser = await User.findOne({ $or: [{ username }, { email }] });
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({ message: '用户名已存在' });
|
res.status(400).json({ message: '用户名或邮箱已存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
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();
|
await user.save();
|
||||||
res.status(201).json({ message: '注册成功' });
|
res.status(201).json({ message: '注册成功' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -23,16 +30,19 @@ 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<void> => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
try {
|
try {
|
||||||
const user = await User.findOne({ username });
|
const user = await User.findOne({ $or: [{ username }, { email: username }] });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).json({ message: '用户不存在' });
|
res.status(400).json({ message: '用户不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const isMatch = await bcrypt.compare(password, user.password);
|
const isMatch = await bcrypt.compare(password, user.password);
|
||||||
if (!isMatch) {
|
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' });
|
const token = jwt.sign({ userId: user._id, username: user.username }, JWT_SECRET, { expiresIn: '7d' });
|
||||||
res.status(200).json({ token, username: user.username });
|
res.status(200).json({ token, username: user.username });
|
||||||
@ -40,3 +50,110 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ message: '登录失败', error });
|
res.status(500).json({ message: '登录失败', error });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 用户忘记密码
|
||||||
|
export const forgotPassword = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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: '更新用户信息失败' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,40 +1,23 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import Todo, { ITodo } from '../models/Todo';
|
import Todo, { ITodo } from '../models/Todo';
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import mongoose from 'mongoose'; // 导入 mongoose
|
import mongoose from 'mongoose'; // 导入 mongoose
|
||||||
|
import { authMiddleware } from '../middleware/authMiddleware'; // Import authMiddleware
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';
|
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<Response> => {
|
export const getAllTodos = async (req: Request & { user?: any }, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const todos = await Todo.find({ user: req.user.userId }).sort({ createdAt: -1 });
|
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) {
|
} catch (error) {
|
||||||
console.error("Error in getAllTodos:", error);
|
console.error("Error in getAllTodos:", error);
|
||||||
return res.status(500).json({ message: '获取待办事项失败', error });
|
res.status(500).json({ message: '获取待办事项失败', error });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建新的待办事项(自动绑定 user)
|
// 创建新的待办事项(自动绑定 user)
|
||||||
export const createTodo = async (req: Request & { user?: any }, res: Response): Promise<Response> => {
|
export const createTodo = async (req: Request & { user?: any }, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { title, description = '', priority = 'medium' } = req.body;
|
const { title, description = '', priority = 'medium' } = req.body;
|
||||||
const todo = new Todo({
|
const todo = new Todo({
|
||||||
@ -44,24 +27,26 @@ export const createTodo = async (req: Request & { user?: any }, res: Response):
|
|||||||
user: req.user.userId
|
user: req.user.userId
|
||||||
});
|
});
|
||||||
const savedTodo = await todo.save();
|
const savedTodo = await todo.save();
|
||||||
return res.status(201).json(savedTodo);
|
res.status(201).json(savedTodo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in createTodo:", 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<Response> => {
|
export const updateTodo = async (req: Request & { user?: any }, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params; // 使用 id 作为参数名,便于匹配
|
const { id } = req.params; // 使用 id 作为参数名,便于匹配
|
||||||
if (!mongoose.Types.ObjectId.isValid(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 });
|
const todo = await Todo.findOne({ _id: id, user: req.user.userId });
|
||||||
if (!todo) {
|
if (!todo) {
|
||||||
return res.status(404).json({ message: '待办事项不存在或无权访问' });
|
res.status(404).json({ message: '待办事项不存在或无权访问' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTodo = await Todo.findByIdAndUpdate(
|
const updatedTodo = await Todo.findByIdAndUpdate(
|
||||||
@ -70,53 +55,57 @@ export const updateTodo = async (req: Request & { user?: any }, res: Response):
|
|||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).json(updatedTodo);
|
res.status(200).json(updatedTodo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in updateTodo:", 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<Response> => {
|
export const toggleTodoStatus = async (req: Request & { user?: any }, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (!mongoose.Types.ObjectId.isValid(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 });
|
const todo = await Todo.findOne({ _id: id, user: req.user.userId });
|
||||||
if (!todo) {
|
if (!todo) {
|
||||||
return res.status(404).json({ message: '待办事项不存在或无权访问' });
|
res.status(404).json({ message: '待办事项不存在或无权访问' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
todo.completed = !todo.completed; // 切换完成状态
|
todo.completed = !todo.completed; // 切换完成状态
|
||||||
const updatedTodo = await todo.save();
|
const updatedTodo = await todo.save();
|
||||||
|
|
||||||
return res.status(200).json(updatedTodo);
|
res.status(200).json(updatedTodo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in toggleTodoStatus:", 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<Response> => {
|
export const deleteTodo = async (req: Request & { user?: any }, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
if (!mongoose.Types.ObjectId.isValid(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 });
|
const todo = await Todo.findOne({ _id: id, user: req.user.userId });
|
||||||
if (!todo) {
|
if (!todo) {
|
||||||
return res.status(404).json({ message: '待办事项不存在或无权访问' });
|
res.status(404).json({ message: '待办事项不存在或无权访问' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Todo.findByIdAndDelete(id);
|
await Todo.findByIdAndDelete(id);
|
||||||
return res.status(200).json({ message: '待办事项已删除' });
|
res.status(200).json({ message: '待办事项已删除' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in deleteTodo:", error);
|
console.error("Error in deleteTodo:", error);
|
||||||
return res.status(400).json({ message: '删除待办事项失败', error });
|
res.status(400).json({ message: '删除待办事项失败', error });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
22
server/src/middleware/authMiddleware.ts
Normal file
22
server/src/middleware/authMiddleware.ts
Normal file
@ -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;
|
||||||
@ -2,12 +2,21 @@ import mongoose, { Schema, Document } from 'mongoose';
|
|||||||
|
|
||||||
export interface IUser extends Document {
|
export interface IUser extends Document {
|
||||||
username: string;
|
username: string;
|
||||||
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
resetPasswordToken?: string;
|
||||||
|
resetPasswordExpires?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSchema: Schema = new Schema({
|
const UserSchema: Schema = new Schema({
|
||||||
username: { type: String, required: true, unique: true },
|
username: { type: String, required: true, unique: true },
|
||||||
|
email: { type: String, required: true, unique: true },
|
||||||
password: { type: String, required: true }
|
password: { type: String, required: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserSchema.add({
|
||||||
|
resetPasswordToken: String,
|
||||||
|
resetPasswordExpires: Date,
|
||||||
|
});
|
||||||
|
|
||||||
export default mongoose.model<IUser>('User', UserSchema);
|
export default mongoose.model<IUser>('User', UserSchema);
|
||||||
@ -1,9 +1,14 @@
|
|||||||
import { Router } from 'express';
|
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();
|
const router = Router();
|
||||||
|
|
||||||
router.post('/register', register);
|
router.post('/register', register);
|
||||||
router.post('/login', login);
|
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;
|
export default router;
|
||||||
@ -5,8 +5,8 @@ import {
|
|||||||
updateTodo,
|
updateTodo,
|
||||||
deleteTodo,
|
deleteTodo,
|
||||||
toggleTodoStatus,
|
toggleTodoStatus,
|
||||||
authMiddleware
|
|
||||||
} from '../controllers/todoController';
|
} from '../controllers/todoController';
|
||||||
|
import authMiddleware from '../middleware/authMiddleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user