- Add forgot-password and reset-password pages and API routes - Add email verification with token generation on registration - Add resend-verification endpoint with 60s rate limit - Add shared email utility (nodemailer, Migadu SMTP) - Add VerificationBanner in dashboard layout - Add PasswordResetToken and EmailVerificationToken models - Add emailVerified field to User model - Extend NextAuth session with isEmailVerified - Add forgot-password link to login page - Wire EMAIL_PASSWORD env var in docker-compose
64 lines
1.7 KiB
TypeScript
64 lines
1.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { createHash } from "crypto";
|
|
import { hash } from "bcryptjs";
|
|
import { z } from "zod";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
const resetPasswordSchema = z.object({
|
|
token: z.string().min(1, "Token is required"),
|
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
});
|
|
|
|
function hashToken(token: string): string {
|
|
return createHash("sha256").update(token).digest("hex");
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const body: unknown = await request.json();
|
|
const parsed = resetPasswordSchema.safeParse(body);
|
|
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: parsed.error.issues[0]?.message ?? "Invalid input" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const { token, password } = parsed.data;
|
|
const tokenHash = hashToken(token);
|
|
|
|
const resetToken = await prisma.passwordResetToken.findUnique({
|
|
where: { token: tokenHash },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!resetToken || resetToken.used || resetToken.expiresAt < new Date()) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid or expired reset link" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const passwordHash = await hash(password, 12);
|
|
|
|
await prisma.$transaction([
|
|
prisma.user.update({
|
|
where: { id: resetToken.userId },
|
|
data: { passwordHash },
|
|
}),
|
|
prisma.passwordResetToken.update({
|
|
where: { id: resetToken.id },
|
|
data: { used: true },
|
|
}),
|
|
]);
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch {
|
|
return NextResponse.json(
|
|
{ error: "Internal server error" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|