拆分打手会话服务
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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<WorkerSession> {
|
||||
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)
|
||||
|
||||
@@ -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<WorkerSession> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user