fix(user): align admin controls with permissions
This commit is contained in:
@@ -46,7 +46,7 @@ export const PERM = {
|
|||||||
PERMISSION_READ: 'auth:permission:read',
|
PERMISSION_READ: 'auth:permission:read',
|
||||||
PERMISSION_MANAGE: 'auth:permission:manage',
|
PERMISSION_MANAGE: 'auth:permission:manage',
|
||||||
USER_READ: 'admin:user:read',
|
USER_READ: 'admin:user:read',
|
||||||
USER_MANAGE: 'admin:user:manage',
|
USER_UPDATE: 'admin:user:update',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Permission = (typeof PERM)[keyof typeof PERM]
|
export type Permission = (typeof PERM)[keyof typeof PERM]
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const NAV_GROUPS: NavGroup[] = [
|
|||||||
{
|
{
|
||||||
label: '用户与系统',
|
label: '用户与系统',
|
||||||
items: [
|
items: [
|
||||||
{ label: '用户列表', to: '/users', icon: Users, permission: 'auth:role:assign' },
|
{ label: '用户列表', to: '/users', icon: Users, permission: 'admin:user:read' },
|
||||||
{ label: '角色管理', to: '/roles', icon: Shield, permission: 'auth:role:read' },
|
{ label: '角色管理', to: '/roles', icon: Shield, permission: 'auth:role:read' },
|
||||||
{ label: '用户角色', to: '/admin-users', icon: Users, permission: 'auth:role:assign' },
|
{ label: '用户角色', to: '/admin-users', icon: Users, permission: 'auth:role:assign' },
|
||||||
{ label: 'SID 管理', to: '/sid', icon: Hash, permission: 'admin:sid:manage' },
|
{ label: 'SID 管理', to: '/sid', icon: Hash, permission: 'admin:sid:manage' },
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { ColumnDef } from '@tanstack/react-table'
|
|||||||
import type { PlatformUser } from '@/api/types'
|
import type { PlatformUser } from '@/api/types'
|
||||||
import { fetchUsers, updateUserStatus } from '@/api/modules/users'
|
import { fetchUsers, updateUserStatus } from '@/api/modules/users'
|
||||||
import { fetchRoles, syncUserRoles } from '@/api/modules/roles'
|
import { fetchRoles, syncUserRoles } from '@/api/modules/roles'
|
||||||
import { useAuthStore } from '@/auth/store'
|
import { useCan, useAuthStore } from '@/auth/store'
|
||||||
import { handleApiError } from '@/lib/errors'
|
import { handleApiError } from '@/lib/errors'
|
||||||
import { DataTable } from '@/components/data-table/data-table'
|
import { DataTable } from '@/components/data-table/data-table'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
@@ -45,13 +45,18 @@ import { Switch } from '@/components/ui/switch'
|
|||||||
import { UserDetailSheet } from '@/features/users/user-detail-sheet'
|
import { UserDetailSheet } from '@/features/users/user-detail-sheet'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authed/users/')({
|
export const Route = createFileRoute('/_authed/users/')({
|
||||||
beforeLoad: () => requirePermission(PERM.ROLE_ASSIGN),
|
beforeLoad: () => requirePermission(PERM.USER_READ),
|
||||||
component: UsersPage,
|
component: UsersPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
function UsersPage() {
|
function UsersPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const myUid = useAuthStore((s) => s.user?.uid)
|
const myUid = useAuthStore((s) => s.user?.uid)
|
||||||
|
const isSuperAdmin = useAuthStore((s) => s.roles.includes('super-admin'))
|
||||||
|
const canUpdateUsers = useCan(PERM.USER_UPDATE)
|
||||||
|
const canReadRoles = useCan(PERM.ROLE_READ)
|
||||||
|
const canAssignRoles = useCan(PERM.ROLE_ASSIGN)
|
||||||
|
const canManageRoles = canReadRoles && canAssignRoles
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [searchInput, setSearchInput] = useState('')
|
const [searchInput, setSearchInput] = useState('')
|
||||||
@@ -68,7 +73,11 @@ function UsersPage() {
|
|||||||
const [togglingUser, setTogglingUser] = useState<{ user: PlatformUser; targetStatus: boolean } | null>(null)
|
const [togglingUser, setTogglingUser] = useState<{ user: PlatformUser; targetStatus: boolean } | null>(null)
|
||||||
|
|
||||||
// Available roles query for filter dropdown
|
// Available roles query for filter dropdown
|
||||||
const rolesQuery = useQuery({ queryKey: ['roles'], queryFn: fetchRoles })
|
const rolesQuery = useQuery({
|
||||||
|
queryKey: ['roles'],
|
||||||
|
queryFn: fetchRoles,
|
||||||
|
enabled: canReadRoles,
|
||||||
|
})
|
||||||
|
|
||||||
// Users listing query
|
// Users listing query
|
||||||
const usersQuery = useQuery({
|
const usersQuery = useQuery({
|
||||||
@@ -172,11 +181,12 @@ function UsersPage() {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const u = row.original
|
const u = row.original
|
||||||
const isSelf = u.uid === myUid
|
const isSelf = u.uid === myUid
|
||||||
|
const isProtectedSuperAdmin = u.roles.includes('super-admin') && !isSuperAdmin
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={u.status}
|
checked={u.status}
|
||||||
disabled={isSelf}
|
disabled={!canUpdateUsers || isSelf || isProtectedSuperAdmin}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
setTogglingUser({ user: u, targetStatus: false })
|
setTogglingUser({ user: u, targetStatus: false })
|
||||||
@@ -215,15 +225,17 @@ function UsersPage() {
|
|||||||
<Eye className="size-3.5 mr-1" />
|
<Eye className="size-3.5 mr-1" />
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{canManageRoles && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
onClick={() => setAssigningUser(u)}
|
variant="outline"
|
||||||
title="分配角色"
|
onClick={() => setAssigningUser(u)}
|
||||||
>
|
title="分配角色"
|
||||||
<Shield className="size-3.5 mr-1" />
|
>
|
||||||
角色
|
<Shield className="size-3.5 mr-1" />
|
||||||
</Button>
|
角色
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -296,25 +308,27 @@ function UsersPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* Role Filter */}
|
{/* Role Filter */}
|
||||||
<Select
|
{canReadRoles && (
|
||||||
value={roleFilter}
|
<Select
|
||||||
onValueChange={(val) => {
|
value={roleFilter}
|
||||||
setRoleFilter(val)
|
onValueChange={(val) => {
|
||||||
setPage(1)
|
setRoleFilter(val)
|
||||||
}}
|
setPage(1)
|
||||||
>
|
}}
|
||||||
<SelectTrigger size="sm" className="w-[130px]">
|
>
|
||||||
<SelectValue placeholder="筛选角色" />
|
<SelectTrigger size="sm" className="w-[130px]">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="筛选角色" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectItem value="all">全部角色</SelectItem>
|
<SelectContent>
|
||||||
{(rolesQuery.data?.data ?? []).map((r) => (
|
<SelectItem value="all">全部角色</SelectItem>
|
||||||
<SelectItem key={r.name} value={r.name}>
|
{(rolesQuery.data?.data ?? []).map((r) => (
|
||||||
{r.name}
|
<SelectItem key={r.name} value={r.name}>
|
||||||
</SelectItem>
|
{r.name}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
@@ -382,12 +396,16 @@ function UsersPage() {
|
|||||||
<UserDetailSheet
|
<UserDetailSheet
|
||||||
uid={detailUid}
|
uid={detailUid}
|
||||||
onClose={() => setDetailUid(null)}
|
onClose={() => setDetailUid(null)}
|
||||||
onAssignRoles={() => {
|
onAssignRoles={
|
||||||
const u = usersQuery.data?.data?.find((x) => x.uid === detailUid)
|
canManageRoles
|
||||||
if (u) {
|
? () => {
|
||||||
setAssigningUser(u)
|
const u = usersQuery.data?.data?.find((x) => x.uid === detailUid)
|
||||||
}
|
if (u) {
|
||||||
}}
|
setAssigningUser(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Assign Roles Modal */}
|
{/* Assign Roles Modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user