MBO-Tech-IT-Webseite/modules/02-admin-auth/files/app/api/admin/login/route.ts

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;
}