import crypto from 'node:crypto' import { runtimeConfig } from '../../config/runtime.js' import { countActiveAdminUsers, createAdminUser, getAdminUserById, getAdminUserByUsername, listAdminUsers, replaceAdminUserInventoryGroupBindings, updateAdminUser, } from '../../repositories/admin-user-repo.js' import { addHours, nowIso } from '../../utils/time.js' import { createHttpError } from '../../utils/http.js' import { normalizePage, normalizePageSize } from './admin-query-utils.js' export async function ensureAdminUsersBootstrapped() { ensureAdminAuthConfigured() const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] for (const configuredUser of configuredUsers) { const username = String(configuredUser?.username || '').trim().toLowerCase() const password = String(configuredUser?.password || '').trim() const role = normalizeAdminRole(configuredUser?.role) if (!username || !password) { continue } if (await getAdminUserByUsername(username)) { continue } const now = nowIso() await createAdminUser({ username, passwordHash: hashAdminPassword(password), role, status: 'active', createdAt: now, updatedAt: now, }) } } export async function loginAdmin(username, password) { ensureAdminAuthConfigured() const normalizedUsername = String(username || '').trim().toLowerCase() const normalizedPassword = String(password || '').trim() if (!normalizedUsername || !normalizedPassword) { throw createHttpError('缺少后台账号或密码', { statusCode: 400, errorCode: 'admin_credentials_required', }) } const user = await getAdminUserByUsername(normalizedUsername) if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) { throw createHttpError('账号或密码错误', { statusCode: 401, errorCode: 'admin_login_failed', }) } return createAdminSession(user) } export async function verifyAdminSessionToken(token) { ensureAdminAuthConfigured() const normalizedToken = String(token || '').trim() if (!normalizedToken) { throw createHttpError('未登录或登录已失效', { statusCode: 401, errorCode: 'admin_auth_required', }) } const [encodedPayload, signature] = normalizedToken.split('.') if (!encodedPayload || !signature) { throw createHttpError('后台登录态无效', { statusCode: 401, errorCode: 'admin_auth_invalid', }) } const expectedSignature = signPayload(encodedPayload) if (!safeCompare(signature, expectedSignature)) { throw createHttpError('后台登录态无效', { statusCode: 401, errorCode: 'admin_auth_invalid', }) } let payload = null try { payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) } catch { throw createHttpError('后台登录态无效', { statusCode: 401, errorCode: 'admin_auth_invalid', }) } const expiresAt = String(payload?.exp || '').trim() if (!expiresAt || Date.parse(expiresAt) <= Date.now()) { throw createHttpError('后台登录已过期,请重新登录', { statusCode: 401, errorCode: 'admin_auth_expired', }) } const userId = Number(payload?.uid || 0) const user = await getAdminUserById(userId) if (!user || user.status !== 'active') { throw createHttpError('后台账号已不可用,请重新登录', { statusCode: 401, errorCode: 'admin_auth_user_invalid', }) } return { sessionId: String(payload?.sid || '').trim(), userId: Number(user.id), username: String(user.username || ''), role: normalizeAdminRole(user.role), allowedInventoryGroups: normalizeInventoryGroupCodes(user.inventory_group_codes), expiresAt, } } export async function getAdminSessionSummary(token) { const session = await verifyAdminSessionToken(token) return { authenticated: true, expiresAt: session.expiresAt, user: { userId: session.userId, username: session.username, role: session.role, inventoryGroupCodes: session.allowedInventoryGroups, }, } } export function requireAdminRole(session, allowedRoles) { if (allowedRoles.includes(session.role)) { return } throw createHttpError('当前账号没有此操作权限', { statusCode: 403, errorCode: 'admin_permission_denied', }) } export async function getAdminUserList(query = {}) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const { items, total } = await listAdminUsers({ page, pageSize, username: String(query.username || '').trim(), role: normalizeRoleQuery(query.role), status: normalizeStatusQuery(query.status), }) return { items: items.map(mapAdminUser), pagination: { page, pageSize, total }, } } export async function createManagedAdminUser(payload = {}) { const username = normalizeUsername(payload.username) const password = normalizePassword(payload.password) if (!username) { throw createHttpError('缺少后台账号', { statusCode: 400, errorCode: 'admin_user_username_required', }) } if (!password) { throw createHttpError('缺少后台密码', { statusCode: 400, errorCode: 'admin_user_password_required', }) } validateUsername(username) validatePassword(password) if (await getAdminUserByUsername(username)) { throw createHttpError('后台账号已存在', { statusCode: 409, errorCode: 'admin_user_exists', }) } const now = nowIso() const created = await createAdminUser({ username, passwordHash: hashAdminPassword(password), role: normalizeAdminRole(payload.role), status: normalizeAdminUserStatus(payload.status || 'active'), createdAt: now, updatedAt: now, }) return { user: mapAdminUser(created), } } export async function updateManagedAdminUserInventoryGroups(userId, payload = {}, session) { const user = await getRequiredAdminUser(userId) const inventoryGroupCodes = normalizeInventoryGroupCodes(payload.inventoryGroupCodes) await ensureAdminUserChangeAllowed(user, {}, session) const updated = await replaceAdminUserInventoryGroupBindings(user.id, inventoryGroupCodes, nowIso()) return { user: mapAdminUser(updated), } } export async function updateManagedAdminUserRole(userId, payload = {}, session) { const user = await getRequiredAdminUser(userId) const role = normalizeAdminRole(payload.role) if (user.role === role) { return { user: mapAdminUser(user), } } await ensureAdminUserChangeAllowed(user, { nextRole: role }, session) const updated = await updateAdminUser(user.id, { role, updated_at: nowIso(), }) return { user: mapAdminUser(updated), } } export async function updateManagedAdminUserStatus(userId, payload = {}, session) { const user = await getRequiredAdminUser(userId) const status = normalizeAdminUserStatus(payload.status) if (user.status === status) { return { user: mapAdminUser(user), } } await ensureAdminUserChangeAllowed(user, { nextStatus: status }, session) const updated = await updateAdminUser(user.id, { status, updated_at: nowIso(), }) return { user: mapAdminUser(updated), } } export async function resetManagedAdminUserPassword(userId, payload = {}) { const user = await getRequiredAdminUser(userId) const password = normalizePassword(payload.password) if (!password) { throw createHttpError('缺少新密码', { statusCode: 400, errorCode: 'admin_user_password_required', }) } validatePassword(password) const updated = await updateAdminUser(user.id, { password_hash: hashAdminPassword(password), updated_at: nowIso(), }) return { user: mapAdminUser(updated), } } export function ensureAdminAuthConfigured() { const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim() const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] if (sessionSecret && configuredUsers.length > 0) { return } throw createHttpError('后台鉴权未配置,请先设置 admin.sessionSecret 和 admin.defaultUsers', { statusCode: 503, errorCode: 'admin_auth_not_configured', }) } function createAdminSession(user) { const issuedAt = nowIso() const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin.sessionTtlHours || 12)) const payload = { sid: crypto.randomBytes(12).toString('hex'), uid: Number(user.id), usr: String(user.username || ''), role: normalizeAdminRole(user.role), iat: issuedAt, exp: expiresAt, } const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url') const signature = signPayload(encodedPayload) return { token: `${encodedPayload}.${signature}`, expiresAt, user: { userId: Number(user.id), username: String(user.username || ''), role: normalizeAdminRole(user.role), inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes), }, } } export function hashAdminPassword(password) { const salt = crypto.randomBytes(16).toString('hex') const derived = crypto.scryptSync(password, salt, 64).toString('hex') return `scrypt$${salt}$${derived}` } function verifyAdminPassword(password, storedHash) { const [algorithm, salt, expectedHash] = String(storedHash || '').split('$') if (algorithm !== 'scrypt' || !salt || !expectedHash) { return false } const actualHash = crypto.scryptSync(password, salt, 64).toString('hex') return safeCompare(actualHash, expectedHash) } function signPayload(encodedPayload) { return crypto .createHmac('sha256', String(runtimeConfig.admin.sessionSecret || '')) .update(encodedPayload) .digest('base64url') } export function normalizeAdminRole(role) { const normalized = String(role || '').trim().toLowerCase() if (normalized === 'admin') { return 'admin' } if (normalized === 'support') { return 'support' } return 'operator' } export function normalizeAdminUserStatus(status) { return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active' } function safeCompare(input, expected) { const left = Buffer.from(String(input || ''), 'utf8') const right = Buffer.from(String(expected || ''), 'utf8') if (left.length !== right.length) { return false } return crypto.timingSafeEqual(left, right) } function normalizeRoleQuery(role) { const normalized = String(role || '').trim().toLowerCase() return ['admin', 'operator', 'support'].includes(normalized) ? normalized : '' } function normalizeStatusQuery(status) { const normalized = String(status || '').trim().toLowerCase() return ['active', 'disabled'].includes(normalized) ? normalized : '' } function normalizeUsername(username) { return String(username || '').trim().toLowerCase() } function normalizePassword(password) { return String(password || '').trim() } function validateUsername(username) { if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) { throw createHttpError('后台账号格式无效,需为 3-32 位字母数字或 ._-', { statusCode: 400, errorCode: 'admin_user_username_invalid', }) } } function validatePassword(password) { if (password.length < 8) { throw createHttpError('后台密码至少 8 位', { statusCode: 400, errorCode: 'admin_user_password_invalid', }) } } async function getRequiredAdminUser(userId) { const user = await getAdminUserById(Number(userId)) if (!user) { throw createHttpError('后台用户不存在', { statusCode: 404, errorCode: 'admin_user_not_found', }) } return user } async function ensureAdminUserChangeAllowed(user, options = {}, session) { const nextRole = options.nextRole || user.role const nextStatus = options.nextStatus || user.status if (session?.userId === Number(user.id) && (nextRole !== 'admin' || nextStatus !== 'active')) { throw createHttpError('不能停用或降级当前登录账号', { statusCode: 409, errorCode: 'admin_user_self_change_not_allowed', }) } if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) { throw createHttpError('至少保留一个启用中的管理员账号', { statusCode: 409, errorCode: 'admin_user_last_admin_not_allowed', }) } } function mapAdminUser(user) { return { userId: Number(user.id), username: String(user.username || ''), role: normalizeAdminRole(user.role), status: normalizeAdminUserStatus(user.status), inventoryGroupCodes: normalizeInventoryGroupCodes(user.inventory_group_codes), createdAt: user.created_at, updatedAt: user.updated_at, } } function normalizeInventoryGroupCodes(values) { return Array.from(new Set((Array.isArray(values) ? values : []) .map((value) => String(value || '').trim()) .filter(Boolean))) }