From 98b7a081435ccf1d1b288641ed6efe800a4aad24 Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Thu, 24 Apr 2025 10:43:57 -0400 Subject: [PATCH] [dev]add change password --- src/components/ChangePasswordModal.jsx | 151 +++++++++++++++++++++++++ src/components/UserSettingsModal.jsx | 27 +++++ src/layouts/HeaderWithNav.jsx | 12 ++ src/services/api.js | 36 ------ src/services/mockApi.js | 87 ++++++++++++-- src/services/websocket.js | 2 +- src/store/auth/auth.thunk.js | 42 ++++++- 7 files changed, 310 insertions(+), 47 deletions(-) create mode 100644 src/components/ChangePasswordModal.jsx diff --git a/src/components/ChangePasswordModal.jsx b/src/components/ChangePasswordModal.jsx new file mode 100644 index 0000000..7d177dd --- /dev/null +++ b/src/components/ChangePasswordModal.jsx @@ -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 ( +
+
+
+
+
修改密码
+ +
+
+
+
+ + + {submitted && errors.old_password && ( +
{errors.old_password}
+ )} +
+ +
+ + + {submitted && errors.new_password && ( +
{errors.new_password}
+ )} +
+ +
+ + + {submitted && errors.confirm_password && ( +
{errors.confirm_password}
+ )} +
+ +
+ + +
+
+
+
+
+
+ ); +} + +export default ChangePasswordModal; diff --git a/src/components/UserSettingsModal.jsx b/src/components/UserSettingsModal.jsx index 389553d..f3d9343 100644 --- a/src/components/UserSettingsModal.jsx +++ b/src/components/UserSettingsModal.jsx @@ -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 }) { +
+
安全设置
+
+
+
+ + 修改密码 +
+ 上次修改:{lastPasswordChange} +
+ +
+
+
安全设置
@@ -260,6 +284,9 @@ function UserSettingsModal({ show, onClose }) {
+ {showChangePassword && ( + setShowChangePassword(false)} /> + )} ); } diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 2578e59..73b27ea 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -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() { 个人设置 +
  • + setShowChangePassword(true)} + > + 修改密码 + +

  • @@ -168,6 +179,7 @@ export default function HeaderWithNav() { setShowSettings(false)} /> setShowNotifications(false)} /> + setShowChangePassword(false)} /> ); } diff --git a/src/services/api.js b/src/services/api.js index 1d55c25..6f190de 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -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), }); diff --git a/src/services/mockApi.js b/src/services/mockApi.js index 10426fc..61eb915 100644 --- a/src/services/mockApi.js +++ b/src/services/mockApi.js @@ -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 }; } diff --git a/src/services/websocket.js b/src/services/websocket.js index 1409a37..169afc0 100644 --- a/src/services/websocket.js +++ b/src/services/websocket.js @@ -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); diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js index 7f8091f..afe37b9 100644 --- a/src/store/auth/auth.thunk.js +++ b/src/store/auth/auth.thunk.js @@ -136,4 +136,44 @@ export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async ( ); return rejectWithValue(errorMessage); } -}); \ No newline at end of file +}); + +// 修改密码 +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); + } + } +); \ No newline at end of file