Spaces:
Runtime error
Runtime error
| import { randomBytes } from 'crypto' | |
| import { NextRequest, NextResponse } from 'next/server' | |
| import { createUser, getUserFromRequest , requireRole } from '@/lib/auth' | |
| import { getDatabase, logAuditEvent } from '@/lib/db' | |
| import { validateBody, accessRequestActionSchema } from '@/lib/validation' | |
| import { mutationLimiter } from '@/lib/rate-limit' | |
| 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 rateCheck = mutationLimiter(request) | |
| if (rateCheck) return rateCheck | |
| const result = await validateBody(request, accessRequestActionSchema) | |
| if ('error' in result) return result.error | |
| const db = getDatabase() | |
| const { request_id: requestId, action, role, note } = result.data | |
| 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 }) | |
| } | |