130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
// @ts-check
|
|
|
|
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
|
|
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
|
|
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
|
|
import { logWarn } from '../../utils/logger.js'
|
|
|
|
/** @typedef {import('express').NextFunction} NextFunction */
|
|
/** @typedef {import('express').Request} ExpressRequest */
|
|
/** @typedef {import('express').Response} ExpressResponse */
|
|
/** @typedef {import('../../types/admin-route-inputs.js').AdminRouteAdminSession} AdminRouteAdminSession */
|
|
/** @typedef {ExpressRequest & { adminSession?: AdminRouteAdminSession | null }} AdminExpressRequest */
|
|
|
|
/**
|
|
* @typedef {{
|
|
* action?: string
|
|
* targetType?: string
|
|
* targetId?: string
|
|
* data?: Record<string, unknown>
|
|
* }} AdminAuditPayload
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* successMessage?: string
|
|
* errorMessage?: string
|
|
* scope?: string
|
|
* audit?: (req: AdminExpressRequest, data: unknown) => (AdminAuditPayload | null | undefined)
|
|
* }} AdminJsonHandlerOptions
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* errorMessage?: string
|
|
* scope?: string
|
|
* }} AdminFileHandlerOptions
|
|
*/
|
|
|
|
/**
|
|
* @param {(req: AdminExpressRequest, res: ExpressResponse) => unknown | Promise<unknown>} action
|
|
* @param {AdminJsonHandlerOptions} [options]
|
|
*/
|
|
export function createJsonHandler(action, { successMessage = 'ok', errorMessage, scope, audit } = {}) {
|
|
return async (req, res) => {
|
|
try {
|
|
const data = await action(req, res)
|
|
await recordAdminAudit(req.adminSession, req, data, audit)
|
|
res.json(buildSuccessPayload(data, successMessage))
|
|
} catch (error) {
|
|
sendRouteError(res, error, errorMessage, scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {(req: AdminExpressRequest, res: ExpressResponse) => string | Promise<string>} action
|
|
* @param {AdminFileHandlerOptions} [options]
|
|
*/
|
|
export function createFileHandler(action, { errorMessage, scope } = {}) {
|
|
return async (req, res) => {
|
|
try {
|
|
const filePath = await action(req, res)
|
|
res.sendFile(filePath)
|
|
} catch (error) {
|
|
sendRouteError(res, error, errorMessage, scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param {AdminExpressRequest} req */
|
|
/** @param {ExpressResponse} res */
|
|
/** @param {NextFunction} next */
|
|
export async function requireAdminSession(req, res, next) {
|
|
try {
|
|
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
|
|
next()
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth]')
|
|
}
|
|
}
|
|
|
|
/** @param {string[]} allowedRoles */
|
|
export function requireAdminRoles(allowedRoles) {
|
|
return (req, res, next) => {
|
|
try {
|
|
requireAdminRole(req.adminSession, allowedRoles)
|
|
next()
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/role]')
|
|
}
|
|
}
|
|
}
|
|
|
|
async function recordAdminAudit(session, req, data, audit) {
|
|
if (!audit) {
|
|
return
|
|
}
|
|
|
|
const payload = typeof audit === 'function' ? audit(req, data) : audit
|
|
|
|
if (!payload) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await writeAdminAuditLog(session, payload)
|
|
} catch (error) {
|
|
logWarn('[admin/audit]', '后台审计日志写入失败,已跳过', {
|
|
action: payload.action,
|
|
targetType: payload.targetType,
|
|
targetId: payload.targetId,
|
|
error,
|
|
})
|
|
}
|
|
}
|
|
|
|
export function extractBearerToken(req) {
|
|
const authorization = String(req.headers.authorization || '').trim()
|
|
const matched = authorization.match(/^Bearer\s+(.+)$/i)
|
|
|
|
if (!matched) {
|
|
throw createHttpError('未登录或登录已失效', {
|
|
statusCode: 401,
|
|
errorCode: 'admin_auth_required',
|
|
})
|
|
}
|
|
|
|
return matched[1]
|
|
}
|