fix: 内置角色可编辑不可删除,修复种子迁移覆盖用户权限
- admin/operator/viewer 均为内置不可删除,但可编辑 - 移除 initDatabase() 中的权限合并迁移逻辑,避免每次启动覆盖用户自定义权限 - admin 在 API 层禁止删除
This commit is contained in:
parent
2d74f0a05b
commit
2697aaaa75
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
## 2026-05-14
|
||||
|
||||
- [新增] 权限系统重构:工单细粒度权限(`tickets:create` 手动建单、`tickets:import` 导入、`tickets:export` 导出),`tickets:write` 缩窄为编辑/删除
|
||||
- [新增] 报告三层权限:`reports:read`(查看列表+预览)、`reports:download`(下载)、`reports:create`(新建+生成文档+删除)
|
||||
- [新增] `/api/auth/me` 返回 `permissions` 字段,前端根据权限数组控制按钮和导航显隐
|
||||
- [新增] 角色管理 UI 权限项从 8 个扩展到 14 个(含 `api-keys:read/write`)
|
||||
- [修复] 角色管理页面:admin/operator/viewer 标记为"内置"不可删除但可编辑,admin 在 API 层禁止删除
|
||||
- [修复] 报告 API 路由全部补全权限检查(此前列表/预览/下载/删除均无权限检查)
|
||||
- [修复] 导出工单 API 新增 `tickets:export` 权限检查(此前仅验证登录态)
|
||||
- [修复] POST 创建报告权限从错误的 `reports:read` 修正为 `reports:create`
|
||||
- [修复] `reports:write` 全面替换为 `reports:create`
|
||||
- [修复] 种子数据迁移逻辑不再每次启动覆盖用户自定义权限,仅首次安装时创建默认权限
|
||||
- [修复] SSO 新用户免登录失败:根页面 `getCurrentUser()` 改为直接 `redirect('/dashboard')`,由中间件统一处理共享 JWT 认证(与 assets-ai 行为一致)
|
||||
- [修复] `next.config.ts` 添加 `ldapts` 到 `serverExternalPackages`,确保 Next.js standalone 构建包含 LLDAP 客户端模块,避免 `ldapUserExists()` 因模块缺失失败导致 SSO 自动创建用户静默中断
|
||||
- [调整] 全局证书切换:Cloudflare Origin CA → Let's Encrypt(`/etc/letsencrypt/live/www.tlyq.ai/`),覆盖全部 7 个子域名,nginx 8 个站点配置同步更新
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface Role {
|
|||
created_at: string
|
||||
}
|
||||
|
||||
const BUILTIN_ROLES = ['admin', 'operator', 'viewer']
|
||||
|
||||
const allPermissions = [
|
||||
{ key: 'tickets:read', label: '查看工单' },
|
||||
{ key: 'tickets:create', label: '手动建单' },
|
||||
|
|
@ -96,8 +98,6 @@ export default function RolesPage() {
|
|||
} catch { return '无权限' }
|
||||
}
|
||||
|
||||
const builtinRoles = ['admin', 'operator', 'viewer']
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -115,12 +115,19 @@ export default function RolesPage() {
|
|||
{roles.map(r => (
|
||||
<tr key={r.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-slate-900 dark:text-slate-100">{r.name}</td>
|
||||
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">{r.display_name}</td>
|
||||
<td className="px-4 py-3 text-slate-700 dark:text-slate-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{r.display_name}</span>
|
||||
{BUILTIN_ROLES.includes(r.name) && (
|
||||
<Badge variant="info">内置</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-500 dark:text-slate-400 text-sm">{formatPermissions(r.permissions)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" onClick={() => openEdit(r)}><Pencil size={14} /></Button>
|
||||
{!builtinRoles.includes(r.name) && (
|
||||
{!BUILTIN_ROLES.includes(r.name) && (
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(r.id)}><Trash2 size={14} className="text-red-500" /></Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||
const body = await request.json()
|
||||
const db = getDb()
|
||||
|
||||
const existing = db.prepare('SELECT * FROM roles WHERE id = ?').get(id)
|
||||
const existing = db.prepare('SELECT * FROM roles WHERE id = ?').get(id) as any
|
||||
if (!existing) return NextResponse.json({ error: '角色不存在' }, { status: 404 })
|
||||
|
||||
const fields: string[] = []
|
||||
|
|
|
|||
|
|
@ -63,20 +63,7 @@ export function initDatabase(): void {
|
|||
{ name: 'viewer', display_name: '查看者', permissions: '["tickets:read","tickets:export","reports:read","reports:download"]' },
|
||||
]
|
||||
for (const r of roles) {
|
||||
const ex = db.prepare('SELECT id, permissions FROM roles WHERE name = ?').get(r.name) as { id: number; permissions: string } | undefined
|
||||
if (!ex) {
|
||||
db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)').run(r.name, r.display_name, r.permissions)
|
||||
} else {
|
||||
// 迁移:更新已有角色的权限(追加新权限,保留已有自定义)
|
||||
const newPerms = JSON.parse(r.permissions) as string[]
|
||||
let existingPerms: string[] = []
|
||||
try { existingPerms = JSON.parse(ex.permissions) } catch {}
|
||||
if (!existingPerms.includes('*')) {
|
||||
for (const p of newPerms) {
|
||||
if (!existingPerms.includes(p)) existingPerms.push(p)
|
||||
}
|
||||
db.prepare('UPDATE roles SET permissions = ? WHERE id = ?').run(JSON.stringify(existingPerms), ex.id)
|
||||
}
|
||||
}
|
||||
const ex = db.prepare('SELECT id FROM roles WHERE name = ?').get(r.name)
|
||||
if (!ex) db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)').run(r.name, r.display_name, r.permissions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue