← back to Hub
app/api/auth/login/route.ts
71 lines
// Login route — admin + guest-tier logins via the shared capability registry.
//
// Credentials + capabilities live in lib/accounts (createCapabilityRegistry):
// the admin account plus any guest tier (guest/viewer/demo) whose password env
// is set. registry.verify is a constant-time scrypt multi-credential check.
// Rate-limits POSTs per IP: 10 fails / 15 min → 30 min lockout (429 + Retry-After).
//
// Capability enforcement (read-only / no-AI guests) is authoritative in
// middleware.ts via capabilityGate.
import { NextRequest, NextResponse } from 'next/server';
import { createSession, buildAuthCookie } from '@/lib/auth';
import { registry } from '@/lib/accounts';
import {
checkLoginAttempt,
recordLoginFailure,
clearLoginCounter,
loginClientIp,
} from '@/lib/rate-limit';
// ── POST /api/auth/login ──────────────────────────────────────────────────
function invalidCredentials(ip: string): NextResponse {
recordLoginFailure(ip);
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
export async function POST(request: NextRequest) {
const ip = loginClientIp(request);
// 1) Rate-limit gate
const gate = checkLoginAttempt(ip);
if (gate.locked) {
return NextResponse.json(
{ error: 'too many attempts', retryAfter: gate.retryAfter },
{ status: 429, headers: { 'Retry-After': String(gate.retryAfter) } },
);
}
// 2) Body validation
let body: { username?: string; password?: string };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}
const { username, password } = body;
if (!username || !password) {
return NextResponse.json(
{ error: 'Username and password are required' },
{ status: 400 },
);
}
// 3) Constant-time multi-credential check (admin + active guest tiers)
const matchedUsername = registry.verify(username, password);
if (!matchedUsername) return invalidCredentials(ip);
// 4) Success — clear counter, mint session for the matched account
clearLoginCounter(ip);
try {
const token = createSession(matchedUsername);
const cookieValue = buildAuthCookie(token);
const response = NextResponse.json({ success: true });
response.headers.set('Set-Cookie', cookieValue);
return response;
} catch (err) {
console.error('[auth/login] Error issuing session:', (err as Error).message);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}