118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import bcrypt from "bcryptjs";
|
|
import { createServiceClient } from "@/lib/supabase";
|
|
import { createSessionToken, verifySessionToken } from "@/lib/admin-auth";
|
|
import { logLoginAttempt, getFailedLoginCount, sendSecurityAlert } from "@/lib/audit-log";
|
|
import { checkRateLimit, resetRateLimit } from "@/lib/rate-limit";
|
|
import { revokeSessionToken } from "@/lib/token-blacklist";
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const { email, password } = await req.json();
|
|
const ipAddr = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "unknown";
|
|
const userAgent = req.headers.get("user-agent") || "unknown";
|
|
|
|
// ──── Validierung ────
|
|
if (!email || !password) {
|
|
await logLoginAttempt("", ipAddr, false, userAgent, "missing_credentials");
|
|
return NextResponse.json({ error: "E-Mail und Passwort erforderlich" }, { status: 400 });
|
|
}
|
|
|
|
// ──── Rate-Limiting ────
|
|
const rateLimitKey = `login:${email.toLowerCase()}:${ipAddr}`;
|
|
const { allowed, delayMs, locked } = checkRateLimit(rateLimitKey);
|
|
|
|
if (locked) {
|
|
await logLoginAttempt(email, ipAddr, false, userAgent, "rate_limit_locked");
|
|
const res = NextResponse.json(
|
|
{ error: "Zu viele Anmeldeversuche. Bitte später versuchen." },
|
|
{ status: 429 }
|
|
);
|
|
res.headers.set("Retry-After", String(Math.ceil(delayMs / 1000)));
|
|
return res;
|
|
}
|
|
|
|
if (!allowed && delayMs > 0) {
|
|
// Künstliche Verzögerung um Timing-Attacken zu verhindern
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
|
|
// ──── Datenbankabfrage ────
|
|
const db = createServiceClient();
|
|
const { data: admin } = await db
|
|
.from("admin_users")
|
|
.select("id, email, name, password_hash, aktiv")
|
|
.eq("email", email.toLowerCase().trim())
|
|
.single();
|
|
|
|
// ──── Admin nicht gefunden oder inaktiv ────
|
|
if (!admin || !admin.aktiv) {
|
|
await logLoginAttempt(email, ipAddr, false, userAgent, "user_not_found_or_inactive");
|
|
|
|
// Prüfe auf verdächtige Aktivität
|
|
const failedCount = await getFailedLoginCount(email, "email", 60);
|
|
if (failedCount >= 10) {
|
|
await sendSecurityAlert(
|
|
"⚠️ Viele fehlgeschlagene Login-Versuche",
|
|
`Email: ${email}\nIPs: Siehe Audit-Logs\nVersuche in der letzten Stunde: ${failedCount}`
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({ error: "Ungültige Zugangsdaten" }, { status: 401 });
|
|
}
|
|
|
|
// ──── Passwort validieren ────
|
|
const valid = await bcrypt.compare(password, admin.password_hash);
|
|
if (!valid) {
|
|
await logLoginAttempt(email, ipAddr, false, userAgent, "invalid_password");
|
|
|
|
// Prüfe auf Brute-Force-Aktivität
|
|
const failedCount = await getFailedLoginCount(email, "email", 60);
|
|
if (failedCount >= 10) {
|
|
await sendSecurityAlert(
|
|
"⚠️ Viele fehlgeschlagene Login-Versuche (falsches Passwort)",
|
|
`Email: ${email}\nIPs: ${ipAddr}\nVersuche in der letzten Stunde: ${failedCount}`
|
|
);
|
|
}
|
|
|
|
return NextResponse.json({ error: "Ungültige Zugangsdaten" }, { status: 401 });
|
|
}
|
|
|
|
// ──── Login erfolgreich ────
|
|
await logLoginAttempt(email, ipAddr, true, userAgent);
|
|
resetRateLimit(rateLimitKey); // Rate-Limit zurücksetzen
|
|
|
|
const token = await createSessionToken({ id: admin.id, email: admin.email, name: admin.name });
|
|
|
|
const res = NextResponse.json({ success: true });
|
|
res.cookies.set("admin_session", token, {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
maxAge: 60 * 60 * 2, // ✅ Geändert: 2 Stunden statt 7 Tage
|
|
path: "/",
|
|
secure: process.env.NODE_ENV === "production",
|
|
});
|
|
return res;
|
|
}
|
|
|
|
export async function DELETE(req: NextRequest) {
|
|
const token = req.cookies.get("admin_session")?.value;
|
|
|
|
if (token) {
|
|
try {
|
|
// ✅ Verify token to get admin_id, then add signature to blacklist
|
|
const session = await verifySessionToken(token);
|
|
if (session) {
|
|
const [, sig] = token.split(".");
|
|
await revokeSessionToken(sig, session.id, "logout");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error revoking session token:", error);
|
|
// Continue with logout even if revocation fails
|
|
}
|
|
}
|
|
|
|
const res = NextResponse.json({ success: true });
|
|
res.cookies.delete("admin_session");
|
|
return res;
|
|
}
|