From c74f6be2504d53848cc0f1fd61447bd4f0a20dfe Mon Sep 17 00:00:00 2001 From: truewhile <779943132@qq.com> Date: Tue, 15 Sep 2026 15:19:54 +0800 Subject: [PATCH] Hide scrape and library admin controls from non-admin users. Co-authored-by: Cursor --- web/src/components/GlobalEvents.tsx | 2 + web/src/components/useLayoutPermissions.ts | 5 +- web/src/hooks/usePermission.ts | 42 ++++++------ web/src/pages/LibrariesPage.tsx | 8 ++- web/src/pages/LibrariesPageSections.tsx | 78 ++++++++++++---------- web/src/stores/permissions.ts | 5 +- 6 files changed, 75 insertions(+), 65 deletions(-) diff --git a/web/src/components/GlobalEvents.tsx b/web/src/components/GlobalEvents.tsx index 9bc5ff5..bdc359f 100644 --- a/web/src/components/GlobalEvents.tsx +++ b/web/src/components/GlobalEvents.tsx @@ -37,6 +37,7 @@ export function GlobalEvents() { } } if (topic === 'scrape' && p.finished) { + if (role !== 'admin') return const processed = Number(p.processed ?? 0) const matched = Number(p.matched ?? 0) const failed = Number(p.failed ?? 0) @@ -51,6 +52,7 @@ export function GlobalEvents() { } } if (topic === 'subscription') { + if (role !== 'admin') return const queued = (p.queued as number | undefined) ?? 0 if (queued > 0) toast.success(`订阅「${p.name}」已加入 ${queued} 项下载`) } diff --git a/web/src/components/useLayoutPermissions.ts b/web/src/components/useLayoutPermissions.ts index 3ed4c0f..27228a7 100644 --- a/web/src/components/useLayoutPermissions.ts +++ b/web/src/components/useLayoutPermissions.ts @@ -5,7 +5,6 @@ import type { User } from '../types' export function useLayoutPermissions(user: User | null | undefined) { const permissions = usePermissionStore((state) => state.permissions) - const isSuper = usePermissionStore((state) => state.isSuper) const isPermissionLoading = usePermissionStore((state) => state.isLoading) const fetchPermissions = usePermissionStore((state) => state.fetchPermissions) @@ -17,8 +16,8 @@ export function useLayoutPermissions(user: User | null | undefined) { const isAdmin = user?.role === 'admin' const can = useCallback( - (key: string) => isAdmin || isSuper || (permissions ?? {})[key] === true, - [isAdmin, isSuper, permissions], + (key: string) => isAdmin || (permissions ?? {})[key] === true, + [isAdmin, permissions], ) return { can, isAdmin } diff --git a/web/src/hooks/usePermission.ts b/web/src/hooks/usePermission.ts index 687cc68..4e5c25d 100644 --- a/web/src/hooks/usePermission.ts +++ b/web/src/hooks/usePermission.ts @@ -5,17 +5,17 @@ import { useAuthStore } from '../stores/auth' /** * usePermission hook - 检查用户是否拥有特定权限 - * + * * @param key - 权限键名 * @param options - 配置选项 * @param options.autoFetch - 是否在权限未加载时自动获取(默认 true) * @returns boolean - 用户是否拥有该权限 - * + * * @example * ```tsx * function MyComponent() { * const canEdit = usePermission('can_edit_media') - * + * * if (canEdit) { * return * } @@ -30,26 +30,24 @@ export function usePermission( const { autoFetch = true } = options // selector 订阅:store 任何无关字段变化不会触发本组件重渲染 const hasPermission = usePermissionStore((s) => s.hasPermission) - const isSuper = usePermissionStore((s) => s.isSuper) const fetchPermissions = usePermissionStore((s) => s.fetchPermissions) - const tier = useAuthStore((state) => state.tier) const role = useAuthStore((state) => state.user?.role) const isAuthenticated = useAuthStore((state) => state.token !== null) - const hasSuperAccess = isSuper || tier === 'plus' || role === 'admin' + const isAdmin = role === 'admin' // 权限未加载时自动获取;用 getState() 读最新快照而不是渲染闭包, // 同一次 commit 内挂载的多个消费方也只会有一个发出请求(store 内还有 inflight 去重兜底)。 useEffect(() => { - if (!hasSuperAccess && isAuthenticated && autoFetch) { + if (!isAdmin && isAuthenticated && autoFetch) { const { permissions, isLoading } = usePermissionStore.getState() if (Object.keys(permissions).length === 0 && !isLoading) { fetchPermissions() } } - }, [autoFetch, fetchPermissions, hasSuperAccess, isAuthenticated]) + }, [autoFetch, fetchPermissions, isAdmin, isAuthenticated]) - // 超级用户有所有权限 - if (hasSuperAccess) { + // 管理员拥有全部能力;plus / is_super 不能用来展示刮削、整理、删除等管理入口 + if (isAdmin) { return true } @@ -63,18 +61,18 @@ export function usePermission( /** * usePermissions hook - 获取所有权限 - * + * * @returns 权限状态和检查函数 - * + * * @example * ```tsx * function MyComponent() { * const { permissions, isSuper, check } = usePermissions() - * + * * if (isSuper) { * return * } - * + * * return ( *
* {check('can_view_dashboard') && } @@ -87,15 +85,14 @@ export function usePermission( export function usePermissions() { // selector 订阅:只关注 permissions/isSuper/isLoading 变化 const permissions = usePermissionStore((s) => s.permissions) - const isSuper = usePermissionStore((s) => s.isSuper) const isLoading = usePermissionStore((s) => s.isLoading) const fetchPermissions = usePermissionStore((s) => s.fetchPermissions) - const tier = useAuthStore((state) => state.tier) const role = useAuthStore((state) => state.user?.role) const isAuthenticated = useAuthStore((state) => state.token !== null) + const isAdmin = role === 'admin' const check = (key: string): boolean => { - if (isSuper || tier === 'plus' || role === 'admin') { + if (isAdmin) { return true } return permissions[key] === true @@ -103,7 +100,8 @@ export function usePermissions() { return { permissions, - isSuper: isSuper || tier === 'plus' || role === 'admin', + // Keep the field name for callers, but only admins are treated as full-access. + isSuper: isAdmin, isLoading, check, refetch: fetchPermissions, @@ -113,10 +111,10 @@ export function usePermissions() { /** * usePermissionMany hook - 批量检查多个权限 - * + * * @param keys - 权限键数组 * @returns 每个权限的布尔值映射 - * + * * @example * ```tsx * function MyComponent() { @@ -125,7 +123,7 @@ export function usePermissions() { * 'can_manage_users', * 'can_access_settings', * ]) - * + * * return ( *
* {perms['can_edit_media'] && } @@ -137,7 +135,7 @@ export function usePermissions() { */ export function usePermissionMany(keys: string[]): Record { const { check } = usePermissions() - + return keys.reduce((acc, key) => { acc[key] = check(key) return acc diff --git a/web/src/pages/LibrariesPage.tsx b/web/src/pages/LibrariesPage.tsx index f02a838..7f936bb 100644 --- a/web/src/pages/LibrariesPage.tsx +++ b/web/src/pages/LibrariesPage.tsx @@ -5,6 +5,7 @@ import { toolsAPI } from '../api/tools' import { openManageLibrariesDialog } from '../components/manageLibrariesDialog' import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference' import { usePinnedLibraries } from '../hooks/usePinnedLibraries' +import { useAuthStore } from '../stores/auth' import { LibrariesContent, LibrariesEmptyState, @@ -18,6 +19,7 @@ import { sortLibraryPreviews } from '../utils/pinnedLibraries' import { partitionPreviewIDs } from '../utils/remoteEmby' export function LibrariesPage() { + const isAdmin = useAuthStore((state) => state.user?.role === 'admin') const [libraries, setLibraries] = useState([]) const [libraryData, setLibraryData] = useState>({}) const { pinnedIds, loading: pinnedLoading, togglePin } = usePinnedLibraries() @@ -103,7 +105,7 @@ export function LibrariesPage() { }, []) async function handleRepairRescrape() { - if (repairing) return + if (!isAdmin || repairing) return setRepairing(true) setRepairMsg('') try { @@ -117,6 +119,7 @@ export function LibrariesPage() { } const handleManageLibraries = async () => { + if (!isAdmin) return await openManageLibrariesDialog() await loadLibraries({ force: true }) } @@ -154,6 +157,7 @@ export function LibrariesPage() { {previews.length === 0 ? ( - + ) : (
-
- {repairMsg && {repairMsg}} - - - - - 刮削队列 - - -
+ {isAdmin && ( +
+ {repairMsg && {repairMsg}} + + + + + 刮削队列 + + +
+ )}
) } -export function LibrariesEmptyState() { +export function LibrariesEmptyState({ isAdmin = false }: { isAdmin?: boolean }) { return (
-

暂无媒体库,请到管理后台添加目录。

+

+ {isAdmin ? '暂无媒体库,请到管理后台添加目录。' : '暂无可用媒体库。'} +

) } diff --git a/web/src/stores/permissions.ts b/web/src/stores/permissions.ts index c781166..258ae07 100644 --- a/web/src/stores/permissions.ts +++ b/web/src/stores/permissions.ts @@ -53,8 +53,9 @@ export const usePermissionStore = create((set, get) => ({ hasPermission: (key: string) => { const state = get() - // Super user (admin or plus) has all permissions - if (state.isSuper) { + // Only admins implicitly have every capability in the UI. + // Plus / is_super must not reveal scrape/organize/delete controls. + if (state.role === 'admin') { return true } return (state.permissions ?? {})[key] === true