import { NextRequest, NextResponse } from 'next/server'; import { createHmac, timingSafeEqual } from 'crypto'; const SECRET = process.env.SESSION_SECRET ?? 'dev-secret-change-in-production'; const LOGIN_USER = process.env.LOGIN_USER ?? 'admin'; const LOGIN_PASS = process.env.LOGIN_PASS ?? 'admin'; // in-memory rate limiter: max 10 attempts per IP per 15 min const attempts = new Map(); function checkRate(ip: string): boolean { const now = Date.now(); const e = attempts.get(ip); if (!e || now > e.resetAt) { attempts.set(ip, { n: 1, resetAt: now + 15 * 60_000 }); return true; } if (e.n >= 10) return false; e.n++; return true; } function safeEq(a: string, b: string): boolean { try { const ab = Buffer.from(a), bb = Buffer.from(b); if (ab.length !== bb.length) { // still run comparison on equal-length buffers to avoid timing leak timingSafeEqual(ab, ab); return false; } return timingSafeEqual(ab, bb); } catch { return false; } } function sign(value: string) { return createHmac('sha256', SECRET).update(value).digest('hex'); } export async function POST(req: NextRequest) { const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? req.headers.get('x-real-ip') ?? 'unknown'; if (!checkRate(ip)) { return NextResponse.json({ error: 'Too many attempts. Try again later.' }, { status: 429 }); } const body = await req.json().catch(() => null); if (!body || typeof body !== 'object') { return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); } const { username, password } = body as Record; if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) { return NextResponse.json({ error: 'Username and password required' }, { status: 400 }); } const validUser = safeEq(username, LOGIN_USER); const validPass = safeEq(password, LOGIN_PASS); if (!validUser || !validPass) { return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); } const payload = `${username}:${Date.now()}`; const token = `${payload}.${sign(payload)}`; const res = NextResponse.json({ ok: true }); res.cookies.set('session', token, { httpOnly: true, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7, secure: process.env.NODE_ENV === 'production', }); return res; }