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:
iven
2026-04-15 01:32:18 +08:00
parent d8a0ac7519
commit 7e8fabb095
8 changed files with 267 additions and 1 deletions

View File

@@ -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,
});
}

View File

@@ -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>

View 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>
);
}