feat: 实现LDAP创建OU和用户(LDAP Add)+弹框风格统一

This commit is contained in:
logan committed 2026-05-30 10:24:29 +08:00
1 parent 7b5f95e702
commit c51f619a7d
2 files changed
+167 -49

No files matched your search

+115 -2
View File
@@ -361,12 +361,125 @@ func ListLDAPUsers(c *gin.Context) {
// POST /api/v1/ldap-config/create-ou // POST /api/v1/ldap-config/create-ou
func CreateLDAPOU(c *gin.Context) { func CreateLDAPOU(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "OU created (placeholder)"}) var req struct {
OuName string `json:"ou_name"`
ParentDN string `json:"parent_dn"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.OuName == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 ou_name 参数"})
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
}
ouDN := fmt.Sprintf("ou=%s,%s", ldap.EscapeFilter(req.OuName), req.ParentDN)
addReq := ldap.NewAddRequest(ouDN, nil)
addReq.Attribute("objectClass", []string{"organizationalUnit", "top"})
addReq.Attribute("ou", []string{req.OuName})
if req.Description != "" {
addReq.Attribute("description", []string{req.Description})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建 OU 失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("OU %s 创建成功", req.OuName), "dn": ouDN})
} }
// POST /api/v1/ldap-config/create-user // POST /api/v1/ldap-config/create-user
func CreateLDAPUser(c *gin.Context) { func CreateLDAPUser(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "User created (placeholder)"}) var req struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
Password string `json:"password"`
OuDn string `json:"ou_dn"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Username == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"detail": "缺少 username 或 password 参数"})
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
}
// Parse user_mapping for attribute names
userMapping := map[string]string{"username": "uid", "email": "mail", "display_name": "cn"}
if cfg.UserMapping != nil && *cfg.UserMapping != "" {
json.Unmarshal([]byte(*cfg.UserMapping), &userMapping)
}
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
}
uidAttr := userMapping["username"]
if uidAttr == "" {
uidAttr = "uid"
}
cnAttr := userMapping["display_name"]
if cnAttr == "" {
cnAttr = "cn"
}
mailAttr := userMapping["email"]
if mailAttr == "" {
mailAttr = "mail"
}
userDN := fmt.Sprintf("%s=%s,%s", uidAttr, ldap.EscapeFilter(req.Username), req.OuDn)
displayName := req.DisplayName
if displayName == "" {
displayName = req.Username
}
addReq := ldap.NewAddRequest(userDN, nil)
addReq.Attribute("objectClass", []string{"inetOrgPerson", "organizationalPerson", "person", "top"})
addReq.Attribute(uidAttr, []string{req.Username})
addReq.Attribute(cnAttr, []string{displayName})
addReq.Attribute("sn", []string{displayName})
addReq.Attribute("userPassword", []string{req.Password})
if req.Email != "" {
addReq.Attribute(mailAttr, []string{req.Email})
}
if err := conn.Add(addReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "创建用户失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("用户 %s 创建成功", req.Username), "dn": userDN})
} }
// POST /api/v1/ldap-config/batch-import-ldap // POST /api/v1/ldap-config/batch-import-ldap
+52 -47
View File
@@ -6,7 +6,7 @@ import { toast } from '@/hooks/useToast';
import { import {
Save, ChevronRight, ChevronDown, RefreshCw, Save, ChevronRight, ChevronDown, RefreshCw,
CheckCircle, XCircle, Trash2, KeyRound, FolderOpen, Users, CheckCircle, XCircle, Trash2, KeyRound, FolderOpen, Users,
Shield, AlertTriangle, Import, Shield, AlertTriangle, Import, UserPlus,
} from 'lucide-react'; } from 'lucide-react';
/* ── types ── */ /* ── types ── */
@@ -613,19 +613,22 @@ export default function LdapConfigPage() {
{/* ── 创建 OU 弹框 ── */} {/* ── 创建 OU 弹框 ── */}
{showCreateOu && ( {showCreateOu && (
<ModalOverlay open={true} onClose={() => setShowCreateOu(false)} width="w-[460px]"> <ModalOverlay open={true} onClose={() => setShowCreateOu(false)} width="max-w-md">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl"> <div className="w-full bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-5 shadow-xl">
<div className="flex items-center justify-between px-5 py-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold">新建组织单元 (OU)</h3> <div className="flex items-center gap-2">
<button onClick={() => setShowCreateOu(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button> <FolderOpen className="w-4 h-4 text-blue-400" />
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)]">新建组织单元 (OU)</h3>
</div>
<button onClick={() => setShowCreateOu(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">✕</button>
</div> </div>
<div className="px-5 space-y-4"> <div className="space-y-3 mb-4">
<div> <div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">OU 名称 *</label> <label className="text-xs text-[var(--dg-text-muted)] mb-1 block">OU 名称 *</label>
<input <input
value={newOuName} value={newOuName}
onChange={e => setNewOuName(e.target.value)} onChange={e => setNewOuName(e.target.value)}
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" 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="如: devops, engineering" placeholder="如: devops, engineering"
/> />
</div> </div>
@@ -634,7 +637,7 @@ export default function LdapConfigPage() {
<select <select
value={newOuParent} value={newOuParent}
onChange={e => setNewOuParent(e.target.value)} onChange={e => setNewOuParent(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" 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"
> >
<option value="">根: {config.base_dn}</option> <option value="">根: {config.base_dn}</option>
{ouList.map(ou => <option key={ou.dn} value={ou.dn}>{ou.dn}</option>)} {ouList.map(ou => <option key={ou.dn} value={ou.dn}>{ou.dn}</option>)}
@@ -645,17 +648,17 @@ export default function LdapConfigPage() {
<input <input
value={newOuDesc} value={newOuDesc}
onChange={e => setNewOuDesc(e.target.value)} onChange={e => setNewOuDesc(e.target.value)}
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" 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="可选,如: 研发团队" placeholder="可选,如: 研发团队"
/> />
</div> </div>
</div> </div>
<div className="flex justify-end gap-3 px-5 py-4"> <div className="flex justify-end gap-2">
<button onClick={() => setShowCreateOu(false)} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">取消</button> <button onClick={() => setShowCreateOu(false)} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button>
<button <button
onClick={handleCreateOu} onClick={handleCreateOu}
disabled={creatingOu || !newOuName.trim()} disabled={creatingOu || !newOuName.trim()}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50" className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 transition-colors"
> >
{creatingOu ? '创建中…' : '创建 OU'} {creatingOu ? '创建中…' : '创建 OU'}
</button> </button>
@@ -666,41 +669,42 @@ export default function LdapConfigPage() {
{/* ── 添加用户弹框 ── */} {/* ── 添加用户弹框 ── */}
{showAddUser && ( {showAddUser && (
<ModalOverlay open={true} onClose={() => setShowAddUser(false)} width="w-[500px]"> <ModalOverlay open={true} onClose={() => setShowAddUser(false)} width="max-w-md">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl"> <div className="w-full bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-5 shadow-xl">
<div className="flex items-center justify-between px-5 py-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold">添加用户到 LDAP</h3> <div className="flex items-center gap-2">
<button onClick={() => setShowAddUser(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button> <UserPlus className="w-4 h-4 text-emerald-400" />
</div> <h3 className="text-sm font-semibold text-[var(--dg-text-primary)]">添加用户到 LDAP</h3>
<div className="px-5 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">用户名 *</label>
<input
value={addUsername}
onChange={e => setAddUsername(e.target.value)}
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"
placeholder="登录名,如 zhangsan"
/>
</div>
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">密码 *</label>
<input
type="password"
value={addPassword}
onChange={e => setAddPassword(e.target.value)}
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"
placeholder="初始密码"
/>
</div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <button onClick={() => setShowAddUser(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">✕</button>
</div>
<div className="space-y-3 mb-4">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">用户名 *</label>
<input
value={addUsername}
onChange={e => setAddUsername(e.target.value)}
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="登录名,如 zhangsan"
/>
</div>
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">密码 *</label>
<input
type="password"
value={addPassword}
onChange={e => setAddPassword(e.target.value)}
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="初始密码"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">显示名称</label> <label className="text-xs text-[var(--dg-text-muted)] mb-1 block">显示名称</label>
<input <input
value={addDisplayName} value={addDisplayName}
onChange={e => setAddDisplayName(e.target.value)} onChange={e => setAddDisplayName(e.target.value)}
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" 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="如: 张三" placeholder="如: 张三"
/> />
</div> </div>
@@ -709,7 +713,7 @@ export default function LdapConfigPage() {
<input <input
value={addEmail} value={addEmail}
onChange={e => setAddEmail(e.target.value)} onChange={e => setAddEmail(e.target.value)}
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" 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="可选" placeholder="可选"
/> />
</div> </div>
@@ -719,20 +723,21 @@ export default function LdapConfigPage() {
<select <select
value={addOuDn} value={addOuDn}
onChange={e => setAddOuDn(e.target.value)} onChange={e => setAddOuDn(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" 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"
> >
<option value="">根: {config.base_dn}</option> <option value="">根: {config.base_dn}</option>
{ouList.map(ou => <option key={ou.dn} value={ou.dn}>{ou.name} ({ou.dn})</option>)} {ouList.map(ou => <option key={ou.dn} value={ou.dn}>{ou.name} ({ou.dn})</option>)}
</select> </select>
</div> </div>
</div> </div>
<div className="flex justify-end gap-3 px-5 py-4"> <div className="flex justify-end gap-2">
<button onClick={() => setShowAddUser(false)} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">取消</button> <button onClick={() => setShowAddUser(false)} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button>
<button <button
onClick={handleAddUser} onClick={handleAddUser}
disabled={addingUser || !addUsername.trim() || !addPassword.trim()} disabled={addingUser || !addUsername.trim() || !addPassword.trim()}
className="px-4 py-2 text-sm font-medium rounded-lg bg-emerald-500 text-white hover:bg-emerald-600 disabled:opacity-50" className="flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-emerald-500 text-white hover:bg-emerald-600 disabled:opacity-50 transition-colors"
> >
<UserPlus className="w-3.5 h-3.5" />
{addingUser ? '创建中…' : '创建用户'} {addingUser ? '创建中…' : '创建用户'}
</button> </button>
</div> </div>