diff --git a/CHANGELOG.md b/CHANGELOG.md index a303f49..450a95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## 2026-05-14 -- [修复] `next.config.ts` 添加 `ldapts` 到 `serverExternalPackages`(预防性修复),确保 Next.js standalone 构建包含 LLDAP 客户端模块,避免 SSO 自动创建用户失败(issue-ai 已实际触发,参见 issue-ai CHANGELOG) -- [调整] 全局证书切换:Cloudflare Origin CA → Let's Encrypt(`/etc/letsencrypt/live/www.tlyq.ai/`),覆盖全部 7 个子域名,nginx 8 个站点配置同步更新 +- [新增] 权限细粒度拆分:`assets:write` → `assets:create`(新增设备)、`assets:import`(导入设备)、`assets:update`(编辑设备),支持独立授权 +- [新增] API 模板下载路由增加 `assets:import` 权限检查(原仅校验登录态) +- [优化] 角色权限选择列表同步新增三个权限选项(新增资产/导入资产/编辑资产) +- [修复] 资产列表页、详情页按钮无权限控制:导入、模板、新增设备、编辑、删除、批量编辑按钮现在由用户权限驱动显隐 +- [修复] 新增/导入/编辑资产页面无权限守卫:无权限用户直接访问会被重定向到资产列表 +- [修复] 资产列表页模板按钮链接指向 404(`/assets/template` → `/api/assets/template`) +- [调整] editor/viewer 角色权限允许自定义编辑(`db-schema.ts` 仅强制同步 admin),三个内置角色均不可删除 ## 2026-05-12 diff --git a/src/app/(app)/assets/[id]/edit/page.tsx b/src/app/(app)/assets/[id]/edit/page.tsx index c5c689d..a8cae53 100644 --- a/src/app/(app)/assets/[id]/edit/page.tsx +++ b/src/app/(app)/assets/[id]/edit/page.tsx @@ -13,6 +13,15 @@ export default function EditAssetPage() { const [asset, setAsset] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) + const [authorized, setAuthorized] = useState(false) + + useEffect(() => { + fetch('/api/auth/me').then(r => r.json()).then(d => { + const perms: string[] = d.user?.permissions || [] + if (!perms.includes('*') && !perms.includes('assets:update')) router.replace('/assets') + else setAuthorized(true) + }).catch(() => router.replace('/assets')) + }, [router]) useEffect(() => { async function load() { @@ -35,6 +44,7 @@ export default function EditAssetPage() { } finally { setSaving(false) } } + if (!authorized) return
验证权限中...
if (loading) return
加载中...
if (!asset) return null diff --git a/src/app/(app)/assets/[id]/page.tsx b/src/app/(app)/assets/[id]/page.tsx index 8763b1f..2f5bb1d 100644 --- a/src/app/(app)/assets/[id]/page.tsx +++ b/src/app/(app)/assets/[id]/page.tsx @@ -16,6 +16,7 @@ export default function AssetDetailPage() { const [loading, setLoading] = useState(true) const [showDelete, setShowDelete] = useState(false) const [deleting, setDeleting] = useState(false) + const [permissions, setPermissions] = useState([]) useEffect(() => { async function load() { @@ -26,6 +27,12 @@ export default function AssetDetailPage() { load().finally(() => setLoading(false)) }, [params.id, router]) + useEffect(() => { + fetch('/api/auth/me').then(r => r.json()).then(d => { + if (d.user?.permissions) setPermissions(d.user.permissions) + }).catch(() => {}) + }, []) + async function handleDelete() { setDeleting(true) try { @@ -48,8 +55,12 @@ export default function AssetDetailPage() {
- - + {(permissions.includes('*') || permissions.includes('assets:update')) && ( + + )} + {(permissions.includes('*') || permissions.includes('assets:delete')) && ( + + )}
diff --git a/src/app/(app)/assets/import/page.tsx b/src/app/(app)/assets/import/page.tsx index d4de382..7a40d8a 100644 --- a/src/app/(app)/assets/import/page.tsx +++ b/src/app/(app)/assets/import/page.tsx @@ -1,9 +1,24 @@ 'use client' +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' import Link from 'next/link' import AssetImport from '@/components/assets/AssetImport' import { ArrowLeft } from 'lucide-react' export default function ImportPage() { + const router = useRouter() + const [authorized, setAuthorized] = useState(false) + + useEffect(() => { + fetch('/api/auth/me').then(r => r.json()).then(d => { + const perms: string[] = d.user?.permissions || [] + if (!perms.includes('*') && !perms.includes('assets:import')) router.replace('/assets') + else setAuthorized(true) + }).catch(() => router.replace('/assets')) + }, [router]) + + if (!authorized) return
验证权限中...
+ async function handleImport(file: File) { const formData = new FormData() formData.append('file', file) diff --git a/src/app/(app)/assets/new/page.tsx b/src/app/(app)/assets/new/page.tsx index eab4d15..7b7fce0 100644 --- a/src/app/(app)/assets/new/page.tsx +++ b/src/app/(app)/assets/new/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import AssetForm from '@/components/assets/AssetForm' @@ -8,6 +8,15 @@ import { ArrowLeft } from 'lucide-react' export default function NewAssetPage() { const router = useRouter() const [saving, setSaving] = useState(false) + const [authorized, setAuthorized] = useState(false) + + useEffect(() => { + fetch('/api/auth/me').then(r => r.json()).then(d => { + const perms: string[] = d.user?.permissions || [] + if (!perms.includes('*') && !perms.includes('assets:create')) router.replace('/assets') + else setAuthorized(true) + }).catch(() => router.replace('/assets')) + }, [router]) async function handleSubmit(data: Record) { setSaving(true) @@ -22,6 +31,8 @@ export default function NewAssetPage() { } finally { setSaving(false) } } + if (!authorized) return
验证权限中...
+ return (
diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx index 6a07f99..bfe11b3 100644 --- a/src/app/(app)/assets/page.tsx +++ b/src/app/(app)/assets/page.tsx @@ -365,8 +365,13 @@ export default function AssetsPage() { // 是否有任何列筛选激活 const hasActiveColumnFilters = Object.values(columnFilterValues).some(v => v.length > 0) - const canExportAll = permissions.includes('*') || permissions.includes('assets:export:all') - const canExportSelected = permissions.includes('*') || permissions.includes('assets:export:selected') + const hasWildcard = permissions.includes('*') + const canExportAll = hasWildcard || permissions.includes('assets:export:all') + const canExportSelected = hasWildcard || permissions.includes('assets:export:selected') + const canCreate = hasWildcard || permissions.includes('assets:create') + const canImport = hasWildcard || permissions.includes('assets:import') + const canUpdate = hasWildcard || permissions.includes('assets:update') + const canDelete = hasWildcard || permissions.includes('assets:delete') return (
@@ -379,15 +384,15 @@ export default function AssetsPage() {

- {selectedIds.size > 0 && permissions.includes('assets:update') && ( + {canUpdate && selectedIds.size > 0 && ( )} - {permissions.includes('assets:import') && ( + {canImport && ( )} - {permissions.includes('assets:import') && ( + {canImport && ( )} {canExportSelected && selectedIds.size > 0 && ( @@ -396,7 +401,9 @@ export default function AssetsPage() { {canExportAll && ( )} - + {canCreate && ( + + )}
@@ -559,14 +566,18 @@ export default function AssetsPage() { className="p-1.5 rounded-lg text-slate-500 hover:text-blue-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"> - - - - + {canUpdate && ( + + + + )} + {canDelete && ( + + )}
diff --git a/src/app/(app)/settings/roles/page.tsx b/src/app/(app)/settings/roles/page.tsx index ba17dc2..1fbccd8 100644 --- a/src/app/(app)/settings/roles/page.tsx +++ b/src/app/(app)/settings/roles/page.tsx @@ -17,7 +17,9 @@ interface Role { const allPermissions = [ { key: 'assets:read', label: '查看资产' }, - { key: 'assets:write', label: '编辑资产' }, + { key: 'assets:create', label: '新增资产' }, + { key: 'assets:import', label: '导入资产' }, + { key: 'assets:update', label: '编辑资产' }, { key: 'assets:delete', label: '删除资产' }, { key: 'assets:export:selected', label: '导出选中资产' }, { key: 'assets:export:all', label: '导出全部资产' }, diff --git a/src/app/api/assets/template/route.ts b/src/app/api/assets/template/route.ts index 79ca1d8..b22e585 100644 --- a/src/app/api/assets/template/route.ts +++ b/src/app/api/assets/template/route.ts @@ -5,7 +5,7 @@ import { generateTemplateBuffer } from '@/lib/excel' export async function GET() { const payload = await getSession() - if (!payload) return NextResponse.json({ error: '未授权' }, { status: 401 }) + if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) if (!checkPermission(payload.role, 'assets:import')) { return NextResponse.json({ error: '权限不足' }, { status: 403 }) } diff --git a/src/lib/db-schema.ts b/src/lib/db-schema.ts index 9a90b66..5c38311 100644 --- a/src/lib/db-schema.ts +++ b/src/lib/db-schema.ts @@ -120,8 +120,8 @@ export function initDatabase() { if (!existing) { db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)') .run(role.name, role.display_name, role.permissions) - } else { - // 内置角色始终同步到最新默认权限 + } else if (role.name === 'admin') { + // 仅 admin 角色始终同步到最新默认权限,editor/viewer 允许用户自定义 db.prepare('UPDATE roles SET permissions = ? WHERE name = ?').run(role.permissions, role.name) } } @@ -137,13 +137,6 @@ export function initDatabase() { if (perms.includes('*')) upgraded.push('assets:export:all') db.prepare('UPDATE roles SET permissions = ? WHERE id = ?').run(JSON.stringify(upgraded), r.id) } - } - - // 迁移自定义角色中遗留的旧 assets:write 权限(拆分为 create/import/update) - const allRoles2 = db.prepare('SELECT id, name, permissions FROM roles').all() as { id: number; name: string; permissions: string }[] - for (const r of allRoles2) { - if (builtinNames.has(r.name)) continue - const perms: string[] = JSON.parse(r.permissions) if (perms.includes('assets:write')) { const upgraded = perms.filter(p => p !== 'assets:write') upgraded.push('assets:create', 'assets:import', 'assets:update')