fix(admin): 密码按字节数校验,对齐 bcrypt 72 字节上限
All checks were successful
CI / check (pull_request) Successful in 2m51s
Dependency Check / check (pull_request) Successful in 1m49s

- 共享 PASSWORD_RULES:新增账号与重置密码弹窗统一
- 原 max:72 按字符数校验,多字节密码(如中文 3 字节)会通过前端校验却被服务端
  @MaxByteLength(72) 拒绝;改为 TextEncoder 字节数校验
This commit is contained in:
2026-08-11 11:54:14 +08:00
parent 48d06b29c1
commit 0b3edb88aa
3 changed files with 16 additions and 22 deletions

View File

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

View File

@@ -11,13 +11,11 @@ describe('user profile form mapping', () => {
profile: {
joinedAt: '2026-07-01',
qualifications: '教师资格证',
subjects: ['语文', '历史'],
},
}),
).toEqual({
joinedAt: '2026-07-01',
qualifications: '教师资格证',
subjects: ['语文', '历史'],
});
});

View File

@@ -1,7 +1,6 @@
export interface UserProfileFormValues {
joinedAt?: string;
qualifications?: string;
subjects?: string[];
}
export interface UserProfileResponse {