增加“验证码登录”模式
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
markWorkerOrderRead,
|
||||
getWorkerMyOrderEvents,
|
||||
loginWorker,
|
||||
loginWorkerBySmsCode,
|
||||
getWorkerSessions,
|
||||
deleteWorkerSession,
|
||||
manageWorkerSessions,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
logoutWorkerSession,
|
||||
registerWorker,
|
||||
sendWorkerSmsCode,
|
||||
sendWorkerLoginSmsCode,
|
||||
sendWorkerPasswordResetSmsCode,
|
||||
resetWorkerPassword,
|
||||
saveWorkerOrderAcceptanceDraft,
|
||||
@@ -133,6 +135,46 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/login/sms-code',
|
||||
createRateLimitMiddleware({
|
||||
scope: 'worker:login-sms-code',
|
||||
windowMs: 60_000,
|
||||
max: 5,
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler((req) => sendWorkerLoginSmsCode(req.body || {}), {
|
||||
successMessage: '验证码已发送',
|
||||
errorMessage: '验证码发送失败',
|
||||
scope: '[worker/auth/login/sms-code]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/login/sms',
|
||||
createRateLimitMiddleware({
|
||||
scope: 'worker:login-sms',
|
||||
windowMs: 60_000,
|
||||
max: 10,
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler(
|
||||
(req) =>
|
||||
loginWorkerBySmsCode(req.body?.phone, req.body?.code, {
|
||||
deviceId: req.body?.deviceId,
|
||||
deviceType: req.body?.deviceType,
|
||||
deviceName: req.body?.deviceName,
|
||||
userAgent: req.headers['user-agent'],
|
||||
ipAddress: req.ip,
|
||||
}),
|
||||
{
|
||||
successMessage: '登录成功',
|
||||
errorMessage: '接单端验证码登录失败',
|
||||
scope: '[worker/auth/login/sms]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/auth/devices/manage/list',
|
||||
createRateLimitMiddleware({
|
||||
|
||||
@@ -287,6 +287,52 @@ export async function sendWorkerPasswordResetSmsCode(payload: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendWorkerLoginSmsCode(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, 'login')
|
||||
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: 'login',
|
||||
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()
|
||||
@@ -522,6 +568,77 @@ export async function loginWorker(
|
||||
const normalizedPassword = normalizePassword(password)
|
||||
const worker = await authenticateWorkerCredentials(username, password)
|
||||
|
||||
return createWorkerLoginSession(worker, deviceInfo, !isWorkerPasswordStrong(normalizedPassword))
|
||||
}
|
||||
|
||||
export async function loginWorkerBySmsCode(
|
||||
phoneValue: unknown,
|
||||
codeValue: unknown,
|
||||
deviceInfo: {
|
||||
deviceId?: unknown
|
||||
deviceType?: unknown
|
||||
deviceName?: unknown
|
||||
userAgent?: unknown
|
||||
ipAddress?: unknown
|
||||
} = {},
|
||||
) {
|
||||
ensureWorkerAuthConfigured()
|
||||
const phone = normalizeWorkerPhone(phoneValue)
|
||||
const code = String(codeValue || '').trim()
|
||||
if (!code) {
|
||||
throw createHttpError('请输入短信验证码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_required',
|
||||
})
|
||||
}
|
||||
const worker = await getWorkerUserByPhone(phone)
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'login')
|
||||
const codeValid = Boolean(
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.expires_at).getTime() > Date.now() &&
|
||||
latest.code_hash === hashSmsCode(code),
|
||||
)
|
||||
if (!worker || !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: 401,
|
||||
errorCode: 'worker_sms_code_invalid',
|
||||
})
|
||||
}
|
||||
if (!(await consumeWorkerSmsCode(latest!.id, nowIso()))) {
|
||||
throw createHttpError('验证码已失效,请重新获取', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_invalid',
|
||||
})
|
||||
}
|
||||
assertWorkerLoginAllowed(worker)
|
||||
return createWorkerLoginSession(worker, deviceInfo, false)
|
||||
}
|
||||
|
||||
async function createWorkerLoginSession(
|
||||
worker: WorkerUserRow,
|
||||
deviceInfo: {
|
||||
deviceId?: unknown
|
||||
deviceType?: unknown
|
||||
deviceName?: unknown
|
||||
userAgent?: unknown
|
||||
ipAddress?: unknown
|
||||
},
|
||||
passwordWeak: boolean,
|
||||
) {
|
||||
const session = createWorkerSession(worker)
|
||||
const deviceType = normalizeWorkerDeviceType(deviceInfo.deviceType)
|
||||
const deviceId = normalizeWorkerDeviceId(deviceInfo.deviceId, session.sessionId)
|
||||
@@ -551,7 +668,7 @@ export async function loginWorker(
|
||||
|
||||
return {
|
||||
...session,
|
||||
passwordWeak: !isWorkerPasswordStrong(normalizedPassword),
|
||||
passwordWeak,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user