[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 React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { updateProfileThunk } from '../store/auth/auth.thunk'; import { updateProfileThunk } from '../store/auth/auth.thunk';
import ChangePasswordModal from './ChangePasswordModal';
// //
const departmentGroups = { const departmentGroups = {
@ -17,6 +18,7 @@ function UserSettingsModal({ show, onClose }) {
const { user, loading } = useSelector((state) => state.auth); const { user, loading } = useSelector((state) => state.auth);
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
const [formData, setFormData] = useState({}); const [formData, setFormData] = useState({});
const [showChangePassword, setShowChangePassword] = useState(false);
// //
const [availableGroups, setAvailableGroups] = useState([]); const [availableGroups, setAvailableGroups] = useState([]);
@ -198,6 +200,28 @@ function UserSettingsModal({ show, onClose }) {
</div> </div>
</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'> <div className='d-none mb-4'>
<h6 className='text-secondary mb-3'>安全设置</h6> <h6 className='text-secondary mb-3'>安全设置</h6>
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'> <div className='d-flex justify-content-between align-items-center p-3 bg-light rounded'>
@ -260,6 +284,9 @@ function UserSettingsModal({ show, onClose }) {
</div> </div>
</form> </form>
</div> </div>
{showChangePassword && (
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
)}
</div> </div>
); );
} }

View File

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

View File

@ -159,11 +159,6 @@ const put = async (url, data) => {
// Handle DELETE requests with fallback to mock API // Handle DELETE requests with fallback to mock API
const del = async (url) => { const del = async (url) => {
try { try {
if (isServerDown) {
console.log(`[MOCK MODE] DELETE ${url}`);
return await mockDelete(url);
}
const res = await api.delete(url); const res = await api.delete(url);
return res.data; return res.data;
} catch (error) { } catch (error) {
@ -217,36 +212,6 @@ export const switchToRealApi = async () => {
// Handle streaming requests // Handle streaming requests
const streamRequest = async (url, data, onChunk, onError) => { const streamRequest = async (url, data, onChunk, onError) => {
try { 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 // 获取认证Token
const encryptedToken = sessionStorage.getItem('token') || ''; const encryptedToken = sessionStorage.getItem('token') || '';
let token = ''; let token = '';
@ -259,7 +224,6 @@ const streamRequest = async (url, data, onChunk, onError) => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: token ? `Token ${token}` : '',
}, },
body: JSON.stringify(data), 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 // Create knowledge base
if (url === '/knowledge-bases/') { if (url === '/knowledge-bases/') {
const newKnowledgeBase = { 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\//)) { if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) {
const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1]; 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 // Mark all notifications as read
if (url === '/notifications/mark-all-as-read/') { if (url === '/notifications/mark-all-as-read/') {
// Update all notifications to be read // Update all notifications to be read
notifications.forEach(notification => { notifications.forEach((notification) => {
notification.is_read = true; notification.is_read = true;
}); });
return { return {
code: 200, code: 200,
message: 'All notifications marked as read successfully', message: 'All notifications marked as read successfully',
data: { success: true } data: { success: true },
}; };
} }
// Mark a notification as read // Mark a notification as read
if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) { if (url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)) {
const notificationId = url.match(/\/notifications\/([^\/]+)\/mark-as-read\//)[1]; 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) { if (notificationIndex !== -1) {
notifications[notificationIndex] = { notifications[notificationIndex] = {
...notifications[notificationIndex], ...notifications[notificationIndex],
is_read: true is_read: true,
}; };
return { return {
code: 200, code: 200,
message: 'Notification marked as read successfully', 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 }; return { code: 404, message: 'Notification not found', data: null };
} }

View File

@ -76,7 +76,7 @@ export const initWebSocket = () => {
return; 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('?'))); console.log('正在连接WebSocket...', wsUrl.substring(0, wsUrl.indexOf('?')));
socket = new WebSocket(wsUrl); socket = new WebSocket(wsUrl);

View File

@ -136,4 +136,44 @@ export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (
); );
return rejectWithValue(errorMessage); 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);
}
}
);