feat: add /api/internal/roles endpoint for OA integration + middleware publicPaths
This commit is contained in:
parent
8886e12b15
commit
b851fbbda8
|
|
@ -21,6 +21,10 @@ services:
|
||||||
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-oa-oidc}
|
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-oa-oidc}
|
||||||
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
|
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
|
||||||
- OIDC_REDIRECT_URI=${OIDC_REDIRECT_URI:-https://oa.tlyq.ai/api/auth/callback}
|
- 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:
|
volumes:
|
||||||
- ./.next:/app/.next
|
- ./.next:/app/.next
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
@ -28,6 +32,7 @@ services:
|
||||||
- /root/docker/ldap-ai/data/lldap:/data/other-sites/lldap
|
- /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/assets-ai_assets-data/_data:/data/other-sites/assets
|
||||||
- /var/lib/docker/volumes/issue-ai_issue-data/_data:/data/other-sites/issue
|
- /var/lib/docker/volumes/issue-ai_issue-data/_data:/data/other-sites/issue
|
||||||
|
- /root/docker/monitor-ai/data:/data/other-sites/monitor
|
||||||
networks:
|
networks:
|
||||||
- webnet
|
- webnet
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,21 @@ export async function POST(request: Request) {
|
||||||
return NextResponse.json({ error: '仅管理员可创建用户' }, { status: 403 })
|
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 (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 })
|
||||||
if (!/^[a-z][a-z0-9_.@-]*$/i.test(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 password = generatePassword()
|
||||||
const [assetsRoles, issueRoles] = await Promise.all([
|
const ASSETS_URL = process.env.ASSETS_INTERNAL_URL || 'http://localhost:6177'
|
||||||
fetchRoles('http://localhost:6177'), fetchRoles('http://localhost:6176'),
|
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 ar = (assetsRole && assetsRoles.includes(assetsRole)) ? assetsRole : 'viewer'
|
||||||
const ir = (issueRole && issueRoles.includes(issueRole)) ? issueRole : 'viewer'
|
const ir = (issueRole && issueRoles.includes(issueRole)) ? issueRole : 'viewer'
|
||||||
|
const mr = (monitorRole && monitorRoles.includes(monitorRole)) ? monitorRole : 'viewer'
|
||||||
|
|
||||||
const safeName = esc(displayName || username)
|
const safeName = esc(displayName || username)
|
||||||
const safeUser = esc(username)
|
const safeUser = esc(username)
|
||||||
|
|
@ -81,17 +85,20 @@ export async function POST(request: Request) {
|
||||||
lldapChangePassword(username, password)
|
lldapChangePassword(username, password)
|
||||||
|
|
||||||
// 3. 自动登录各站点触发用户同步
|
// 3. 自动登录各站点触发用户同步
|
||||||
const [assetsOk, issueOk] = await Promise.all([
|
const [assetsOk, issueOk, monitorOk] = await Promise.all([
|
||||||
syncToSite('http://localhost:6177', username, password),
|
syncToSite(ASSETS_URL, username, password),
|
||||||
syncToSite('http://localhost:6176', username, password),
|
syncToSite(ISSUE_URL, username, password),
|
||||||
|
syncToSite(MONITOR_URL, username, password),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 4. 直接更新各站点角色
|
// 4. 直接更新各站点角色
|
||||||
const assetsDb = process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db'
|
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 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 (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 (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. 如果提供了邮箱,发送密码设置链接
|
// 5. 如果提供了邮箱,发送密码设置链接
|
||||||
let emailSent = false
|
let emailSent = false
|
||||||
|
|
@ -106,8 +113,8 @@ export async function POST(request: Request) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
password: emailSent ? undefined : password,
|
password: emailSent ? undefined : password,
|
||||||
synced: { assets: assetsOk, issue: issueOk },
|
synced: { assets: assetsOk, issue: issueOk, monitor: monitorOk },
|
||||||
roles: { assets: ar, issue: ir, applied: roleResults },
|
roles: { assets: ar, issue: ir, monitor: mr, applied: roleResults },
|
||||||
emailSent,
|
emailSent,
|
||||||
message: emailSent ? `用户已创建,密码设置链接已发送至 ${email}` : '用户已创建并同步至所有站点',
|
message: emailSent ? `用户已创建,密码设置链接已发送至 ${email}` : '用户已创建并同步至所有站点',
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ import { isLldapAdmin } from '@/lib/ldap'
|
||||||
|
|
||||||
const INTERNAL_KEY = 'oa-internal-key-tlyq-2026'
|
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 {
|
try {
|
||||||
const res = await fetch(`${url}/api/internal/roles`, {
|
const res = await fetch(`${siteUrl}/api/internal/roles`, {
|
||||||
headers: { 'x-internal-key': INTERNAL_KEY },
|
headers: { 'x-internal-key': INTERNAL_KEY },
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.roles || []
|
return (data.roles || []).map((r: { name: string }) => r.name)
|
||||||
} catch { return [] }
|
} catch { return [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,10 +23,13 @@ export async function GET() {
|
||||||
const session = verifySharedJwt(token)
|
const session = verifySharedJwt(token)
|
||||||
if (!session || !(await isLldapAdmin(session.username))) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
if (!session || !(await isLldapAdmin(session.username))) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
|
||||||
const [assetsRoles, issueRoles] = await Promise.all([
|
const A_URL = process.env.ASSETS_INTERNAL_URL || 'http://localhost:6177'
|
||||||
fetchRoles('http://localhost:6177'),
|
const I_URL = process.env.ISSUE_INTERNAL_URL || 'http://localhost:6176'
|
||||||
fetchRoles('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 })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,22 +38,39 @@ async function checkAdmin() {
|
||||||
return session ? isLldapAdmin(session.username) : false
|
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 — 列出各站点用户及其角色
|
// GET — 列出各站点用户及其角色
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
if (!(await checkAdmin())) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
if (!(await checkAdmin())) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [assetsRoles, issueRoles] = await Promise.all([
|
const [assetsRoles, issueRoles, monitorRoles] = await Promise.all([
|
||||||
fetchRoles('http://localhost:6177'),
|
fetchRoles(A_URL), fetchRoles(I_URL), fetchRoles(M_URL),
|
||||||
fetchRoles('http://localhost:6176'),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const [assetsUsers, issueUsers] = await Promise.all([
|
const [assetsUsers, issueUsers, monitorUsers] = await Promise.all([
|
||||||
getSiteUsers(process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db', assetsRoles),
|
getSiteUsers(A_DB, assetsRoles),
|
||||||
getSiteUsers(process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db', issueRoles),
|
getSiteUsers(I_DB, issueRoles),
|
||||||
|
getSiteUsers(M_DB, monitorRoles),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 直连 LLDAP SQLite 获取邮箱
|
|
||||||
let emails: Record<string, string> = {}
|
let emails: Record<string, string> = {}
|
||||||
try {
|
try {
|
||||||
const out = queryLldap(`SELECT user_id, email FROM users`)
|
const out = queryLldap(`SELECT user_id, email FROM users`)
|
||||||
|
|
@ -63,7 +80,11 @@ export async function GET() {
|
||||||
})
|
})
|
||||||
} catch {}
|
} 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 {
|
} catch {
|
||||||
return NextResponse.json({ error: '查询失败' }, { status: 500 })
|
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 || !site || !role) return NextResponse.json({ error: '参数不完整' }, { status: 400 })
|
||||||
if (username === 'admin' || username === 'localadmin') return NextResponse.json({ error: '不能修改系统保留用户角色' }, { status: 400 })
|
if (username === 'admin' || username === 'localadmin') return NextResponse.json({ error: '不能修改系统保留用户角色' }, { status: 400 })
|
||||||
|
|
||||||
const dbPath = site === 'assets'
|
const roles = await fetchRoles(siteUrl(site))
|
||||||
? (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}`)
|
|
||||||
if (!roles.includes(role)) return NextResponse.json({ error: '无效的角色' }, { status: 400 })
|
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}';`,
|
input: `UPDATE users SET role='${role}', updated_at=datetime('now', '+8 hours') WHERE username='${username}';`,
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue