feat: 实现LDAP创建OU和用户(LDAP Add)+弹框风格统一
This commit is contained in:
1 parent
7b5f95e702
commit
c51f619a7d
2 files changed
+150
-32
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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>
|
</div>
|
||||||
<div className="px-5 space-y-4">
|
<button onClick={() => setShowCreateOu(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>
|
<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,20 +669,22 @@ 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" />
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)]">添加用户到 LDAP</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 space-y-4">
|
<button onClick={() => setShowAddUser(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">✕</button>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</div>
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
<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={addUsername}
|
value={addUsername}
|
||||||
onChange={e => setAddUsername(e.target.value)}
|
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"
|
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"
|
placeholder="登录名,如 zhangsan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -689,18 +694,17 @@ export default function LdapConfigPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={addPassword}
|
value={addPassword}
|
||||||
onChange={e => setAddPassword(e.target.value)}
|
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"
|
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 className="grid grid-cols-2 gap-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<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>
|
||||||
|
|||||||
Reference in new issue
Block a user