feat: 流水线三级权限体系(手动多选+通配符授权)+ 菜单分级 + SSE 认证修复

权限模型(借鉴 Jenkins Role-Strategy 与云效,默认拒绝):
- 三级权限:查看(流水线+成功/失败日志)/ 发布(+构建、批量发布、重试)/ 管理(+配置、成员);admin 全局豁免
- 权限组(管理员配置):默认手动多选流水线(免正则,操作友好);高级模式仅 * 通配符(cex-*)
- 单流水线成员直授权作为例外补充;AI 批量发布逐 job 校验,越权明确拒绝

后端:
- 新增 PipelineRole/PipelineMember 表与 /api/pipeline-roles、/api/pipelines/:id/members CRUD
- RequirePipelineAccess/RequireRunAccess 中间件挂全流水线与运行路由;列表按可见性过滤并附 accessLevel/canRetry
- /api/me 会话接口;logs/stream SSE 支持 ?token= 认证(原先完全免鉴权)并校验查看权限
- 凭证/主机/集群/构建节点/工具/Webhook/Jenkins/GitOps/插件/设置全部 AdminOnly;触发人以会话身份落库

前端:
- useSession 会话 hook;Sidebar 菜单分级(资源/集成/系统管理 admin-only);admin 路由守卫
- 按钮级权限:执行/配置/删除/保存/重试按等级显隐;新页「流水线权限」与流水线成员弹窗(纯点选)
- 登录页记住密码;GUIDE/API 文档补权限章节
This commit is contained in:
Logan.Li committed 2026-10-27 17:34:41 +08:00
1 parent 7090c8efc2
commit 9103b13510
27 files changed
+1690 -190

No files matched your search

+33 -2
View File
@@ -3,9 +3,26 @@ import { Rocket, Loader2, KeyRound, User } from 'lucide-react';
import { toast } from 'sonner';
import { apiClient, auth } from '@/lib/api';
const REMEMBER_KEY = 'flowstack-remember';
// 记住密码:明文存 localStorage 仅适合内网工具场景;不勾选则彻底不落盘
function readRemember(): { username: string; password: string } | null {
try {
const raw = localStorage.getItem(REMEMBER_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (parsed?.username && parsed?.password) return parsed;
} catch {
// 脏数据忽略
}
return null;
}
export default function Login({ onSuccess }: { onSuccess: () => void }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const remembered = readRemember();
const [username, setUsername] = useState(remembered?.username ?? '');
const [password, setPassword] = useState(remembered?.password ?? '');
const [remember, setRemember] = useState(!!remembered);
const [loading, setLoading] = useState(false);
const submit = async (e?: React.FormEvent) => {
@@ -18,6 +35,11 @@ export default function Login({ onSuccess }: { onSuccess: () => void }) {
try {
const res = await apiClient.login(username.trim(), password);
auth.setToken(res.token);
if (remember) {
localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username: username.trim(), password }));
} else {
localStorage.removeItem(REMEMBER_KEY);
}
toast.success(`欢迎,${res.nickname || res.username}`);
onSuccess();
} catch (err) {
@@ -69,6 +91,15 @@ export default function Login({ onSuccess }: { onSuccess: () => void }) {
/>
</div>
</div>
<label className="flex items-center gap-1.5 cursor-pointer select-none" title="在本机记住账号与密码,公共电脑请勿勾选">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="w-3.5 h-3.5 accent-[var(--accent-blue)]"
/>
<span className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>记住密码</span>
</label>
<button
type="submit"
disabled={loading}