拆分打手登录服务
This commit is contained in:
@@ -7,6 +7,7 @@ export * from './worker-session-auth-service.js'
|
|||||||
export * from './worker-credentials-service.js'
|
export * from './worker-credentials-service.js'
|
||||||
export * from './worker-sms-service.js'
|
export * from './worker-sms-service.js'
|
||||||
export * from './worker-registration-service.js'
|
export * from './worker-registration-service.js'
|
||||||
|
export * from './worker-login-service.js'
|
||||||
export * from './worker-platform-defaults.js'
|
export * from './worker-platform-defaults.js'
|
||||||
export * from './work-order-timeout-policy.js'
|
export * from './work-order-timeout-policy.js'
|
||||||
export * from './after-sales-service.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,
|
normalizeWorkerDeviceType,
|
||||||
} from './worker-auth-policy.js'
|
} from './worker-auth-policy.js'
|
||||||
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.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 { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
|
||||||
import {
|
import {
|
||||||
hashSmsCode,
|
hashSmsCode,
|
||||||
@@ -175,6 +181,12 @@ export {
|
|||||||
export type { WorkerSession } from './worker-session-context-service.js'
|
export type { WorkerSession } from './worker-session-context-service.js'
|
||||||
export { assertWorkerLoginAllowed } from './worker-auth-policy.js'
|
export { assertWorkerLoginAllowed } from './worker-auth-policy.js'
|
||||||
export { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
|
export { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
|
||||||
|
export {
|
||||||
|
deleteWorkerSessionWithCredentials,
|
||||||
|
loginWorker,
|
||||||
|
loginWorkerBySmsCode,
|
||||||
|
manageWorkerSessions,
|
||||||
|
} from './worker-login-service.js'
|
||||||
export {
|
export {
|
||||||
isWorkOrderTimeoutPolicy,
|
isWorkOrderTimeoutPolicy,
|
||||||
normalizeWorkOrderTimeoutPolicy,
|
normalizeWorkOrderTimeoutPolicy,
|
||||||
@@ -196,185 +208,6 @@ export {
|
|||||||
verifyWorkerSessionToken,
|
verifyWorkerSessionToken,
|
||||||
} from './worker-session-auth-service.js'
|
} 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) {
|
export async function getWorkerProfile(session: WorkerSession) {
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const [
|
const [
|
||||||
|
|||||||
Reference in New Issue
Block a user