fix(admin): 密码按字节数校验,对齐 bcrypt 72 字节上限
- 共享 PASSWORD_RULES:新增账号与重置密码弹窗统一 - 原 max:72 按字符数校验,多字节密码(如中文 3 字节)会通过前端校验却被服务端 @MaxByteLength(72) 拒绝;改为 TextEncoder 字节数校验
This commit is contained in:
@@ -20,6 +20,7 @@ import { useDirtyGuard } from '../../hooks/useDirtyGuard';
|
||||
import { validateResponse } from '../../utils/validate';
|
||||
import { rolesSchema, usersSchema } from '../../api/schemas';
|
||||
import { getErrorMessage } from '../../utils/error';
|
||||
import type { Rule } from 'antd/es/form';
|
||||
|
||||
const USER_FIELDS = {
|
||||
username: 'username',
|
||||
@@ -29,6 +30,19 @@ const USER_FIELDS = {
|
||||
status: 'status',
|
||||
} as const;
|
||||
|
||||
// 与后端 CreateUserDto/ResetPasswordDto 保持一致:bcrypt 截断于 72 字节(UTF-8),
|
||||
// 多字节字符(如中文 3 字节)下按字符数上限会漏检,这里按字节数校验。
|
||||
const PASSWORD_RULES: Rule[] = [
|
||||
{ required: true, min: 8, message: '密码至少8位' },
|
||||
{
|
||||
validator: (_: unknown, value: string) =>
|
||||
!value || new TextEncoder().encode(value).length <= 72
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('密码不能超过72字节(bcrypt 上限)')),
|
||||
},
|
||||
{ pattern: /^\S+$/, message: '密码不能包含空格' },
|
||||
];
|
||||
|
||||
const UsersPage: React.FC = () => {
|
||||
const { modal } = App.useApp();
|
||||
const { hasPermission } = usePermission();
|
||||
@@ -464,7 +478,7 @@ const UsersPage: React.FC = () => {
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[{ required: true, min: 4, message: '密码至少4位' }]}
|
||||
rules={PASSWORD_RULES}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
@@ -503,7 +517,7 @@ const UsersPage: React.FC = () => {
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="新密码"
|
||||
rules={[{ required: true, min: 4, message: '密码至少4位' }]}
|
||||
rules={PASSWORD_RULES}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
@@ -525,23 +539,6 @@ const UsersPage: React.FC = () => {
|
||||
<Form.Item name="qualifications" label="资质">
|
||||
<Input.TextArea placeholder="教师资格证号、学历等" rows={2} />
|
||||
</Form.Item>
|
||||
<Form.Item name="subjects" label="任教学科">
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="输入学科后回车添加"
|
||||
options={[
|
||||
{ value: '语文', label: '语文' },
|
||||
{ value: '数学', label: '数学' },
|
||||
{ value: '英语', label: '英语' },
|
||||
{ value: '政治', label: '政治' },
|
||||
{ value: '历史', label: '历史' },
|
||||
{ value: '地理', label: '地理' },
|
||||
{ value: '物理', label: '物理' },
|
||||
{ value: '化学', label: '化学' },
|
||||
{ value: '生物', label: '生物' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,11 @@ describe('user profile form mapping', () => {
|
||||
profile: {
|
||||
joinedAt: '2026-07-01',
|
||||
qualifications: '教师资格证',
|
||||
subjects: ['语文', '历史'],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
joinedAt: '2026-07-01',
|
||||
qualifications: '教师资格证',
|
||||
subjects: ['语文', '历史'],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface UserProfileFormValues {
|
||||
joinedAt?: string;
|
||||
qualifications?: string;
|
||||
subjects?: string[];
|
||||
}
|
||||
|
||||
export interface UserProfileResponse {
|
||||
|
||||
Reference in New Issue
Block a user