From 8d18daa0fe3460e469762799b37b28be0b4ef463 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 17 Aug 2026 15:02:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=93=E6=89=8B=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=BC=BA=E5=BA=A6=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/worker-platform/mappers.ts | 14 +++++++-- .../worker-platform-service.test.ts | 12 ++++++++ .../worker-platform/worker-service.ts | 6 +++- .../src/pages/worker/WorkerLoginPage.tsx | 30 +++++++++++++++---- .../src/pages/worker/WorkerProfilePage.tsx | 11 ++++++- apps/frontend/src/types/worker-platform.ts | 1 + apps/frontend/src/utils/worker-password.ts | 15 ++++++++++ 7 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 apps/frontend/src/utils/worker-password.ts diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index f84ce7ea..edea6cc4 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -973,14 +973,24 @@ export function validateWorkerUsername(username: string) { } export function validateWorkerPassword(password: string) { - if (password.length < 6) { - throw createHttpError('接单密码至少 6 位', { + if (!isWorkerPasswordStrong(password)) { + throw createHttpError('接单密码至少 6 位,且必须包含大写字母、小写字母、数字和标点符号', { statusCode: 400, errorCode: 'worker_password_invalid', }) } } +export function isWorkerPasswordStrong(password: string): boolean { + return ( + password.length >= 6 && + /[A-Z]/.test(password) && + /[a-z]/.test(password) && + /\d/.test(password) && + /[\p{P}\p{S}]/u.test(password) + ) +} + export function normalizeSessionVersion(value: unknown): number { const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : 1 diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 7cd177f0..6298648d 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -5,11 +5,13 @@ import type { WorkOrderRow, WorkerUserRow } from '../../repositories/worker-plat import { assertWithdrawChannelAllowedForAmount, assertWorkerLoginAllowed, + isWorkerPasswordStrong, normalizeWithdrawChannel, normalizeRequirementFields, resolveCollectSubmitTargetWorkOrder, resolveWorkerPermissions, resolveSkuNameQuantity, + validateWorkerPassword, } from './index.js' function buildWorkerUserRow(overrides: Partial = {}): WorkerUserRow { @@ -36,6 +38,16 @@ test('assertWorkerLoginAllowed allows active workers', () => { assert.doesNotThrow(() => assertWorkerLoginAllowed(buildWorkerUserRow())) }) +test('接单密码必须包含大小写字母、数字和标点符号', () => { + assert.equal(isWorkerPasswordStrong('Aa1234!'), true) + assert.equal(isWorkerPasswordStrong('aa1234!'), false) + assert.equal(isWorkerPasswordStrong('AA1234!'), false) + assert.equal(isWorkerPasswordStrong('Aaabcd!'), false) + assert.equal(isWorkerPasswordStrong('Aa123456'), false) + assert.equal(isWorkerPasswordStrong('Aa1!'), false) + assert.throws(() => validateWorkerPassword('Aa123456'), { errorCode: 'worker_password_invalid' }) +}) + test('assertWorkerLoginAllowed blocks rejected (frozen) workers with worker_rejected', () => { assert.throws( () => assertWorkerLoginAllowed(buildWorkerUserRow({ status: 'rejected' })), diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 6aa7dc10..5b497d45 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -126,6 +126,7 @@ import { signWorkerPayload, throwGrabWorkOrderFailure, validateWorkerPassword, + isWorkerPasswordStrong, verifyWorkerPassword, } from './mappers.js' @@ -386,7 +387,10 @@ export async function loginWorker(username: unknown, password: unknown) { assertWorkerLoginAllowed(worker) - return createWorkerSession(worker) + return { + ...createWorkerSession(worker), + passwordWeak: !isWorkerPasswordStrong(normalizedPassword), + } } export function assertWorkerLoginAllowed(worker: WorkerUserRow) { diff --git a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx index 33d7fe83..16fe4d6f 100644 --- a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx @@ -13,6 +13,7 @@ import { Navigate, useNavigate } from 'react-router' import { loginWorker, registerWorker, sendWorkerSmsCode } from '@/services/worker' import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth' +import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password' const SMS_COUNTDOWN_SECONDS = 60 @@ -36,7 +37,7 @@ const brandFeatures = [ export default function WorkerLoginPage() { const navigate = useNavigate() - const { message } = App.useApp() + const { message, modal } = App.useApp() const [loading, setLoading] = useState(false) const [registerForm] = Form.useForm() const [countdown, setCountdown] = useState(0) @@ -56,8 +57,19 @@ export default function WorkerLoginPage() { try { const response = await loginWorker(values) setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker) - message.success('登录成功') - void navigate('/hall', { replace: true }) + if (response.data.passwordWeak) { + modal.confirm({ + title: '密码安全提醒', + content: '当前密码强度较弱,为保障账号安全,建议立即修改密码。', + okText: '去修改密码', + cancelText: '稍后再说', + onOk: () => void navigate('/profile', { replace: true }), + onCancel: () => void navigate('/hall', { replace: true }), + }) + } else { + message.success('登录成功') + void navigate('/hall', { replace: true }) + } } catch (error) { message.error(error instanceof Error ? error.message : '登录失败') } finally { @@ -275,10 +287,18 @@ export default function WorkerLoginPage() { rules={[ { required: true, - min: 6, - message: '密码至少 6 位', + message: '请输入密码', + }, + { + validator: (_, value) => { + if (!value || isWorkerPasswordStrong(value)) { + return Promise.resolve() + } + return Promise.reject(new Error(WORKER_PASSWORD_RULE_MESSAGE)) + }, }, ]} + extra={WORKER_PASSWORD_RULE_MESSAGE} > } autoComplete="new-password" /> diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index 5a519009..832fdd93 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -57,6 +57,7 @@ import type { import { formatDateTime } from '@/utils/date-time' import { useIsMobile } from '@/utils/use-is-mobile' import { clearWorkerSession } from '@/utils/worker-auth' +import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password' type RechargeFormValues = { amount?: number @@ -1650,8 +1651,16 @@ export default function WorkerProfilePage() { name="newPassword" rules={[ { required: true, message: '请输入新密码' }, - { min: 6, message: '新密码至少 6 位' }, + { + validator: (_, value) => { + if (!value || isWorkerPasswordStrong(value)) { + return Promise.resolve() + } + return Promise.reject(new Error(WORKER_PASSWORD_RULE_MESSAGE)) + }, + }, ]} + extra={WORKER_PASSWORD_RULE_MESSAGE} > diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index b95a9653..f9d6fd7e 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -334,6 +334,7 @@ export type WorkerLoginResponse = { token: string expiresAt: string worker: WorkerUser + passwordWeak?: boolean } export type WorkerListResponse = { diff --git a/apps/frontend/src/utils/worker-password.ts b/apps/frontend/src/utils/worker-password.ts new file mode 100644 index 00000000..977adbff --- /dev/null +++ b/apps/frontend/src/utils/worker-password.ts @@ -0,0 +1,15 @@ +export const WORKER_PASSWORD_MIN_LENGTH = 6 + +export const WORKER_PASSWORD_RULE_MESSAGE = + '密码至少 6 位,且必须包含大写字母、小写字母、数字和标点符号' + +export function isWorkerPasswordStrong(value: unknown): boolean { + const password = String(value || '') + return ( + password.length >= WORKER_PASSWORD_MIN_LENGTH && + /[A-Z]/.test(password) && + /[a-z]/.test(password) && + /\d/.test(password) && + /[\p{P}\p{S}]/u.test(password) + ) +}