拆分打手登录服务

This commit is contained in:
yml2213
2026-08-21 16:44:56 +08:00
parent cc8c95bb58
commit 27e9a574e4
3 changed files with 200 additions and 179 deletions
@@ -7,6 +7,7 @@ export * from './worker-session-auth-service.js'
export * from './worker-credentials-service.js'
export * from './worker-sms-service.js'
export * from './worker-registration-service.js'
export * from './worker-login-service.js'
export * from './worker-platform-defaults.js'
export * from './work-order-timeout-policy.js'
export * from './after-sales-service.js'
@@ -0,0 +1,187 @@
import {
consumeWorkerSmsCode,
createWorkerSessionRecord,
getLatestWorkerSmsCode,
getWorkerUserByDisplayName,
getWorkerUserByPhone,
getWorkerUserByUsername,
recordWorkerSmsCodeFailure,
type WorkerUserRow,
} from '../../repositories/worker-platform/index.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import {
createWorkerSession,
ensureWorkerAuthConfigured,
isWorkerPasswordStrong,
normalizePassword,
normalizeSessionVersion,
normalizeUsername,
verifyWorkerPassword,
} from './mappers.js'
import {
assertWorkerLoginAllowed,
normalizeWorkerDeviceId,
normalizeWorkerDeviceName,
normalizeWorkerDeviceType,
} from './worker-auth-policy.js'
import { deleteWorkerSession, getWorkerSessions } from './worker-session-auth-service.js'
import type { WorkerSession } from './worker-session-context-service.js'
import { hashSmsCode, normalizeWorkerPhone } from './worker-sms-service.js'
const WORKER_MAX_DEVICES = 3
const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5
type WorkerLoginDeviceInfo = {
deviceId?: unknown
deviceType?: unknown
deviceName?: unknown
userAgent?: unknown
ipAddress?: unknown
}
export async function loginWorker(
username: unknown,
password: unknown,
deviceInfo: WorkerLoginDeviceInfo = {},
) {
ensureWorkerAuthConfigured()
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: WorkerLoginDeviceInfo = {},
) {
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)
}
export async function manageWorkerSessions(username: unknown, password: unknown) {
const worker = await authenticateWorkerCredentials(username, password)
return getWorkerSessions(createCredentialSession(worker))
}
export async function deleteWorkerSessionWithCredentials(
username: unknown,
password: unknown,
targetSessionId: unknown,
) {
const worker = await authenticateWorkerCredentials(username, password)
return deleteWorkerSession(createCredentialSession(worker), targetSessionId)
}
async function createWorkerLoginSession(
worker: WorkerUserRow,
deviceInfo: WorkerLoginDeviceInfo,
passwordWeak: boolean,
) {
const session = createWorkerSession(worker)
const deviceType = normalizeWorkerDeviceType(deviceInfo.deviceType)
const sessionRecord = await createWorkerSessionRecord({
sessionId: session.sessionId,
workerId: Number(worker.id),
deviceId: normalizeWorkerDeviceId(deviceInfo.deviceId, session.sessionId),
deviceType,
deviceName: normalizeWorkerDeviceName(deviceInfo.deviceName, deviceType),
userAgent: String(deviceInfo.userAgent || '')
.trim()
.slice(0, 500),
ipAddress: String(deviceInfo.ipAddress || '')
.trim()
.slice(0, 100),
issuedAt: new Date().toISOString(),
expiresAt: session.expiresAt,
now: nowIso(),
maxDevices: WORKER_MAX_DEVICES,
})
if (!sessionRecord.created) {
throw createHttpError('在线设备已达到 3 台,请先删除旧的登录设备', {
statusCode: 409,
errorCode: 'worker_device_limit',
})
}
return { ...session, passwordWeak }
}
async function authenticateWorkerCredentials(
username: unknown,
password: unknown,
): Promise<WorkerUserRow> {
const normalizedUsername = normalizeUsername(username)
const normalizedPassword = normalizePassword(password)
if (!normalizedUsername || !normalizedPassword) {
throw createHttpError('缺少接单账号或密码', {
statusCode: 400,
errorCode: 'worker_credentials_required',
})
}
const worker =
(await getWorkerUserByUsername(normalizedUsername)) ||
(await getWorkerUserByPhone(normalizedUsername)) ||
(await getWorkerUserByDisplayName(normalizedUsername))
if (!worker || !verifyWorkerPassword(normalizedPassword, worker.password_hash)) {
throw createHttpError('账号或密码错误', {
statusCode: 401,
errorCode: 'worker_login_failed',
})
}
assertWorkerLoginAllowed(worker)
return worker
}
function createCredentialSession(worker: WorkerUserRow): WorkerSession {
return {
sessionId: '',
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt: '',
sessionVersion: normalizeSessionVersion(worker.session_version),
}
}
@@ -145,6 +145,12 @@ import {
normalizeWorkerDeviceType,
} from './worker-auth-policy.js'
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
import {
deleteWorkerSessionWithCredentials,
loginWorker,
loginWorkerBySmsCode,
manageWorkerSessions,
} from './worker-login-service.js'
import { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
import {
hashSmsCode,
@@ -175,6 +181,12 @@ export {
export type { WorkerSession } from './worker-session-context-service.js'
export { assertWorkerLoginAllowed } from './worker-auth-policy.js'
export { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
export {
deleteWorkerSessionWithCredentials,
loginWorker,
loginWorkerBySmsCode,
manageWorkerSessions,
} from './worker-login-service.js'
export {
isWorkOrderTimeoutPolicy,
normalizeWorkOrderTimeoutPolicy,
@@ -196,185 +208,6 @@ export {
verifyWorkerSessionToken,
} from './worker-session-auth-service.js'
const WORKER_MAX_DEVICES = 3
export async function loginWorker(
username: unknown,
password: unknown,
deviceInfo: {
deviceId?: unknown
deviceType?: unknown
deviceName?: unknown
userAgent?: unknown
ipAddress?: unknown
} = {},
) {
ensureWorkerAuthConfigured()
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)
const sessionRecord = await createWorkerSessionRecord({
sessionId: session.sessionId,
workerId: Number(worker.id),
deviceId,
deviceType,
deviceName: normalizeWorkerDeviceName(deviceInfo.deviceName, deviceType),
userAgent: String(deviceInfo.userAgent || '')
.trim()
.slice(0, 500),
ipAddress: String(deviceInfo.ipAddress || '')
.trim()
.slice(0, 100),
issuedAt: new Date().toISOString(),
expiresAt: session.expiresAt,
now: nowIso(),
maxDevices: WORKER_MAX_DEVICES,
})
if (!sessionRecord.created) {
throw createHttpError('在线设备已达到 3 台,请先删除旧的登录设备', {
statusCode: 409,
errorCode: 'worker_device_limit',
})
}
return {
...session,
passwordWeak,
}
}
export async function manageWorkerSessions(username: unknown, password: unknown) {
const worker = await authenticateWorkerCredentials(username, password)
return getWorkerSessions({
sessionId: '',
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt: '',
sessionVersion: normalizeSessionVersion(worker.session_version),
})
}
export async function deleteWorkerSessionWithCredentials(
username: unknown,
password: unknown,
targetSessionId: unknown,
) {
const worker = await authenticateWorkerCredentials(username, password)
return deleteWorkerSession(
{
sessionId: '',
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt: '',
sessionVersion: normalizeSessionVersion(worker.session_version),
},
targetSessionId,
)
}
async function authenticateWorkerCredentials(
username: unknown,
password: unknown,
): Promise<WorkerUserRow> {
const normalizedUsername = normalizeUsername(username)
const normalizedPassword = normalizePassword(password)
if (!normalizedUsername || !normalizedPassword) {
throw createHttpError('缺少接单账号或密码', {
statusCode: 400,
errorCode: 'worker_credentials_required',
})
}
const worker =
(await getWorkerUserByUsername(normalizedUsername)) ||
(await getWorkerUserByPhone(normalizedUsername)) ||
(await getWorkerUserByDisplayName(normalizedUsername))
if (!worker || !verifyWorkerPassword(normalizedPassword, worker.password_hash)) {
throw createHttpError('账号或密码错误', {
statusCode: 401,
errorCode: 'worker_login_failed',
})
}
assertWorkerLoginAllowed(worker)
return worker
}
export async function getWorkerProfile(session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const [