mission-control/src/app/api/auth/access-requests/route.ts

151 lines
5.6 KiB
TypeScript

import { randomBytes } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
import { createUser, getUserFromRequest , requireRole } from '@/lib/auth'
import { getDatabase, logAuditEvent } from '@/lib/db'
function makeUsernameFromEmail(email: string): string {
const base = email.split('@')[0].replace(/[^a-z0-9._-]/gi, '').toLowerCase() || 'user'
return base.slice(0, 28)
}
function ensureUniqueUsername(base: string): string {
const db = getDatabase()
let candidate = base
let i = 0
while (db.prepare('SELECT 1 FROM users WHERE username = ?').get(candidate)) {
i += 1
candidate = `${base.slice(0, 24)}-${i}`
}
return candidate
}
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const user = getUserFromRequest(request)
if (!user || user.role !== 'admin') {
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
}
const db = getDatabase()
db.exec(`
CREATE TABLE IF NOT EXISTS access_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider TEXT NOT NULL DEFAULT 'google',
email TEXT NOT NULL,
provider_user_id TEXT,
display_name TEXT,
avatar_url TEXT,
status TEXT NOT NULL DEFAULT 'pending',
requested_at INTEGER NOT NULL DEFAULT (unixepoch()),
last_attempt_at INTEGER NOT NULL DEFAULT (unixepoch()),
attempt_count INTEGER NOT NULL DEFAULT 1,
reviewed_by TEXT,
reviewed_at INTEGER,
review_note TEXT,
approved_user_id INTEGER
)
`)
const status = String(request.nextUrl.searchParams.get('status') || 'all')
const rows = status === 'all'
? db.prepare("SELECT * FROM access_requests ORDER BY status = 'pending' DESC, last_attempt_at DESC, id DESC").all()
: db.prepare('SELECT * FROM access_requests WHERE status = ? ORDER BY last_attempt_at DESC, id DESC').all(status)
return NextResponse.json({ requests: rows })
}
export async function POST(request: NextRequest) {
const admin = getUserFromRequest(request)
if (!admin || admin.role !== 'admin') {
return NextResponse.json({ error: 'Admin access required' }, { status: 403 })
}
const db = getDatabase()
const body = await request.json().catch(() => ({}))
const requestId = Number(body?.request_id)
const action = String(body?.action || '')
const role = String(body?.role || 'viewer') as 'admin' | 'operator' | 'viewer'
const note = body?.note ? String(body.note) : null
if (!Number.isInteger(requestId) || requestId <= 0) {
return NextResponse.json({ error: 'request_id is required' }, { status: 400 })
}
if (!['approve', 'reject'].includes(action)) {
return NextResponse.json({ error: 'action must be approve or reject' }, { status: 400 })
}
if (!['admin', 'operator', 'viewer'].includes(role)) {
return NextResponse.json({ error: 'Invalid role' }, { status: 400 })
}
const reqRow = db.prepare('SELECT * FROM access_requests WHERE id = ?').get(requestId) as any
if (!reqRow) return NextResponse.json({ error: 'Request not found' }, { status: 404 })
if (action === 'reject') {
db.prepare(`
UPDATE access_requests
SET status = 'rejected', reviewed_by = ?, reviewed_at = (unixepoch()), review_note = ?
WHERE id = ?
`).run(admin.username, note, requestId)
logAuditEvent({
action: 'access_request_rejected',
actor: admin.username,
actor_id: admin.id,
detail: { request_id: requestId, email: reqRow.email, note },
})
return NextResponse.json({ ok: true })
}
const email = String(reqRow.email || '').toLowerCase()
const providerUserId = reqRow.provider_user_id ? String(reqRow.provider_user_id) : null
const displayName = String(reqRow.display_name || email.split('@')[0] || 'Google User')
const avatarUrl = reqRow.avatar_url ? String(reqRow.avatar_url) : null
const user = db.transaction(() => {
const existing = db.prepare('SELECT * FROM users WHERE lower(email) = ? OR (provider = ? AND provider_user_id = ?) ORDER BY id ASC LIMIT 1').get(email, 'google', providerUserId || '') as any
let userId: number
if (existing) {
db.prepare(`
UPDATE users
SET provider = 'google', provider_user_id = ?, email = ?, avatar_url = COALESCE(?, avatar_url), is_approved = 1, role = ?, approved_by = ?, approved_at = (unixepoch()), updated_at = (unixepoch())
WHERE id = ?
`).run(providerUserId, email, avatarUrl, role, admin.username, existing.id)
userId = Number(existing.id)
} else {
const username = ensureUniqueUsername(makeUsernameFromEmail(email))
const randomPwd = randomBytes(24).toString('hex')
const created = createUser(username, randomPwd, displayName, role, {
provider: 'google',
provider_user_id: providerUserId,
email,
avatar_url: avatarUrl,
is_approved: 1,
approved_by: admin.username,
approved_at: Math.floor(Date.now() / 1000),
})
userId = created.id
}
db.prepare(`
UPDATE access_requests
SET status = 'approved', reviewed_by = ?, reviewed_at = (unixepoch()), review_note = ?, approved_user_id = ?
WHERE id = ?
`).run(admin.username, note, userId, requestId)
return db.prepare('SELECT id, username, display_name, role, provider, email, avatar_url, is_approved FROM users WHERE id = ?').get(userId)
})() as any
logAuditEvent({
action: 'access_request_approved',
actor: admin.username,
actor_id: admin.id,
detail: { request_id: requestId, email, role, user_id: user?.id, note },
})
return NextResponse.json({ ok: true, user })
}