133 lines
5.4 KiB
TypeScript
133 lines
5.4 KiB
TypeScript
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 })
|
||
}
|
||
}
|