import { NextResponse } from 'next/server' import { authenticateUser, seedAdminIfEmpty } from '@/lib/users' import { createSession } from '@/lib/session' // ── In-memory rate limiter (single-instance, resets on restart) ────────────── interface RateLimitEntry { attempts: number lockedUntil: number } const rateLimitMap = new Map() const MAX_ATTEMPTS = 5 const LOCKOUT_MS = 15 * 60 * 1000 // 15 minutes function getClientIp(req: Request): string { return ( req.headers.get('x-forwarded-for')?.split(',')[0].trim() ?? req.headers.get('x-real-ip') ?? 'unknown' ) } function checkRateLimit(ip: string): { allowed: boolean; retryAfterSecs?: number } { const now = Date.now() const entry = rateLimitMap.get(ip) if (!entry) return { allowed: true } if (entry.lockedUntil > now) { return { allowed: false, retryAfterSecs: Math.ceil((entry.lockedUntil - now) / 1000) } } // Lock has expired — reset rateLimitMap.delete(ip) return { allowed: true } } function recordFailedAttempt(ip: string): void { const now = Date.now() const entry = rateLimitMap.get(ip) ?? { attempts: 0, lockedUntil: 0 } entry.attempts++ if (entry.attempts >= MAX_ATTEMPTS) { entry.lockedUntil = now + LOCKOUT_MS entry.attempts = 0 } rateLimitMap.set(ip, entry) } function clearAttempts(ip: string): void { rateLimitMap.delete(ip) } // ── Handler ─────────────────────────────────────────────────────────────────── export async function POST(request: Request) { const ip = getClientIp(request) const { allowed, retryAfterSecs } = checkRateLimit(ip) if (!allowed) { return NextResponse.json( { error: `Too many attempts. Try again in ${retryAfterSecs} seconds.` }, { status: 429, headers: { 'Retry-After': String(retryAfterSecs) }, }, ) } let body: { username?: string; password?: string } try { body = (await request.json()) as { username?: string; password?: string } } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) } const username = String(body.username ?? '').trim() const password = String(body.password ?? '') if (!username || !password) { return NextResponse.json({ error: 'Username and password are required' }, { status: 400 }) } // Seed admin on first login attempt if no users exist yet await seedAdminIfEmpty() const user = await authenticateUser(username, password) if (!user) { recordFailedAttempt(ip) // Return generic error — don't reveal whether the username exists return NextResponse.json({ error: 'Invalid username or password' }, { status: 401 }) } clearAttempts(ip) await createSession({ userId: user.id, username: user.username, role: user.role }) return NextResponse.json({ ok: true, username: user.username, role: user.role }) }