import { defaultResponderForAppDir } from "app/api/defaultResponderForAppDir"; import { parseRequestData } from "app/api/parseRequestData"; import { cookies } from "next/headers"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; import { validPassword } from "@calcom/features/auth/lib/validPassword"; import { hashPassword } from "@calcom/lib/auth/hashPassword"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { piiHasher } from "@calcom/lib/server/PiiHasher"; import prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; const passwordResetRequestSchema = z.object({ csrfToken: z.string(), password: z.string().refine(validPassword, () => ({ message: "Password does not meet the requirements", })), requestId: z.string(), // format doesn't matter. }); async function handler(req: NextRequest) { const body = await parseRequestData(req); const { password: rawPassword, requestId: rawRequestId, csrfToken: submittedToken, } = passwordResetRequestSchema.parse(body); const cookieStore = await cookies(); const cookieToken = cookieStore.get("calcom.csrf_token")?.value; if (submittedToken !== cookieToken) { return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 }); } // token verified, delete the cookie / a resubmit on failure requires a new csrf token. cookieStore.delete("calcom.csrf_token"); const remoteIp = getIP(req); await checkRateLimitAndThrowError({ rateLimitingType: "core", identifier: `api:reset-password:${piiHasher.hash(remoteIp)}`, }); // Note: There is a low, very low chance that a password request stays valid long enough // to brute force 3.8126967e+40 options, but rate limiting provides additional protection. const maybeRequest = await prisma.resetPasswordRequest.findFirstOrThrow({ where: { id: rawRequestId, expires: { gt: new Date(), }, }, select: { email: true, }, }); const hashedPassword = await hashPassword(rawPassword); // this can fail if a password request has been made for an email that has since changed or- // never existed within Cal. In this case we do not want to disclose the email's existence. // instead, we just return 404 try { await prisma.user.update({ where: { email: maybeRequest.email, }, data: { password: { upsert: { create: { hash: hashedPassword }, update: { hash: hashedPassword }, }, }, emailVerified: new Date(), identityProvider: IdentityProvider.CAL, identityProviderId: null, }, }); } catch (e) { return NextResponse.json({}, { status: 404 }); } await expireResetPasswordRequest(rawRequestId); return NextResponse.json({ message: "Password reset." }, { status: 201 }); } async function expireResetPasswordRequest(rawRequestId: string) { await prisma.resetPasswordRequest.update({ where: { id: rawRequestId, }, data: { // We set the expiry to now to invalidate the request expires: new Date(), }, }); } export const POST = defaultResponderForAppDir(handler);