{error}
) : null} {success ? ({t("login", "forgot_success")}
) : null}import { redirect } from "next/navigation"; import { AuthTokenType, UserStatus } from "@prisma/client"; import { Button, PageHeader, SectionCard } from "@/components/ui"; import { createAuthToken, makeResetUrl } from "@/lib/auth-tokens"; import { getLocale, getTranslator } from "@/lib/i18n"; import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit"; import { consumeRateLimit } from "@/lib/rate-limit"; import { prisma } from "@/lib/prisma"; import { sendTransactionalNotification } from "@/lib/notification"; async function requestPasswordReset(formData: FormData) { "use server"; const requestContext = await getRequestAuditContext(); const rawEmail = formData.get("email"); const email = typeof rawEmail === "string" ? rawEmail.trim().toLowerCase() : ""; const rateLimit = consumeRateLimit(requestContext.ipAddress || "unknown", { scope: "password_reset_request", limit: 6, windowMs: 15 * 60 * 1000 }); if (!rateLimit.allowed) { redirect("/forgot-password?error=rate_limited"); } if (!email) { redirect("/forgot-password?error=missing_email"); } const user = await prisma.user.findUnique({ where: { email } }); if (user && user.status === UserStatus.ACTIVE) { const created = await createAuthToken({ userId: user.id, tenantId: user.tenantId, tokenType: AuthTokenType.PASSWORD_RESET }); const resetUrl = makeResetUrl(created.rawToken); const notificationResult = await sendTransactionalNotification({ to: user.email, subject: "Reset password to continue your account access", text: `Gunakan tautan ini untuk mengatur ulang password: ${resetUrl}`, html: `
Gunakan tautan berikut untuk mengatur ulang password: ${resetUrl}
` }); if (!notificationResult.ok) { await writeAuditTrail({ tenantId: user.tenantId, actorUserId: user.id, entityType: "user", entityId: user.id, action: "password_reset_notified_failed", metadata: { email, reason: notificationResult.error, source: "web", provider: notificationResult.provider ?? null }, ipAddress: requestContext.ipAddress, userAgent: requestContext.userAgent }); } await writeAuditTrail({ tenantId: user.tenantId, actorUserId: user.id, entityType: "user", entityId: user.id, action: "password_reset_requested", metadata: { email, expiresAt: created.expiresAt.toISOString(), source: "web", notifyProvider: notificationResult?.provider ?? null, notifyQueued: notificationResult?.ok ?? false }, ipAddress: requestContext.ipAddress, userAgent: requestContext.userAgent }); if (notificationResult.ok === true && notificationResult.provider === "console") { console.log(`RESET_PASSWORD_LINK=${resetUrl}`); } } else if (user) { await writeAuditTrail({ tenantId: user.tenantId, actorUserId: user.id, entityType: "user", entityId: user.id, action: "password_reset_denied", metadata: { email, status: user.status, source: "web" }, ipAddress: requestContext.ipAddress, userAgent: requestContext.userAgent }); } // Always respond with generic success to avoid user enumeration. redirect("/forgot-password?success=sent"); } export default async function ForgotPasswordPage({ searchParams }: { searchParams?: Promise<{ error?: string; success?: string }>; }) { const t = getTranslator(await getLocale()); const params = await (searchParams ?? Promise.resolve({ error: undefined, success: undefined })); const error = params?.error === "missing_email" ? t("login", "missing_email") : params?.error === "rate_limited" ? t("login", "error_rate_limited") : null; const success = params?.success === "sent"; return ({error}
) : null} {success ? ({t("login", "forgot_success")}
) : null}