feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全) - AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus - 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错 - 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder) - 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断 - 同步未提交的前端类型安全迁移(140+ 文件) - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新 验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常, 锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
1 parent
5ca0e6d83a
commit
c68aef4444
150 files changed
+1038
-690
No files matched your search
@@ -19,6 +19,7 @@ import {
|
||||
import { credentialApi } from '@/api';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
type CredentialType = 'ssh_key' | 'api_token' | 'password' | 'access_key';
|
||||
|
||||
@@ -56,7 +57,7 @@ export function CredentialManager() {
|
||||
const fetchList = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res: Record<string, any> = await credentialApi.list();
|
||||
const res: JsonData = await credentialApi.list();
|
||||
setCredentials(res?.list || []);
|
||||
} catch (e) {
|
||||
console.error('加载凭据列表失败:', e);
|
||||
|
||||
@@ -18,15 +18,16 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Upload, Download, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { userApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface CsvImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||||
importPreview: any[];
|
||||
setImportPreview: ( p: any[]) => void;
|
||||
importPreview: JsonData[];
|
||||
setImportPreview: ( p: JsonData[]) => void;
|
||||
importing: boolean;
|
||||
importResult: any;
|
||||
setImportResult: ( r: any) => void;
|
||||
importResult?: JsonData;
|
||||
setImportResult: (r: JsonData) => void;
|
||||
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleBatchImport: () => void;
|
||||
}
|
||||
|
||||
@@ -17,23 +17,24 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Network, Loader2, RefreshCw, CheckCircle, XCircle } from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface LdapImportDialogProps {
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||||
ldapUsers: any[];
|
||||
setLdapUsers: ( u: any[]) => void;
|
||||
ldapUsers: JsonData[];
|
||||
setLdapUsers: ( u: JsonData[]) => void;
|
||||
ldapSelected: Set<string>;
|
||||
setLdapSelected: (s: Set<string>) => void;
|
||||
ldapLoading: boolean;
|
||||
ldapImportOUs: any[];
|
||||
ldapImportOUs: JsonData[];
|
||||
ldapImportOUDN: string;
|
||||
setLdapImportOUDN: (v: string) => void;
|
||||
loadLdapUsers: (ouDn?: string) => void;
|
||||
handleLdapImport: () => void;
|
||||
importing: boolean;
|
||||
importResult: any;
|
||||
setImportResult: ( r: any) => void;
|
||||
importResult?: JsonData;
|
||||
setImportResult: (r: JsonData) => void;
|
||||
}
|
||||
|
||||
// LdapImportDialog 展示 LDAP 用户检索和多选导入流程 // Logan.Li
|
||||
|
||||
@@ -36,15 +36,16 @@ import {
|
||||
Trash2,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface LdapManagementSectionProps {
|
||||
ldapConfig: any;
|
||||
ldapConfig?: JsonData;
|
||||
ldapMgmtOpen: boolean;
|
||||
setLdapMgmtOpen: (v: boolean) => void;
|
||||
ldapOUs: any[];
|
||||
ldapOUs: JsonData[];
|
||||
selectedOUDN: string;
|
||||
setSelectedOUDN: (v: string) => void;
|
||||
ldapUsers: any[];
|
||||
ldapUsers: JsonData[];
|
||||
selectedLDAPUsers: string[];
|
||||
setSelectedLDAPUsers: (v: string[]) => void;
|
||||
loadLdapOUs: () => void;
|
||||
@@ -75,16 +76,16 @@ interface LdapManagementSectionProps {
|
||||
ldapTestPass: string;
|
||||
setLdapTestPass: (v: string) => void;
|
||||
ldapUserTesting: boolean;
|
||||
ldapUserTestResult: any;
|
||||
ldapUserTestResult?: JsonData;
|
||||
handleLdapUserTest: () => void;
|
||||
// Edit LDAP user
|
||||
editLdapUser: any | null;
|
||||
setEditLdapUser: (u: any | null) => void;
|
||||
editLdapUser: JsonData | null;
|
||||
setEditLdapUser: (u: JsonData | null) => void;
|
||||
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||
handleUpdateLdapUser: () => void;
|
||||
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||
openEditLdapUser: ( user: any) => void;
|
||||
openEditLdapUser: (user: JsonData) => void;
|
||||
}
|
||||
|
||||
export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
@@ -219,7 +220,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value={ldapConfig.base_dn}>根 ({ldapConfig.base_dn})</SelectItem>
|
||||
{ldapOUs.map(( ou: any) => (
|
||||
{ldapOUs.map((ou: JsonData) => (
|
||||
<SelectItem key={ou.dn} value={ou.dn}>
|
||||
{ou.description ? `${ou.name} (${ou.description})` : ou.name}
|
||||
</SelectItem>
|
||||
@@ -343,7 +344,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
type="checkbox"
|
||||
className="rounded bg-slate-800 border-slate-600"
|
||||
checked={selectedLDAPUsers.length > 0 && selectedLDAPUsers.length === ldapUsers.length}
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map(( u: any) => u.dn) : [])}
|
||||
onChange={(e) => setSelectedLDAPUsers(e.target.checked ? ldapUsers.map((u: JsonData) => u.dn) : [])}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-slate-400">用户名</TableHead>
|
||||
@@ -361,7 +362,7 @@ export function LdapManagementSection(props: LdapManagementSectionProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ldapUsers.map(( user: any) => (
|
||||
ldapUsers.map((user: JsonData) => (
|
||||
<TableRow key={user.dn} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<input
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Shield } from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export interface RoleFormData {
|
||||
name: string;
|
||||
@@ -25,7 +26,7 @@ export interface PermissionOption {
|
||||
interface RoleFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingRole: any | null;
|
||||
editingRole: JsonData | null;
|
||||
form: RoleFormData;
|
||||
setForm: React.Dispatch<React.SetStateAction<RoleFormData>>;
|
||||
saving: boolean;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/components/ui/table';
|
||||
import { auditApi } from '@/api';
|
||||
import type { OperationLog } from '@/types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface SystemLogsTabProps {
|
||||
searchQuery: string;
|
||||
@@ -21,7 +22,7 @@ interface SystemLogsTabProps {
|
||||
}
|
||||
|
||||
// 将后端审计日志规范化为前端 OperationLog 类型 // Logan.Li
|
||||
function normalizeLog( raw: any): OperationLog {
|
||||
function normalizeLog(raw: JsonData): OperationLog {
|
||||
return {
|
||||
id: String(raw.id ?? ''),
|
||||
userId: String(raw.user_id ?? raw.userId ?? ''),
|
||||
|
||||
@@ -11,9 +11,10 @@ import { SystemRolesTab } from './SystemRolesTab';
|
||||
import { SystemSettingsTab } from './SystemSettingsTab';
|
||||
import { useLdapManagement } from './useLdapManagement';
|
||||
import type { UserFormData } from './UserFormDialog';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
// 后端用户数据转换为前端兼容格式
|
||||
function mapUser( u: any) {
|
||||
function mapUser(u: JsonData) {
|
||||
return {
|
||||
...u,
|
||||
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
|
||||
@@ -53,21 +54,21 @@ export function SystemManager() {
|
||||
}
|
||||
}, [location.pathname]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [users, setUsers] = useState<JsonData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<JsonData | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
|
||||
// 创建/编辑用户弹窗
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<any | null>(null);
|
||||
const [editingUser, setEditingUser] = useState<JsonData | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState<UserFormData>({ ...emptyForm });
|
||||
|
||||
// CSV 批量导入弹窗
|
||||
const [importDialogOpen, setImportDialogOpen] = useState<'csv' | 'ldap' | null>(null);
|
||||
const [importPreview, setImportPreview] = useState<any[]>([]);
|
||||
const [importResult, setImportResult] = useState<any>(null);
|
||||
const [importPreview, setImportPreview] = useState<JsonData[]>([]);
|
||||
const [importResult, setImportResult] = useState<JsonData>(null);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
@@ -125,7 +126,7 @@ export function SystemManager() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = ( user: any) => {
|
||||
const openEdit = (user: JsonData) => {
|
||||
setEditingUser(user);
|
||||
setForm({
|
||||
username: user.username || '',
|
||||
@@ -167,7 +168,7 @@ export function SystemManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async ( user: any) => {
|
||||
const handleDelete = async (user: JsonData) => {
|
||||
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Lock, History, Key, Settings, Network, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { LdapManagementSection } from './LdapManagementSection';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export interface LdapFormData {
|
||||
ldap_host: string;
|
||||
@@ -67,13 +68,13 @@ interface SystemSettingsTabProps {
|
||||
ldapUserTesting: boolean;
|
||||
ldapUserTestResult: unknown;
|
||||
handleLdapUserTest: () => void;
|
||||
editLdapUser: any | null;
|
||||
setEditLdapUser: (u: any | null) => void;
|
||||
editLdapUser: JsonData | null;
|
||||
setEditLdapUser: (u: JsonData | null) => void;
|
||||
editLdapForm: { cn: string; mail: string; phone: string; title: string };
|
||||
setEditLdapForm: React.Dispatch<React.SetStateAction<{ cn: string; mail: string; phone: string; title: string }>>;
|
||||
handleUpdateLdapUser: () => void;
|
||||
handleDeleteLDAPEntry: (dn: string, silent?: boolean) => void;
|
||||
openEditLdapUser: ( user: any) => void;
|
||||
openEditLdapUser: (user: JsonData) => void;
|
||||
}
|
||||
|
||||
export function SystemSettingsTab(props: SystemSettingsTabProps) {
|
||||
|
||||
@@ -26,49 +26,50 @@ import { UserDetailDialog } from './UserDetailDialog';
|
||||
import { CsvImportDialog } from './CsvImportDialog';
|
||||
import { LdapImportDialog } from './LdapImportDialog';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface SystemUserTabProps {
|
||||
// Search
|
||||
searchQuery: string;
|
||||
setSearchQuery: (v: string) => void;
|
||||
// Users
|
||||
filteredUsers: any[];
|
||||
filteredUsers: JsonData[];
|
||||
loading: boolean;
|
||||
selectedUser: any | null;
|
||||
setSelectedUser: (u: any | null) => void;
|
||||
selectedUser: JsonData | null;
|
||||
setSelectedUser: (u: JsonData | null) => void;
|
||||
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
|
||||
// Create / Edit
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
editingUser: any | null;
|
||||
editingUser: JsonData | null;
|
||||
form: UserFormData;
|
||||
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
|
||||
saving: boolean;
|
||||
handleSubmit: () => void;
|
||||
openCreate: () => void;
|
||||
openEdit: ( user: any) => void;
|
||||
handleDelete: ( user: any) => void;
|
||||
openEdit: (user: JsonData) => void;
|
||||
handleDelete: (user: JsonData) => void;
|
||||
// CSV import
|
||||
importDialogOpen: 'csv' | 'ldap' | null;
|
||||
setImportDialogOpen: (v: 'csv' | 'ldap' | null) => void;
|
||||
importPreview: any[];
|
||||
setImportPreview: ( p: any[]) => void;
|
||||
importPreview: JsonData[];
|
||||
setImportPreview: ( p: JsonData[]) => void;
|
||||
importing: boolean;
|
||||
importResult: any;
|
||||
setImportResult: ( r: any) => void;
|
||||
importResult?: JsonData;
|
||||
setImportResult: (r: JsonData) => void;
|
||||
handleCsvUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleBatchImport: () => void;
|
||||
// LDAP import
|
||||
ldapUsers: any[];
|
||||
ldapUsers: JsonData[];
|
||||
ldapSelected: Set<string>;
|
||||
setLdapSelected: (s: Set<string>) => void;
|
||||
ldapLoading: boolean;
|
||||
ldapImportOUs: any[];
|
||||
ldapImportOUs: JsonData[];
|
||||
ldapImportOUDN: string;
|
||||
setLdapImportOUDN: (v: string) => void;
|
||||
loadLdapUsers: (ouDn?: string) => void;
|
||||
handleLdapImport: () => void;
|
||||
setLdapUsers: ( u: any[]) => void;
|
||||
setLdapUsers: ( u: JsonData[]) => void;
|
||||
}
|
||||
|
||||
export function SystemUserTab({
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Key, Edit, User as UserIcon } from 'lucide-react';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface UserDetailDialogProps {
|
||||
selectedUser: any | null;
|
||||
setSelectedUser: (u: any | null) => void;
|
||||
selectedUser: JsonData | null;
|
||||
setSelectedUser: (u: JsonData | null) => void;
|
||||
getUserStatusBadge: (status: 'active' | 'inactive') => React.ReactNode;
|
||||
openEdit: ( user: any) => void;
|
||||
openEdit: (user: JsonData) => void;
|
||||
}
|
||||
|
||||
// UserDetailDialog 仅展示选中用户的基本信息和角色列表 // Logan.Li
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { User } from 'lucide-react';
|
||||
import { userApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export interface UserFormData {
|
||||
username: string;
|
||||
@@ -32,7 +33,7 @@ export interface UserFormData {
|
||||
interface UserFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingUser: any | null;
|
||||
editingUser: JsonData | null;
|
||||
form: UserFormData;
|
||||
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
|
||||
saving: boolean;
|
||||
@@ -60,7 +61,7 @@ export function UserFormDialog({
|
||||
if (cancelled) return;
|
||||
const list = resp?.list ?? [];
|
||||
setRoles(
|
||||
list.map(( r: any) => ({
|
||||
list.map((r: JsonData) => ({
|
||||
id: String(r.id ?? ''),
|
||||
name: r.name ?? r.code ?? '',
|
||||
}))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { ldapApi } from '@/api';
|
||||
import type { LdapFormData } from './SystemSettingsTab';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
/**
|
||||
* useLdapManagement — 封装 SystemManager 中所有 LDAP 相关状态与业务逻辑
|
||||
@@ -21,14 +22,14 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const confirm = useConfirm();
|
||||
|
||||
// ===== LDAP 用户导入(用户管理 Tab 的导入弹窗用) =====
|
||||
const [ldapUsers, setLdapUsers] = useState<any[]>([]);
|
||||
const [ldapUsers, setLdapUsers] = useState<JsonData[]>([]);
|
||||
const [ldapSelected, setLdapSelected] = useState<Set<string>>(new Set());
|
||||
const [ldapLoading, setLdapLoading] = useState(false);
|
||||
const [ldapImportOUs, setLdapImportOUs] = useState<any[]>([]);
|
||||
const [ldapImportOUs, setLdapImportOUs] = useState<JsonData[]>([]);
|
||||
const [ldapImportOUDN, setLdapImportOUDN] = useState('');
|
||||
|
||||
// ===== LDAP 配置 =====
|
||||
const [ldapConfig, setLdapConfig] = useState<any>(null);
|
||||
const [ldapConfig, setLdapConfig] = useState<JsonData>(null);
|
||||
const [ldapForm, setLdapForm] = useState<LdapFormData>({
|
||||
ldap_host: '', ldap_port: 389, use_ldaps: false,
|
||||
admin_dn: '', admin_password: '', base_dn: '', search_filter: '',
|
||||
@@ -41,7 +42,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const [importResultOpen, setImportResultOpen] = useState(false);
|
||||
|
||||
// ===== LDAP 账号管理 =====
|
||||
const [ldapOUs, setLdapOUs] = useState<any[]>([]);
|
||||
const [ldapOUs, setLdapOUs] = useState<JsonData[]>([]);
|
||||
const [selectedOUDN, setSelectedOUDN] = useState('');
|
||||
const [selectedLDAPUsers, setSelectedLDAPUsers] = useState<string[]>([]);
|
||||
const [createOUOpen, setCreateOUOpen] = useState(false);
|
||||
@@ -52,8 +53,8 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
const [ldapTestUser, setLdapTestUser] = useState('');
|
||||
const [ldapTestPass, setLdapTestPass] = useState('');
|
||||
const [ldapUserTesting, setLdapUserTesting] = useState(false);
|
||||
const [ldapUserTestResult, setLdapUserTestResult] = useState<any>(null);
|
||||
const [editLdapUser, setEditLdapUser] = useState<any>(null);
|
||||
const [ldapUserTestResult, setLdapUserTestResult] = useState<JsonData>(null);
|
||||
const [editLdapUser, setEditLdapUser] = useState<JsonData>(null);
|
||||
const [editLdapForm, setEditLdapForm] = useState({ cn: '', mail: '', phone: '', title: '' });
|
||||
|
||||
// 导入进行中状态(CSV 与 LDAP 导入共用)
|
||||
@@ -176,7 +177,7 @@ export function useLdapManagement(onUsersChanged: () => Promise<void> | void) {
|
||||
};
|
||||
|
||||
/** 打开 LDAP 用户编辑 // Logan.Li */
|
||||
const openEditLdapUser = ( user: any) => {
|
||||
const openEditLdapUser = (user: JsonData) => {
|
||||
setEditLdapUser(user);
|
||||
setEditLdapForm({ cn: user.cn || '', mail: user.mail || '', phone: user.phone || '', title: user.title || '' });
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user