登录界面增加忘记密码. 后台增加密码重置

This commit is contained in:
yml2213
2026-08-20 14:40:01 +08:00
parent 3546036b43
commit 757eee9742
8 changed files with 386 additions and 0 deletions
@@ -52,6 +52,7 @@ import {
updateWorkOrder,
updateWorkOrderBasic,
updateWorkerUser,
updateWorkerPassword,
revokeAllWorkerSessions,
upsertWorkerWithdrawalAccount,
upsertWorkCategory,
@@ -123,6 +124,8 @@ import {
normalizeRequirementFieldsFromPayload,
normalizeReviewStatus,
normalizeSessionVersion,
hashWorkerPassword,
validateWorkerPassword,
normalizeSlugKey,
normalizeStatuses,
normalizeSubmittedFields,
@@ -941,6 +944,30 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
return { worker: mapWorkerUser(updated || worker) }
}
export async function resetAdminWorkerPassword(
workerId: number | string,
payload: JsonObject = {},
) {
const worker = await getRequiredWorker(workerId)
const password = String(payload.password || payload.newPassword || '').trim()
if (!password) {
throw createHttpError('请输入新密码', {
statusCode: 400,
errorCode: 'worker_password_required',
})
}
validateWorkerPassword(password)
const now = nowIso()
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(password),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now,
})
await revokeAllWorkerSessions(worker.id, now)
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
}
export async function assignAdminWorkOrderToWorker(
workOrderId: number | string,
payload: JsonObject = {},
@@ -11,6 +11,7 @@ import {
countWorkerWithdrawRequestsOnDay,
countWorkOrderPendingSharingSubmissions,
countWorkerSmsCodesOnDay,
consumeWorkerSmsCode,
createWorkerSmsCode,
createWorkOrderEvent,
createWorkerFinanceRequest,
@@ -236,6 +237,119 @@ export async function sendWorkerSmsCode(payload: JsonObject = {}) {
}
}
export async function sendWorkerPasswordResetSmsCode(payload: JsonObject = {}) {
const phone = normalizeWorkerPhone(payload.phone)
const config = runtimeConfig.worker.sms
const cooldownMs = Math.max(10, Number(config.cooldownSeconds || 60)) * 1000
const dailyLimit = Math.max(1, Number(config.dailyLimit || 10))
const ttlSeconds = Math.max(60, Number(config.codeTtlSeconds || 300))
if (!(await getWorkerUserByPhone(phone))) {
throw createHttpError('该手机号未注册', {
statusCode: 404,
errorCode: 'worker_phone_not_registered',
})
}
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
if (
latest &&
!latest.consumed_at &&
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
) {
throw createHttpError('验证码发送过于频繁,请稍后再试', {
statusCode: 429,
errorCode: 'worker_sms_cooldown',
})
}
const dayRange = resolveChinaDayRange()
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
statusCode: 429,
errorCode: 'worker_sms_daily_limit',
})
}
const code = generateSmsCode()
await getSmsProvider().sendCode(phone, code)
await createWorkerSmsCode({
phone,
purpose: 'password_reset',
codeHash: hashSmsCode(code),
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
now: nowIso(),
})
return {
sent: true,
expiresInSeconds: ttlSeconds,
provider: String(runtimeConfig.worker.sms.provider || 'mock'),
}
}
export async function resetWorkerPassword(payload: JsonObject = {}) {
const phone = normalizeWorkerPhone(payload.phone)
const code = String(payload.code || payload.smsCode || '').trim()
const password = normalizePassword(payload.password || payload.newPassword)
validateWorkerPassword(password)
if (!code) {
throw createHttpError('请输入短信验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_required',
})
}
const worker = await getWorkerUserByPhone(phone)
if (!worker) {
throw createHttpError('该手机号未注册', {
statusCode: 404,
errorCode: 'worker_phone_not_registered',
})
}
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
const codeValid = Boolean(
latest &&
!latest.consumed_at &&
new Date(latest.expires_at).getTime() > Date.now() &&
latest.code_hash === hashSmsCode(code),
)
if (!codeValid) {
if (latest && !latest.consumed_at) {
const failed = await recordWorkerSmsCodeFailure(
latest.id,
nowIso(),
WORKER_SMS_MAX_VERIFY_ATTEMPTS,
)
if (failed?.consumed_at) {
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_locked',
})
}
}
throw createHttpError('短信验证码错误或已过期', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
const consumed = await consumeWorkerSmsCode(latest!.id, nowIso())
if (!consumed) {
throw createHttpError('验证码已失效,请重新获取', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
const now = nowIso()
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(password),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now,
})
await revokeAllWorkerSessions(worker.id, now)
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
}
export async function registerWorker(payload: JsonObject = {}) {
ensureWorkerAuthConfigured()
const phone = normalizeWorkerPhone(payload.phone)