Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
87
app/api/auth/login/route.ts
Normal file
87
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
*
|
||||
* Login mit Rate-Limiting (plan.md Abschnitt 11).
|
||||
* Fehlermeldung ist bewusst generisch, um Account-Enumeration
|
||||
* zu verhindern.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db/client';
|
||||
import { verifyPassword } from '@/lib/auth/password';
|
||||
import { createSessionToken, setSessionCookie } from '@/lib/auth/session';
|
||||
import { loginSchema } from '@/lib/validation';
|
||||
import { errorResponse } from '@/lib/api/responses';
|
||||
import {
|
||||
checkRateLimit,
|
||||
recordRateLimitHit,
|
||||
resetRateLimit,
|
||||
getClientIp,
|
||||
} from '@/lib/security/rate-limit';
|
||||
|
||||
/**
|
||||
* Maximal 5 Fehlversuche pro E-Mail und Zeitfenster (In-Memory, MVP).
|
||||
* Zusaetzlich ein IP-Limit: verhindert, dass ein Angreifer mit vielen
|
||||
* erfundenen E-Mails den Speicher flutet oder verteiltes Brute-Forcing
|
||||
* gegen ein einzelnes Konto betreibt.
|
||||
*/
|
||||
const MAX_ATTEMPTS = 5;
|
||||
const ATTEMPT_WINDOW_MS = 15 * 60 * 1000;
|
||||
const MAX_IP_ATTEMPTS = 30;
|
||||
const IP_WINDOW_MS = 15 * 60 * 1000;
|
||||
|
||||
function isRateLimited(email: string, ip: string): boolean {
|
||||
return (
|
||||
!checkRateLimit(`login:email:${email}`, MAX_ATTEMPTS, ATTEMPT_WINDOW_MS)
|
||||
.allowed ||
|
||||
!checkRateLimit(`login:ip:${ip}`, MAX_IP_ATTEMPTS, IP_WINDOW_MS).allowed
|
||||
);
|
||||
}
|
||||
|
||||
function recordFailedAttempt(email: string, ip: string): void {
|
||||
recordRateLimitHit(`login:email:${email}`, ATTEMPT_WINDOW_MS);
|
||||
recordRateLimitHit(`login:ip:${ip}`, IP_WINDOW_MS);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const body = loginSchema.parse(await request.json());
|
||||
const ip = getClientIp(request);
|
||||
|
||||
if (isRateLimited(body.email, ip)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Zu viele Anmeldeversuche. Bitte spaeter erneut.' },
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: body.email },
|
||||
select: { id: true, email: true, role: true, passwordHash: true },
|
||||
});
|
||||
|
||||
// Auch bei unbekannter E-Mail bcrypt ausfuehren (Timing-Abgleich).
|
||||
const passwordHash =
|
||||
user?.passwordHash ??
|
||||
'$2a$12$C6UzMDM.H6dfI/f/IKcEeO7ZBpQqQqQqQqQqQqQqQqQqQqQqQqQqQ';
|
||||
const passwordValid = await verifyPassword(body.password, passwordHash);
|
||||
|
||||
if (!user || !passwordValid) {
|
||||
recordFailedAttempt(body.email, ip);
|
||||
return NextResponse.json(
|
||||
{ error: 'E-Mail oder Passwort ist falsch.' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
resetRateLimit(`login:email:${body.email}`);
|
||||
|
||||
const token = await createSessionToken(user);
|
||||
await setSessionCookie(token);
|
||||
|
||||
return NextResponse.json({
|
||||
user: { id: user.id, email: user.email, role: user.role },
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user