后台新增RBAC权限模型支持按角色配置接单管理权限
- 新增 admin_roles/admin_permissions/admin_role_permissions 三表与种子数据(059迁移) - 接单平台全部管理路由从固定角色切换为细粒度权限校验 - 撤单按动作二次校验:退回大厅与取消订单分别要求对应权限 - 管理员账号页新增角色权限配置卡片(admin专属,带审计日志) - 权限更新走事务保证原子性;session实时解析权限确保修改立即生效
This commit is contained in:
@@ -32,10 +32,11 @@ import {
|
||||
} from 'antd'
|
||||
import type { MenuProps } from 'antd'
|
||||
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 {
|
||||
fetchAdminSession,
|
||||
fetchAdminNotifications,
|
||||
fetchDevMockStatus,
|
||||
logoutAdmin,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
getAdminToken,
|
||||
getAdminTokenExpiresAt,
|
||||
getAdminUsername,
|
||||
setAdminPermissions,
|
||||
} from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||
@@ -69,6 +71,19 @@ export default function AdminLayout() {
|
||||
() => 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({
|
||||
queryKey: ['admin-dev-mock-status'],
|
||||
queryFn: () => fetchDevMockStatus(),
|
||||
@@ -546,7 +561,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
type NotificationAlertKind =
|
||||
'withdraw' | 'recharge' | 'acceptance' | 'material' | 'reminder' | 'default'
|
||||
| 'withdraw'
|
||||
| 'recharge'
|
||||
| 'acceptance'
|
||||
| 'material'
|
||||
| 'reminder'
|
||||
| 'default'
|
||||
|
||||
function playNotificationSound(kind: NotificationAlertKind) {
|
||||
try {
|
||||
|
||||
@@ -24,10 +24,12 @@ import PageHeader from '@/components/admin/PageHeader'
|
||||
import StatusTag from '@/components/admin/StatusTag'
|
||||
import {
|
||||
createAdminUser,
|
||||
fetchAdminRolePermissions,
|
||||
fetchAdminUsers,
|
||||
resetAdminUserPassword,
|
||||
updateAdminUserRole,
|
||||
updateAdminUserStatus,
|
||||
updateAdminRolePermissions,
|
||||
} from '@/services/admin'
|
||||
import type { AdminRole, AdminUserListItem } from '@/types/admin'
|
||||
import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth'
|
||||
@@ -83,11 +85,33 @@ export default function AdminUsersPage() {
|
||||
queryFn: () => fetchAdminUsers(queryParams),
|
||||
})
|
||||
const data = query.data?.data
|
||||
const rolePermissionsQuery = useQuery({
|
||||
queryKey: ['admin-role-permissions'],
|
||||
enabled: isAdmin,
|
||||
queryFn: () => fetchAdminRolePermissions(),
|
||||
})
|
||||
|
||||
async function reloadUsers() {
|
||||
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) {
|
||||
setCreating(true)
|
||||
|
||||
@@ -420,6 +444,37 @@ export default function AdminUsersPage() {
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
deductAdminWorkOrderPendingDeposit,
|
||||
deleteAdminWorkOrder,
|
||||
fetchAdminWorkCategories,
|
||||
fetchAdminSession,
|
||||
fetchAdminWorkOrderSharing,
|
||||
fetchAdminWorkerLevels,
|
||||
fetchAdminWorkerUsers,
|
||||
@@ -72,7 +73,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||
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 {
|
||||
DeadlineCountdown,
|
||||
@@ -151,6 +152,51 @@ export default function WorkOrdersPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const canEditRequirementFields = 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 [targetWorkOrderId, setTargetWorkOrderId] = useState<number | undefined>()
|
||||
const [keyword, setKeyword] = useState('')
|
||||
@@ -976,17 +1022,17 @@ export default function WorkOrdersPanel() {
|
||||
render: (_, row) => (
|
||||
<Space wrap>
|
||||
<Button onClick={() => openDetailDrawer(row)}>详情</Button>
|
||||
{row.capabilities?.canEdit ? (
|
||||
{row.capabilities?.canEdit && canEditOrder ? (
|
||||
<Button icon={<EditOutlined />} onClick={() => openEditModal(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canDelete ? (
|
||||
{row.capabilities?.canDelete && canDeleteOrder ? (
|
||||
<Button danger icon={<DeleteOutlined />} onClick={() => requestDeleteWorkOrder(row)}>
|
||||
删除
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canReopen ? (
|
||||
{row.capabilities?.canReopen && canReopenOrder ? (
|
||||
<Button
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
@@ -1006,29 +1052,30 @@ export default function WorkOrdersPanel() {
|
||||
重新启用
|
||||
</Button>
|
||||
) : null}
|
||||
{row.sharing?.enabled || row.capabilities?.canConfigureSharing ? (
|
||||
{row.sharing?.enabled ||
|
||||
(row.capabilities?.canConfigureSharing && canConfigureSharing) ? (
|
||||
<Button icon={<TeamOutlined />} onClick={() => openSharingConfigModal(row)}>
|
||||
{row.sharing?.enabled && !row.capabilities?.canConfigureSharing
|
||||
? '拼单明细'
|
||||
: '拼单配置'}
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canAssign ? (
|
||||
{row.capabilities?.canAssign && canAssignOrder ? (
|
||||
<Button icon={<UserAddOutlined />} onClick={() => openAssignModal(row)}>
|
||||
指派并核销
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCancel ? (
|
||||
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
||||
撤单
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canUpdateMaterial ? (
|
||||
{row.capabilities?.canUpdateMaterial && canUpdateMaterial ? (
|
||||
<Button icon={<FormOutlined />} onClick={() => openMaterialModal(row)}>
|
||||
{['open', 'in_progress'].includes(row.status) ? '更新资料' : '补资料'}
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canPublish ? (
|
||||
{row.capabilities?.canPublish && canPublishOrder ? (
|
||||
<Button
|
||||
icon={<SendOutlined />}
|
||||
disabled={Number(row.rewardAmount || 0) <= 0}
|
||||
@@ -1040,7 +1087,7 @@ export default function WorkOrdersPanel() {
|
||||
发布并核销
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canPin ? (
|
||||
{row.capabilities?.canPin && canPublishOrder ? (
|
||||
<Button
|
||||
icon={<PushpinOutlined />}
|
||||
title={row.pinnedAt ? '取消置顶' : '置顶后抢单大厅优先展示'}
|
||||
@@ -1054,7 +1101,7 @@ export default function WorkOrdersPanel() {
|
||||
{row.pinnedAt ? '取消置顶' : '置顶'}
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canUnpublish ? (
|
||||
{row.capabilities?.canUnpublish && canPublishOrder ? (
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
@@ -1071,7 +1118,7 @@ export default function WorkOrdersPanel() {
|
||||
下架
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canAccept ? (
|
||||
{row.capabilities?.canAccept && canAcceptOrder ? (
|
||||
<Button
|
||||
icon={<CheckOutlined />}
|
||||
type="primary"
|
||||
@@ -1080,7 +1127,9 @@ export default function WorkOrdersPanel() {
|
||||
验收
|
||||
</Button>
|
||||
) : 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>
|
||||
@@ -1098,7 +1147,7 @@ export default function WorkOrdersPanel() {
|
||||
售后
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canConfirmFriend ? (
|
||||
{row.capabilities?.canConfirmFriend && canManageProblem ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UserAddOutlined />}
|
||||
@@ -1107,17 +1156,17 @@ export default function WorkOrdersPanel() {
|
||||
确认加好友
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canResetGiftFlow ? (
|
||||
{row.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||
<Button danger onClick={() => requestResetGiftMaterial(row)}>
|
||||
重置打手资料
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canMarkProblem ? (
|
||||
{row.capabilities?.canMarkProblem && canManageProblem ? (
|
||||
<Button icon={<WarningOutlined />} danger onClick={() => setProblemOrder(row)}>
|
||||
问题单
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canResolveProblem ? (
|
||||
{row.capabilities?.canResolveProblem && canManageProblem ? (
|
||||
<Button icon={<FormOutlined />} onClick={() => setResolutionOrder(row)}>
|
||||
处置
|
||||
</Button>
|
||||
@@ -1470,7 +1519,7 @@ export default function WorkOrdersPanel() {
|
||||
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
|
||||
{formatGiftPhase(detailOrder.gift.phase)}
|
||||
</Tag>
|
||||
{detailOrder.capabilities?.canConfirmFriend ? (
|
||||
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -1479,7 +1528,7 @@ export default function WorkOrdersPanel() {
|
||||
确认加好友
|
||||
</Button>
|
||||
) : null}
|
||||
{detailOrder.capabilities?.canResetGiftFlow ? (
|
||||
{detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
@@ -1814,7 +1863,7 @@ export default function WorkOrdersPanel() {
|
||||
<Form
|
||||
form={cancelForm}
|
||||
layout="vertical"
|
||||
initialValues={{ action: 'return_to_hall' }}
|
||||
initialValues={{ action: canReturnToHall ? 'return_to_hall' : 'cancel_order' }}
|
||||
onFinish={submitCancelOrder}
|
||||
>
|
||||
<Alert
|
||||
@@ -1830,8 +1879,12 @@ export default function WorkOrdersPanel() {
|
||||
>
|
||||
<Radio.Group>
|
||||
<Space direction="vertical">
|
||||
<Radio value="return_to_hall">退回大厅:订单重新开放给其他打手接单</Radio>
|
||||
<Radio value="cancel_order">取消订单:用户退款后终止订单,不再回大厅</Radio>
|
||||
{canReturnToHall ? (
|
||||
<Radio value="return_to_hall">退回大厅:订单重新开放给其他打手接单</Radio>
|
||||
) : null}
|
||||
{canCancelOrder ? (
|
||||
<Radio value="cancel_order">取消订单:用户退款后终止订单,不再回大厅</Radio>
|
||||
) : null}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
@@ -1988,7 +2041,7 @@ export default function WorkOrdersPanel() {
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`${sharingOrder?.capabilities?.canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
|
||||
title={`${sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? '拼单配置' : '拼单明细'} · ${sharingOrder?.productName || ''}`}
|
||||
open={Boolean(sharingOrder)}
|
||||
onCancel={() => {
|
||||
setSharingOrder(null)
|
||||
@@ -1997,7 +2050,9 @@ export default function WorkOrdersPanel() {
|
||||
cancelShareForm.resetFields()
|
||||
}}
|
||||
onOk={() => sharingForm.submit()}
|
||||
footer={sharingOrder?.capabilities?.canConfigureSharing ? undefined : null}
|
||||
footer={
|
||||
sharingOrder?.capabilities?.canConfigureSharing && canConfigureSharing ? undefined : null
|
||||
}
|
||||
destroyOnHidden
|
||||
width={720}
|
||||
>
|
||||
@@ -2012,7 +2067,7 @@ export default function WorkOrdersPanel() {
|
||||
unitReward: 0,
|
||||
}}
|
||||
onFinish={submitSharingConfig}
|
||||
disabled={!sharingOrder.capabilities?.canConfigureSharing}
|
||||
disabled={!sharingOrder.capabilities?.canConfigureSharing || !canConfigureSharing}
|
||||
>
|
||||
<Form.Item
|
||||
label="启用拼单"
|
||||
@@ -2169,17 +2224,19 @@ export default function WorkOrdersPanel() {
|
||||
) : (
|
||||
<Typography.Text type="secondary">待提交</Typography.Text>
|
||||
)}
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
setCancelShare(share)
|
||||
cancelShareForm.setFieldsValue({ reason: '' })
|
||||
}}
|
||||
>
|
||||
撤单
|
||||
</Button>
|
||||
{canCancelShare ? (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={() => {
|
||||
setCancelShare(share)
|
||||
cancelShareForm.setFieldsValue({ reason: '' })
|
||||
}}
|
||||
>
|
||||
撤单
|
||||
</Button>
|
||||
) : null}
|
||||
</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'
|
||||
|
||||
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>) {
|
||||
return apiGet<{ items: AdminUserListItem[]; pagination: AdminPagination }>(
|
||||
'/api/v1/admin/users',
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AdminLoginResponse {
|
||||
userId: number
|
||||
username: string
|
||||
role: AdminRole
|
||||
permissions?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +19,6 @@ export interface AdminSessionSummary {
|
||||
userId: number
|
||||
username: string
|
||||
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_USERNAME_KEY = 'order-site-admin-username'
|
||||
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> = {
|
||||
support: 1,
|
||||
@@ -36,6 +37,26 @@ export function getAdminRole() {
|
||||
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') {
|
||||
if (!hasAdminSession()) {
|
||||
return false
|
||||
@@ -47,7 +68,12 @@ export function hasAdminRole(role: 'admin' | 'operator' | 'support') {
|
||||
export function setAdminSession(
|
||||
token: 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_EXPIRES_AT_KEY, expiresAt)
|
||||
@@ -63,6 +89,8 @@ export function setAdminSession(
|
||||
if (user?.role) {
|
||||
localStorage.setItem(ADMIN_ROLE_KEY, user.role)
|
||||
}
|
||||
|
||||
localStorage.setItem(ADMIN_PERMISSIONS_KEY, JSON.stringify(user?.permissions || []))
|
||||
}
|
||||
|
||||
export function clearAdminSession() {
|
||||
@@ -71,6 +99,7 @@ export function clearAdminSession() {
|
||||
localStorage.removeItem(ADMIN_USER_ID_KEY)
|
||||
localStorage.removeItem(ADMIN_USERNAME_KEY)
|
||||
localStorage.removeItem(ADMIN_ROLE_KEY)
|
||||
localStorage.removeItem(ADMIN_PERMISSIONS_KEY)
|
||||
}
|
||||
|
||||
export function hasAdminSession() {
|
||||
|
||||
Reference in New Issue
Block a user