diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 2e711fc07..bc8581c5c 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -338,5 +338,7 @@ "helpUsImproveDesc": "Take a few minutes to share your feedback and help us shape the future of Rallly.", "giveFeedback": "Give feedback", "homeActionsTitle": "Actions", - "dismissFeedback": "Don't show again" + "dismissFeedback": "Don't show again", + "tooManyAttempts": "Too many attempts, please try again later.", + "unknownError": "Something went wrong" } diff --git a/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx b/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx index f776a09c1..b29ccfcdb 100644 --- a/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/verify/components/otp-form.tsx @@ -41,16 +41,36 @@ export function OTPForm({ email }: { email: string }) { }/api/auth/callback/email?email=${encodeURIComponent(email.toLowerCase())}&token=${data.otp}`; const res = await fetch(url); - const resUrl = new URL(res.url); - const hasError = !!resUrl.searchParams.get("error"); - - if (hasError) { - form.setError("otp", { - message: t("wrongVerificationCode"), - }); + if (!res.ok) { + switch (res.status) { + case 429: + form.setError("otp", { + message: t("tooManyAttempts", { + defaultValue: "Too many attempts, please try again later.", + }), + }); + break; + default: + form.setError("otp", { + message: t("unknownError", { + defaultValue: "Something went wrong", + }), + }); + break; + } } else { - window.location.href = searchParams?.get("redirectTo") ?? "/"; + const resUrl = new URL(res.url); + const hasError = !!resUrl.searchParams.get("error"); + if (hasError) { + form.setError("otp", { + message: t("wrongVerificationCode", { + defaultValue: "The code you entered is incorrect", + }), + }); + } else { + window.location.href = searchParams?.get("redirectTo") ?? "/"; + } } }); diff --git a/apps/web/src/app/api/auth/[...nextauth]/route.ts b/apps/web/src/app/api/auth/[...nextauth]/route.ts index 4b6eabc11..ded00e870 100644 --- a/apps/web/src/app/api/auth/[...nextauth]/route.ts +++ b/apps/web/src/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,20 @@ +import { rateLimit } from "@/features/rate-limit"; import { handlers } from "@/next-auth"; import { withPosthog } from "@/utils/posthog"; +import type { NextRequest } from "next/server"; + +export const GET = withPosthog(async (req: NextRequest) => { + if (req.nextUrl.pathname.includes("callback/email")) { + const { success } = await rateLimit("login_otp_attempt", 20, "15m"); + + if (!success) { + return new Response("Too many requests", { + status: 429, + }); + } + } + + return handlers.GET(req); +}); -export const GET = withPosthog(handlers.GET); export const POST = withPosthog(handlers.POST); diff --git a/apps/web/src/auth/providers/email.ts b/apps/web/src/auth/providers/email.ts index 59db1f48d..a5df9e392 100644 --- a/apps/web/src/auth/providers/email.ts +++ b/apps/web/src/auth/providers/email.ts @@ -9,6 +9,7 @@ export const EmailProvider = NodemailerProvider({ server: "none", // This value is required even though we don't need it from: process.env.NOREPLY_EMAIL, id: "email", + maxAge: 15 * 60, generateVerificationToken() { return generateOtp(); },