feat: add /api/internal/roles endpoint for OA integration + middleware publicPaths

This commit is contained in:
aiyimickey 2026-07-07 18:33:35 +08:00
parent 8886e12b15
commit b851fbbda8
4 changed files with 62 additions and 30 deletions

View File

@ -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:

View File

@ -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}` : '用户已创建并同步至所有站点',
})

View File

@ -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<string[]> {
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 })
}

View File

@ -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<string, string> = {}
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,
})