diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index d170a019..3901fcbc 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -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' diff --git a/apps/backend/src/services/worker-platform/worker-login-service.ts b/apps/backend/src/services/worker-platform/worker-login-service.ts new file mode 100644 index 00000000..c6de2243 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-login-service.ts @@ -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 { + 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), + } +} diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 11f421f4..5aae8eb7 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -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 { - 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 [