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/forgot-password/route.ts
Normal file
87
app/api/auth/forgot-password/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* POST /api/auth/forgot-password
|
||||
*
|
||||
* Passwort vergessen (plan.md Abschnitt 11).
|
||||
*
|
||||
* MVP-Auspraegung: Es wird ein zeitlich begrenzter Reset-Token erzeugt
|
||||
* und (im MVP ohne Mail-Versand) in der Konsole geloggt. Der Token
|
||||
* liegt nur gehasht in der Datenbank. Der Aufruf antwortet IMMER 200,
|
||||
* um Account-Enumeration zu verhindern.
|
||||
*
|
||||
* Sicherheit:
|
||||
* - Der Klartext-Token wird NIEMALS geloggt (Logs koennen gesammelt
|
||||
* oder geleakt werden); nur ein gekuerzter Hash-Fingerprint
|
||||
* erscheint, damit Entwickler Anfragen zuordnen koennen.
|
||||
* - Rate-Limit pro IP und E-Mail gegen Log-Flooding/DoS.
|
||||
*
|
||||
* Hinweis: Fuer Produktion ist ein Mailversand (z. B. Resend) zu
|
||||
* ergaenzen; die Schnittstelle ist darauf vorbereitet.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db/client';
|
||||
import { emailSchema } from '@/lib/validation';
|
||||
import { errorResponse } from '@/lib/api/responses';
|
||||
import {
|
||||
checkRateLimit,
|
||||
recordRateLimitHit,
|
||||
getClientIp,
|
||||
} from '@/lib/security/rate-limit';
|
||||
|
||||
/** Reset-Tokens laufen nach 1 Stunde ab. */
|
||||
const RESET_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
/** Maximal 5 Anfragen pro E-Mail und Stunde (gegen Log-Flooding). */
|
||||
const MAX_PER_EMAIL = 5;
|
||||
const EMAIL_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
/** Maximal 20 Anfragen pro IP und Stunde. */
|
||||
const MAX_PER_IP = 20;
|
||||
const IP_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const requestBody = (await request.json()) as { email?: unknown };
|
||||
const email = emailSchema.parse(requestBody.email);
|
||||
const ip = getClientIp(request);
|
||||
|
||||
if (
|
||||
!checkRateLimit(`forgot:email:${email}`, MAX_PER_EMAIL, EMAIL_WINDOW_MS)
|
||||
.allowed ||
|
||||
!checkRateLimit(`forgot:ip:${ip}`, MAX_PER_IP, IP_WINDOW_MS).allowed
|
||||
) {
|
||||
// Bewusst 200: Auch das Rate-Limit verrät nichts ueber die
|
||||
// Existenz der E-Mail.
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message:
|
||||
'Falls die E-Mail registriert ist, wurde ein Reset-Link erstellt.',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (user) {
|
||||
// Temporaerer, sicherer Token; nur der Hash wird gespeichert.
|
||||
const { randomBytes, createHash } = await import('node:crypto');
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
const tokenHash = createHash('sha256').update(token).digest('hex');
|
||||
|
||||
// MVP: Ausgabe im Server-Log statt E-Mail-Versand. NUR der
|
||||
// gekuerzte Hash-Fingerprint, niemals der Klartext-Token.
|
||||
console.log(
|
||||
`[auth] Passwort-Reset angefordert fuer Benutzer ${user.id}. ` +
|
||||
`Token-Hash (gekuerzt): ${tokenHash.slice(0, 12)}… ` +
|
||||
`(TTL: ${RESET_TTL_MS} ms)`,
|
||||
);
|
||||
recordRateLimitHit(`forgot:email:${email}`, EMAIL_WINDOW_MS);
|
||||
}
|
||||
recordRateLimitHit(`forgot:ip:${ip}`, IP_WINDOW_MS);
|
||||
|
||||
// Immer 200: Es wird nicht verraten, ob die E-Mail existiert.
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message: 'Falls die E-Mail registriert ist, wurde ein Reset-Link erstellt.',
|
||||
});
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user