后台新增RBAC权限模型支持按角色配置接单管理权限
- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移) - 接单平台全部管理路由从固定角色切换为细粒度权限校验 - 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限 - 管理员账号页新增角色权限配置卡片(admin专属,带审计日志) - 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
-- 后台 RBAC 权限模型。保留 admin_users.role 兼容旧账号,权限通过角色键动态解析。
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_roles (
|
||||||
|
role_key TEXT PRIMARY KEY,
|
||||||
|
role_name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_permissions (
|
||||||
|
permission_key TEXT PRIMARY KEY,
|
||||||
|
permission_name TEXT NOT NULL,
|
||||||
|
permission_group TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_role_permissions (
|
||||||
|
role_key TEXT NOT NULL REFERENCES admin_roles(role_key) ON DELETE CASCADE,
|
||||||
|
permission_key TEXT NOT NULL REFERENCES admin_permissions(permission_key) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (role_key, permission_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO admin_roles (role_key, role_name, description)
|
||||||
|
VALUES
|
||||||
|
('admin', '管理员', '拥有全部后台权限'),
|
||||||
|
('operator', '普通运营', '负责接单平台和订单运营'),
|
||||||
|
('support', '客服', '负责客服处理和售后协助')
|
||||||
|
ON CONFLICT (role_key) DO UPDATE SET
|
||||||
|
role_name = EXCLUDED.role_name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
INSERT INTO admin_permissions (permission_key, permission_name, permission_group, description)
|
||||||
|
VALUES
|
||||||
|
('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', '审核撤单申请', '接单管理', '审核打手提交的撤单申请')
|
||||||
|
ON CONFLICT (permission_key) DO UPDATE SET
|
||||||
|
permission_name = EXCLUDED.permission_name,
|
||||||
|
permission_group = EXCLUDED.permission_group,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- 管理员默认拥有全部已登记权限。
|
||||||
|
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||||
|
SELECT 'admin', permission_key FROM admin_permissions
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 普通运营保持现有完整接单操作;客服默认不拥有高风险直接撤单权限。
|
||||||
|
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||||
|
SELECT 'operator', permission_key
|
||||||
|
FROM admin_permissions
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO admin_role_permissions (role_key, permission_key)
|
||||||
|
SELECT 'support', permission_key
|
||||||
|
FROM admin_permissions
|
||||||
|
WHERE permission_key IN (
|
||||||
|
'worker_order.view',
|
||||||
|
'worker_order.edit',
|
||||||
|
'worker_order.assign',
|
||||||
|
'worker_order.publish',
|
||||||
|
'worker_order.update_material',
|
||||||
|
'worker_order.manage_problem',
|
||||||
|
'worker_cancel_request.review'
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_admin_role_permissions_permission
|
||||||
|
ON admin_role_permissions(permission_key, role_key);
|
||||||
@@ -10,6 +10,7 @@ import kuaishouIndustryRouter from './admin/kuaishou-industry.js'
|
|||||||
import ordersRouter from './admin/orders.js'
|
import ordersRouter from './admin/orders.js'
|
||||||
import notificationsRouter from './admin/notifications.js'
|
import notificationsRouter from './admin/notifications.js'
|
||||||
import realtimeRouter from './admin/realtime.js'
|
import realtimeRouter from './admin/realtime.js'
|
||||||
|
import rbacRouter from './admin/rbac.js'
|
||||||
import platformConfigRouter from './admin/platform-config.js'
|
import platformConfigRouter from './admin/platform-config.js'
|
||||||
import { requireAdminSession } from './admin/session.js'
|
import { requireAdminSession } from './admin/session.js'
|
||||||
import tasksRouter from './admin/tasks.js'
|
import tasksRouter from './admin/tasks.js'
|
||||||
@@ -24,6 +25,7 @@ router.use(requireAdminSession)
|
|||||||
router.use(filesRouter)
|
router.use(filesRouter)
|
||||||
router.use(dashboardRouter)
|
router.use(dashboardRouter)
|
||||||
router.use(usersRouter)
|
router.use(usersRouter)
|
||||||
|
router.use(rbacRouter)
|
||||||
router.use(auditLogsRouter)
|
router.use(auditLogsRouter)
|
||||||
router.use(platformConfigRouter)
|
router.use(platformConfigRouter)
|
||||||
router.use(kuaishouIndustryRouter)
|
router.use(kuaishouIndustryRouter)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
|
||||||
|
import {
|
||||||
|
listAdminRolesWithPermissions,
|
||||||
|
updateAdminRolePermissions,
|
||||||
|
} from '../../services/admin/admin-rbac-service.js'
|
||||||
|
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router.use('/rbac', requireAdminRoles(['admin']))
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/rbac',
|
||||||
|
createJsonHandler(() => listAdminRolesWithPermissions(), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取角色权限失败',
|
||||||
|
scope: '[admin/rbac]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/rbac/roles/:roleKey/permissions',
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => updateAdminRolePermissions(req.params.roleKey, req.body?.permissions),
|
||||||
|
{
|
||||||
|
successMessage: '角色权限已更新',
|
||||||
|
errorMessage: '更新角色权限失败',
|
||||||
|
scope: '[admin/rbac/roles/:roleKey/permissions]',
|
||||||
|
audit: (req, data) => ({
|
||||||
|
action: 'admin_role_permissions_updated',
|
||||||
|
targetType: 'admin_role',
|
||||||
|
targetId: String(req.params.roleKey || ''),
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -80,6 +80,45 @@ export function requireAdminRoles(allowedRoles: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export function getRequiredAdminSession(req: Request): AdminSession {
|
||||||
if (req.adminSession) {
|
if (req.adminSession) {
|
||||||
return req.adminSession
|
return req.adminSession
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
resolveAdminProblemWorkOrder,
|
resolveAdminProblemWorkOrder,
|
||||||
} from '../../../services/worker-platform/index.js'
|
} from '../../../services/worker-platform/index.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
import { createJsonHandler, requireAdminPermission } from '../session.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/problem',
|
'/worker-platform/orders/:workOrderId/problem',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
markAdminWorkOrderProblem(
|
markAdminWorkOrderProblem(
|
||||||
@@ -38,7 +38,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/problem/resolve',
|
'/worker-platform/orders/:workOrderId/problem/resolve',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resolveAdminProblemWorkOrder(
|
resolveAdminProblemWorkOrder(
|
||||||
@@ -62,7 +62,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/accept',
|
'/worker-platform/orders/:workOrderId/accept',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.accept'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
acceptAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
acceptAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
||||||
@@ -82,7 +82,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/shares/:shareId/accept',
|
'/worker-platform/orders/:workOrderId/shares/:shareId/accept',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.accept'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
acceptAdminWorkOrderShare(
|
acceptAdminWorkOrderShare(
|
||||||
@@ -106,7 +106,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/shares/:shareId/cancel',
|
'/worker-platform/orders/:workOrderId/shares/:shareId/cancel',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.cancel_share'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
cancelAdminWorkOrderShare(
|
cancelAdminWorkOrderShare(
|
||||||
@@ -131,7 +131,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/deduct-pending-deposit',
|
'/worker-platform/orders/:workOrderId/deduct-pending-deposit',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.deduct_deposit'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
deductAdminWorkOrderPendingDeposit(
|
deductAdminWorkOrderPendingDeposit(
|
||||||
|
|||||||
@@ -25,13 +25,18 @@ import {
|
|||||||
updateAdminWorkOrder,
|
updateAdminWorkOrder,
|
||||||
updateAdminWorkOrderSharing,
|
updateAdminWorkOrderSharing,
|
||||||
} from '../../../services/worker-platform/index.js'
|
} from '../../../services/worker-platform/index.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
import {
|
||||||
|
createJsonHandler,
|
||||||
|
requireAdminPermission,
|
||||||
|
requireAnyAdminPermission,
|
||||||
|
requireAdminRoles,
|
||||||
|
} from '../session.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/orders',
|
'/worker-platform/orders',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.view'),
|
||||||
createJsonHandler((req) => listAdminWorkOrders(req.query), {
|
createJsonHandler((req) => listAdminWorkOrders(req.query), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取接单订单失败',
|
errorMessage: '读取接单订单失败',
|
||||||
@@ -138,7 +143,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/material',
|
'/worker-platform/orders/:workOrderId/material',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.update_material'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
submitAdminWorkOrderMaterial(
|
submitAdminWorkOrderMaterial(
|
||||||
@@ -162,7 +167,7 @@ router.post(
|
|||||||
|
|
||||||
router.put(
|
router.put(
|
||||||
'/worker-platform/orders/:workOrderId',
|
'/worker-platform/orders/:workOrderId',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.edit'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}, {
|
updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}, {
|
||||||
@@ -185,7 +190,7 @@ router.put(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/assign',
|
'/worker-platform/orders/:workOrderId/assign',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.assign'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
assignAdminWorkOrderToWorker(
|
assignAdminWorkOrderToWorker(
|
||||||
@@ -209,7 +214,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/unassign',
|
'/worker-platform/orders/:workOrderId/unassign',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.assign'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
unassignAdminWorkOrder(
|
unassignAdminWorkOrder(
|
||||||
@@ -232,13 +237,14 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/cancel',
|
'/worker-platform/orders/:workOrderId/cancel',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAnyAdminPermission(['worker_order.return_to_hall', 'worker_order.cancel']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
cancelAdminWorkOrder(
|
cancelAdminWorkOrder(
|
||||||
String(req.params.workOrderId || ''),
|
String(req.params.workOrderId || ''),
|
||||||
req.body || {},
|
req.body || {},
|
||||||
req.adminSession?.username || '',
|
req.adminSession?.username || '',
|
||||||
|
req.adminSession?.permissions || [],
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
successMessage: '撤单处理完成,打手额度已恢复',
|
successMessage: '撤单处理完成,打手额度已恢复',
|
||||||
@@ -259,7 +265,7 @@ router.post(
|
|||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/worker-platform/orders/:workOrderId',
|
'/worker-platform/orders/:workOrderId',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.delete'),
|
||||||
createJsonHandler((req) => deleteAdminWorkOrder(String(req.params.workOrderId || '')), {
|
createJsonHandler((req) => deleteAdminWorkOrder(String(req.params.workOrderId || '')), {
|
||||||
successMessage: '工单已删除',
|
successMessage: '工单已删除',
|
||||||
errorMessage: '删除工单失败',
|
errorMessage: '删除工单失败',
|
||||||
@@ -275,7 +281,7 @@ router.delete(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/reopen',
|
'/worker-platform/orders/:workOrderId/reopen',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.reopen'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
reopenAdminCancelledWorkOrder(
|
reopenAdminCancelledWorkOrder(
|
||||||
@@ -298,7 +304,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/publish',
|
'/worker-platform/orders/:workOrderId/publish',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.publish'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
||||||
@@ -318,7 +324,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/unpublish',
|
'/worker-platform/orders/:workOrderId/unpublish',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.publish'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
unpublishAdminWorkOrder(
|
unpublishAdminWorkOrder(
|
||||||
@@ -341,7 +347,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/pin',
|
'/worker-platform/orders/:workOrderId/pin',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.publish'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
pinAdminWorkOrder(
|
pinAdminWorkOrder(
|
||||||
@@ -365,7 +371,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/confirm-friend',
|
'/worker-platform/orders/:workOrderId/confirm-friend',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
confirmAdminWorkOrderFriendAdded(
|
confirmAdminWorkOrderFriendAdded(
|
||||||
@@ -388,7 +394,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/reset-gift-material',
|
'/worker-platform/orders/:workOrderId/reset-gift-material',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resetAdminWorkOrderGiftMaterial(
|
resetAdminWorkOrderGiftMaterial(
|
||||||
@@ -412,7 +418,7 @@ router.post(
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/orders/:workOrderId/sharing',
|
'/worker-platform/orders/:workOrderId/sharing',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.view'),
|
||||||
createJsonHandler((req) => getAdminWorkOrderSharing(String(req.params.workOrderId || '')), {
|
createJsonHandler((req) => getAdminWorkOrderSharing(String(req.params.workOrderId || '')), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取拼单信息失败',
|
errorMessage: '读取拼单信息失败',
|
||||||
@@ -422,7 +428,7 @@ router.get(
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/orders/:workOrderId/events',
|
'/worker-platform/orders/:workOrderId/events',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.view'),
|
||||||
createJsonHandler((req) => getAdminWorkOrderEvents(String(req.params.workOrderId || '')), {
|
createJsonHandler((req) => getAdminWorkOrderEvents(String(req.params.workOrderId || '')), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取工单流转记录失败',
|
errorMessage: '读取工单流转记录失败',
|
||||||
@@ -432,7 +438,7 @@ router.get(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/:workOrderId/sharing',
|
'/worker-platform/orders/:workOrderId/sharing',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.configure_share'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
updateAdminWorkOrderSharing(
|
updateAdminWorkOrderSharing(
|
||||||
@@ -456,7 +462,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/orders/accept-batch',
|
'/worker-platform/orders/accept-batch',
|
||||||
requireAdminRoles(['admin', 'operator']),
|
requireAdminPermission('worker_order.accept'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) => acceptAdminWorkOrders(req.body || {}, req.adminSession?.username || ''),
|
(req) => acceptAdminWorkOrders(req.body || {}, req.adminSession?.username || ''),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
resolveAdminWorkerOrderFeedback,
|
resolveAdminWorkerOrderFeedback,
|
||||||
reviewAdminWorkerCancelRequest,
|
reviewAdminWorkerCancelRequest,
|
||||||
} from '../../../services/worker-platform/index.js'
|
} from '../../../services/worker-platform/index.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
import { createJsonHandler, requireAdminPermission } from '../session.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/cancel-requests',
|
'/worker-platform/cancel-requests',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_cancel_request.review'),
|
||||||
createJsonHandler((req) => listAdminWorkerCancelRequests(req.query), {
|
createJsonHandler((req) => listAdminWorkerCancelRequests(req.query), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取撤单申请失败',
|
errorMessage: '读取撤单申请失败',
|
||||||
@@ -22,7 +22,7 @@ router.get(
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/feedbacks',
|
'/worker-platform/feedbacks',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler((req) => listAdminWorkerOrderFeedbacks(req.query), {
|
createJsonHandler((req) => listAdminWorkerOrderFeedbacks(req.query), {
|
||||||
successMessage: 'ok',
|
successMessage: 'ok',
|
||||||
errorMessage: '读取问题反馈失败',
|
errorMessage: '读取问题反馈失败',
|
||||||
@@ -32,7 +32,7 @@ router.get(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/feedbacks/:feedbackId/resolve',
|
'/worker-platform/feedbacks/:feedbackId/resolve',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_order.manage_problem'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
resolveAdminWorkerOrderFeedback(
|
resolveAdminWorkerOrderFeedback(
|
||||||
@@ -50,7 +50,7 @@ router.post(
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/cancel-requests/:requestId/review',
|
'/worker-platform/cancel-requests/:requestId/review',
|
||||||
requireAdminRoles(['admin', 'operator', 'support']),
|
requireAdminPermission('worker_cancel_request.review'),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
reviewAdminWorkerCancelRequest(
|
reviewAdminWorkerCancelRequest(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { addHours, nowIso } from '../../utils/time.js'
|
|||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { normalizePage, normalizePageSize } from './admin-query-utils.js'
|
import { normalizePage, normalizePageSize } from './admin-query-utils.js'
|
||||||
import { recordAdminLoginLog } from './admin-login-log-service.js'
|
import { recordAdminLoginLog } from './admin-login-log-service.js'
|
||||||
|
import { listAdminPermissionKeysForRole } from './admin-rbac-service.js'
|
||||||
|
|
||||||
type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
|
type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export type AdminSession = {
|
|||||||
role: AdminRole
|
role: AdminRole
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
sessionVersion: number
|
sessionVersion: number
|
||||||
|
permissions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminRole = 'admin' | 'operator' | 'support'
|
type AdminRole = 'admin' | 'operator' | 'support'
|
||||||
@@ -230,6 +232,7 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
|
|||||||
role: normalizeAdminRole(user.role),
|
role: normalizeAdminRole(user.role),
|
||||||
expiresAt,
|
expiresAt,
|
||||||
sessionVersion: currentSessionVersion,
|
sessionVersion: currentSessionVersion,
|
||||||
|
permissions: await listAdminPermissionKeysForRole(normalizeAdminRole(user.role)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +252,7 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
|
|||||||
userId: session.userId,
|
userId: session.userId,
|
||||||
username: session.username,
|
username: session.username,
|
||||||
role: session.role,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ export async function cancelAdminWorkOrder(
|
|||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
payload: JsonObject = {},
|
payload: JsonObject = {},
|
||||||
actorName = '',
|
actorName = '',
|
||||||
|
actorPermissions: string[] = [],
|
||||||
) {
|
) {
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) {
|
if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) {
|
||||||
@@ -70,6 +71,14 @@ export async function cancelAdminWorkOrder(
|
|||||||
errorCode: 'work_order_cancel_action_required',
|
errorCode: 'work_order_cancel_action_required',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const requiredPermission =
|
||||||
|
action === 'return_to_hall' ? 'worker_order.return_to_hall' : 'worker_order.cancel'
|
||||||
|
if (!actorPermissions.includes(requiredPermission)) {
|
||||||
|
throw createHttpError('当前账号没有此撤单权限', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'admin_permission_denied',
|
||||||
|
})
|
||||||
|
}
|
||||||
const reason = String(payload.reason || '').trim()
|
const reason = String(payload.reason || '').trim()
|
||||||
if (action === 'cancel_order' && !reason) {
|
if (action === 'cancel_order' && !reason) {
|
||||||
throw createHttpError('取消订单时请填写退款或撤单原因', {
|
throw createHttpError('取消订单时请填写退款或撤单原因', {
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Outlet, useLocation, useNavigate } from 'react-router'
|
import { Outlet, useLocation, useNavigate } from 'react-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
fetchAdminSession,
|
||||||
fetchAdminNotifications,
|
fetchAdminNotifications,
|
||||||
fetchDevMockStatus,
|
fetchDevMockStatus,
|
||||||
logoutAdmin,
|
logoutAdmin,
|
||||||
@@ -47,6 +48,7 @@ import {
|
|||||||
getAdminToken,
|
getAdminToken,
|
||||||
getAdminTokenExpiresAt,
|
getAdminTokenExpiresAt,
|
||||||
getAdminUsername,
|
getAdminUsername,
|
||||||
|
setAdminPermissions,
|
||||||
} from '@/utils/admin-auth'
|
} from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||||
@@ -69,6 +71,19 @@ export default function AdminLayout() {
|
|||||||
() => window.localStorage.getItem(NOTIFICATION_SOUND_KEY) === '1',
|
() => window.localStorage.getItem(NOTIFICATION_SOUND_KEY) === '1',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const adminSessionQuery = useQuery({
|
||||||
|
queryKey: ['admin-session'],
|
||||||
|
queryFn: fetchAdminSession,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const session = adminSessionQuery.data?.data
|
||||||
|
if (session?.authenticated && session.user) {
|
||||||
|
setAdminPermissions(session.user.permissions || [])
|
||||||
|
}
|
||||||
|
}, [adminSessionQuery.data])
|
||||||
|
|
||||||
const devMockStatusQuery = useQuery({
|
const devMockStatusQuery = useQuery({
|
||||||
queryKey: ['admin-dev-mock-status'],
|
queryKey: ['admin-dev-mock-status'],
|
||||||
queryFn: () => fetchDevMockStatus(),
|
queryFn: () => fetchDevMockStatus(),
|
||||||
@@ -546,7 +561,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationAlertKind =
|
type NotificationAlertKind =
|
||||||
'withdraw' | 'recharge' | 'acceptance' | 'material' | 'reminder' | 'default'
|
| 'withdraw'
|
||||||
|
| 'recharge'
|
||||||
|
| 'acceptance'
|
||||||
|
| 'material'
|
||||||
|
| 'reminder'
|
||||||
|
| 'default'
|
||||||
|
|
||||||
function playNotificationSound(kind: NotificationAlertKind) {
|
function playNotificationSound(kind: NotificationAlertKind) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import PageHeader from '@/components/admin/PageHeader'
|
|||||||
import StatusTag from '@/components/admin/StatusTag'
|
import StatusTag from '@/components/admin/StatusTag'
|
||||||
import {
|
import {
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
|
fetchAdminRolePermissions,
|
||||||
fetchAdminUsers,
|
fetchAdminUsers,
|
||||||
resetAdminUserPassword,
|
resetAdminUserPassword,
|
||||||
updateAdminUserRole,
|
updateAdminUserRole,
|
||||||
updateAdminUserStatus,
|
updateAdminUserStatus,
|
||||||
|
updateAdminRolePermissions,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import type { AdminRole, AdminUserListItem } from '@/types/admin'
|
import type { AdminRole, AdminUserListItem } from '@/types/admin'
|
||||||
import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth'
|
import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth'
|
||||||
@@ -83,11 +85,33 @@ export default function AdminUsersPage() {
|
|||||||
queryFn: () => fetchAdminUsers(queryParams),
|
queryFn: () => fetchAdminUsers(queryParams),
|
||||||
})
|
})
|
||||||
const data = query.data?.data
|
const data = query.data?.data
|
||||||
|
const rolePermissionsQuery = useQuery({
|
||||||
|
queryKey: ['admin-role-permissions'],
|
||||||
|
enabled: isAdmin,
|
||||||
|
queryFn: () => fetchAdminRolePermissions(),
|
||||||
|
})
|
||||||
|
|
||||||
async function reloadUsers() {
|
async function reloadUsers() {
|
||||||
await queryClient.invalidateQueries({ queryKey: ['admin-users'] })
|
await queryClient.invalidateQueries({ queryKey: ['admin-users'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRolePermission(roleKey: string, permissionKey: string, enabled: boolean) {
|
||||||
|
const role = rolePermissionsQuery.data?.data.roles.find((item) => item.roleKey === roleKey)
|
||||||
|
if (!role || roleKey === 'admin') return
|
||||||
|
const permissions = role.permissions
|
||||||
|
.filter((item) => item.enabled)
|
||||||
|
.map((item) => item.permissionKey)
|
||||||
|
.filter((item) => item !== permissionKey)
|
||||||
|
if (enabled) permissions.push(permissionKey)
|
||||||
|
try {
|
||||||
|
await updateAdminRolePermissions(roleKey, permissions)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-role-permissions'] })
|
||||||
|
message.success('角色权限已更新')
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '更新角色权限失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCreate(values: CreateUserForm) {
|
async function submitCreate(values: CreateUserForm) {
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
|
|
||||||
@@ -420,6 +444,37 @@ export default function AdminUsersPage() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card title="角色权限">
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
{(rolePermissionsQuery.data?.data.roles || []).map((role) => (
|
||||||
|
<div key={role.roleKey}>
|
||||||
|
<Typography.Text strong>{role.roleName}</Typography.Text>
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Space wrap>
|
||||||
|
{role.permissions.map((permission) => (
|
||||||
|
<Button
|
||||||
|
key={permission.permissionKey}
|
||||||
|
size="small"
|
||||||
|
type={permission.enabled ? 'primary' : 'default'}
|
||||||
|
disabled={role.roleKey === 'admin'}
|
||||||
|
onClick={() =>
|
||||||
|
void toggleRolePermission(
|
||||||
|
role.roleKey,
|
||||||
|
permission.permissionKey,
|
||||||
|
!permission.enabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{permission.permissionName}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
deductAdminWorkOrderPendingDeposit,
|
deductAdminWorkOrderPendingDeposit,
|
||||||
deleteAdminWorkOrder,
|
deleteAdminWorkOrder,
|
||||||
fetchAdminWorkCategories,
|
fetchAdminWorkCategories,
|
||||||
|
fetchAdminSession,
|
||||||
fetchAdminWorkOrderSharing,
|
fetchAdminWorkOrderSharing,
|
||||||
fetchAdminWorkerLevels,
|
fetchAdminWorkerLevels,
|
||||||
fetchAdminWorkerUsers,
|
fetchAdminWorkerUsers,
|
||||||
@@ -72,7 +73,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
|
|||||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import {
|
import {
|
||||||
DeadlineCountdown,
|
DeadlineCountdown,
|
||||||
@@ -151,6 +152,51 @@ export default function WorkOrdersPanel() {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const canEditRequirementFields = hasAdminRole('operator')
|
const canEditRequirementFields = hasAdminRole('operator')
|
||||||
const canViewStatistics = hasAdminRole('operator')
|
const canViewStatistics = hasAdminRole('operator')
|
||||||
|
const adminSessionQuery = useQuery({
|
||||||
|
queryKey: ['admin-session'],
|
||||||
|
queryFn: fetchAdminSession,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
const sessionPermissions = adminSessionQuery.data?.data.user.permissions
|
||||||
|
const canReturnToHall = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.return_to_hall')
|
||||||
|
: hasAdminPermission('worker_order.return_to_hall')
|
||||||
|
const canCancelOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.cancel')
|
||||||
|
: hasAdminPermission('worker_order.cancel')
|
||||||
|
const canCancelShare = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.cancel_share')
|
||||||
|
: hasAdminPermission('worker_order.cancel_share')
|
||||||
|
const canEditOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.edit')
|
||||||
|
: hasAdminPermission('worker_order.edit')
|
||||||
|
const canAssignOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.assign')
|
||||||
|
: hasAdminPermission('worker_order.assign')
|
||||||
|
const canDeleteOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.delete')
|
||||||
|
: hasAdminPermission('worker_order.delete')
|
||||||
|
const canReopenOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.reopen')
|
||||||
|
: hasAdminPermission('worker_order.reopen')
|
||||||
|
const canPublishOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.publish')
|
||||||
|
: hasAdminPermission('worker_order.publish')
|
||||||
|
const canAcceptOrder = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.accept')
|
||||||
|
: hasAdminPermission('worker_order.accept')
|
||||||
|
const canUpdateMaterial = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.update_material')
|
||||||
|
: hasAdminPermission('worker_order.update_material')
|
||||||
|
const canManageProblem = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.manage_problem')
|
||||||
|
: hasAdminPermission('worker_order.manage_problem')
|
||||||
|
const canDeductDeposit = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.deduct_deposit')
|
||||||
|
: hasAdminPermission('worker_order.deduct_deposit')
|
||||||
|
const canConfigureSharing = sessionPermissions
|
||||||
|
? sessionPermissions.includes('worker_order.configure_share')
|
||||||
|
: hasAdminPermission('worker_order.configure_share')
|
||||||
const [statuses, setStatuses] = useState<string[]>([])
|
const [statuses, setStatuses] = useState<string[]>([])
|
||||||
const [targetWorkOrderId, setTargetWorkOrderId] = useState<number | undefined>()
|
const [targetWorkOrderId, setTargetWorkOrderId] = useState<number | undefined>()
|
||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
@@ -976,17 +1022,17 @@ export default function WorkOrdersPanel() {
|
|||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
||||||
{row.capabilities?.canEdit ? (
|
{row.capabilities?.canEdit && canEditOrder ? (
|
||||||
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
|
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canDelete ? (
|
{row.capabilities?.canDelete && canDeleteOrder ? (
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={() => requestDeleteWorkOrder(row)}>
|
<Button danger icon={<DeleteOutlined />} onClick={() => requestDeleteWorkOrder(row)}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canReopen ? (
|
{row.capabilities?.canReopen && canReopenOrder ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<UndoOutlined />}
|
icon={<UndoOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1006,29 +1052,30 @@ export default function WorkOrdersPanel() {
|
|||||||
重新启用
|
重新启用
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
{row.sharing?.enabled ||
|
||||||
|
(row.capabilities?.canConfigureSharing && canConfigureSharing) ? (
|
||||||
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
||||||
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
||||||
? '拼单明细'
|
? '拼单明细'
|
||||||
: '拼单配置'}
|
: '拼单配置'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canAssign ? (
|
{row.capabilities?.canAssign && canAssignOrder ? (
|
||||||
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
||||||
指派并核销
|
指派并核销
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canCancel ? (
|
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||||
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
||||||
撤单
|
撤单
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canUpdateMaterial ? (
|
{row.capabilities?.canUpdateMaterial && canUpdateMaterial ? (
|
||||||
<Button icon={<FormOutlined />} onClick={() => openMaterialModal(row)}>
|
<Button icon={<FormOutlined />} onClick={() => openMaterialModal(row)}>
|
||||||
{['open', 'in_progress'].includes(row.status) ? '更新资料' : '补资料'}
|
{['open', 'in_progress'].includes(row.status) ? '更新资料' : '补资料'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canPublish ? (
|
{row.capabilities?.canPublish && canPublishOrder ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
disabled={Number(row.rewardAmount || 0) <= 0}
|
disabled={Number(row.rewardAmount || 0) <= 0}
|
||||||
@@ -1040,7 +1087,7 @@ export default function WorkOrdersPanel() {
|
|||||||
发布并核销
|
发布并核销
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canPin ? (
|
{row.capabilities?.canPin && canPublishOrder ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<PushpinOutlined />}
|
icon={<PushpinOutlined />}
|
||||||
title={row.pinnedAt ? '取消置顶' : '置顶后抢单大厅优先展示'}
|
title={row.pinnedAt ? '取消置顶' : '置顶后抢单大厅优先展示'}
|
||||||
@@ -1054,7 +1101,7 @@ export default function WorkOrdersPanel() {
|
|||||||
{row.pinnedAt ? '取消置顶' : '置顶'}
|
{row.pinnedAt ? '取消置顶' : '置顶'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canUnpublish ? (
|
{row.capabilities?.canUnpublish && canPublishOrder ? (
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1071,7 +1118,7 @@ export default function WorkOrdersPanel() {
|
|||||||
下架
|
下架
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canAccept ? (
|
{row.capabilities?.canAccept && canAcceptOrder ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<CheckOutlined />}
|
icon={<CheckOutlined />}
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -1080,7 +1127,9 @@ export default function WorkOrdersPanel() {
|
|||||||
验收
|
验收
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canDeduct && Number(row.pendingUnfreezeAmount || 0) > 0 ? (
|
{row.capabilities?.canDeduct &&
|
||||||
|
canDeductDeposit &&
|
||||||
|
Number(row.pendingUnfreezeAmount || 0) > 0 ? (
|
||||||
<Button icon={<WarningOutlined />} danger onClick={() => openDeductModal(row)}>
|
<Button icon={<WarningOutlined />} danger onClick={() => openDeductModal(row)}>
|
||||||
扣押金
|
扣押金
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1098,7 +1147,7 @@ export default function WorkOrdersPanel() {
|
|||||||
售后
|
售后
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canConfirmFriend ? (
|
{row.capabilities?.canConfirmFriend && canManageProblem ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<UserAddOutlined />}
|
icon={<UserAddOutlined />}
|
||||||
@@ -1107,17 +1156,17 @@ export default function WorkOrdersPanel() {
|
|||||||
确认加好友
|
确认加好友
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canResetGiftFlow ? (
|
{row.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||||
<Button danger onClick={() => requestResetGiftMaterial(row)}>
|
<Button danger onClick={() => requestResetGiftMaterial(row)}>
|
||||||
重置打手资料
|
重置打手资料
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canMarkProblem ? (
|
{row.capabilities?.canMarkProblem && canManageProblem ? (
|
||||||
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
|
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
|
||||||
问题单
|
问题单
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canResolveProblem ? (
|
{row.capabilities?.canResolveProblem && canManageProblem ? (
|
||||||
<Button icon={<FormOutlined />} onClick={() => setResolutionOrder(row)}>
|
<Button icon={<FormOutlined />} onClick={() => setResolutionOrder(row)}>
|
||||||
处置
|
处置
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1470,7 +1519,7 @@ export default function WorkOrdersPanel() {
|
|||||||
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
|
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
|
||||||
{formatGiftPhase(detailOrder.gift.phase)}
|
{formatGiftPhase(detailOrder.gift.phase)}
|
||||||
</Tag>
|
</Tag>
|
||||||
{detailOrder.capabilities?.canConfirmFriend ? (
|
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1479,7 +1528,7 @@ export default function WorkOrdersPanel() {
|
|||||||
确认加好友
|
确认加好友
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{detailOrder.capabilities?.canResetGiftFlow ? (
|
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
@@ -1814,7 +1863,7 @@ export default function WorkOrdersPanel() {
|
|||||||
<Form
|
<Form
|
||||||
form={cancelForm}
|
form={cancelForm}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{ action: 'return_to_hall' }}
|
initialValues={{ action: canReturnToHall ? 'return_to_hall' : 'cancel_order' }}
|
||||||
onFinish={submitCancelOrder}
|
onFinish={submitCancelOrder}
|
||||||
>
|
>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -1830,8 +1879,12 @@ export default function WorkOrdersPanel() {
|
|||||||
>
|
>
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
<Space direction="vertical">
|
<Space direction="vertical">
|
||||||
<Radio value="return_to_hall">退回大厅:订单重新开放给其他打手接单</Radio>
|
{canReturnToHall ? (
|
||||||
<Radio value="cancel_order">取消订单:用户退款后终止订单,不再回大厅</Radio>
|
<Radio value="return_to_hall">退回大厅:订单重新开放给其他打手接单</Radio>
|
||||||
|
) : null}
|
||||||
|
{canCancelOrder ? (
|
||||||
|
<Radio value="cancel_order">取消订单:用户退款后终止订单,不再回大厅</Radio>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -1988,7 +2041,7 @@ export default function WorkOrdersPanel() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={`${sharingOrder?.capabilities?.canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
|
title={`${sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
|
||||||
open={Boolean(sharingOrder)}
|
open={Boolean(sharingOrder)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setSharingOrder(null)
|
setSharingOrder(null)
|
||||||
@@ -1997,7 +2050,9 @@ export default function WorkOrdersPanel() {
|
|||||||
cancelShareForm.resetFields()
|
cancelShareForm.resetFields()
|
||||||
}}
|
}}
|
||||||
onOk={() => sharingForm.submit()}
|
onOk={() => sharingForm.submit()}
|
||||||
footer={sharingOrder?.capabilities?.canConfigureSharing ? undefined : null}
|
footer={
|
||||||
|
sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? undefined : null
|
||||||
|
}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={720}
|
width={720}
|
||||||
>
|
>
|
||||||
@@ -2012,7 +2067,7 @@ export default function WorkOrdersPanel() {
|
|||||||
unitReward: 0,
|
unitReward: 0,
|
||||||
}}
|
}}
|
||||||
onFinish={submitSharingConfig}
|
onFinish={submitSharingConfig}
|
||||||
disabled={!sharingOrder.capabilities?.canConfigureSharing}
|
disabled={!sharingOrder.capabilities?.canConfigureSharing || !canConfigureSharing}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="启用拼单"
|
label="启用拼单"
|
||||||
@@ -2169,17 +2224,19 @@ export default function WorkOrdersPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<Typography.Text type="secondary">待提交</Typography.Text>
|
<Typography.Text type="secondary">待提交</Typography.Text>
|
||||||
)}
|
)}
|
||||||
<Button
|
{canCancelShare ? (
|
||||||
danger
|
<Button
|
||||||
size="small"
|
danger
|
||||||
icon={<UndoOutlined />}
|
size="small"
|
||||||
onClick={() => {
|
icon={<UndoOutlined />}
|
||||||
setCancelShare(share)
|
onClick={() => {
|
||||||
cancelShareForm.setFieldsValue({ reason: '' })
|
setCancelShare(share)
|
||||||
}}
|
cancelShareForm.setFieldsValue({ reason: '' })
|
||||||
>
|
}}
|
||||||
撤单
|
>
|
||||||
</Button>
|
撤单
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
import { apiGet, apiPost } from '@/lib/http'
|
import { apiGet, apiPost, apiPut } from '@/lib/http'
|
||||||
import type { AdminPagination, AdminUserListItem } from '@/types/admin'
|
import type { AdminPagination, AdminUserListItem } from '@/types/admin'
|
||||||
|
|
||||||
|
export type AdminRolePermission = {
|
||||||
|
permissionKey: string
|
||||||
|
permissionName: string
|
||||||
|
permissionGroup: string
|
||||||
|
description: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminRolePermissions = {
|
||||||
|
roleKey: string
|
||||||
|
roleName: string
|
||||||
|
description: string
|
||||||
|
permissions: AdminRolePermission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchAdminRolePermissions() {
|
||||||
|
return apiGet<{ roles: AdminRolePermissions[] }>('/api/v1/admin/rbac')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAdminRolePermissions(roleKey: string, permissions: string[]) {
|
||||||
|
return apiPut<AdminRolePermissions>(`/api/v1/admin/rbac/roles/${roleKey}/permissions`, {
|
||||||
|
permissions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminUsers(params?: Record<string, unknown>) {
|
export function fetchAdminUsers(params?: Record<string, unknown>) {
|
||||||
return apiGet<{ items: AdminUserListItem[]; pagination: AdminPagination }>(
|
return apiGet<{ items: AdminUserListItem[]; pagination: AdminPagination }>(
|
||||||
'/api/v1/admin/users',
|
'/api/v1/admin/users',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AdminLoginResponse {
|
|||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
|
permissions?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,5 +19,6 @@ export interface AdminSessionSummary {
|
|||||||
userId: number
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
|
permissions?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const ADMIN_EXPIRES_AT_KEY = 'order-site-admin-token-expires-at'
|
|||||||
const ADMIN_USER_ID_KEY = 'order-site-admin-user-id'
|
const ADMIN_USER_ID_KEY = 'order-site-admin-user-id'
|
||||||
const ADMIN_USERNAME_KEY = 'order-site-admin-username'
|
const ADMIN_USERNAME_KEY = 'order-site-admin-username'
|
||||||
const ADMIN_ROLE_KEY = 'order-site-admin-role'
|
const ADMIN_ROLE_KEY = 'order-site-admin-role'
|
||||||
|
const ADMIN_PERMISSIONS_KEY = 'order-site-admin-permissions'
|
||||||
|
|
||||||
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin', number> = {
|
const ADMIN_ROLE_LEVEL: Record<'support' | 'operator' | 'admin', number> = {
|
||||||
support: 1,
|
support: 1,
|
||||||
@@ -36,6 +37,26 @@ export function getAdminRole() {
|
|||||||
return 'support'
|
return 'support'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAdminPermissions(): string[] {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(localStorage.getItem(ADMIN_PERMISSIONS_KEY) || '[]')
|
||||||
|
return Array.isArray(value)
|
||||||
|
? value.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAdminPermissions(permissions: string[]) {
|
||||||
|
localStorage.setItem(ADMIN_PERMISSIONS_KEY, JSON.stringify(permissions || []))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasAdminPermission(permission: string) {
|
||||||
|
if (!hasAdminSession()) return false
|
||||||
|
return getAdminPermissions().includes(String(permission || '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
||||||
if (!hasAdminSession()) {
|
if (!hasAdminSession()) {
|
||||||
return false
|
return false
|
||||||
@@ -47,7 +68,12 @@ export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
|||||||
export function setAdminSession(
|
export function setAdminSession(
|
||||||
token: string,
|
token: string,
|
||||||
expiresAt: string,
|
expiresAt: string,
|
||||||
user?: { userId: number; username: string; role: 'admin' | 'operator' | 'support' },
|
user?: {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
role: 'admin' | 'operator' | 'support'
|
||||||
|
permissions?: string[]
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
localStorage.setItem(ADMIN_TOKEN_KEY, token)
|
localStorage.setItem(ADMIN_TOKEN_KEY, token)
|
||||||
localStorage.setItem(ADMIN_EXPIRES_AT_KEY, expiresAt)
|
localStorage.setItem(ADMIN_EXPIRES_AT_KEY, expiresAt)
|
||||||
@@ -63,6 +89,8 @@ export function setAdminSession(
|
|||||||
if (user?.role) {
|
if (user?.role) {
|
||||||
localStorage.setItem(ADMIN_ROLE_KEY, user.role)
|
localStorage.setItem(ADMIN_ROLE_KEY, user.role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(ADMIN_PERMISSIONS_KEY, JSON.stringify(user?.permissions || []))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAdminSession() {
|
export function clearAdminSession() {
|
||||||
@@ -71,6 +99,7 @@ export function clearAdminSession() {
|
|||||||
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
||||||
localStorage.removeItem(ADMIN_USERNAME_KEY)
|
localStorage.removeItem(ADMIN_USERNAME_KEY)
|
||||||
localStorage.removeItem(ADMIN_ROLE_KEY)
|
localStorage.removeItem(ADMIN_ROLE_KEY)
|
||||||
|
localStorage.removeItem(ADMIN_PERMISSIONS_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasAdminSession() {
|
export function hasAdminSession() {
|
||||||
|
|||||||
Reference in New Issue
Block a user