feat: integrate Sensei Spook mascot in 4 stances, speech bubbles, and style updates
This commit is contained in:
commit
e04672db2a
104 files changed
+8262
No files matched your search
@@ -0,0 +1,95 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isAuthenticated } from "@/lib/admin-auth";
|
||||
import { getGenerationStats } from "@/lib/storage";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized. Please login." },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const periodParam = searchParams.get("period") as any;
|
||||
const rawFromDate = searchParams.get("from") || undefined;
|
||||
const rawToDate = searchParams.get("to") || undefined;
|
||||
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const fromDate = rawFromDate && dateRegex.test(rawFromDate) ? rawFromDate : undefined;
|
||||
const toDate = rawToDate && dateRegex.test(rawToDate) ? rawToDate : undefined;
|
||||
|
||||
const validPeriods = ["day", "week", "month", "all", "custom"];
|
||||
const period = validPeriods.includes(periodParam) ? periodParam : "week";
|
||||
|
||||
const stats = getGenerationStats({
|
||||
period,
|
||||
fromDate,
|
||||
toDate,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
stats,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[Portal Stats API] Error:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Failed to retrieve statistics" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user