oa-ai/src/app/api/admin/users/route.ts

133 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, boolean> = {}
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, displayName } = await request.json()
if (!username) return NextResponse.json({ error: '用户名不能为空' }, { status: 400 })
if (email === undefined && displayName === undefined) {
return NextResponse.json({ error: '至少需要 email 或 displayName' }, { status: 400 })
}
if (email !== undefined && email !== '' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ error: '邮箱格式不合法' }, { status: 400 })
}
const safeUser = username.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')}`
// 更新 LLDAP
let lldapSets: string[] = []
let siteSets: string[] = []
if (email !== undefined) {
const safeEmail = (email || '').replace(/'/g, "''")
lldapSets.push(`email = '${safeEmail}'`, `lowercase_email = LOWER('${safeEmail}')`)
siteSets.push(`email = '${safeEmail}'`)
}
if (displayName !== undefined) {
const safeName = displayName.replace(/'/g, "''")
lldapSets.push(`display_name = '${safeName}'`)
siteSets.push(`display_name = '${safeName}'`)
}
lldapSets.push(`modified_date = '${now}'`)
siteSets.push(`updated_at = datetime('now', '+8 hours')`)
const lldapSQL = `UPDATE users SET ${lldapSets.join(', ')} WHERE user_id = '${safeUser}';`
await execAsync(
`docker exec lldap /bin/sh -c "cat > /tmp/up.sql <<'EOSQL'\n${lldapSQL}\nEOSQL\nsqlite3 /data/users.db < /tmp/up.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'
const siteSQL = `UPDATE users SET ${siteSets.join(', ')} WHERE username = '${safeUser}';`
for (const dbPath of [assetsDb, issueDb]) {
try { await execAsync(`sqlite3 "${dbPath}" "${siteSQL}"`, { timeout: 3000 }) } catch {}
}
return NextResponse.json({ success: true, username, email, displayName })
} catch (e) {
return NextResponse.json({ error: '修改失败' }, { status: 500 })
}
}