feat(ldap): LDAP 改为只读接入,彻底移除账号验证与批量导入写入功能

按需求(sql.astx.club/settings):

1. 移除「测试账号验证」按钮
   - LdapConfigForm.tsx 删按钮 + onOpenAccountTest prop
   - 不再渲染 LdapAccountTestDialog

2. 移除批量导入的 LDAP 写操作(BatchImportModal.tsx)
   - TABS 去掉「导入到 LDAP」「从 LDAP 导入」
   - 删除对应两个 JSX 代码块(tab === 'to-ldap' / 'from-ldap')
   - 删除「创建 OU」子弹窗
   - 只保留「本地导入」(CSV 建本地账号,不触碰 LDAP 目录)

3. 移除 LDAP 账号管理面板(含危险写操作)
   - 不再渲染 LdapManagePanel(新建 OU / 添加用户 / 删除用户)
   - 删除 LdapCreateOuDialog / LdapAddUserDialog / ConfirmDialog 引用
   - LdapConfigPage.tsx 421 → 340 行;BatchImportModal.tsx 557 → 329 行

4. 保留(只读用途)
   - LDAP 配置表单(服务器 / 绑定 / 搜索 / 属性映射)
   - 「测试连接」按钮(验证配置可用,不写 LDAP 目录)

设计原则:LDAP 仅用于域账号登录认证(只读 bind + search),
         目录维护请到域控侧操作。

验证(前端 bundle grep,全部归零):
  测试账号验证 0 | 导入到 LDAP 0 | 从 LDAP 导入 0
  新建 OU 0 | 创建 OU 0 | 危险操作 0 | 此操作不可逆 0
保留:
  测试连接 6 | 本地导入 1

镜像 sha256:3970db141d23
/health → {"backend":"go","status":"ok","version":"0.4.0"}
http://sql.astx.club/settings → HTTP 200
This commit is contained in:
opsx committed 2026-10-26 17:49:34 +00:00
1 parent e8355e5d5f
commit 6b81dbbd4a
2 files changed
-311

No files matched your search

@@ -303,184 +303,6 @@ export function BatchImportModal({ onClose, onImported }: BatchImportModalProps)
</>
)}
{/* ── Import to LDAP ── */}
{tab === 'to-ldap' && (
<>
{ldapLoading && <p className="text-sm text-[var(--dg-text-muted)] text-center py-4">加载中...</p>}
{!ldapLoading && !ldapReady && (
<div className="p-3 rounded-lg bg-yellow-500/10 text-yellow-400 text-sm">
LDAP 未配置,请先在「平台配置 → LDAP」中配置 LDAP 连接
</div>
)}
{!ldapLoading && ldapReady && (
<>
{/* OU selector */}
<div>
<label className="block text-sm font-medium mb-1.5">目标 OU</label>
<div className="flex items-center gap-2">
<select
value={targetOu}
onChange={(e) => setTargetOu(e.target.value)}
className="flex-1 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none"
>
<option value="">Base DN (根)</option>
{ldapOus.map((ou: any) => (
<option key={ou.dn} value={ou.dn}>
{ou.name} ({ou.dn})
</option>
))}
</select>
<button
onClick={loadLdapOus}
className="p-2 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)]"
title="刷新 OU"
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={() => setShowCreateOu(true)}
className="flex items-center gap-1 px-3 py-2 rounded-lg bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-sm hover:bg-[var(--dg-bg-hover)]"
>
<Plus className="w-3.5 h-3.5" />
新建 OU
</button>
</div>
</div>
{/* File upload (same CSV format) */}
<div className="flex items-center gap-3">
<button
onClick={() => fileRef.current?.click()}
className="flex items-center gap-2 px-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)]"
>
<Upload className="w-4 h-4" />
选择 CSV 文件
</button>
<input ref={fileRef} type="file" accept=".csv" className="hidden" onChange={handleFile} />
<span className="text-xs text-[var(--dg-text-muted)]">
CSV 需包含"姓名"列,系统自动生成拼音用户名和密码
</span>
</div>
{preview.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="py-2 pr-3">姓名</th>
<th className="py-2 pr-3">用户名(自动)</th>
<th className="py-2">密码(自动)</th>
</tr>
</thead>
<tbody>
{preview.slice(0, 30).map((r, i) => (
<tr key={`${r.username}-${i}`} className="border-b border-[var(--dg-border)]/50">
<td className="py-2 pr-3">{r.display_name}</td>
<td className="py-2 pr-3 text-[var(--dg-blue)]">{r.username}</td>
<td className="py-2 text-[var(--dg-text-muted)]">{r.password}</td>
</tr>
))}
{preview.length > 30 && (
<tr>
<td colSpan={3} className="py-2 text-center text-[var(--dg-text-muted)]">
... 共 {preview.length} 条
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</>
)}
</>
)}
{/* ── From LDAP Import ── */}
{tab === 'from-ldap' && (
<>
{ldapLoading && <p className="text-sm text-[var(--dg-text-muted)] text-center py-4">加载中...</p>}
{!ldapLoading && !ldapReady && (
<div className="p-3 rounded-lg bg-yellow-500/10 text-yellow-400 text-sm">
LDAP 未配置,请先在「平台配置 → LDAP」中配置 LDAP 连接
</div>
)}
{!ldapLoading && ldapReady && ldapOus.length > 0 && ldapUsers.length === 0 && (
<div className="space-y-1">
<p className="text-sm text-[var(--dg-text-muted)] mb-2">选择组织单元:</p>
{ldapOus.map((ou) => (
<button
key={ou.dn}
onClick={() => loadLdapUsers(ou.dn)}
className="w-full text-left px-3 py-2 rounded-lg text-sm hover:bg-[var(--dg-bg-hover)] flex items-center gap-2"
>
<UsersIcon className="w-4 h-4 text-[var(--dg-text-muted)]" />
<span>{ou.name}</span>
{ou.description && <span className="text-[var(--dg-text-muted)]">— {ou.description}</span>}
</button>
))}
</div>
)}
{ldapUsers.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm text-[var(--dg-text-muted)]">{ldapUsers.length} 个用户</p>
<button
onClick={() =>
setSelectedLdapUsers((prev) =>
prev.size === ldapUsers.length ? new Set() : new Set(ldapUsers.map((u) => u.dn)),
)
}
className="text-xs text-[var(--dg-blue)]"
>
{selectedLdapUsers.size === ldapUsers.length ? '取消全选' : '全选'}
</button>
</div>
{ldapUsers.map((u) => (
<label
key={u.dn}
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-[var(--dg-bg-hover)] cursor-pointer"
>
<input
type="checkbox"
checked={selectedLdapUsers.has(u.dn)}
onChange={() =>
setSelectedLdapUsers((prev) => {
const n = new Set(prev);
n.has(u.dn) ? n.delete(u.dn) : n.add(u.dn);
return n;
})
}
/>
<span className="text-sm font-medium">{u.cn || u.nickname || u.uid}</span>
<span className="text-xs text-[var(--dg-text-muted)]">@{u.uid || u.username}</span>
{u.mail && <span className="text-xs text-[var(--dg-text-muted)]">{u.mail}</span>}
</label>
))}
</div>
)}
{!ldapLoading && ldapReady && ldapOus.length === 0 && (
<p className="text-sm text-[var(--dg-text-muted)] text-center py-4">
LDAP 目录中无可用 OU,请先创建 OU 或检查配置
</p>
)}
</>
)}
{/* Result */}
{result && (
<div className="p-3 rounded-lg bg-[var(--dg-bg-tertiary)] text-sm space-y-1">
<p className="text-green-400">成功: {result.success_count ?? (result.success ? 1 : 0)}</p>
{result.skipped_count > 0 && <p className="text-yellow-400">跳过: {result.skipped_count}</p>}
{result.failed_count > 0 && <p className="text-red-400">失败: {result.failed_count}</p>}
{result.failed_users?.map((f: any, i: number) => (
<p key={`${f.username}-${i}`} className="text-red-400 text-xs">
{f.username}: {f.error}
</p>
))}
{result.message && !result.success_count && (
<p className="text-[var(--dg-text-secondary)]">{result.message}</p>
)}
</div>
)}
</div>
{/* Footer */}
@@ -500,57 +322,6 @@ export function BatchImportModal({ onClose, onImported }: BatchImportModalProps)
</button>
</div>
{/* Create OU sub-dialog */}
{showCreateOu && (
<ModalOverlay open={showCreateOu} onClose={() => setShowCreateOu(false)} width="max-w-md">
<div className="w-full bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-xl p-6 shadow-xl">
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold">创建新的组织单位 (OU)</h3>
<button
onClick={() => setShowCreateOu(false)}
className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-1">OU 名称 *</label>
<input
value={newOuName}
onChange={(e) => setNewOuName(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:border-blue-400 focus:outline-none"
placeholder="例如:研发部、市场部"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">描述</label>
<input
value={newOuDesc}
onChange={(e) => setNewOuDesc(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:border-blue-400 focus:outline-none"
placeholder="可选描述"
/>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
onClick={() => setShowCreateOu(false)}
className="px-4 py-2 text-sm rounded-lg bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)]"
>
取消
</button>
<button
onClick={handleCreateOu}
disabled={creatingOu || !newOuName.trim()}
className="px-4 py-2 text-sm rounded-lg bg-[var(--dg-blue)] text-white disabled:opacity-50"
>
{creatingOu ? '创建中...' : '创建 OU'}
</button>
</div>
</div>
</div>
</ModalOverlay>
)}
</div>
</ModalOverlay>
);