All checks were successful
Automated Container Build / build-and-push (push) Successful in 54s
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
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 setting = await prisma.setting.findUnique({
|
|
where: { key: "admin_password_hash" },
|
|
});
|
|
|
|
let isValid = false;
|
|
|
|
if (!setting) {
|
|
// Initial setup: hash and save the new password
|
|
const argon2 = await import("argon2");
|
|
const newHash = await argon2.hash(body.password);
|
|
await prisma.setting.create({
|
|
data: {
|
|
key: "admin_password_hash",
|
|
value: newHash,
|
|
},
|
|
});
|
|
isValid = true;
|
|
} else {
|
|
// Verify existing password
|
|
const passwordHash = setting.value;
|
|
try {
|
|
const argon2 = await import("argon2");
|
|
isValid = await argon2.verify(passwordHash, body.password);
|
|
} catch {
|
|
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 });
|
|
}
|