From 8384006ae9bd85fa03d75e442b434ebea447b88b Mon Sep 17 00:00:00 2001 From: "hermes-$(hostname)" Date: Fri, 29 May 2026 17:05:56 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=A2=9E=E5=8A=A0=E6=9D=A5=E6=BA=90=E5=AD=97?= =?UTF-8?q?=E6=AE=B5(auth=5Fsource)=EF=BC=8C=E6=94=AF=E6=8C=81=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0/LDAP=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth.go | 1 + backend/internal/handler/ldap.go | 34 +++- backend/internal/model/model.go | 1 + frontend/src/pages/LdapConfigPage.tsx | 243 ++++++++++++++------------ frontend/src/pages/UsersPage.tsx | 34 +++- 5 files changed, 196 insertions(+), 117 deletions(-) diff --git a/backend/internal/handler/auth.go b/backend/internal/handler/auth.go index 452d66d..e30bad5 100644 --- a/backend/internal/handler/auth.go +++ b/backend/internal/handler/auth.go @@ -75,6 +75,7 @@ func Login(c *gin.Context) { DisplayName: ldapInfo.DisplayName, Email: ldapInfo.Email, HashedPassword: "-", // LDAP 用户不存本地密码 + AuthSource: "ldap", Role: defaultRole, IsActive: &isActive, } diff --git a/backend/internal/handler/ldap.go b/backend/internal/handler/ldap.go index 842ee97..bb91471 100644 --- a/backend/internal/handler/ldap.go +++ b/backend/internal/handler/ldap.go @@ -488,7 +488,39 @@ func BatchImportLDAP(c *gin.Context) { // POST /api/v1/ldap-config/delete-user func DeleteLDAPUser(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "User deleted (placeholder)"}) + var req struct { + DN string `json:"dn" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 dn 参数"}) + return + } + + var cfg model.LDAPConfig + if err := model.DB.Order("id").First(&cfg).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 配置不存在"}) + return + } + + conn, err := dialLDAP(&cfg) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 连接失败: " + err.Error()}) + return + } + defer conn.Close() + + adminPw, _ := decryptPassword(cfg.AdminPassword) + if err := conn.Bind(cfg.AdminDN, adminPw); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "LDAP 管理员绑定失败"}) + return + } + + if err := conn.Del(ldap.NewDelRequest(req.DN, nil)); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"detail": "删除用户失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "用户已从 LDAP 删除"}) } // helper: dial LDAP with config diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go index 0785004..c353466 100644 --- a/backend/internal/model/model.go +++ b/backend/internal/model/model.go @@ -16,6 +16,7 @@ type User struct { Email *string `gorm:"size:100" json:"email"` Phone *string `gorm:"size:20" json:"phone"` HashedPassword string `gorm:"size:200;not null" json:"-"` + AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap Role string `gorm:"size:20;not null" json:"role"` // admin, dba, developer, auditor IsActive *bool `gorm:"default:true" json:"is_active"` Preferences *string `gorm:"type:text" json:"preferences"` // JSON: {"theme":"dark","login_style":"split"} diff --git a/frontend/src/pages/LdapConfigPage.tsx b/frontend/src/pages/LdapConfigPage.tsx index 3d7e4a4..fb0d240 100644 --- a/frontend/src/pages/LdapConfigPage.tsx +++ b/frontend/src/pages/LdapConfigPage.tsx @@ -78,6 +78,7 @@ export default function LdapConfigPage() { const [addingUser, setAddingUser] = useState(false); const [deleting, setDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); /* ── 加载配置 ── */ const fetchConfig = useCallback(async () => { @@ -275,28 +276,25 @@ export default function LdapConfigPage() { /* ── 删除选中用户 ── */ const handleDeleteSelected = async () => { if (selectedDns.size === 0) return; - if (!confirm(`确认删除 ${selectedDns.size} 个用户?此操作不可逆!`)) return; + setShowDeleteConfirm(true); + }; + + const confirmDelete = async () => { + setShowDeleteConfirm(false); setDeleting(true); - try { - // 后端需要一个 delete endpoint — 暂时逐个调用 - // 先用 delete-user 端点 - let ok = 0, fail = 0; - for (const dn of selectedDns) { - try { - await api.post('/ldap-config/delete-user', { dn }); - ok++; - } catch { - fail++; - } + let ok = 0, fail = 0; + for (const dn of selectedDns) { + try { + await api.post('/ldap-config/delete-user', { dn }); + ok++; + } catch { + fail++; } - toast(ok > 0 ? 'success' : 'error', `删除完成: 成功 ${ok}, 失败 ${fail}`); - setSelectedDns(new Set()); - if (selectedOu) fetchUsers(selectedOu); - } catch (e: any) { - toast('error', e?.response?.data?.detail || '删除失败'); - } finally { - setDeleting(false); } + toast(ok > 0 ? 'success' : 'error', `删除完成: 成功 ${ok}, 失败 ${fail}`); + setSelectedDns(new Set()); + if (selectedOu) fetchUsers(selectedOu); + setDeleting(false); }; const toggleManage = () => { @@ -744,107 +742,126 @@ export default function LdapConfigPage() { {/* ── 账号测试弹框 ── */} {showAccountTest && ( - { setShowAccountTest(false); setAccountResult(null); }} width="w-[440px]"> - {/* 标题 */} -
-
- -

账号验证测试

+ { setShowAccountTest(false); setAccountResult(null); }} width="max-w-md"> +
+ {/* 标题 */} +
+
+ +

账号验证测试

+
+
- -
- {/* 表单区 */} -
-
- - { setTestUser(e.target.value); setAccountResult(null); }} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" - placeholder="LDAP 用户名" - /> + {/* 表单区 */} +
+
+ + { setTestUser(e.target.value); setAccountResult(null); }} + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" + placeholder="LDAP 用户名" + /> +
+
+ + { setTestPass(e.target.value); setAccountResult(null); }} + className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" + placeholder="密码" + onKeyDown={e => { if (e.key === 'Enter' && testUser && testPass) handleTestAccount(); }} + /> +
-
- - { setTestPass(e.target.value); setAccountResult(null); }} - className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors" - placeholder="密码" - onKeyDown={e => { if (e.key === 'Enter' && testUser && testPass) handleTestAccount(); }} - /> + + {/* 结果区 */} + {accountResult && ( +
+ {accountResult.ok ? ( +
+
+ + 验证通过 +
+ {accountResult.info && ( +
+ {Object.entries(accountResult.info) + .filter(([, v]) => v && String(v).length > 0) + .map(([k, v]) => ( +
+ { + k === 'Display Name' ? '姓名' : + k === 'Email' ? '邮箱' : + k === 'User DN' ? 'DN' : + k === 'User ID' ? '用户名' : k + } + { + Array.isArray(v) ? v.join(', ') : String(v) + } +
+ ))} +
+ )} +
+ ) : ( +
+
+ + 验证失败 +
+

{accountResult.msg}

+
+ )} +
+ )} + + {/* 操作栏 */} +
+ +
+ + )} - {/* 结果区 — 独立视觉层级 */} - {accountResult && ( -
- {accountResult.ok ? ( - /* 成功 */ -
-
-
- -
- 验证通过 -
- {accountResult.info && ( -
- {Object.entries(accountResult.info) - .filter(([, v]) => v && String(v).length > 0) - .map(([k, v]) => ( -
- { - k === 'Display Name' ? '姓名' : - k === 'Email' ? '邮箱' : - k === 'User DN' ? 'DN' : - k === 'User ID' ? '用户名' : k - } - {Array.isArray(v) ? ( - v.length > 0 ? ( - - {v.join(', ')} - - ) : null - ) : {String(v)}} -
- ))} -
- )} -
- ) : ( - /* 失败 */ -
-
-
- -
- 验证失败 -
-

{accountResult.msg}

-
- )} + {/* ── 删除确认弹框 ── */} + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} width="max-w-sm"> +
+
+
+ +

确认删除

+
+ +
+

+ 确认删除 {selectedDns.size} 个 LDAP 用户?此操作不可逆。 +

+
+ +
- )} - - {/* 操作栏 */} -
- -
)} diff --git a/frontend/src/pages/UsersPage.tsx b/frontend/src/pages/UsersPage.tsx index d668b7a..41f84bf 100644 --- a/frontend/src/pages/UsersPage.tsx +++ b/frontend/src/pages/UsersPage.tsx @@ -24,6 +24,7 @@ interface UserItem { is_active: boolean; created_at: string; last_login?: string | null; + auth_source?: string; // 'local' | 'ldap' } interface UserActivity { @@ -70,6 +71,12 @@ const statusOptions = [ { value: 'disabled', label: '禁用' }, ]; +const sourceOptions = [ + { value: '', label: '全部来源' }, + { value: 'local', label: '本地' }, + { value: 'ldap', label: 'LDAP' }, +]; + const BATCH_OPTIONS = [ { key: 'enable', label: '批量启用', icon: UserCheck }, { key: 'disable', label: '批量禁用', icon: UserX }, @@ -124,6 +131,7 @@ export default function UsersPage() { const [search, setSearch] = useState(''); const [roleFilter, setRoleFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); + const [sourceFilter, setSourceFilter] = useState(''); const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); @@ -159,7 +167,10 @@ export default function UsersPage() { useEffect(() => { fetchUsers(); }, [fetchUsers]); - const filtered = users; + const filtered = users.filter(u => { + if (sourceFilter && u.auth_source !== sourceFilter) return false; + return true; + }); const allSelected = filtered.length > 0 && filtered.every(u => selectedIds.has(u.id)); @@ -298,9 +309,16 @@ export default function UsersPage() { > {statusOptions.map(s => )} - {(search || roleFilter || statusFilter) && ( + + {(search || roleFilter || statusFilter || sourceFilter) && ( + + + {user.auth_source === 'ldap' ? 'LDAP' : '本地'} + +