- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移) - 接单平台全部管理路由从固定角色切换为细粒度权限校验 - 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限 - 管理员账号页新增角色权限配置卡片(admin专属,带审计日志) - 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
import type { Request, Response, NextFunction } from 'express'
|
|
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'
|
|
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
|
|
|
|
type AdminAuditPayload = {
|
|
action?: string
|
|
targetType?: string
|
|
targetId?: string
|
|
data?: Record<string, unknown>
|
|
}
|
|
|
|
type AdminJsonHandlerOptions = {
|
|
successMessage?: string
|
|
errorMessage?: string
|
|
scope?: string
|
|
audit?: (req: Request, data: unknown) => AdminAuditPayload | null | undefined
|
|
}
|
|
|
|
type AdminFileHandlerOptions = {
|
|
errorMessage?: string
|
|
scope?: string
|
|
}
|
|
|
|
export function createJsonHandler(
|
|
action: (req: Request, res: Response) => unknown | Promise<unknown>,
|
|
{ successMessage = 'ok', errorMessage, scope, audit }: AdminJsonHandlerOptions = {},
|
|
) {
|
|
return async (req: Request, res: Response): Promise<void> => {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createFileHandler(
|
|
action: (req: Request, res: Response) => string | Promise<string>,
|
|
{ errorMessage, scope }: AdminFileHandlerOptions = {},
|
|
) {
|
|
return async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const filePath = await action(req, res)
|
|
res.sendFile(filePath)
|
|
} catch (error) {
|
|
sendRouteError(res, error, errorMessage, scope)
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function requireAdminSession(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
): Promise<void> {
|
|
try {
|
|
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
|
|
next()
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth]')
|
|
}
|
|
}
|
|
|
|
export function requireAdminRoles(allowedRoles: string[]) {
|
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
try {
|
|
requireAdminRole(req.adminSession, allowedRoles)
|
|
next()
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/role]')
|
|
}
|
|
}
|
|
}
|
|
|
|
export function requireAdminPermission(permission: string) {
|
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
try {
|
|
const session = getRequiredAdminSession(req)
|
|
if (!session.permissions.includes(String(permission || '').trim())) {
|
|
throw createHttpError('当前账号没有此操作权限', {
|
|
statusCode: 403,
|
|
errorCode: 'admin_permission_denied',
|
|
})
|
|
}
|
|
next()
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/permission]')
|
|
}
|
|
}
|
|
}
|
|
|
|
export function requireAnyAdminPermission(permissions: string[]) {
|
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
try {
|
|
const session = getRequiredAdminSession(req)
|
|
if (
|
|
permissions.some((permission) =>
|
|
session.permissions.includes(String(permission || '').trim()),
|
|
)
|
|
) {
|
|
next()
|
|
return
|
|
}
|
|
throw createHttpError('当前账号没有此操作权限', {
|
|
statusCode: 403,
|
|
errorCode: 'admin_permission_denied',
|
|
})
|
|
} catch (error) {
|
|
sendRouteError(res, error, '后台鉴权失败', '[admin/auth/permission]')
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getRequiredAdminSession(req: Request): AdminSession {
|
|
if (req.adminSession) {
|
|
return req.adminSession
|
|
}
|
|
|
|
throw createHttpError('未登录或登录已失效', {
|
|
statusCode: 401,
|
|
errorCode: 'admin_auth_required',
|
|
})
|
|
}
|
|
|
|
async function recordAdminAudit(
|
|
session: AdminSession | null | undefined,
|
|
req: Request,
|
|
data: unknown,
|
|
audit: AdminJsonHandlerOptions['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: Request): string {
|
|
const authorization = String(req.headers.authorization || '').trim()
|
|
const matched = authorization.match(/^Bearer\s+(.+)$/i)
|
|
|
|
if (!matched) {
|
|
throw createHttpError('未登录或登录已失效', {
|
|
statusCode: 401,
|
|
errorCode: 'admin_auth_required',
|
|
})
|
|
}
|
|
|
|
const token = matched[1]?.trim()
|
|
if (!token) {
|
|
throw createHttpError('未登录或登录已失效', {
|
|
statusCode: 401,
|
|
errorCode: 'admin_auth_required',
|
|
})
|
|
}
|
|
|
|
return token
|
|
}
|