拆分打手登录服务
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-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'
|
||||
|
||||
@@ -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,
|
||||
} 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<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) {
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
const [
|
||||
|
||||
Reference in New Issue
Block a user