feat: v0.4.0 主题切换 + LLM 全面优化 + 登录会话 + 用户管理/LDAP

主题:
- 深色/浅色/跟随系统(useTheme:本地缓存先行+远端同步+matchMedia),
  侧边栏快捷切换,设置页三档即选即生效

LLM(参考 opsxdb/aiops):
- llm.Client.ChatStream 流式(SSE 增量解析,透传 reasoning_content,
  ctx 控制超时);POST /api/ai/chat/stream(平台实时数据注入降幻觉,
  未配 LLM 降级规则 JSON,指令解析 action 不变)
- test-llm 支持草稿直测(保存前验证,apiKey 空/掩码回退已存)
- 发布失败自动 AI 分析日志尾部(结论/根因/建议),发布记录与发布
  Tab 展示分析卡片
- 设置页 Provider 预设(DeepSeek/GLM/GLM编码/Qwen/Kimi/Ollama/自定义)
  自动填 endpoint+模型+获取 Key 直达链接

登录与会话:
- HMAC 签名 token(7 天,密钥派生自 encryption.key);
  authMiddleware 保护 /api/*(豁免登录/健康/SSE日志流/入站webhook)
- 登录页 /login + 未登录跳转 + 登出;401 自动清理跳登录

用户管理与 LDAP(借鉴 LokiStack):
- /users 页面:本地用户 CRUD、一次性密码、角色切换、来源徽章
  (本地·蓝 | LDAP导入·紫);LDAP 导入按钮仅在启用且配置齐备时显示
- LDAP 配置卡(未启用仅提示语)、测试连接草稿直测+可选测试账号
- go-ldap:管理 Bind/用户搜索(防注入)/用户 DN 直连验证;
  批量导入冲突策略(已LDAP跳过/本地同名转LDAP/新建);
  登录分流本地 argon2id 或 LDAP 目录验证 + 首登 JIT 建号
This commit is contained in:
Logan.Li committed 2026-10-27 12:58:21 +08:00
1 parent 555934cc64
commit 97193eb090
24 files changed
+2422 -98

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
import { useState } from 'react';
import { Rocket, Loader2, KeyRound, User } from 'lucide-react';
import { toast } from 'sonner';
import { apiClient, auth } from '@/lib/api';
export default function Login({ onSuccess }: { onSuccess: () => void }) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const submit = async (e?: React.FormEvent) => {
e?.preventDefault();
if (!username.trim() || !password) {
toast.error('请输入用户名和密码');
return;
}
setLoading(true);
try {
const res = await apiClient.login(username.trim(), password);
auth.setToken(res.token);
toast.success(`欢迎,${res.nickname || res.username}`);
onSuccess();
} catch (err) {
toast.error('登录失败', { description: (err as Error).message });
} finally {
setLoading(false);
}
};
const inputStyle = { background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' };
return (
<div className="h-screen w-screen flex items-center justify-center" style={{ background: 'var(--canvas-bg)' }}>
<div className="w-[380px] rounded-xl border shadow-lg p-8" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<div className="flex flex-col items-center mb-6">
<div className="w-12 h-12 rounded-xl flex items-center justify-center mb-3" style={{ background: 'linear-gradient(135deg, #0969da, #8250df)' }}>
<Rocket className="w-6 h-6 text-white" />
</div>
<h1 className="text-[18px] font-bold" style={{ color: 'var(--text-primary)' }}>FlowStack</h1>
<p className="text-[12px] mt-1" style={{ color: 'var(--text-tertiary)' }}>发布系统 · 请登录</p>
</div>
<form onSubmit={submit} className="space-y-4">
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>用户名</label>
<div className="relative">
<User className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--text-tertiary)' }} />
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
className="w-full pl-9 pr-3 py-2 text-[13px] rounded-md border outline-none"
style={inputStyle}
placeholder="admin"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>密码</label>
<div className="relative">
<KeyRound className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2" style={{ color: 'var(--text-tertiary)' }} />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-9 pr-3 py-2 text-[13px] rounded-md border outline-none"
style={inputStyle}
placeholder="初始密码见 data/initial-admin-password.txt"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 text-[13px] font-medium rounded-md text-white disabled:opacity-60 flex items-center justify-center gap-2"
style={{ background: 'var(--accent-blue)' }}
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
登 录
</button>
</form>
<p className="text-[11px] text-center mt-4" style={{ color: 'var(--text-tertiary)' }}>
支持本地账号与 LDAP 账号(启用后自动识别)
</p>
</div>
</div>
);
}