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 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 06:42:31 +08:00
parent 6ed8de4de6
commit 46056ae4b0
26 changed files with 1480 additions and 133 deletions

53
src/auth/permissions.ts Normal file
View File

@ -0,0 +1,53 @@
import { redirect } from '@tanstack/react-router'
import { can, useAuthStore } from '@/auth/store'
/**
* Admin permission keys — mirrors the backend catalog
* (Modules/{Module}/app/Permissions/{Module}Permissions.php).
*/
export const PERM = {
CATEGORY_READ: 'admin:category:read',
CATEGORY_CREATE: 'admin:category:create',
CATEGORY_UPDATE: 'admin:category:update',
CATEGORY_DELETE: 'admin:category:delete',
CATEGORY_APPROVE: 'admin:category:approve',
COMMENT_READ: 'admin:comment:read',
COMMENT_MODERATE: 'admin:comment:moderate',
FILEX_READ: 'admin:filex:read',
FILEX_MODERATE: 'admin:filex:moderate',
DIMZOU_READ: 'admin:dimzou:read',
DIMZOU_RETRANSLATE: 'admin:dimzou:retranslate',
EXC_READ: 'admin:exc:read',
STUDIO_READ: 'admin:studio:read',
STUDIO_MANAGE: 'admin:studio:manage',
STUDIO_AGENT_TOKEN_ISSUE: 'admin:studio:agent-token:issue',
LOCALE_READ: 'admin:locale:read',
LOCALE_CREATE: 'admin:locale:create',
LOCALE_UPDATE: 'admin:locale:update',
LOCALE_DELETE: 'admin:locale:delete',
SID_MANAGE: 'admin:sid:manage',
SID_OVERRIDE: 'admin:sid:override',
NOTIFICATION_READ: 'admin:notification:read',
ROBOT_TOKEN_ISSUE: 'auth:robot-token:issue',
// role management — only super-admin holds these
ROLE_READ: 'auth:role:read',
ROLE_CREATE: 'auth:role:create',
ROLE_UPDATE: 'auth:role:update',
ROLE_DELETE: 'auth:role:delete',
ROLE_ASSIGN: 'auth:role:assign',
PERMISSION_READ: 'auth:permission:read',
} as const
export type Permission = (typeof PERM)[keyof typeof PERM]
/**
* Route `beforeLoad` guard. Redirects home when the session is loaded and
* lacks the permission; lets the request through while the session is still
* empty (the API's 403 is the backstop, and /auth/me refreshes the store).
*/
export function requirePermission(permission: Permission): void {
const { user } = useAuthStore.getState()
if (user && !can(permission)) {
throw redirect({ to: '/' })
}
}