95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|||
|
|
import { getExpectedToken, isAuthenticated } from "@/lib/admin-auth";
|
||
|
|
import {
|
||
|
|
getClientIp,
|
||
|
|
constantTimeCompare,
|
||
|
|
checkAuthRateLimit,
|
||
|
|
recordAuthFailure,
|
||
|
|
recordAuthSuccess,
|
||
|
|
} from "@/lib/security";
|
||
|
|
|
||
|
|
export async function POST(req: NextRequest) {
|
||
|
|
const ip = getClientIp(req);
|
||
|
|
|
||
|
|
// 1. Check brute-force lockout
|
||
|
|
const authLimit = checkAuthRateLimit(ip, 5, 15);
|
||
|
|
if (!authLimit.allowed) {
|
||
|
|
const waitMins = Math.ceil(authLimit.lockoutSeconds / 60);
|
||
|
|
console.warn(`[Portal Auth Security] IP ${ip} blocked due to too many failed login attempts.`);
|
||
|
|
return NextResponse.json(
|
||
|
|
{
|
||
|
|
success: false,
|
||
|
|
error: `Too many failed login attempts. IP temporarily blocked. Try again in ${waitMins} minute${waitMins > 1 ? "s" : ""}.`,
|
||
|
|
},
|
||
|
|
{ status: 429 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const body = await req.json().catch(() => ({}));
|
||
|
|
const password = body.password;
|
||
|
|
const expectedPassword = process.env.ADMIN_PASSWORD || "admin123";
|
||
|
|
|
||
|
|
// 2. Validate password using timing-safe comparison
|
||
|
|
if (!password || typeof password !== "string" || !constantTimeCompare(password, expectedPassword)) {
|
||
|
|
recordAuthFailure(ip, 5, 15);
|
||
|
|
const remaining = authLimit.remainingAttempts - 1;
|
||
|
|
return NextResponse.json(
|
||
|
|
{
|
||
|
|
success: false,
|
||
|
|
error: "Incorrect password",
|
||
|
|
remainingAttempts: Math.max(0, remaining),
|
||
|
|
},
|
||
|
|
{ status: 401 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Clear failed attempts on successful login
|
||
|
|
recordAuthSuccess(ip);
|
||
|
|
|
||
|
|
const token = getExpectedToken();
|
||
|
|
const response = NextResponse.json({
|
||
|
|
success: true,
|
||
|
|
token,
|
||
|
|
message: "Authentication successful",
|
||
|
|
});
|
||
|
|
|
||
|
|
response.cookies.set({
|
||
|
|
name: "booth_portal_token",
|
||
|
|
value: token,
|
||
|
|
httpOnly: true,
|
||
|
|
path: "/",
|
||
|
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||
|
|
sameSite: "lax",
|
||
|
|
});
|
||
|
|
|
||
|
|
return response;
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("[Portal Auth] Login error:", error);
|
||
|
|
return NextResponse.json(
|
||
|
|
{ success: false, error: "Internal authentication error" },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function GET(req: NextRequest) {
|
||
|
|
const authed = isAuthenticated(req);
|
||
|
|
if (authed) {
|
||
|
|
return NextResponse.json({ authenticated: true });
|
||
|
|
}
|
||
|
|
return NextResponse.json({ authenticated: false }, { status: 401 });
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function DELETE() {
|
||
|
|
const response = NextResponse.json({ success: true, message: "Logged out" });
|
||
|
|
response.cookies.set({
|
||
|
|
name: "booth_portal_token",
|
||
|
|
value: "",
|
||
|
|
httpOnly: true,
|
||
|
|
path: "/",
|
||
|
|
maxAge: 0,
|
||
|
|
sameSite: "lax",
|
||
|
|
});
|
||
|
|
return response;
|
||
|
|
}
|