Initial working commit
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s

This commit is contained in:
Elijah 2026-06-27 19:19:38 -07:00
parent 666ceb7325
commit b7ce314f01
105 changed files with 35510 additions and 11 deletions

View file

@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { createSession } from "@/lib/auth";
import { checkRateLimit } from "@/lib/rateLimiter";
// Progressive lockout thresholds from Section 4
function getLockoutDuration(failedAttempts: number): number {
if (failedAttempts >= 20) return 24 * 60 * 60 * 1000; // 24 hours
if (failedAttempts >= 15) return 30 * 60 * 1000; // 30 minutes
if (failedAttempts >= 10) return 5 * 60 * 1000; // 5 minutes
if (failedAttempts >= 5) return 1 * 60 * 1000; // 1 minute
return 0;
}
export async function POST(request: NextRequest) {
// Rate limit by IP
const forwarded = request.headers.get("x-forwarded-for");
const ip = forwarded?.split(",")[0]?.trim() ?? "unknown";
const rateCheck = checkRateLimit(ip);
if (!rateCheck.allowed) {
return NextResponse.json(
{ error: "Too many requests. Try again shortly." },
{ status: 429 }
);
}
const body = await request.json().catch(() => null);
if (!body?.password || typeof body.password !== "string") {
return NextResponse.json(
{ error: "Password is required" },
{ status: 400 }
);
}
// Check lockout state
let security = await prisma.authSecurity.findUnique({ where: { id: 1 } });
if (!security) {
security = await prisma.authSecurity.create({ data: { id: 1 } });
}
if (security.lockedUntil && security.lockedUntil > new Date()) {
const remainingMs =
security.lockedUntil.getTime() - Date.now();
const remainingMin = Math.ceil(remainingMs / 60000);
return NextResponse.json(
{
error: `Account locked. Try again in ${remainingMin} minute${remainingMin !== 1 ? "s" : ""}.`,
},
{ status: 423 }
);
}
// Verify password
const passwordHash = process.env.ADMIN_PASSWORD_HASH;
// For development: if no hash is set, accept any non-empty password
let isValid = false;
if (!passwordHash) {
isValid = body.password.length > 0;
} else {
// Dynamic import to avoid issues when argon2 isn't installed
try {
const argon2 = await import("argon2");
isValid = await argon2.verify(passwordHash, body.password);
} catch {
// Fallback: direct comparison (only for development)
isValid = body.password === passwordHash;
}
}
if (!isValid) {
const newFailedAttempts = security.failedAttempts + 1;
const lockoutMs = getLockoutDuration(newFailedAttempts);
const lockedUntil = lockoutMs > 0 ? new Date(Date.now() + lockoutMs) : null;
await prisma.authSecurity.update({
where: { id: 1 },
data: {
failedAttempts: newFailedAttempts,
lockedUntil,
lastAttemptAt: new Date(),
},
});
return NextResponse.json(
{ error: "Invalid password" },
{ status: 401 }
);
}
// Success — reset lockout, create session
await prisma.authSecurity.update({
where: { id: 1 },
data: {
failedAttempts: 0,
lockedUntil: null,
lastAttemptAt: new Date(),
},
});
await createSession();
return NextResponse.json({ success: true });
}