登录界面增加忘记密码. 后台增加密码重置
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
reviewAdminWorkerFinanceRequest,
|
||||
resolveAdminProblemWorkOrder,
|
||||
reviewAdminWorkerUser,
|
||||
resetAdminWorkerPassword,
|
||||
saveAdminWorkerFinanceConfig,
|
||||
saveAdminWorkerProductMatchConfig,
|
||||
saveAdminWorkerPlatformNotificationConfig,
|
||||
@@ -318,6 +319,25 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/workers/:workerId/reset-password',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) => resetAdminWorkerPassword(String(req.params.workerId || ''), req.body || {}),
|
||||
{
|
||||
successMessage: '打手密码已重置',
|
||||
errorMessage: '重置打手密码失败',
|
||||
scope: '[admin/worker-platform/workers/:workerId/reset-password]',
|
||||
audit: (req, data) => ({
|
||||
action: 'worker_password_reset',
|
||||
targetType: 'worker',
|
||||
targetId: String(req.params.workerId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/workers/:workerId/wallet-credit',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
logoutWorkerSession,
|
||||
registerWorker,
|
||||
sendWorkerSmsCode,
|
||||
sendWorkerPasswordResetSmsCode,
|
||||
resetWorkerPassword,
|
||||
saveWorkerOrderAcceptanceDraft,
|
||||
saveWorkerOrderNote,
|
||||
remindWorkerOrderAcceptance,
|
||||
@@ -76,6 +78,36 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/password-reset/sms-code',
|
||||
createRateLimitMiddleware({
|
||||
scope: 'worker:password-reset-sms-code',
|
||||
windowMs: 60_000,
|
||||
max: 5,
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler((req) => sendWorkerPasswordResetSmsCode(req.body || {}), {
|
||||
successMessage: '验证码已发送',
|
||||
errorMessage: '验证码发送失败',
|
||||
scope: '[worker/auth/password-reset/sms-code]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/password-reset',
|
||||
createRateLimitMiddleware({
|
||||
scope: 'worker:password-reset',
|
||||
windowMs: 60_000,
|
||||
max: 10,
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler((req) => resetWorkerPassword(req.body || {}), {
|
||||
successMessage: '密码已重置,请重新登录',
|
||||
errorMessage: '密码重置失败',
|
||||
scope: '[worker/auth/password-reset]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/login',
|
||||
createRateLimitMiddleware({
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user