fix(auth): reject consumer-only admin sessions

This commit is contained in:
2026-09-07 20:08:27 +08:00
parent e3fcaa4d38
commit 2bc5c5df50
6 changed files with 41 additions and 7 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+2 -2
View File
@@ -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' })
+3 -1
View File
@@ -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 })
+2 -2
View File
@@ -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