From ce3a26ab4c07a7ffdceba32bc333eb87dc7253fb Mon Sep 17 00:00:00 2001 From: aiyimickey <39365912+aiyimickey@users.noreply.github.com> Date: Mon, 18 May 2026 17:50:57 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=E5=9E=83=E5=9C=BE?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "src/app/api/admin/create-user/route.ts\"\"" | 151 ------------------ "src/app/api/admin/roles/route.ts\"\"" | 32 ---- "src/app/api/admin/user-roles/route.ts\"\"" | 104 ------------ "src/app/api/admin/users/route.ts\"\"" | 115 ------------- .../api/auth/change-password/route.ts\"\"" | 56 ------- "src/app/api/auth/login/route.ts\"\"" | 32 ---- "src/app/api/auth/logout/route.ts\"\"" | 8 - "src/app/api/auth/me/route.ts\"\"" | 82 ---------- .../app/api/auth/setup-password/route.ts\"\"" | 54 ------- "src/app/page.tsx\"\"" | 109 ------------- 10 files changed, 743 deletions(-) delete mode 100644 "src/app/api/admin/create-user/route.ts\"\"" delete mode 100644 "src/app/api/admin/roles/route.ts\"\"" delete mode 100644 "src/app/api/admin/user-roles/route.ts\"\"" delete mode 100644 "src/app/api/admin/users/route.ts\"\"" delete mode 100644 "src/app/api/auth/change-password/route.ts\"\"" delete mode 100644 "src/app/api/auth/login/route.ts\"\"" delete mode 100644 "src/app/api/auth/logout/route.ts\"\"" delete mode 100644 "src/app/api/auth/me/route.ts\"\"" delete mode 100644 "src/app/api/auth/setup-password/route.ts\"\"" delete mode 100644 "src/app/page.tsx\"\"" diff --git "a/src/app/api/admin/create-user/route.ts\"\"" "b/src/app/api/admin/create-user/route.ts\"\"" deleted file mode 100644 index b6d3ad6..0000000 --- "a/src/app/api/admin/create-user/route.ts\"\"" +++ /dev/null @@ -1,151 +0,0 @@ -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://assets-ai:3000'), - fetchRoles('http://issue-ai:3000'), - ]) - - 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://assets-ai:3000', username, password), - syncToSite('http://issue-ai:3000', 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 }) - } -} diff --git "a/src/app/api/admin/roles/route.ts\"\"" "b/src/app/api/admin/roles/route.ts\"\"" deleted file mode 100644 index ae6c4f5..0000000 --- "a/src/app/api/admin/roles/route.ts\"\"" +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { verifySharedJwt } from '@/lib/jwt' -import { isLldapAdmin } from '@/lib/ldap' - -const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' - -async function fetchRoles(url: string): Promise<{ name: string; display_name: string }[]> { - try { - const res = await fetch(`${url}/api/internal/roles`, { - headers: { 'x-internal-key': INTERNAL_KEY }, - signal: AbortSignal.timeout(5000), - }) - const data = await res.json() - return data.roles || [] - } catch { return [] } -} - -export async function GET() { - 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: 'Forbidden' }, { status: 403 }) - - const [assetsRoles, issueRoles] = await Promise.all([ - fetchRoles('http://assets-ai:3000'), - fetchRoles('http://issue-ai:3000'), - ]) - - return NextResponse.json({ assets: assetsRoles, issue: issueRoles }) -} diff --git "a/src/app/api/admin/user-roles/route.ts\"\"" "b/src/app/api/admin/user-roles/route.ts\"\"" deleted file mode 100644 index a0ff2e4..0000000 --- "a/src/app/api/admin/user-roles/route.ts\"\"" +++ /dev/null @@ -1,104 +0,0 @@ -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' - -const execAsync = promisify(exec) -const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' - -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 [] } -} - -function queryDb(dbPath: string, sql: string): Promise { - return execAsync(`sqlite3 "${dbPath}" "${sql.replace(/"/g, '\\"')}"`, { timeout: 3000 }).then(r => r.stdout).catch(() => '') -} - -async function getSiteUsers(dbPath: string, roles: string[]): Promise<{ username: string; display_name: string; role: string }[]> { - const out = await queryDb(dbPath, 'SELECT username, display_name, role FROM users WHERE is_active=1 ORDER BY username;') - return out.trim().split('\n').filter(Boolean).map(line => { - const [username, display_name, role] = line.split('|') - return { username, display_name: display_name || username, role: roles.includes(role) ? role : 'viewer' } - }) -} - -async function checkAdmin() { - const cookieStore = await cookies() - const token = cookieStore.get('tlyq_session')?.value - if (!token) return false - const session = verifySharedJwt(token) - return session ? isLldapAdmin(session.username) : false -} - -// GET — 列出各站点用户及其角色 -export async function GET() { - if (!(await checkAdmin())) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - - try { - const [assetsRoles, issueRoles] = await Promise.all([ - fetchRoles('http://assets-ai:3000'), - fetchRoles('http://issue-ai:3000'), - ]) - - const [assetsUsers, issueUsers] = await Promise.all([ - getSiteUsers(process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db', assetsRoles), - getSiteUsers(process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db', issueRoles), - ]) - - // 从 LLDAP 获取所有用户邮箱 - let emails: Record = {} - try { - const { stdout } = await execAsync( - `docker exec lldap /bin/sh -c "echo 'SELECT user_id, email FROM users;' | sqlite3 /data/users.db"`, - { timeout: 3000 } - ) - stdout.trim().split('\n').filter(Boolean).forEach(line => { - const [uid, e] = line.split('|') - emails[uid] = e || '' - }) - } catch {} - - return NextResponse.json({ - assetsRoles, - issueRoles, - users: { assets: assetsUsers, issue: issueUsers }, - emails, - }) - } catch (e) { - return NextResponse.json({ error: '查询失败' }, { status: 500 }) - } -} - -// PUT — 更新用户角色 -export async function PUT(request: Request) { - if (!(await checkAdmin())) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - - try { - const { username, site, role } = await request.json() - if (!username || !site || !role) return NextResponse.json({ error: '参数不完整' }, { status: 400 }) - if (username === 'admin' || username === 'localadmin') return NextResponse.json({ error: '不能修改系统保留用户角色' }, { status: 400 }) - - const dbPath = site === 'assets' - ? (process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db') - : (process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db') - - // 验证角色合法性 - const roles = await fetchRoles(`http://${site}-ai:3000`) - if (!roles.includes(role)) return NextResponse.json({ error: '无效的角色' }, { status: 400 }) - - await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET role='${role}', updated_at=datetime('now', '+8 hours') WHERE username='${username}';"`, { timeout: 3000 }) - - return NextResponse.json({ success: true }) - } catch (e) { - return NextResponse.json({ error: '更新失败' }, { status: 500 }) - } -} diff --git "a/src/app/api/admin/users/route.ts\"\"" "b/src/app/api/admin/users/route.ts\"\"" deleted file mode 100644 index 8d621ee..0000000 --- "a/src/app/api/admin/users/route.ts\"\"" +++ /dev/null @@ -1,115 +0,0 @@ -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' - -const execAsync = promisify(exec) - -function checkAdmin() { - return async () => { - const cookieStore = await cookies() - const token = cookieStore.get('tlyq_session')?.value - if (!token) return false - const session = verifySharedJwt(token) - return session ? isLldapAdmin(session.username) : false - } -} - -// GET — 列出 LLDAP 中所有用户 -export async function GET() { - const isAdmin = await checkAdmin()() - if (!isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - - try { - const { stdout } = await execAsync( - `docker exec lldap /bin/sh -c "echo 'SELECT user_id, email, display_name, creation_date FROM users ORDER BY creation_date DESC;' | sqlite3 /data/users.db"`, - { timeout: 5000 } - ) - const users = stdout.trim().split('\n').filter(Boolean).map(line => { - const [user_id, email, display_name, creation_date] = line.split('|') - return { username: user_id, email, displayName: display_name || user_id, createdAt: creation_date } - }) - return NextResponse.json({ users }) - } catch (e) { - return NextResponse.json({ error: '查询失败' }, { status: 500 }) - } -} - -// DELETE — 删除用户(LLDAP + 各站点) -export async function DELETE(request: Request) { - const isAdmin = await checkAdmin()() - if (!isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - - try { - const { username } = await request.json() - if (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 }) - if (username === 'admin' || username === 'localadmin') { - return NextResponse.json({ error: '不能删除系统保留用户' }, { status: 400 }) - } - - const safeUser = username.replace(/'/g, "''") - - // 删除 LLDAP 用户 - const lldapSQL = `DELETE FROM users WHERE user_id='${safeUser}';` - await execAsync( - `docker exec lldap /bin/sh -c "cat > /tmp/del.sql <<'EOSQL'\n${lldapSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/del.sql"`, - { timeout: 5000 } - ) - - // 删除各站点本地用户 - const results: Record = {} - for (const [site, dbPath] of Object.entries({ - assets: process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db', - issue: process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db', - })) { - try { - await execAsync(`sqlite3 "${dbPath}" "DELETE FROM users WHERE username='${safeUser}';"`, { timeout: 3000 }) - results[site] = true - } catch { results[site] = false } - } - - return NextResponse.json({ success: true, deleted: results }) - } catch (e) { - return NextResponse.json({ error: '删除失败' }, { status: 500 }) - } -} - -// PATCH — 修改用户邮箱(admin 权限) -export async function PATCH(request: Request) { - const isAdmin = await checkAdmin()() - if (!isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - - try { - const { username, email } = await request.json() - if (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 }) - if (email !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return NextResponse.json({ error: '邮箱格式不合法' }, { status: 400 }) - } - - const safeUser = username.replace(/'/g, "''") - const safeEmail = (email || '').replace(/'/g, "''") - 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 updateSQL = `UPDATE users SET email = '${safeEmail}', lowercase_email = LOWER('${safeEmail}'), modified_date = '${now}' WHERE user_id = '${safeUser}';` - await execAsync( - `docker exec lldap /bin/sh -c "cat > /tmp/uea.sql <<'EOSQL'\n${updateSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/uea.sql"`, - { timeout: 5000 } - ) - - // 同步更新 assets / issue 本地用户表 - 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' - for (const dbPath of [assetsDb, issueDb]) { - try { - await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET email = '${safeEmail}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';"`, { timeout: 3000 }) - } catch {} - } - - return NextResponse.json({ success: true, username, email: email || '' }) - } catch (e) { - return NextResponse.json({ error: '修改失败' }, { status: 500 }) - } -} diff --git "a/src/app/api/auth/change-password/route.ts\"\"" "b/src/app/api/auth/change-password/route.ts\"\"" deleted file mode 100644 index 420b7c9..0000000 --- "a/src/app/api/auth/change-password/route.ts\"\"" +++ /dev/null @@ -1,56 +0,0 @@ -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { exec } from 'child_process' -import { promisify } from 'util' -import { verifySharedJwt } from '@/lib/jwt' - -const execAsync = promisify(exec) - -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) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) - - const { currentPassword, newPassword } = await request.json() - if (!currentPassword || !newPassword) { - return NextResponse.json({ error: '请输入当前密码和新密码' }, { status: 400 }) - } - if (newPassword.length < 8) { - return NextResponse.json({ error: '新密码至少 8 位' }, { status: 400 }) - } - // 密码复杂度:大写/小写/数字/特殊字符 4选3 - const hasUpper = /[A-Z]/.test(newPassword) - const hasLower = /[a-z]/.test(newPassword) - const hasDigit = /[0-9]/.test(newPassword) - const hasSpecial = /[^A-Za-z0-9]/.test(newPassword) - const complexityScore = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length - if (complexityScore < 3) { - return NextResponse.json({ error: '密码需包含大写字母、小写字母、数字、特殊字符中至少 3 种' }, { status: 400 }) - } - - // 从 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, "'\\''") - - const safeUser = session.username.replace(/'/g, "'\\''") - const safePass = newPassword.replace(/'/g, "'\\''") - const cmd = `docker exec lldap ./lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password '${adminPass}' --username '${safeUser}' --password '${safePass}'` - - const { stdout, stderr } = await execAsync(cmd, { timeout: 10000 }) - if (stderr && !stderr.includes('Successfully')) { - return NextResponse.json({ error: stderr.trim() || '修改失败' }, { status: 500 }) - } - - return NextResponse.json({ success: true }) - } catch (err) { - const msg = err instanceof Error ? err.message : '修改失败' - if (msg.includes('command not found') || msg.includes('No such container')) { - return NextResponse.json({ error: '密码服务不可用' }, { status: 503 }) - } - return NextResponse.json({ error: msg }, { status: 500 }) - } -} diff --git "a/src/app/api/auth/login/route.ts\"\"" "b/src/app/api/auth/login/route.ts\"\"" deleted file mode 100644 index 95a5acf..0000000 --- "a/src/app/api/auth/login/route.ts\"\"" +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' -import { signSharedJwt, sharedCookieConfig } from '@/lib/jwt' -import { ldapAuth } from '@/lib/ldap' - -export async function POST(request: Request) { - try { - const { username, password } = await request.json() - if (!username || !password) { - return NextResponse.json({ error: '请输入用户名和密码' }, { status: 400 }) - } - - const result = await ldapAuth(username, password) - if (!result.success) { - if (result.unreachable) { - return NextResponse.json({ error: '认证服务暂时不可用,请稍后再试' }, { status: 503 }) - } - return NextResponse.json({ error: '用户名或密码错误' }, { status: 401 }) - } - - const token = signSharedJwt({ username: result.username!, displayName: result.displayName! }) - const cfg = sharedCookieConfig() - const cookieStore = await cookies() - cookieStore.set(cfg.name, token, cfg) - - return NextResponse.json({ - user: { username: result.username, displayName: result.displayName }, - }) - } catch { - return NextResponse.json({ error: '登录失败' }, { status: 500 }) - } -} diff --git "a/src/app/api/auth/logout/route.ts\"\"" "b/src/app/api/auth/logout/route.ts\"\"" deleted file mode 100644 index 0b6db2d..0000000 --- "a/src/app/api/auth/logout/route.ts\"\"" +++ /dev/null @@ -1,8 +0,0 @@ -import { NextResponse } from 'next/server' -import { cookies } from 'next/headers' - -export async function POST() { - const cookieStore = await cookies() - cookieStore.set('tlyq_session', '', { maxAge: 0, path: '/' }) - return NextResponse.redirect(new URL('/login', process.env.NEXT_PUBLIC_URL || 'https://oa.tlyq.ai')) -} diff --git "a/src/app/api/auth/me/route.ts\"\"" "b/src/app/api/auth/me/route.ts\"\"" deleted file mode 100644 index 5ef8da0..0000000 --- "a/src/app/api/auth/me/route.ts\"\"" +++ /dev/null @@ -1,82 +0,0 @@ -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' - -const execAsync = promisify(exec) - -async function getLldapEmail(username: string): Promise { - try { - const safeUser = username.replace(/'/g, "''") - const { stdout } = await execAsync( - `docker exec lldap /bin/sh -c "echo 'SELECT email FROM users WHERE user_id='\\''${safeUser}'\\'';' | sqlite3 /data/users.db"`, - { timeout: 3000 } - ) - return stdout.trim() || '' - } catch { return '' } -} - -export async function GET() { - try { - const cookieStore = await cookies() - const token = cookieStore.get('tlyq_session')?.value - if (!token) return NextResponse.json({ error: '未登录' }, { status: 401 }) - - const payload = verifySharedJwt(token) - if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) - - const [admin, email] = await Promise.all([ - isLldapAdmin(payload.username), - getLldapEmail(payload.username), - ]) - - return NextResponse.json({ - user: { username: payload.username, displayName: payload.displayName, email, isAdmin: admin }, - }) - } catch { - return NextResponse.json({ error: '获取用户信息失败' }, { status: 500 }) - } -} - -export async function PUT(request: Request) { - try { - const cookieStore = await cookies() - const token = cookieStore.get('tlyq_session')?.value - if (!token) return NextResponse.json({ error: '未登录' }, { status: 401 }) - - const payload = verifySharedJwt(token) - if (!payload) return NextResponse.json({ error: '会话已过期' }, { status: 401 }) - - const { email } = await request.json() - if (email !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return NextResponse.json({ error: '邮箱格式不合法' }, { status: 400 }) - } - - const safeUser = payload.username.replace(/'/g, "''") - const safeEmail = (email || '').replace(/'/g, "''") - 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 updateSQL = `UPDATE users SET email = '${safeEmail}', lowercase_email = LOWER('${safeEmail}'), modified_date = '${now}' WHERE user_id = '${safeUser}';` - await execAsync( - `docker exec lldap /bin/sh -c "cat > /tmp/ue.sql <<'EOSQL'\n${updateSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/ue.sql"`, - { timeout: 5000 } - ) - - // 同步更新 assets / issue 本地用户表 - 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' - for (const dbPath of [assetsDb, issueDb]) { - try { - await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET email = '${safeEmail}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';"`, { timeout: 3000 }) - } catch {} - } - - return NextResponse.json({ success: true, email: email || '' }) - } catch (e) { - const msg = e instanceof Error ? e.message : '修改失败' - return NextResponse.json({ error: msg }, { status: 500 }) - } -} diff --git "a/src/app/api/auth/setup-password/route.ts\"\"" "b/src/app/api/auth/setup-password/route.ts\"\"" deleted file mode 100644 index 312dfe2..0000000 --- "a/src/app/api/auth/setup-password/route.ts\"\"" +++ /dev/null @@ -1,54 +0,0 @@ -import { NextResponse } from 'next/server' -import { exec } from 'child_process' -import { promisify } from 'util' -import { verifySetupToken } from '@/lib/setup-token' - -const execAsync = promisify(exec) - -export async function POST(request: Request) { - try { - const { token, password } = await request.json() - if (!token || !password) { - return NextResponse.json({ error: '参数不完整' }, { status: 400 }) - } - - const payload = verifySetupToken(token) - if (!payload) { - return NextResponse.json({ error: '链接已过期或无效,请联系管理员重新创建账号' }, { status: 403 }) - } - - if (password.length < 8) { - return NextResponse.json({ error: '密码至少 8 位' }, { status: 400 }) - } - const hasUpper = /[A-Z]/.test(password) - const hasLower = /[a-z]/.test(password) - const hasDigit = /[0-9]/.test(password) - const hasSpecial = /[^A-Za-z0-9]/.test(password) - const score = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length - if (score < 3) { - return NextResponse.json({ error: '密码需包含大写字母、小写字母、数字、特殊字符中至少 3 种' }, { status: 400 }) - } - - const { stdout: adminPassOut } = await execAsync( - 'docker exec lldap printenv LLDAP_ADMIN_PASSWORD', { timeout: 3000 } - ) - const adminPass = (adminPassOut.trim() || 'admin123').replace(/'/g, "'\\''") - - const safeUser = payload.username.replace(/'/g, "'\\''") - const safePass = password.replace(/'/g, "'\\''") - const cmd = `docker exec lldap ./lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password '${adminPass}' --username '${safeUser}' --password '${safePass}'` - - const { stderr } = await execAsync(cmd, { timeout: 10000 }) - if (stderr && !stderr.includes('Successfully')) { - return NextResponse.json({ error: stderr.trim() || '设置失败' }, { status: 500 }) - } - - return NextResponse.json({ success: true }) - } catch (err) { - const msg = err instanceof Error ? err.message : '设置失败' - if (msg.includes('command not found') || msg.includes('No such container')) { - return NextResponse.json({ error: '密码服务不可用' }, { status: 503 }) - } - return NextResponse.json({ error: msg }, { status: 500 }) - } -} diff --git "a/src/app/page.tsx\"\"" "b/src/app/page.tsx\"\"" deleted file mode 100644 index 895c993..0000000 --- "a/src/app/page.tsx\"\"" +++ /dev/null @@ -1,109 +0,0 @@ -import { cookies } from 'next/headers' -import { redirect } from 'next/navigation' -import { verifySharedJwt } from '@/lib/jwt' -import Header from '@/components/Header' - -function siteUrl(url: string, domain: string): string { - if (process.env.NODE_ENV === 'production') { - return `https://${domain}` - } - return url -} - -const CORE_SITES = [ - { name: '资产管理', url: 'https://assets.tlyq.ai', desc: 'GPU 服务器、存储服务器等硬件设备信息管理与实时监控', tag: 'CMDB', dot: '#2563eb', domain: 'assets.tlyq.ai' }, - { name: '工单跟踪', url: 'https://issue.tlyq.ai', desc: '故障工单全流程管理,SLA 自动计算,月度/周度报告导出', tag: 'ITS', dot: '#7c3aed', domain: 'issue.tlyq.ai' }, -] - -const OTHER_SITES = [ - { name: '官网', url: 'https://www.tlyq.ai', desc: 'tlyq.ai 企业官方网站', tag: 'WWW', dot: '#059669', domain: 'www.tlyq.ai' }, - { name: '云平台', url: 'https://cloud.tlyq.ai', desc: '云服务登录入口与资源概览', tag: 'CLOUD', dot: '#d97706', domain: 'cloud.tlyq.ai' }, - { name: 'Token 工厂', url: 'https://token.tlyq.ai', desc: 'Token 管理与发放平台', tag: 'TOKEN', dot: '#e11d48', domain: 'token.tlyq.ai' }, - { name: '代码仓库', url: 'https://git.tlyq.ai', desc: 'Gitea 代码托管与版本管理', tag: 'GIT', dot: '#db2777', domain: 'git.tlyq.ai' }, -] - -const COLORS: Record = { - '#2563eb': { light: 'rgba(37,99,235,0.08)', tag: '#2563eb' }, - '#7c3aed': { light: 'rgba(124,58,237,0.08)', tag: '#7c3aed' }, - '#059669': { light: 'rgba(5,150,105,0.08)', tag: '#059669' }, - '#d97706': { light: 'rgba(217,119,6,0.08)', tag: '#d97706' }, - '#e11d48': { light: 'rgba(225,29,72,0.08)', tag: '#e11d48' }, - '#db2777': { light: 'rgba(219,39,119,0.08)', tag: '#db2777' }, -} - -export default async function HomePage() { - const cookieStore = await cookies() - const sessionCookie = cookieStore.get('session')?.value - let username = '' - if (sessionCookie) { - try { username = JSON.parse(sessionCookie).username || '' } catch { } - } - if (!username) redirect('/login') - - const tlyqToken = cookieStore.get('tlyq_session')?.value - let displayName = username - if (tlyqToken) { - const shared = verifySharedJwt(tlyqToken) - if (shared && shared.displayName && shared.displayName !== shared.username) { - displayName = shared.displayName - } - } - - return ( -
-
- -
-
-
tlyq.ai / OA PORTAL
-

欢迎回来,{displayName}

-
- -
核心系统
-
- {CORE_SITES.map(site => { - const c = COLORS[site.dot] - return ( - -
-
-
{site.name}
- {site.tag} -
-
{site.desc}
-
{site.domain} →
-
- ) - })} -
- -
其他站点
-
- {OTHER_SITES.map(site => { - const c = COLORS[site.dot] - return ( - -
-
-
{site.name}
- {site.tag} -
-
{site.desc}
-
{site.domain} →
-
- ) - })} -
- -
-
- ) -}