增加打手密码强度校验
This commit is contained in:
@@ -973,14 +973,24 @@ export function validateWorkerUsername(username: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateWorkerPassword(password: string) {
|
export function validateWorkerPassword(password: string) {
|
||||||
if (password.length < 6) {
|
if (!isWorkerPasswordStrong(password)) {
|
||||||
throw createHttpError('接单密码至少 6 位', {
|
throw createHttpError('接单密码至少 6 位,且必须包含大写字母、小写字母、数字和标点符号', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'worker_password_invalid',
|
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 {
|
export function normalizeSessionVersion(value: unknown): number {
|
||||||
const parsed = Number(value)
|
const parsed = Number(value)
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import type { WorkOrderRow, WorkerUserRow } from '../../repositories/worker-plat
|
|||||||
import {
|
import {
|
||||||
assertWithdrawChannelAllowedForAmount,
|
assertWithdrawChannelAllowedForAmount,
|
||||||
assertWorkerLoginAllowed,
|
assertWorkerLoginAllowed,
|
||||||
|
isWorkerPasswordStrong,
|
||||||
normalizeWithdrawChannel,
|
normalizeWithdrawChannel,
|
||||||
normalizeRequirementFields,
|
normalizeRequirementFields,
|
||||||
resolveCollectSubmitTargetWorkOrder,
|
resolveCollectSubmitTargetWorkOrder,
|
||||||
resolveWorkerPermissions,
|
resolveWorkerPermissions,
|
||||||
resolveSkuNameQuantity,
|
resolveSkuNameQuantity,
|
||||||
|
validateWorkerPassword,
|
||||||
} from './index.js'
|
} from './index.js'
|
||||||
|
|
||||||
function buildWorkerUserRow(overrides: Partial<WorkerUserRow> = {}): WorkerUserRow {
|
function buildWorkerUserRow(overrides: Partial<WorkerUserRow> = {}): WorkerUserRow {
|
||||||
@@ -36,6 +38,16 @@ test('assertWorkerLoginAllowed allows active workers', () => {
|
|||||||
assert.doesNotThrow(() => assertWorkerLoginAllowed(buildWorkerUserRow()))
|
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', () => {
|
test('assertWorkerLoginAllowed blocks rejected (frozen) workers with worker_rejected', () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => assertWorkerLoginAllowed(buildWorkerUserRow({ status: 'rejected' })),
|
() => assertWorkerLoginAllowed(buildWorkerUserRow({ status: 'rejected' })),
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ import {
|
|||||||
signWorkerPayload,
|
signWorkerPayload,
|
||||||
throwGrabWorkOrderFailure,
|
throwGrabWorkOrderFailure,
|
||||||
validateWorkerPassword,
|
validateWorkerPassword,
|
||||||
|
isWorkerPasswordStrong,
|
||||||
verifyWorkerPassword,
|
verifyWorkerPassword,
|
||||||
} from './mappers.js'
|
} from './mappers.js'
|
||||||
|
|
||||||
@@ -386,7 +387,10 @@ export async function loginWorker(username: unknown, password: unknown) {
|
|||||||
|
|
||||||
assertWorkerLoginAllowed(worker)
|
assertWorkerLoginAllowed(worker)
|
||||||
|
|
||||||
return createWorkerSession(worker)
|
return {
|
||||||
|
...createWorkerSession(worker),
|
||||||
|
passwordWeak: !isWorkerPasswordStrong(normalizedPassword),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertWorkerLoginAllowed(worker: WorkerUserRow) {
|
export function assertWorkerLoginAllowed(worker: WorkerUserRow) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Navigate, useNavigate } from 'react-router'
|
|||||||
|
|
||||||
import { loginWorker, registerWorker, sendWorkerSmsCode } from '@/services/worker'
|
import { loginWorker, registerWorker, sendWorkerSmsCode } from '@/services/worker'
|
||||||
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
||||||
|
import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password'
|
||||||
|
|
||||||
const SMS_COUNTDOWN_SECONDS = 60
|
const SMS_COUNTDOWN_SECONDS = 60
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ const brandFeatures = [
|
|||||||
|
|
||||||
export default function WorkerLoginPage() {
|
export default function WorkerLoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { message } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [registerForm] = Form.useForm()
|
const [registerForm] = Form.useForm()
|
||||||
const [countdown, setCountdown] = useState(0)
|
const [countdown, setCountdown] = useState(0)
|
||||||
@@ -56,8 +57,19 @@ export default function WorkerLoginPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await loginWorker(values)
|
const response = await loginWorker(values)
|
||||||
setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker)
|
setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker)
|
||||||
|
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('登录成功')
|
message.success('登录成功')
|
||||||
void navigate('/hall', { replace: true })
|
void navigate('/hall', { replace: true })
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '登录失败')
|
message.error(error instanceof Error ? error.message : '登录失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -275,10 +287,18 @@ export default function WorkerLoginPage() {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
min: 6,
|
message: '请输入密码',
|
||||||
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}
|
||||||
>
|
>
|
||||||
<Input.Password prefix={<LockOutlined />} autoComplete="new-password" />
|
<Input.Password prefix={<LockOutlined />} autoComplete="new-password" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import type {
|
|||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||||
import { clearWorkerSession } from '@/utils/worker-auth'
|
import { clearWorkerSession } from '@/utils/worker-auth'
|
||||||
|
import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password'
|
||||||
|
|
||||||
type RechargeFormValues = {
|
type RechargeFormValues = {
|
||||||
amount?: number
|
amount?: number
|
||||||
@@ -1650,8 +1651,16 @@ export default function WorkerProfilePage() {
|
|||||||
name="newPassword"
|
name="newPassword"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入新密码' },
|
{ 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}
|
||||||
>
|
>
|
||||||
<Input.Password autoComplete="new-password" />
|
<Input.Password autoComplete="new-password" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ export type WorkerLoginResponse = {
|
|||||||
token: string
|
token: string
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
worker: WorkerUser
|
worker: WorkerUser
|
||||||
|
passwordWeak?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerListResponse<T> = {
|
export type WorkerListResponse<T> = {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user