fix(auth): reject consumer-only admin sessions
This commit is contained in:
@@ -44,6 +44,7 @@ export const PERM = {
|
||||
ROLE_DELETE: 'auth:role:delete',
|
||||
ROLE_ASSIGN: 'auth:role:assign',
|
||||
PERMISSION_READ: 'auth:permission:read',
|
||||
PERMISSION_MANAGE: 'auth:permission:manage',
|
||||
USER_READ: 'admin:user:read',
|
||||
USER_MANAGE: 'admin:user:manage',
|
||||
} as const
|
||||
|
||||
+23
-1
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { can, hasRole, useAuthStore } from '@/auth/store'
|
||||
import { can, hasAdminAccess, hasRole, isAuthenticated, useAuthStore } from '@/auth/store'
|
||||
|
||||
const user = {
|
||||
kind: 'user' as const,
|
||||
@@ -34,4 +34,26 @@ describe('can()', () => {
|
||||
expect(hasRole('admin')).toBe(true)
|
||||
expect(hasRole('super-admin')).toBe(false)
|
||||
})
|
||||
|
||||
it('does not treat consumer roles or permissions as admin access', () => {
|
||||
expect(hasAdminAccess(['client'], ['exc:demand:read'])).toBe(false)
|
||||
expect(hasAdminAccess(['service-provider'], ['exc:quote:create'])).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts administrative permissions and the super-admin role', () => {
|
||||
expect(hasAdminAccess(['admin'], ['admin:category:read'])).toBe(true)
|
||||
expect(hasAdminAccess(['developer'], ['auth:api-docs:read'])).toBe(true)
|
||||
expect(hasAdminAccess(['super-admin'], [])).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a hydrated consumer session', () => {
|
||||
useAuthStore.getState().setTokens({
|
||||
access_token: 'token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 3600,
|
||||
})
|
||||
useAuthStore.getState().setSession(user, ['client'], ['exc:demand:read'])
|
||||
|
||||
expect(isAuthenticated()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
+10
-1
@@ -50,9 +50,18 @@ export function useCan(permission: string): boolean {
|
||||
return roles.includes('super-admin') || permissions.includes(permission)
|
||||
}
|
||||
|
||||
export function hasAdminAccess(roles: string[], permissions: string[]): boolean {
|
||||
return (
|
||||
roles.includes('super-admin') ||
|
||||
permissions.some((permission) =>
|
||||
permission.startsWith('admin:') || permission.startsWith('auth:'),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
const { accessToken, user, roles, permissions } = useAuthStore.getState()
|
||||
if (!accessToken) return false
|
||||
if (user && roles.length === 0 && permissions.length === 0) return false
|
||||
if (user && !hasAdminAccess(roles, permissions)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect } from 'react'
|
||||
import { LogOut, Moon, Sun } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { fetchMe } from '@/api/modules/auth'
|
||||
import { can, isAuthenticated, useAuthStore } from '@/auth/store'
|
||||
import { can, hasAdminAccess, isAuthenticated, useAuthStore } from '@/auth/store'
|
||||
import { NAV_GROUPS } from '@/components/layout/nav'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -44,7 +44,7 @@ function AuthedLayout() {
|
||||
if (me.data) {
|
||||
const roles = me.data.meta?.roles ?? []
|
||||
const permissions = me.data.meta?.permissions ?? []
|
||||
if (roles.length === 0 && permissions.length === 0) {
|
||||
if (!hasAdminAccess(roles, permissions)) {
|
||||
useAuthStore.getState().clear()
|
||||
toast.error('账号未分配管理后台权限,已退出登录')
|
||||
void navigate({ to: '/login' })
|
||||
|
||||
@@ -47,7 +47,9 @@ export const Route = createFileRoute('/_authed/roles')({
|
||||
|
||||
function RolesPage() {
|
||||
const canCreate = useCan(PERM.ROLE_CREATE)
|
||||
const canUpdate = useCan(PERM.ROLE_UPDATE)
|
||||
const canUpdateRole = useCan(PERM.ROLE_UPDATE)
|
||||
const canManagePermissions = useCan(PERM.PERMISSION_MANAGE)
|
||||
const canUpdate = canUpdateRole && canManagePermissions
|
||||
const canDelete = useCan(PERM.ROLE_DELETE)
|
||||
|
||||
const roles = useQuery({ queryKey: ['roles'], queryFn: fetchRoles })
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { loginWithPassword, fetchMe } from '@/api/modules/auth'
|
||||
import { ApiError } from '@/api/client'
|
||||
import { useAuthStore, isAuthenticated } from '@/auth/store'
|
||||
import { hasAdminAccess, useAuthStore, isAuthenticated } from '@/auth/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -46,7 +46,7 @@ function LoginPage() {
|
||||
const me = await fetchMe()
|
||||
const roles = me.meta?.roles ?? []
|
||||
const permissions = me.meta?.permissions ?? []
|
||||
if (roles.length === 0 && permissions.length === 0) {
|
||||
if (!hasAdminAccess(roles, permissions)) {
|
||||
useAuthStore.getState().clear()
|
||||
toast.error('该账号未分配管理后台权限,无法登录')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user