From b851fbbda85b6343153c1e99b5795345f9848632 Mon Sep 17 00:00:00 2001 From: aiyimickey <39365912+aiyimickey@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:33:35 +0800 Subject: [PATCH] feat: add /api/internal/roles endpoint for OA integration + middleware publicPaths --- docker-compose.yml | 5 +++ src/app/api/admin/create-user/route.ts | 25 ++++++++------ src/app/api/admin/roles/route.ts | 17 ++++++---- src/app/api/admin/user-roles/route.ts | 45 ++++++++++++++++++-------- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3b6d7fe..c762120 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,10 @@ services: - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-oa-oidc} - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} - OIDC_REDIRECT_URI=${OIDC_REDIRECT_URI:-https://oa.tlyq.ai/api/auth/callback} + - ASSETS_INTERNAL_URL=http://assets-ai:3000 + - ISSUE_INTERNAL_URL=http://issue-ai:3000 + - MONITOR_INTERNAL_URL=http://monitor-ai:3000 + - MONITOR_DB_PATH=/data/other-sites/monitor/monitor.db volumes: - ./.next:/app/.next - /var/run/docker.sock:/var/run/docker.sock @@ -28,6 +32,7 @@ services: - /root/docker/ldap-ai/data/lldap:/data/other-sites/lldap - /var/lib/docker/volumes/assets-ai_assets-data/_data:/data/other-sites/assets - /var/lib/docker/volumes/issue-ai_issue-data/_data:/data/other-sites/issue + - /root/docker/monitor-ai/data:/data/other-sites/monitor networks: - webnet healthcheck: diff --git a/src/app/api/admin/create-user/route.ts b/src/app/api/admin/create-user/route.ts index 74661d6..6cf7a1f 100644 --- a/src/app/api/admin/create-user/route.ts +++ b/src/app/api/admin/create-user/route.ts @@ -55,17 +55,21 @@ export async function POST(request: Request) { return NextResponse.json({ error: '仅管理员可创建用户' }, { status: 403 }) } - const { username, displayName, assetsRole, issueRole, email } = await request.json() + const { username, displayName, assetsRole, issueRole, monitorRole, 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 ASSETS_URL = process.env.ASSETS_INTERNAL_URL || 'http://localhost:6177' + const ISSUE_URL = process.env.ISSUE_INTERNAL_URL || 'http://localhost:6176' + const MONITOR_URL = process.env.MONITOR_INTERNAL_URL || 'http://localhost:6181' + const [assetsRoles, issueRoles, monitorRoles] = await Promise.all([ + fetchRoles(ASSETS_URL), fetchRoles(ISSUE_URL), fetchRoles(MONITOR_URL), ]) const ar = (assetsRole && assetsRoles.includes(assetsRole)) ? assetsRole : 'viewer' const ir = (issueRole && issueRoles.includes(issueRole)) ? issueRole : 'viewer' + const mr = (monitorRole && monitorRoles.includes(monitorRole)) ? monitorRole : 'viewer' const safeName = esc(displayName || username) const safeUser = esc(username) @@ -81,17 +85,20 @@ export async function POST(request: Request) { lldapChangePassword(username, password) // 3. 自动登录各站点触发用户同步 - const [assetsOk, issueOk] = await Promise.all([ - syncToSite('http://localhost:6177', username, password), - syncToSite('http://localhost:6176', username, password), + const [assetsOk, issueOk, monitorOk] = await Promise.all([ + syncToSite(ASSETS_URL, username, password), + syncToSite(ISSUE_URL, username, password), + syncToSite(MONITOR_URL, username, password), ]) // 4. 直接更新各站点角色 const assetsDb = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db' const issueDb = process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db' - const roleResults = { assets: false, issue: false } + const monitorDb = process.env.MONITOR_DB_PATH || '/data/other-sites/monitor/monitor.db' + const roleResults = { assets: false, issue: false, monitor: false } if (assetsOk) try { execFileSync('sqlite3', [assetsDb], { input: `UPDATE users SET role = '${ar}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';`, timeout: 3000 }); roleResults.assets = true } catch {} if (issueOk) try { execFileSync('sqlite3', [issueDb], { input: `UPDATE users SET role = '${ir}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';`, timeout: 3000 }); roleResults.issue = true } catch {} + if (monitorOk) try { execFileSync('sqlite3', [monitorDb], { input: `UPDATE users SET role = '${mr}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';`, timeout: 3000 }); roleResults.monitor = true } catch {} // 5. 如果提供了邮箱,发送密码设置链接 let emailSent = false @@ -106,8 +113,8 @@ export async function POST(request: Request) { return NextResponse.json({ success: true, password: emailSent ? undefined : password, - synced: { assets: assetsOk, issue: issueOk }, - roles: { assets: ar, issue: ir, applied: roleResults }, + synced: { assets: assetsOk, issue: issueOk, monitor: monitorOk }, + roles: { assets: ar, issue: ir, monitor: mr, applied: roleResults }, emailSent, message: emailSent ? `用户已创建,密码设置链接已发送至 ${email}` : '用户已创建并同步至所有站点', }) diff --git a/src/app/api/admin/roles/route.ts b/src/app/api/admin/roles/route.ts index 6061f7b..418926d 100644 --- a/src/app/api/admin/roles/route.ts +++ b/src/app/api/admin/roles/route.ts @@ -5,14 +5,14 @@ import { isLldapAdmin } from '@/lib/ldap' const INTERNAL_KEY = 'oa-internal-key-tlyq-2026' -async function fetchRoles(url: string): Promise<{ name: string; display_name: string }[]> { +async function fetchRoles(siteUrl: string): Promise { try { - const res = await fetch(`${url}/api/internal/roles`, { + 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 || [] + return (data.roles || []).map((r: { name: string }) => r.name) } catch { return [] } } @@ -23,10 +23,13 @@ export async function GET() { 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://localhost:6177'), - fetchRoles('http://localhost:6176'), + const A_URL = process.env.ASSETS_INTERNAL_URL || 'http://localhost:6177' + const I_URL = process.env.ISSUE_INTERNAL_URL || 'http://localhost:6176' + const M_URL = process.env.MONITOR_INTERNAL_URL || 'http://localhost:6181' + + const [assetsRoles, issueRoles, monitorRoles] = await Promise.all([ + fetchRoles(A_URL), fetchRoles(I_URL), fetchRoles(M_URL), ]) - return NextResponse.json({ assets: assetsRoles, issue: issueRoles }) + return NextResponse.json({ assets: assetsRoles, issue: issueRoles, monitor: monitorRoles }) } diff --git a/src/app/api/admin/user-roles/route.ts b/src/app/api/admin/user-roles/route.ts index 8fcce08..4eed516 100644 --- a/src/app/api/admin/user-roles/route.ts +++ b/src/app/api/admin/user-roles/route.ts @@ -38,22 +38,39 @@ async function checkAdmin() { return session ? isLldapAdmin(session.username) : false } +const A_URL = process.env.ASSETS_INTERNAL_URL || 'http://localhost:6177' +const I_URL = process.env.ISSUE_INTERNAL_URL || 'http://localhost:6176' +const M_URL = process.env.MONITOR_INTERNAL_URL || 'http://localhost:6181' +const A_DB = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db' +const I_DB = process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db' +const M_DB = process.env.MONITOR_DB_PATH || '/data/other-sites/monitor/monitor.db' + +function siteDb(site: string): string { + if (site === 'assets') return A_DB + if (site === 'issue') return I_DB + return M_DB +} +function siteUrl(site: string): string { + if (site === 'assets') return A_URL + if (site === 'issue') return I_URL + return M_URL +} + // GET — 列出各站点用户及其角色 export async function GET() { if (!(await checkAdmin())) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) try { - const [assetsRoles, issueRoles] = await Promise.all([ - fetchRoles('http://localhost:6177'), - fetchRoles('http://localhost:6176'), + const [assetsRoles, issueRoles, monitorRoles] = await Promise.all([ + fetchRoles(A_URL), fetchRoles(I_URL), fetchRoles(M_URL), ]) - const [assetsUsers, issueUsers] = await Promise.all([ - getSiteUsers(process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db', assetsRoles), - getSiteUsers(process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db', issueRoles), + const [assetsUsers, issueUsers, monitorUsers] = await Promise.all([ + getSiteUsers(A_DB, assetsRoles), + getSiteUsers(I_DB, issueRoles), + getSiteUsers(M_DB, monitorRoles), ]) - // 直连 LLDAP SQLite 获取邮箱 let emails: Record = {} try { const out = queryLldap(`SELECT user_id, email FROM users`) @@ -63,7 +80,11 @@ export async function GET() { }) } catch {} - return NextResponse.json({ assetsRoles, issueRoles, users: { assets: assetsUsers, issue: issueUsers }, emails }) + return NextResponse.json({ + assetsRoles, issueRoles, monitorRoles, + users: { assets: assetsUsers, issue: issueUsers, monitor: monitorUsers }, + emails, + }) } catch { return NextResponse.json({ error: '查询失败' }, { status: 500 }) } @@ -78,14 +99,10 @@ export async function PUT(request: Request) { 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 || '/data/other-sites/assets/assets.db') - : (process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db') - - const roles = await fetchRoles(`http://localhost:${site === 'assets' ? 6177 : 6176}`) + const roles = await fetchRoles(siteUrl(site)) if (!roles.includes(role)) return NextResponse.json({ error: '无效的角色' }, { status: 400 }) - execFileSync('sqlite3', [dbPath], { + execFileSync('sqlite3', [siteDb(site)], { input: `UPDATE users SET role='${role}', updated_at=datetime('now', '+8 hours') WHERE username='${username}';`, timeout: 3000, })