[dev]add change password

This commit is contained in:
susie-laptop 2025-04-24 10:43:57 -04:00
parent 01e60c5674
commit 98b7a08143
7 changed files with 310 additions and 47 deletions

View File

@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { changePasswordThunk } from '../store/auth/auth.thunk';
function ChangePasswordModal({ show, onClose }) {
const dispatch = useDispatch();
const navigate = useNavigate();
const [formData, setFormData] = useState({
old_password: '',
new_password: '',
confirm_password: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value.trim(),
});
};
const validateForm = () => {
const newErrors = {};
if (!formData.old_password) {
newErrors.old_password = '请输入当前密码';
}
if (!formData.new_password) {
newErrors.new_password = '请输入新密码';
} else if (formData.new_password.length < 6) {
newErrors.new_password = '新密码长度不能少于6个字符';
}
if (!formData.confirm_password) {
newErrors.confirm_password = '请确认新密码';
} else if (formData.new_password !== formData.confirm_password) {
newErrors.confirm_password = '两次输入的密码不一致';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
setSubmitted(true);
if (validateForm()) {
setIsSubmitting(true);
try {
await dispatch(
changePasswordThunk({
old_password: formData.old_password,
new_password: formData.new_password,
})
).unwrap();
// thunk
navigate('/login');
} catch (error) {
// thunk
console.error('Change password failed:', error);
} finally {
setIsSubmitting(false);
}
}
};
if (!show) return null;
return (
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className='modal-dialog modal-dialog-centered'>
<div className='modal-content'>
<div className='modal-header border-0'>
<h5 className='modal-title'>修改密码</h5>
<button type='button' className='btn-close' onClick={onClose}></button>
</div>
<div className='modal-body'>
<form onSubmit={handleSubmit} noValidate>
<div className='mb-3'>
<label className='form-label'>当前密码</label>
<input
value={formData.old_password}
name='old_password'
type='password'
className={`form-control${submitted && errors.old_password ? ' is-invalid' : ''}`}
required
onChange={handleChange}
/>
{submitted && errors.old_password && (
<div className='invalid-feedback'>{errors.old_password}</div>
)}
</div>
<div className='mb-3'>
<label className='form-label'>新密码</label>
<input
value={formData.new_password}
name='new_password'
type='password'
className={`form-control${submitted && errors.new_password ? ' is-invalid' : ''}`}
required
onChange={handleChange}
/>
{submitted && errors.new_password && (
<div className='invalid-feedback'>{errors.new_password}</div>
)}
</div>
<div className='mb-3'>
<label className='form-label'>确认新密码</label>
<input
value={formData.confirm_password}
name='confirm_password'
type='password'
className={`form-control${
submitted && errors.confirm_password ? ' is-invalid' : ''
}`}
required
onChange={handleChange}
/>
{submitted && errors.confirm_password && (
<div className='invalid-feedback'>{errors.confirm_password}</div>
)}
</div>
<div className='modal-footer border-0'>
<button type='button' className='btn btn-outline-dark' onClick={onClose}>
取消
</button>
<button type='submit' className='btn btn-dark' disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '修改密码'}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}
export default ChangePasswordModal;

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { updateProfileThunk } from '../store/auth/auth.thunk';
import ChangePasswordModal from './ChangePasswordModal';
//
const departmentGroups = {
@ -17,6 +18,7 @@ function UserSettingsModal({ show, onClose }) {
const { user, loading } = useSelector((state) => state.auth);
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
const [formData, setFormData] = useState({});
const [showChangePassword, setShowChangePassword] = useState(false);
//
const [availableGroups, setAvailableGroups] = useState([]);
@ -198,6 +200,28 @@ function UserSettingsModal({ show, onClose }) {
</div>
</div>
<div className='mb-4'>
<h6 className='text-secondary mb-3'>安全设置</h6>
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
<div>
<div className='d-flex align-items-center gap-2'>
<i className='bi bi-key'></i>
<span>修改密码</span>
</div>
<small className='text-secondary'>上次修改{lastPasswordChange}</small>
</div>
<button
className='btn btn-outline-dark btn-sm'
onClick={(e) => {
e.preventDefault();
setShowChangePassword(true);
}}
>
修改
</button>
</div>
</div>
<div className='d-none mb-4'>
<h6 className='text-secondary mb-3'>安全设置</h6>
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
@ -260,6 +284,9 @@ function UserSettingsModal({ show, onClose }) {
</div>
</form>
</div>
{showChangePassword && (
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
)}
</div>
);
}

View File

@ -5,6 +5,7 @@ import { logoutThunk } from '../store/auth/auth.thunk';
import UserSettingsModal from '../components/UserSettingsModal';
import NotificationCenter from '../components/NotificationCenter';
import SvgIcon from '../components/SvgIcon';
import ChangePasswordModal from '../components/ChangePasswordModal';
import { fetchNotifications } from '../store/notificationCenter/notificationCenter.thunks';
export default function HeaderWithNav() {
@ -14,6 +15,7 @@ export default function HeaderWithNav() {
const { user } = useSelector((state) => state.auth);
const [showSettings, setShowSettings] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
const handleLogout = async () => {
@ -135,6 +137,15 @@ export default function HeaderWithNav() {
个人设置
</Link>
</li>
<li>
<Link
className='dropdown-item'
to='#'
onClick={() => setShowChangePassword(true)}
>
修改密码
</Link>
</li>
<li className='d-none'>
<hr className='dropdown-divider' />
</li>
@ -168,6 +179,7 @@ export default function HeaderWithNav() {
</nav>
<UserSettingsModal show={showSettings} onClose={() => setShowSettings(false)} />
<NotificationCenter show={showNotifications} onClose={() => setShowNotifications(false)} />
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
</header>
);
}

View File

@ -159,11 +159,6 @@ const put = async (url, data) => {
// Handle DELETE requests with fallback to mock API
const del = async (url) => {
try {
if (isServerDown) {
console.log(`[MOCK MODE] DELETE ${url}`);
return await mockDelete(url);
}
const res = await api.delete(url);
return res.data;
} catch (error) {
@ -217,36 +212,6 @@ export const switchToRealApi = async () => {
// Handle streaming requests
const streamRequest = async (url, data, onChunk, onError) => {
try {
if (isServerDown) {
console.log(`[MOCK MODE] STREAM ${url}`);
// 模拟流式响应
setTimeout(
() =>
onChunk(
'{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'
),
300
);
setTimeout(
() =>
onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'),
600
);
setTimeout(
() =>
onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'),
900
);
setTimeout(
() =>
onChunk(
'{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'
),
1200
);
return { success: true, conversation_id: 'mock-1234' };
}
// 获取认证Token
const encryptedToken = sessionStorage.getItem('token') || '';
let token = '';
@ -259,7 +224,6 @@ const streamRequest = async (url, data, onChunk, onError) => {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token ? `Token ${token}` : '',
},
body: JSON.stringify(data),
});

View File

@ -819,6 +819,32 @@ export const mockPost = async (url, data, isMultipart = false) => {
};
}
// 验证Token
if (url === '/auth/verify-token/') {
// 在实际应用中这里会验证请求头中的Token
// 由于是mock API我们假设所有token都是有效的
// 并返回第一个用户作为当前用户
const user = mockUsers[0];
return {
code: 200,
message: 'Token验证成功',
data: {
user: {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
department: user.department,
group: user.group,
role: user.role,
avatar: user.avatar,
},
},
};
}
// Create knowledge base
if (url === '/knowledge-bases/') {
const newKnowledgeBase = {
@ -978,6 +1004,49 @@ export const mockPost = async (url, data, isMultipart = false) => {
};
}
// 修改密码
if (url === '/auth/change-password/') {
const { old_password, new_password } = data;
// 从请求头中获取token找到当前用户
const token = 'mock-token'; // 在实际情况下,会从请求头中获取并验证
const currentUser = mockUsers.find(user => user.id === 'user-001'); // 假设当前用户是第一个用户
// 验证旧密码
if (!currentUser || currentUser.password !== old_password) {
throw {
response: {
status: 400,
data: {
code: 400,
message: '原密码不正确',
},
},
};
}
// 更新密码
const userIndex = mockUsers.findIndex(user => user.id === currentUser.id);
if (userIndex !== -1) {
mockUsers[userIndex].password = new_password;
}
return {
code: 200,
message: '密码修改成功',
data: { success: true }
};
}
// 用户登出
if (url === '/auth/logout/') {
return {
code: 200,
message: '登出成功',
data: { success: true }
};
}
// 上传知识库文档
if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) {
const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1];
@ -1001,34 +1070,34 @@ export const mockPost = async (url, data, isMultipart = false) => {
// Mark all notifications as read
if (url === '/notifications/mark-all-as-read/') {
// Update all notifications to be read
notifications.forEach(notification => {
notifications.forEach((notification) => {
notification.is_read = true;
});
return {
code: 200,
message: 'All notifications marked as read successfully',
data: { success: true }
data: { success: true },
};
}
// Mark a notification as read
if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) {
const notificationId = url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)[1];
const notificationIndex = notifications.findIndex(n => n.id === notificationId);
const notificationIndex = notifications.findIndex((n) => n.id === notificationId);
if (notificationIndex !== -1) {
notifications[notificationIndex] = {
...notifications[notificationIndex],
is_read: true
is_read: true,
};
return {
return {
code: 200,
message: 'Notification marked as read successfully',
data: { success: true, notification: notifications[notificationIndex] }
data: { success: true, notification: notifications[notificationIndex] },
};
}
return { code: 404, message: 'Notification not found', data: null };
}

View File

@ -76,7 +76,7 @@ export const initWebSocket = () => {
return;
}
const wsUrl = `${WS_BASE_URL}/ws/notifications/?token=${token}`;
const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${token}`;
console.log('正在连接WebSocket...', wsUrl.substring(0, wsUrl.indexOf('?')));
socket = new WebSocket(wsUrl);

View File

@ -136,4 +136,44 @@ export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (
);
return rejectWithValue(errorMessage);
}
});
});
// 修改密码
export const changePasswordThunk = createAsyncThunk(
'auth/changePassword',
async ({ old_password, new_password }, { rejectWithValue, dispatch }) => {
try {
const { message, code } = await post('/auth/change-password/', {
old_password,
new_password
});
if (code !== 200) {
throw new Error(message);
}
// 显示密码修改成功通知
dispatch(
showNotification({
message: '密码修改成功,请重新登录',
type: 'success',
})
);
// 修改成功后清除登录状态
sessionStorage.removeItem('token');
dispatch(logout());
return { success: true };
} catch (error) {
const errorMessage = error.response?.data?.message || '密码修改失败,请稍后重试';
dispatch(
showNotification({
message: errorMessage,
type: 'danger',
})
);
return rejectWithValue(errorMessage);
}
}
);