后台新增RBAC权限模型支持按角色配置接单管理权限
- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移) - 接单平台全部管理路由从固定角色切换为细粒度权限校验 - 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限 - 管理员账号页新增角色权限配置卡片(admin专属,带审计日志) - 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
This commit is contained in:
@@ -20,6 +20,7 @@ import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { normalizePage, normalizePageSize } from './admin-query-utils.js'
|
||||
import { recordAdminLoginLog } from './admin-login-log-service.js'
|
||||
import { listAdminPermissionKeysForRole } from './admin-rbac-service.js'
|
||||
|
||||
type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
|
||||
|
||||
@@ -30,6 +31,7 @@ export type AdminSession = {
|
||||
role: AdminRole
|
||||
expiresAt: string
|
||||
sessionVersion: number
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
type AdminRole = 'admin' | 'operator' | 'support'
|
||||
@@ -230,6 +232,7 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
|
||||
role: normalizeAdminRole(user.role),
|
||||
expiresAt,
|
||||
sessionVersion: currentSessionVersion,
|
||||
permissions: await listAdminPermissionKeysForRole(normalizeAdminRole(user.role)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +252,7 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
role: session.role,
|
||||
permissions: session.permissions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
export const ADMIN_PERMISSION_KEYS = [
|
||||
'worker_order.view',
|
||||
'worker_order.edit',
|
||||
'worker_order.configure_share',
|
||||
'worker_order.assign',
|
||||
'worker_order.return_to_hall',
|
||||
'worker_order.cancel',
|
||||
'worker_order.cancel_share',
|
||||
'worker_order.delete',
|
||||
'worker_order.publish',
|
||||
'worker_order.accept',
|
||||
'worker_order.reopen',
|
||||
'worker_order.update_material',
|
||||
'worker_order.manage_problem',
|
||||
'worker_order.deduct_deposit',
|
||||
'worker_cancel_request.review',
|
||||
] as const
|
||||
|
||||
export type AdminPermissionKey = (typeof ADMIN_PERMISSION_KEYS)[number]
|
||||
|
||||
type PermissionRow = {
|
||||
permission_key: string
|
||||
permission_name: string
|
||||
permission_group: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export async function listAdminPermissionKeysForRole(role: string): Promise<string[]> {
|
||||
const result = await query<{ permission_key: string }>(
|
||||
`
|
||||
SELECT arp.permission_key
|
||||
FROM admin_role_permissions arp
|
||||
WHERE arp.role_key = $1
|
||||
ORDER BY arp.permission_key
|
||||
`,
|
||||
[
|
||||
String(role || '')
|
||||
.trim()
|
||||
.toLowerCase(),
|
||||
],
|
||||
)
|
||||
return result.rows.map((row) => String(row.permission_key || '').trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export async function listAdminRolesWithPermissions() {
|
||||
const result = await query<
|
||||
PermissionRow & { role_key: string; role_name: string; role_description: string }
|
||||
>(
|
||||
`
|
||||
SELECT
|
||||
ar.role_key,
|
||||
ar.role_name,
|
||||
ar.description AS role_description,
|
||||
ap.permission_key,
|
||||
ap.permission_name,
|
||||
ap.permission_group,
|
||||
ap.description
|
||||
FROM admin_roles ar
|
||||
CROSS JOIN admin_permissions ap
|
||||
ORDER BY ar.role_key, ap.permission_group, ap.permission_key
|
||||
`,
|
||||
)
|
||||
|
||||
const roleMap = new Map<
|
||||
string,
|
||||
{
|
||||
roleKey: string
|
||||
roleName: string
|
||||
description: string
|
||||
permissions: Array<{
|
||||
permissionKey: string
|
||||
permissionName: string
|
||||
permissionGroup: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
}>
|
||||
}
|
||||
>()
|
||||
|
||||
const enabled = await query<{ role_key: string; permission_key: string }>(
|
||||
'SELECT role_key, permission_key FROM admin_role_permissions',
|
||||
)
|
||||
const enabledSet = new Set(enabled.rows.map((row) => `${row.role_key}:${row.permission_key}`))
|
||||
|
||||
for (const row of result.rows) {
|
||||
let role = roleMap.get(row.role_key)
|
||||
if (!role) {
|
||||
role = {
|
||||
roleKey: row.role_key,
|
||||
roleName: row.role_name,
|
||||
description: row.role_description,
|
||||
permissions: [],
|
||||
}
|
||||
roleMap.set(row.role_key, role)
|
||||
}
|
||||
role.permissions.push({
|
||||
permissionKey: row.permission_key,
|
||||
permissionName: row.permission_name,
|
||||
permissionGroup: row.permission_group,
|
||||
description: row.description,
|
||||
enabled: enabledSet.has(`${row.role_key}:${row.permission_key}`),
|
||||
})
|
||||
}
|
||||
|
||||
return { roles: Array.from(roleMap.values()) }
|
||||
}
|
||||
|
||||
export async function updateAdminRolePermissions(roleKey: unknown, permissionKeys: unknown) {
|
||||
const normalizedRole = String(roleKey || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!['admin', 'operator', 'support'].includes(normalizedRole)) {
|
||||
throw createHttpError('角色不存在或不可修改', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_role_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
if (!Array.isArray(permissionKeys)) {
|
||||
throw createHttpError('权限列表格式无效', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_permissions_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const requested = Array.from(
|
||||
new Set(
|
||||
permissionKeys
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter((item): item is AdminPermissionKey =>
|
||||
(ADMIN_PERMISSION_KEYS as readonly string[]).includes(item),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (normalizedRole === 'admin' && requested.length !== ADMIN_PERMISSION_KEYS.length) {
|
||||
throw createHttpError('管理员角色必须保留全部权限', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_role_full_permissions_required',
|
||||
})
|
||||
}
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
await client.query('DELETE FROM admin_role_permissions WHERE role_key = $1', [normalizedRole])
|
||||
if (requested.length > 0) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||
SELECT $1, permission_key
|
||||
FROM admin_permissions
|
||||
WHERE permission_key = ANY($2::text[])
|
||||
ON CONFLICT DO NOTHING
|
||||
`,
|
||||
[normalizedRole, requested],
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (await listAdminRolesWithPermissions()).roles.find(
|
||||
(role) => role.roleKey === normalizedRole,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user