From f0d1da4e1aa0a5692fe36daa91b10fbf3a9685f1 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 16:37:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=89=93=E6=89=8B=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/worker-platform/index.ts | 3 + .../worker-credentials-service.ts | 65 +++++ .../worker-platform/worker-service.ts | 226 ++---------------- .../worker-session-auth-service.ts | 165 +++++++++++++ .../worker-platform/worker-session-view.ts | 16 ++ 5 files changed, 265 insertions(+), 210 deletions(-) create mode 100644 apps/backend/src/services/worker-platform/worker-credentials-service.ts create mode 100644 apps/backend/src/services/worker-platform/worker-session-auth-service.ts create mode 100644 apps/backend/src/services/worker-platform/worker-session-view.ts diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 0cb58bc7..5372483d 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -2,6 +2,9 @@ export * from './mappers.js' export * from './worker-session-context-service.js' export * from './worker-service.js' export * from './worker-auth-policy.js' +export * from './worker-session-view.js' +export * from './worker-session-auth-service.js' +export * from './worker-credentials-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-credentials-service.ts b/apps/backend/src/services/worker-platform/worker-credentials-service.ts new file mode 100644 index 00000000..aa21a623 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-credentials-service.ts @@ -0,0 +1,65 @@ +import { + revokeAllWorkerSessions, + updateWorkerPassword, +} from '../../repositories/worker-platform/index.js' +import type { JsonObject } from '../../types/json.js' +import { createHttpError } from '../../utils/http.js' +import { nowIso } from '../../utils/time.js' +import { + hashWorkerPassword, + mapWorkerUser, + normalizePassword, + normalizeSessionVersion, + validateWorkerPassword, + verifyWorkerPassword, +} from './mappers.js' +import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js' + +export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) { + const worker = await getRequiredWorker(session.workerId) + const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword) + const nextPassword = normalizePassword(payload.newPassword || payload.password) + + if (!currentPassword) { + throw createHttpError('请输入当前密码', { + statusCode: 400, + errorCode: 'worker_password_current_required', + }) + } + + if (!verifyWorkerPassword(currentPassword, worker.password_hash)) { + throw createHttpError('当前密码不正确', { + statusCode: 400, + errorCode: 'worker_password_current_invalid', + }) + } + + if (!nextPassword) { + throw createHttpError('请输入新密码', { + statusCode: 400, + errorCode: 'worker_password_new_required', + }) + } + + validateWorkerPassword(nextPassword) + if (currentPassword === nextPassword) { + throw createHttpError('新密码不能与当前密码一致', { + statusCode: 400, + errorCode: 'worker_password_unchanged', + }) + } + + const now = nowIso() + const updated = await updateWorkerPassword({ + workerId: worker.id, + passwordHash: hashWorkerPassword(nextPassword), + sessionVersion: normalizeSessionVersion(worker.session_version) + 1, + now, + }) + await revokeAllWorkerSessions(worker.id, now) + + return { + worker: mapWorkerUser(updated || worker), + reloginRequired: true, + } +} diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 11813f0f..674b3671 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -111,7 +111,6 @@ import { createWorkerSession, ensureWorkerAuthConfigured, hashWorkerPassword, - isIgnorableWorkerAuthError, mapCollectLookupOrder, mapFinanceRequest, mapWalletLedger, @@ -145,8 +144,6 @@ import { resolveRequirementFields, resolveWorkerPermissions, canWorkerAutoAcceptWholeOrder, - safeCompare, - signWorkerPayload, throwGrabWorkOrderFailure, validateWorkerPassword, isWorkerPasswordStrong, @@ -159,6 +156,14 @@ import { normalizeWorkerDeviceType, } from './worker-auth-policy.js' import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js' +import { changeWorkerPassword } from './worker-credentials-service.js' +import { + deleteWorkerSession, + getWorkerSessionSummary, + getWorkerSessions, + logoutWorkerSession, + verifyWorkerSessionToken, +} from './worker-session-auth-service.js' import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js' const WORKER_DAILY_WITHDRAW_LIMIT = 3 @@ -179,6 +184,14 @@ export { WORK_ORDER_TIMEOUT_POLICIES, } from './work-order-timeout-policy.js' export type { WorkOrderTimeoutPolicy } from './work-order-timeout-policy.js' +export { changeWorkerPassword } from './worker-credentials-service.js' +export { + deleteWorkerSession, + getWorkerSessionSummary, + getWorkerSessions, + logoutWorkerSession, + verifyWorkerSessionToken, +} from './worker-session-auth-service.js' const WORKER_MAX_DEVICES = 3 @@ -726,164 +739,6 @@ async function authenticateWorkerCredentials( return worker } -export async function logoutWorkerSession(token: unknown) { - const normalizedToken = String(token || '').trim() - if (!normalizedToken) { - return { success: true, revoked: false } - } - - try { - const session = await verifyWorkerSessionToken(normalizedToken) - return { - success: true, - revoked: await revokeWorkerSession(session.workerId, session.sessionId, nowIso()), - } - } catch (error) { - if (isIgnorableWorkerAuthError(error)) { - return { success: true, revoked: false } - } - throw error - } -} - -export async function verifyWorkerSessionToken(token: unknown): Promise { - ensureWorkerAuthConfigured() - const normalizedToken = String(token || '').trim() - if (!normalizedToken) { - throw createHttpError('未登录或登录已失效', { - statusCode: 401, - errorCode: 'worker_auth_required', - }) - } - - const parts = normalizedToken.split('.') - const encodedPayload = parts[0] - const signature = parts[1] - if ( - parts.length !== 2 || - !encodedPayload || - !signature || - !safeCompare(signature, signWorkerPayload(encodedPayload)) - ) { - throw createHttpError('接单登录态无效', { - statusCode: 401, - errorCode: 'worker_auth_invalid', - }) - } - - let payload: JsonObject - try { - payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) - } catch { - throw createHttpError('接单登录态无效', { - statusCode: 401, - errorCode: 'worker_auth_invalid', - }) - } - - if (String(payload.typ || '') !== 'worker') { - throw createHttpError('接单登录态无效', { - statusCode: 401, - errorCode: 'worker_auth_invalid', - }) - } - - const expiresAt = String(payload.exp || '').trim() - if (!expiresAt || Date.parse(expiresAt) <= Date.now()) { - throw createHttpError('登录已过期,请重新登录', { - statusCode: 401, - errorCode: 'worker_auth_expired', - }) - } - - const workerId = Number(payload.uid || 0) - const worker = await getWorkerUserById(workerId) - if (!worker || worker.status !== 'active') { - throw createHttpError('接单账号已不可用,请重新登录', { - statusCode: 401, - errorCode: 'worker_auth_user_invalid', - }) - } - - const currentSessionVersion = normalizeSessionVersion(worker.session_version) - if (Number(payload.ver || 0) !== currentSessionVersion) { - throw createHttpError('登录态已失效,请重新登录', { - statusCode: 401, - errorCode: 'worker_auth_stale', - }) - } - - const sessionId = String(payload.sid || '').trim() - const sessionRecord = sessionId ? await getWorkerSessionBySessionId(sessionId) : null - if ( - !sessionRecord || - Number(sessionRecord.worker_id) !== Number(worker.id) || - sessionRecord.status !== 'active' || - Date.parse(String(sessionRecord.expires_at || '')) <= Date.now() || - !(await touchWorkerSession(sessionId, nowIso())) - ) { - throw createHttpError('登录设备已被删除,请重新登录', { - statusCode: 401, - errorCode: 'worker_auth_device_revoked', - }) - } - - return { - sessionId, - workerId: Number(worker.id), - username: worker.username, - status: worker.status, - expiresAt, - sessionVersion: currentSessionVersion, - } -} - -export async function getWorkerSessions(session: WorkerSession) { - const sessions = await listWorkerSessions(session.workerId, nowIso()) - return { - maxDevices: WORKER_MAX_DEVICES, - items: sessions.map((item) => mapWorkerSession(item, session.sessionId)), - } -} - -export async function deleteWorkerSession(session: WorkerSession, targetSessionId: unknown) { - const normalizedTarget = String(targetSessionId || '').trim() - if (!normalizedTarget) { - throw createHttpError('缺少登录设备标识', { - statusCode: 400, - errorCode: 'worker_session_id_required', - }) - } - return { - deleted: await revokeWorkerSession(session.workerId, normalizedTarget, nowIso()), - } -} - -function mapWorkerSession(session: WorkerSessionRow, currentSessionId: string) { - return { - sessionId: session.session_id, - deviceId: session.device_id, - deviceType: session.device_type, - deviceName: session.device_name, - userAgent: session.user_agent, - ipAddress: session.ip_address, - issuedAt: session.issued_at, - lastSeenAt: session.last_seen_at, - expiresAt: session.expires_at, - current: session.session_id === currentSessionId, - } -} - -export async function getWorkerSessionSummary(token: unknown) { - const session = await verifyWorkerSessionToken(token) - const worker = await getRequiredWorker(session.workerId) - return { - authenticated: true, - expiresAt: session.expiresAt, - worker: mapWorkerUser(worker), - } -} - export async function getWorkerProfile(session: WorkerSession) { const worker = await getRequiredWorker(session.workerId) const [ @@ -1342,55 +1197,6 @@ function maskWithdrawalAccountNo(accountNo: string) { return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}` } -export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) { - const worker = await getRequiredWorker(session.workerId) - const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword) - const nextPassword = normalizePassword(payload.newPassword || payload.password) - - if (!currentPassword) { - throw createHttpError('请输入当前密码', { - statusCode: 400, - errorCode: 'worker_password_current_required', - }) - } - - if (!verifyWorkerPassword(currentPassword, worker.password_hash)) { - throw createHttpError('当前密码不正确', { - statusCode: 400, - errorCode: 'worker_password_current_invalid', - }) - } - - if (!nextPassword) { - throw createHttpError('请输入新密码', { - statusCode: 400, - errorCode: 'worker_password_new_required', - }) - } - - validateWorkerPassword(nextPassword) - - if (currentPassword === nextPassword) { - throw createHttpError('新密码不能与当前密码一致', { - statusCode: 400, - errorCode: 'worker_password_unchanged', - }) - } - - const updated = await updateWorkerPassword({ - workerId: worker.id, - passwordHash: hashWorkerPassword(nextPassword), - sessionVersion: normalizeSessionVersion(worker.session_version) + 1, - now: nowIso(), - }) - await revokeAllWorkerSessions(worker.id, nowIso()) - - return { - worker: mapWorkerUser(updated || worker), - reloginRequired: true, - } -} - export async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) { requireActiveWorkerSession(session) const page = normalizePage(query.page) diff --git a/apps/backend/src/services/worker-platform/worker-session-auth-service.ts b/apps/backend/src/services/worker-platform/worker-session-auth-service.ts new file mode 100644 index 00000000..f7e23119 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-session-auth-service.ts @@ -0,0 +1,165 @@ +import { + getWorkerSessionBySessionId, + getWorkerUserById, + listWorkerSessions, + revokeWorkerSession, + touchWorkerSession, +} from '../../repositories/worker-platform/index.js' +import type { JsonObject } from '../../types/json.js' +import { createHttpError } from '../../utils/http.js' +import { nowIso } from '../../utils/time.js' +import { + ensureWorkerAuthConfigured, + isIgnorableWorkerAuthError, + mapWorkerUser, + normalizeSessionVersion, + safeCompare, + signWorkerPayload, +} from './mappers.js' +import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js' +import { mapWorkerSession } from './worker-session-view.js' + +const WORKER_MAX_DEVICES = 3 + +export async function logoutWorkerSession(token: unknown) { + const normalizedToken = String(token || '').trim() + if (!normalizedToken) { + return { success: true, revoked: false } + } + + try { + const session = await verifyWorkerSessionToken(normalizedToken) + return { + success: true, + revoked: await revokeWorkerSession(session.workerId, session.sessionId, nowIso()), + } + } catch (error) { + if (isIgnorableWorkerAuthError(error)) { + return { success: true, revoked: false } + } + throw error + } +} + +export async function verifyWorkerSessionToken(token: unknown): Promise { + ensureWorkerAuthConfigured() + const normalizedToken = String(token || '').trim() + if (!normalizedToken) { + throw createHttpError('未登录或登录已失效', { + statusCode: 401, + errorCode: 'worker_auth_required', + }) + } + + const parts = normalizedToken.split('.') + const encodedPayload = parts[0] + const signature = parts[1] + if ( + parts.length !== 2 || + !encodedPayload || + !signature || + !safeCompare(signature, signWorkerPayload(encodedPayload)) + ) { + throw createHttpError('接单登录态无效', { + statusCode: 401, + errorCode: 'worker_auth_invalid', + }) + } + + let payload: JsonObject + try { + payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) + } catch { + throw createHttpError('接单登录态无效', { + statusCode: 401, + errorCode: 'worker_auth_invalid', + }) + } + + if (String(payload.typ || '') !== 'worker') { + throw createHttpError('接单登录态无效', { + statusCode: 401, + errorCode: 'worker_auth_invalid', + }) + } + + const expiresAt = String(payload.exp || '').trim() + if (!expiresAt || Date.parse(expiresAt) <= Date.now()) { + throw createHttpError('登录已过期,请重新登录', { + statusCode: 401, + errorCode: 'worker_auth_expired', + }) + } + + const workerId = Number(payload.uid || 0) + const worker = await getWorkerUserById(workerId) + if (!worker || worker.status !== 'active') { + throw createHttpError('接单账号已不可用,请重新登录', { + statusCode: 401, + errorCode: 'worker_auth_user_invalid', + }) + } + + const currentSessionVersion = normalizeSessionVersion(worker.session_version) + if (Number(payload.ver || 0) !== currentSessionVersion) { + throw createHttpError('登录态已失效,请重新登录', { + statusCode: 401, + errorCode: 'worker_auth_stale', + }) + } + + const sessionId = String(payload.sid || '').trim() + const sessionRecord = sessionId ? await getWorkerSessionBySessionId(sessionId) : null + if ( + !sessionRecord || + Number(sessionRecord.worker_id) !== Number(worker.id) || + sessionRecord.status !== 'active' || + Date.parse(String(sessionRecord.expires_at || '')) <= Date.now() || + !(await touchWorkerSession(sessionId, nowIso())) + ) { + throw createHttpError('登录设备已被删除,请重新登录', { + statusCode: 401, + errorCode: 'worker_auth_device_revoked', + }) + } + + return { + sessionId, + workerId: Number(worker.id), + username: worker.username, + status: worker.status, + expiresAt, + sessionVersion: currentSessionVersion, + } +} + +export async function getWorkerSessions(session: WorkerSession) { + const sessions = await listWorkerSessions(session.workerId, nowIso()) + return { + maxDevices: WORKER_MAX_DEVICES, + items: sessions.map((item) => mapWorkerSession(item, session.sessionId)), + } +} + +export async function deleteWorkerSession(session: WorkerSession, targetSessionId: unknown) { + const normalizedTarget = String(targetSessionId || '').trim() + if (!normalizedTarget) { + throw createHttpError('缺少登录设备标识', { + statusCode: 400, + errorCode: 'worker_session_id_required', + }) + } + return { + deleted: await revokeWorkerSession(session.workerId, normalizedTarget, nowIso()), + } +} + +export async function getWorkerSessionSummary(token: unknown) { + const session = await verifyWorkerSessionToken(token) + const worker = await getRequiredWorker(session.workerId) + return { + authenticated: true, + expiresAt: session.expiresAt, + worker: mapWorkerUser(worker), + } +} diff --git a/apps/backend/src/services/worker-platform/worker-session-view.ts b/apps/backend/src/services/worker-platform/worker-session-view.ts new file mode 100644 index 00000000..e18091aa --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-session-view.ts @@ -0,0 +1,16 @@ +import type { WorkerSessionRow } from '../../repositories/worker-platform/index.js' + +export function mapWorkerSession(session: WorkerSessionRow, currentSessionId: string) { + return { + sessionId: session.session_id, + deviceId: session.device_id, + deviceType: session.device_type, + deviceName: session.device_name, + userAgent: session.user_agent, + ipAddress: session.ip_address, + issuedAt: session.issued_at, + lastSeenAt: session.last_seen_at, + expiresAt: session.expires_at, + current: session.session_id === currentSessionId, + } +}