优化了一些文件 增加多店铺

This commit is contained in:
yml
2026-04-09 01:31:47 +08:00
parent c2ec3aa6d2
commit e4ab1f559e
43 changed files with 1839 additions and 91 deletions
@@ -0,0 +1,83 @@
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
import { nowIso } from '../../utils/time.js'
export function writeAdminAuditLog(session, payload = {}) {
if (!session?.userId) {
return null
}
return createAdminAuditLog({
actorUserId: session.userId,
actorUsername: session.username,
actorRole: session.role,
action: String(payload.action || '').trim(),
targetType: String(payload.targetType || '').trim() || 'unknown',
targetId: String(payload.targetId || '').trim(),
payloadJson: JSON.stringify(payload.data || {}),
createdAt: nowIso(),
})
}
export function getAdminAuditLogs(query = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = listAdminAuditLogs({
page,
pageSize,
actorUsername: String(query.actorUsername || '').trim(),
action: String(query.action || '').trim(),
targetType: String(query.targetType || '').trim(),
dateFrom: normalizeDateQuery(query.dateFrom),
dateTo: normalizeDateQuery(query.dateTo, true),
})
return {
items: items.map((item) => ({
logId: item.id,
actorUserId: item.actor_user_id,
actorUsername: item.actor_username,
actorRole: item.actor_role,
action: item.action,
targetType: item.target_type,
targetId: item.target_id,
payload: safeParseJson(item.payload_json),
createdAt: item.created_at,
})),
pagination: { page, pageSize, total },
}
}
function normalizePage(rawValue) {
const parsed = Number(rawValue)
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1
}
function normalizePageSize(rawValue) {
const parsed = Number(rawValue)
if (!Number.isFinite(parsed) || parsed <= 0) {
return 20
}
return Math.min(100, Math.floor(parsed))
}
function normalizeDateQuery(rawValue, endOfDay = false) {
const normalized = String(rawValue || '').trim()
if (!normalized) {
return ''
}
if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
return endOfDay ? `${normalized}T23:59:59.999Z` : `${normalized}T00:00:00.000Z`
}
return normalized
}
function safeParseJson(value) {
try {
return JSON.parse(String(value || '{}'))
} catch {
return {}
}
}
@@ -0,0 +1,453 @@
import crypto from 'node:crypto'
import { runtimeConfig } from '../../config/runtime.js'
import {
countActiveAdminUsers,
createAdminUser,
getAdminUserById,
getAdminUserByUsername,
listAdminUsers,
updateAdminUser,
} from '../../repositories/admin-user-repo.js'
import { addHours, nowIso } from '../../utils/time.js'
import { createHttpError } from '../../utils/http.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()
const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role)
if (!username || !password) {
continue
}
if (getAdminUserByUsername(username)) {
continue
}
const now = nowIso()
createAdminUser({
username,
passwordHash: hashAdminPassword(password),
role,
status: 'active',
createdAt: now,
updatedAt: now,
})
}
}
export function loginAdmin(username, password) {
ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim()
const normalizedPassword = String(password || '').trim()
if (!normalizedUsername || !normalizedPassword) {
throw createHttpError('缺少后台账号或密码', {
statusCode: 400,
errorCode: 'admin_credentials_required',
})
}
const user = getAdminUserByUsername(normalizedUsername)
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) {
throw createHttpError('账号或密码错误', {
statusCode: 401,
errorCode: 'admin_login_failed',
})
}
return createAdminSession(user)
}
export 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 = getAdminUserById(userId)
if (!user || user.status !== 'active') {
throw createHttpError('后台账号已不可用,请重新登录', {
statusCode: 401,
errorCode: 'admin_auth_user_invalid',
})
}
return {
sessionId: String(payload.sid || '').trim(),
userId: user.id,
username: user.username,
role: normalizeAdminRole(user.role),
expiresAt,
}
}
export function getAdminSessionSummary(token) {
const session = verifyAdminSessionToken(token)
return {
authenticated: true,
expiresAt: session.expiresAt,
user: {
userId: session.userId,
username: session.username,
role: session.role,
},
}
}
export function requireAdminRole(session, allowedRoles) {
if (allowedRoles.includes(session.role)) {
return
}
throw createHttpError('当前账号没有此操作权限', {
statusCode: 403,
errorCode: 'admin_permission_denied',
})
}
export function getAdminUserList(query = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = 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 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 (getAdminUserByUsername(username)) {
throw createHttpError('后台账号已存在', {
statusCode: 409,
errorCode: 'admin_user_exists',
})
}
const now = nowIso()
const created = createAdminUser({
username,
passwordHash: hashAdminPassword(password),
role: normalizeAdminRole(payload.role),
status: normalizeAdminUserStatus(payload.status || 'active'),
createdAt: now,
updatedAt: now,
})
return {
user: mapAdminUser(created),
}
}
export function updateManagedAdminUserRole(userId, payload = {}, session) {
const user = getRequiredAdminUser(userId)
const role = normalizeAdminRole(payload.role)
if (user.role === role) {
return {
user: mapAdminUser(user),
}
}
ensureAdminUserChangeAllowed(user, { nextRole: role }, session)
const updated = updateAdminUser(user.id, {
role,
updated_at: nowIso(),
})
return {
user: mapAdminUser(updated),
}
}
export function updateManagedAdminUserStatus(userId, payload = {}, session) {
const user = getRequiredAdminUser(userId)
const status = normalizeAdminUserStatus(payload.status)
if (user.status === status) {
return {
user: mapAdminUser(user),
}
}
ensureAdminUserChangeAllowed(user, { nextStatus: status }, session)
const updated = updateAdminUser(user.id, {
status,
updated_at: nowIso(),
})
return {
user: mapAdminUser(updated),
}
}
export function resetManagedAdminUserPassword(userId, payload = {}) {
const user = getRequiredAdminUser(userId)
const password = normalizePassword(payload.password)
if (!password) {
throw createHttpError('缺少新密码', {
statusCode: 400,
errorCode: 'admin_user_password_required',
})
}
validatePassword(password)
const updated = 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: user.id,
usr: 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: user.id,
username: user.username,
role: normalizeAdminRole(user.role),
},
}
}
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) {
return String(role || '').trim().toLowerCase() === 'admin' ? 'admin' : '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 normalizePage(rawValue) {
const parsed = Number(rawValue)
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1
}
function normalizePageSize(rawValue) {
const parsed = Number(rawValue)
if (!Number.isFinite(parsed) || parsed <= 0) {
return 20
}
return Math.min(100, Math.floor(parsed))
}
function normalizeRoleQuery(role) {
const normalized = String(role || '').trim().toLowerCase()
return ['admin', 'operator'].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',
})
}
}
function getRequiredAdminUser(userId) {
const user = getAdminUserById(Number(userId))
if (!user) {
throw createHttpError('后台用户不存在', {
statusCode: 404,
errorCode: 'admin_user_not_found',
})
}
return user
}
function ensureAdminUserChangeAllowed(user, options = {}, session) {
const nextRole = options.nextRole || user.role
const nextStatus = options.nextStatus || user.status
if (session?.userId === 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') && countActiveAdminUsers() <= 1) {
throw createHttpError('至少保留一个启用中的管理员账号', {
statusCode: 409,
errorCode: 'admin_user_last_admin_not_allowed',
})
}
}
function mapAdminUser(user) {
return {
userId: user.id,
username: user.username,
role: normalizeAdminRole(user.role),
status: normalizeAdminUserStatus(user.status),
createdAt: user.created_at,
updatedAt: user.updated_at,
}
}
File diff suppressed because it is too large Load Diff