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} }
-
-
-
- {repairing ? '正在启动…' : '全库修复+重刮'}
-
-
-
- 刮削队列
-
-
- 管理媒体库
-
-
+ {isAdmin && (
+
+ {repairMsg && {repairMsg} }
+
+
+
+ {repairing ? '正在启动…' : '全库修复+重刮'}
+
+
+
+ 刮削队列
+
+
+ 管理媒体库
+
+
+ )}
)
}
-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