feat(auth): wire permission gating through menu, routes and actions

The store's can()/useCan() helpers existed but were unused. Now:
- src/auth/permissions.ts PERM catalog mirrors backend keys
- every nav item carries a permission; sidebar filter reuses can()
- requirePermission() beforeLoad guard on all child routes (direct URLs
  redirect home when the session lacks the key)
- useCan() hides/disables mutation buttons: moderation actions,
  retranslate, robot-author edit/pause, category + locale CRUD, and the
  reserved-SID assign path (admin:sid:override hint)
- 403 responses toast 无权限执行此操作; can() unit tests added

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-12 06:42:31 +08:00
co-authored by Claude Fable 5
parent 6ed8de4de6
commit 46056ae4b0
26 changed files with 1480 additions and 133 deletions
+13 -1
View File
@@ -1,4 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { PERM, requirePermission } from '@/auth/permissions'
import { useCan } from '@/auth/store'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { Plus, Trash2 } from 'lucide-react'
@@ -26,6 +28,7 @@ import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
export const Route = createFileRoute('/_authed/sid')({
beforeLoad: () => requirePermission(PERM.SID_MANAGE),
component: SidPage,
})
@@ -190,6 +193,7 @@ function LuckyTab() {
}
function AssignTab() {
const canOverride = useCan(PERM.SID_OVERRIDE)
const [sid, setSid] = useState('')
const [checkResult, setCheckResult] = useState<SidCheckResult | null>(null)
const [userUid, setUserUid] = useState('')
@@ -254,6 +258,11 @@ function AssignTab() {
</Badge>
)}
{checkResult.reserved && !canOverride && (
<p className="mt-1 text-xs text-muted-foreground">
admin:sid:override
</p>
)}
</div>
)}
<div className="grid gap-1.5">
@@ -266,7 +275,10 @@ function AssignTab() {
placeholder="01JXXXXXXXXXXXXXXXXXXXXXXX"
/>
</div>
<Button onClick={assign} disabled={!sid || !userUid || busy}>
<Button
onClick={assign}
disabled={!sid || !userUid || busy || (checkResult?.reserved === true && !canOverride)}
>
SID
</Button>
</CardContent>