增加打手登录设备管理与三台设备限制
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
updateWorkOrder,
|
||||
updateWorkOrderBasic,
|
||||
updateWorkerUser,
|
||||
revokeAllWorkerSessions,
|
||||
upsertWorkerWithdrawalAccount,
|
||||
upsertWorkCategory,
|
||||
upsertWorkProductRule,
|
||||
@@ -406,6 +407,7 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
||||
reviewed_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
await revokeAllWorkerSessions(worker.id, now)
|
||||
return { worker: mapWorkerUser(updated || worker) }
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export function createWorkerSession(worker: WorkerUserRow) {
|
||||
}
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
return {
|
||||
sessionId: payload.sid,
|
||||
token: `${encodedPayload}.${signWorkerPayload(encodedPayload)}`,
|
||||
expiresAt,
|
||||
worker: mapWorkerUser(worker),
|
||||
@@ -1055,6 +1056,7 @@ export const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
|
||||
'worker_auth_expired',
|
||||
'worker_auth_user_invalid',
|
||||
'worker_auth_stale',
|
||||
'worker_auth_device_revoked',
|
||||
])
|
||||
|
||||
export type WorkOrderEventView = {
|
||||
|
||||
@@ -30,7 +30,12 @@ import {
|
||||
getWorkOrderShare,
|
||||
getWorkerOrderOverview,
|
||||
grabWorkOrder,
|
||||
incrementWorkerSessionVersion,
|
||||
createWorkerSessionRecord,
|
||||
getWorkerSessionBySessionId,
|
||||
listWorkerSessions,
|
||||
revokeWorkerSession,
|
||||
revokeAllWorkerSessions,
|
||||
touchWorkerSession,
|
||||
joinWorkOrderShare,
|
||||
listPendingMaterialWorkOrdersByPlatformOrderId,
|
||||
listWorkerFinanceRequests,
|
||||
@@ -60,6 +65,7 @@ import {
|
||||
upsertWorkerLevel,
|
||||
type WorkOrderRow,
|
||||
type WorkOrderShareRow,
|
||||
type WorkerSessionRow,
|
||||
type WorkerUserRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
@@ -142,6 +148,8 @@ export type WorkerSession = {
|
||||
sessionVersion: number
|
||||
}
|
||||
|
||||
const WORKER_MAX_DEVICES = 3
|
||||
|
||||
export async function ensureWorkerPlatformDefaults() {
|
||||
const now = nowIso()
|
||||
const existingLevel = await getWorkerLevelByKey(DEFAULT_LEVEL_KEY)
|
||||
@@ -362,11 +370,91 @@ async function generateInviteCode(_now: string): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function loginWorker(username: unknown, password: unknown) {
|
||||
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)
|
||||
|
||||
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: !isWorkerPasswordStrong(normalizedPassword),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -384,13 +472,8 @@ export async function loginWorker(username: unknown, password: unknown) {
|
||||
errorCode: 'worker_login_failed',
|
||||
})
|
||||
}
|
||||
|
||||
assertWorkerLoginAllowed(worker)
|
||||
|
||||
return {
|
||||
...createWorkerSession(worker),
|
||||
passwordWeak: !isWorkerPasswordStrong(normalizedPassword),
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
export function assertWorkerLoginAllowed(worker: WorkerUserRow) {
|
||||
@@ -426,10 +509,9 @@ export async function logoutWorkerSession(token: unknown) {
|
||||
|
||||
try {
|
||||
const session = await verifyWorkerSessionToken(normalizedToken)
|
||||
const revokedWorker = await incrementWorkerSessionVersion(session.workerId, nowIso())
|
||||
return {
|
||||
success: true,
|
||||
revoked: Boolean(revokedWorker),
|
||||
revoked: await revokeWorkerSession(session.workerId, session.sessionId, nowIso()),
|
||||
}
|
||||
} catch (error) {
|
||||
if (isIgnorableWorkerAuthError(error)) {
|
||||
@@ -499,8 +581,23 @@ export async function verifyWorkerSessionToken(token: unknown): Promise<WorkerSe
|
||||
})
|
||||
}
|
||||
|
||||
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: String(payload.sid || '').trim(),
|
||||
sessionId,
|
||||
workerId: Number(worker.id),
|
||||
username: worker.username,
|
||||
status: worker.status,
|
||||
@@ -509,6 +606,59 @@ export async function verifyWorkerSessionToken(token: unknown): Promise<WorkerSe
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorkerDeviceType(value: unknown): string {
|
||||
const type = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return type === 'mobile' || type === 'tablet' ? type : 'pc'
|
||||
}
|
||||
|
||||
function normalizeWorkerDeviceId(value: unknown, fallback: string): string {
|
||||
const deviceId = String(value || '').trim()
|
||||
return (deviceId || fallback).slice(0, 200)
|
||||
}
|
||||
|
||||
function normalizeWorkerDeviceName(value: unknown, deviceType: string): string {
|
||||
const name = String(value || '').trim()
|
||||
return (name || (deviceType === 'pc' ? 'PC 浏览器' : '移动设备')).slice(0, 100)
|
||||
}
|
||||
|
||||
export async function getWorkerSessionSummary(token: unknown) {
|
||||
const session = await verifyWorkerSessionToken(token)
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
@@ -953,6 +1103,7 @@ export async function changeWorkerPassword(payload: JsonObject = {}, session: Wo
|
||||
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
|
||||
now: nowIso(),
|
||||
})
|
||||
await revokeAllWorkerSessions(worker.id, nowIso())
|
||||
|
||||
return {
|
||||
worker: mapWorkerUser(updated || worker),
|
||||
|
||||
Reference in New Issue
Block a user