chore: 初始化 idsync 项目

飞书通讯录 ↔ LDAP 同步工具的首个提交。

- internal/feishu: 飞书通讯录客户端、快照与重试
- internal/ldaplib: LDAP 读写与批量操作
- internal/mapping: 飞书部门/用户到 LDAP 条目的映射
- internal/syncer: 同步计划生成、执行与调度
- internal/store: SQLite 台账
- internal/server + internal/webui: 管理后台与内嵌前端产物
- internal/password: SSHA 口令处理
- config.example.yml / Dockerfile / Makefile / web 前端源码

真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
This commit is contained in:
2026-09-29 00:10:59 +08:00
commit 947b1a1475
43 changed files with 10079 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>idsync · 飞书通讯录同步</title>
<link rel="stylesheet" href="/static/app.css" />
</head>
<body class="bg-base-200 min-h-screen">
<!-- 未登录:登录卡片 -->
<div id="loginView" class="hidden items-center justify-center min-h-screen p-4">
<div class="card w-full max-w-sm bg-base-100 shadow-lg">
<div class="card-body">
<div class="flex items-center gap-2 mb-2">
<span class="inline-block size-3 rounded-full bg-primary"></span>
<h1 class="card-title text-lg m-0">idsync</h1>
</div>
<p class="text-sm text-base-content/60 mb-4">飞书通讯录 → OpenLDAP 同步</p>
<div id="loginError" class="alert alert-error text-sm mb-3 hidden"></div>
<label class="label" for="username"><span class="label-text">用户名</span></label>
<input id="username" type="text" class="input w-full mb-3" autocomplete="username" />
<label class="label" for="password"><span class="label-text">密码</span></label>
<input id="password" type="password" class="input w-full mb-5" autocomplete="current-password" />
<button id="loginBtn" class="btn btn-primary w-full">登录</button>
</div>
</div>
</div>
<!-- 已登录:主界面 -->
<div id="appView" class="hidden">
<header class="navbar bg-base-100 shadow-sm px-4 sticky top-0 z-20">
<div class="flex-1 flex items-center gap-2">
<span class="inline-block size-3 rounded-full bg-primary"></span>
<span class="text-lg font-semibold">idsync</span>
<span class="hidden sm:inline text-sm text-base-content/50">飞书通讯录 → OpenLDAP</span>
</div>
<div class="flex-none flex items-center gap-2">
<button id="themeToggle" class="btn btn-sm btn-ghost" title="切换明暗主题">主题</button>
<button id="logoutBtn" class="btn btn-sm btn-outline">退出</button>
</div>
</header>
<main class="max-w-7xl mx-auto p-4">
<!-- 页签 -->
<div class="tabs tabs-border mb-4" role="tablist">
<button class="tab tab-active" data-view="overview" role="tab">概览</button>
<button class="tab" data-view="users" role="tab">用户</button>
<button class="tab" data-view="groups" role="tab">部门分组</button>
<button class="tab" data-view="plan" role="tab">变更预览</button>
<button class="tab" data-view="runs" role="tab">同步历史</button>
</div>
<div id="globalError" class="alert alert-error text-sm mb-4 hidden"></div>
<!-- 概览 -->
<section id="view-overview" class="view">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<span class="text-sm text-base-content/60">在职账号</span>
<span id="statActive" class="text-3xl font-semibold">–</span>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<span class="text-sm text-base-content/60">离职账号</span>
<span id="statInactive" class="text-3xl font-semibold">–</span>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<span class="text-sm text-base-content/60">部门分组</span>
<span id="statGroups" class="text-3xl font-semibold">–</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-base">服务状态</h2>
<div id="connStatus" class="space-y-2 text-sm"></div>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-base">上次同步</h2>
<div id="lastRunInfo" class="text-sm text-base-content/70">暂无记录</div>
</div>
</div>
</div>
</section>
<!-- 用户 -->
<section id="view-users" class="view hidden">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
<h2 class="card-title text-base">受管用户</h2>
<div class="flex items-center gap-2">
<input id="userFilter" type="search" class="input input-sm w-56" placeholder="按 uid / 姓名 / 邮箱过滤" />
<button id="reloadUsers" class="btn btn-sm btn-outline">刷新</button>
</div>
</div>
<div class="idsync-scroll">
<table class="table table-sm">
<thead>
<tr>
<th>uid</th><th>姓名</th><th>企业邮箱</th><th>状态</th>
<th>归属部门数</th><th>最后同步</th><th>一致性</th><th>DN</th>
</tr>
</thead>
<tbody id="userRows"></tbody>
</table>
</div>
<p id="userEmpty" class="hidden text-sm text-base-content/50 py-4 text-center">暂无受管用户,请先执行一次同步。</p>
</div>
</div>
</section>
<!-- 部门分组 -->
<section id="view-groups" class="view hidden">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-base mb-3">受管部门分组</h2>
<div class="idsync-scroll">
<table class="table table-sm">
<thead>
<tr><th>部门</th><th>飞书部门 ID</th><th>父部门</th><th>DN</th></tr>
</thead>
<tbody id="groupRows"></tbody>
</table>
</div>
<p id="groupEmpty" class="hidden text-sm text-base-content/50 py-4 text-center">暂无受管分组。</p>
</div>
</div>
</section>
<!-- 变更预览 -->
<section id="view-plan" class="view hidden">
<div class="card bg-base-100 shadow-sm mb-4">
<div class="card-body">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="card-title text-base">变更预览(dry-run)</h2>
<p class="text-sm text-base-content/60 mt-1">
预览与实际执行使用同一份计划代码,因此这里显示的就是将要发生的变更。
</p>
</div>
<div class="flex gap-2">
<button id="previewBtn" class="btn btn-sm btn-outline">重新计算</button>
<button id="applyBtn" class="btn btn-sm btn-primary">应用变更</button>
</div>
</div>
</div>
</div>
<div id="planWarnings" class="hidden mb-4"></div>
<div id="planSummary" class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-3 mb-4"></div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h3 class="card-title text-base mb-2">操作明细</h3>
<div class="idsync-scroll">
<table class="table table-sm">
<thead>
<tr><th class="w-40">类型</th><th class="w-56">对象</th><th>说明</th></tr>
</thead>
<tbody id="planRows"></tbody>
</table>
</div>
<p id="planEmpty" class="hidden text-sm text-base-content/50 py-4 text-center">
没有需要变更的内容,两侧状态已一致。
</p>
</div>
</div>
</section>
<!-- 同步历史 -->
<section id="view-runs" class="view hidden">
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-center justify-between mb-3">
<h2 class="card-title text-base">同步历史</h2>
<button id="reloadRuns" class="btn btn-sm btn-outline">刷新</button>
</div>
<div class="idsync-scroll">
<table class="table table-sm">
<thead>
<tr>
<th>开始时间</th><th>模式</th><th>触发</th><th>结果</th>
<th>新建</th><th>更新</th><th>迁移</th><th>加组</th><th>移组</th><th>失败</th>
</tr>
</thead>
<tbody id="runRows"></tbody>
</table>
</div>
<p id="runEmpty" class="hidden text-sm text-base-content/50 py-4 text-center">暂无同步记录。</p>
</div>
</div>
</section>
</main>
</div>
<!-- 应用变更的确认对话框 -->
<dialog id="applyModal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-semibold mb-2">确认应用变更</h3>
<p class="text-sm text-base-content/70 mb-4">
将对 OpenLDAP 执行下列变更。用户密码不会被修改,Authelia 重置的密码不受影响。
</p>
<div id="applyModalBody" class="text-sm mb-4"></div>
<div class="modal-action">
<button id="applyCancel" class="btn btn-ghost">取消</button>
<button id="applyConfirm" class="btn btn-primary">确认执行</button>
</div>
</div>
</dialog>
<script src="/static/flyonui.js"></script>
<script src="/static/app.js"></script>
</body>
</html>