oa-ai/src/app/api/admin/user-roles/route.ts""

105 lines
4.1 KiB
Plaintext

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