feat(auth): add change password API and frontend page
Backend: - Add ChangePasswordReq DTO with validation (current + new password) - Add AuthService::change_password() method with credential verification, password rehash, and token revocation - Add POST /api/v1/auth/change-password endpoint with utoipa annotation Frontend: - Add changePassword() API function in auth.ts - Add ChangePassword.tsx page with form validation and confirmation - Add "修改密码" tab in Settings page After password change, all refresh tokens are revoked and the user is redirected to the login page.
This commit is contained in:
@@ -51,3 +51,13 @@ export async function refresh(refreshToken: string): Promise<LoginResponse> {
|
||||
export async function logout(): Promise<void> {
|
||||
await client.post('/auth/logout');
|
||||
}
|
||||
|
||||
export async function changePassword(
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Promise<void> {
|
||||
await client.post('/auth/change-password', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SettingOutlined,
|
||||
BgColorsOutlined,
|
||||
AuditOutlined,
|
||||
LockOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import DictionaryManager from './settings/DictionaryManager';
|
||||
import LanguageManager from './settings/LanguageManager';
|
||||
@@ -15,6 +16,7 @@ import NumberingRules from './settings/NumberingRules';
|
||||
import SystemSettings from './settings/SystemSettings';
|
||||
import ThemeSettings from './settings/ThemeSettings';
|
||||
import AuditLogViewer from './settings/AuditLogViewer';
|
||||
import ChangePassword from './settings/ChangePassword';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
@@ -100,6 +102,16 @@ const Settings: React.FC = () => {
|
||||
),
|
||||
children: <AuditLogViewer />,
|
||||
},
|
||||
{
|
||||
key: 'change-password',
|
||||
label: (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<LockOutlined style={{ fontSize: 14 }} />
|
||||
修改密码
|
||||
</span>
|
||||
),
|
||||
children: <ChangePassword />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
95
apps/web/src/pages/settings/ChangePassword.tsx
Normal file
95
apps/web/src/pages/settings/ChangePassword.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Form, Input, Button, message, Card, Typography } from 'antd';
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { changePassword } from '../../api/auth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function ChangePassword() {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values: {
|
||||
current_password: string;
|
||||
new_password: string;
|
||||
confirm_password: string;
|
||||
}) => {
|
||||
if (values.new_password !== values.confirm_password) {
|
||||
messageApi.error('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await changePassword(values.current_password, values.new_password);
|
||||
messageApi.success('密码修改成功,请重新登录');
|
||||
await logout();
|
||||
navigate('/login');
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '密码修改失败';
|
||||
messageApi.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ maxWidth: 480, margin: '0 auto' }}>
|
||||
{contextHolder}
|
||||
<Title level={4} style={{ marginBottom: 24 }}>
|
||||
修改密码
|
||||
</Title>
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="current_password"
|
||||
label="当前密码"
|
||||
rules={[{ required: true, message: '请输入当前密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="new_password"
|
||||
label="新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码长度不能少于6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请输入新密码(至少6位)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="confirm_password"
|
||||
label="确认新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('new_password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
确认修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user