import { NextResponse } from 'next/server' import { cookies } from 'next/headers' import { exec } from 'child_process' import { promisify } from 'util' import { verifySharedJwt } from '@/lib/jwt' import { isLldapAdmin } from '@/lib/ldap' import { sendSetupLinkEmail } from '@/lib/email' import { signSetupToken } from '@/lib/setup-token' const execAsync = promisify(exec) const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' function generatePassword(): string { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ' const lower = 'abcdefghjkmnpqrstuvwxyz' const digits = '23456789' const special = '!@#$%&*' const all = upper + lower + digits + special const crypto = globalThis.crypto const pick = (s: string) => s[crypto.getRandomValues(new Uint32Array(1))[0] % s.length] // 确保每种类型至少一个,其余随机填充到 12 位 let pwd = pick(upper) + pick(lower) + pick(digits) + pick(special) for (let i = 4; i < 12; i++) pwd += pick(all) // 打乱顺序 return pwd.split('').sort(() => crypto.getRandomValues(new Uint32Array(1))[0] - 0x80000000).join('') } async function fetchRoles(siteUrl: string): Promise { try { const res = await fetch(`${siteUrl}/api/internal/roles`, { headers: { 'x-internal-key': INTERNAL_KEY }, signal: AbortSignal.timeout(5000), }) const data = await res.json() return (data.roles || []).map((r: { name: string }) => r.name) } catch { return [] } } async function syncToSite(siteUrl: string, username: string, password: string): Promise { try { const res = await fetch(`${siteUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), signal: AbortSignal.timeout(10000), }) return res.ok } catch { return false } } // 直接更新站点 SQLite 数据库中的用户角色 function setRoleSQL(dbPath: string, username: string, role: string): string { return `sqlite3 "${dbPath}" "UPDATE users SET role = '${role}', updated_at = datetime('now', '+8 hours') WHERE username = '${username}';"` } export async function POST(request: Request) { try { const cookieStore = await cookies() const token = cookieStore.get('tlyq_session')?.value if (!token) return NextResponse.json({ error: '未登录' }, { status: 401 }) const session = verifySharedJwt(token) if (!session || !(await isLldapAdmin(session.username))) { return NextResponse.json({ error: '仅管理员可创建用户' }, { status: 403 }) } const { username, displayName, assetsRole, issueRole, email } = await request.json() if (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 }) if (!/^[a-z][a-z0-9_.@-]*$/i.test(username)) return NextResponse.json({ error: '用户名格式不合法' }, { status: 400 }) const password = generatePassword() // 从各站点实时获取可用角色列表 const [assetsRoles, issueRoles] = await Promise.all([ fetchRoles('http://localhost:6177'), fetchRoles('http://localhost:6176'), ]) const ar = (assetsRole && assetsRoles.includes(assetsRole)) ? assetsRole : 'viewer' const ir = (issueRole && issueRoles.includes(issueRole)) ? issueRole : 'viewer' const safeName = (displayName || username).replace(/'/g, "'\\''") const safeUser = username.replace(/'/g, "'\\''") const lldapEmail = email || '' const d = new Date() const now = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}` const userUuid = crypto.randomUUID() // 1. LLDAP SQLite 插入用户 const insertSQL = `INSERT OR IGNORE INTO users (user_id, email, display_name, creation_date, uuid, lowercase_email, modified_date, password_modified_date) VALUES ('${username}', '${lldapEmail}', '${safeName}', '${now}', '${userUuid}', LOWER('${lldapEmail}'), '${now}', '${now}');` await execAsync(`docker exec lldap /bin/sh -c "cat > /tmp/iu.sql <<'EOSQL'\n${insertSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/iu.sql"`, { timeout: 5000 }) // 2. 从 LLDAP 容器动态获取 admin 密码(不硬编码,admin 改密码后无需改 OA 配置) const { stdout: adminPassOut } = await execAsync('docker exec lldap printenv LLDAP_ADMIN_PASSWORD', { timeout: 3000 }) const adminPass = (adminPassOut.trim() || 'admin123').replace(/'/g, "'\\''") // 3. LLDAP 设置密码 —— 通过 base64 传输避免 shell 特殊字符问题 const b64Pass = Buffer.from(password).toString('base64') await execAsync(`docker exec lldap /bin/sh -c "echo '${b64Pass}' | base64 -d > /tmp/userpwd.txt"`, { timeout: 3000 }) const pwdCmd = `LLDAP_USER_PASSWORD=$(cat /tmp/userpwd.txt) ./lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password '${adminPass}' --username '${safeUser}'` await execAsync(`docker exec lldap /bin/sh -c '${pwdCmd}'`, { timeout: 10000 }) // 3. 自动登录各站点触发用户同步 const [assetsOk, issueOk] = await Promise.all([ syncToSite('http://localhost:6177', username, password), syncToSite('http://localhost:6176', username, password), ]) // 4. 直接更新各站点 SQLite 的角色(覆盖 viewer 默认值) const assetsDb = process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db' const issueDb = process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db' const roleResults = { assets: false, issue: false } if (assetsOk) { try { await execAsync(setRoleSQL(assetsDb, username, ar), { timeout: 3000 }); roleResults.assets = true } catch {} } if (issueOk) { try { await execAsync(setRoleSQL(issueDb, username, ir), { timeout: 3000 }); roleResults.issue = true } catch {} } // 5. 如果提供了邮箱,发送密码设置链接(不再在邮件中发送明文密码) let emailSent = false if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { try { const setupToken = signSetupToken(username) const setupUrl = `https://oa.tlyq.ai/setup-password?token=${setupToken}` await sendSetupLinkEmail(email, username, setupUrl, displayName || username) emailSent = true } catch (e) { console.error('发送邮件失败:', e) } } return NextResponse.json({ success: true, password: emailSent ? undefined : password, synced: { assets: assetsOk, issue: issueOk }, roles: { assets: ar, issue: ir, applied: roleResults }, emailSent, message: emailSent ? `用户已创建,密码设置链接已发送至 ${email}` : '用户已创建并同步至所有站点', }) } catch (e) { const msg = e instanceof Error ? e.message : '创建失败' return NextResponse.json({ error: msg }, { status: 500 }) } }