Add rate limiting to verification emails during OAuth flow (#13255)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-03-05 19:10:25 -05:00
committed by GitHub
parent 4c380e5a58
commit 6c394cc415
8 changed files with 160 additions and 5 deletions

View File

@@ -36,6 +36,21 @@ describe("EmailVerificationModal", () => {
).toBeInTheDocument();
});
it("should render the rate limited message when wasRateLimited is true", () => {
// Arrange & Act
renderWithRouter(
<EmailVerificationModal onClose={mockOnClose} wasRateLimited />,
);
// Assert - should show the rate limited message instead of the default one
expect(
screen.getByText("AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL"),
).toBeInTheDocument();
expect(
screen.queryByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).not.toBeInTheDocument();
});
it("should render the TermsAndPrivacyNotice component", () => {
// Arrange & Act
renderWithRouter(<EmailVerificationModal onClose={mockOnClose} />);

View File

@@ -10,11 +10,13 @@ import { useEmailVerification } from "#/hooks/use-email-verification";
interface EmailVerificationModalProps {
onClose: () => void;
userId?: string | null;
wasRateLimited?: boolean;
}
export function EmailVerificationModal({
onClose,
userId,
wasRateLimited = false,
}: EmailVerificationModalProps) {
const { t } = useTranslation();
const {
@@ -33,14 +35,18 @@ export function EmailVerificationModal({
resendButtonLabel = t(I18nKey.SETTINGS$RESEND_VERIFICATION);
}
// Show different message when rate limited - user should check their inbox
// for the verification email sent earlier
const headerMessage = wasRateLimited
? t(I18nKey.AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL)
: t(I18nKey.AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY);
return (
<ModalBackdrop onClose={onClose}>
<ModalBody className="border border-tertiary">
<OpenHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{t(I18nKey.AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY)}
</h1>
<h1 className="text-2xl font-bold">{headerMessage}</h1>
</div>
<div className="flex flex-col gap-3 w-full mt-4">

View File

@@ -21,6 +21,7 @@ import { useResendEmailVerification } from "#/hooks/mutation/use-resend-email-ve
* - isCooldownActive: boolean indicating if cooldown is currently active
* - cooldownRemaining: number of milliseconds remaining in cooldown
* - formattedCooldownTime: string formatted as "M:SS" for display
* - wasRateLimited: boolean indicating if the user was rate limited during OAuth flow
*/
export function useEmailVerification() {
const [searchParams, setSearchParams] = useSearchParams();
@@ -29,6 +30,7 @@ export function useEmailVerification() {
const [emailVerified, setEmailVerified] = React.useState(false);
const [hasDuplicatedEmail, setHasDuplicatedEmail] = React.useState(false);
const [recaptchaBlocked, setRecaptchaBlocked] = React.useState(false);
const [wasRateLimited, setWasRateLimited] = React.useState(false);
const [userId, setUserId] = React.useState<string | null>(null);
const [lastSentTimestamp, setLastSentTimestamp] = React.useState<
number | null
@@ -85,6 +87,13 @@ export function useEmailVerification() {
shouldUpdate = true;
}
const rateLimitedParam = searchParams.get("rate_limited");
if (rateLimitedParam === "true") {
setWasRateLimited(true);
searchParams.delete("rate_limited");
shouldUpdate = true;
}
if (userIdParam) {
setUserId(userIdParam);
searchParams.delete("user_id");
@@ -136,6 +145,7 @@ export function useEmailVerification() {
setEmailVerified,
hasDuplicatedEmail,
recaptchaBlocked,
wasRateLimited,
userId,
resendEmailVerification: resendEmailVerificationMutation.mutate,
isResendingVerification: resendEmailVerificationMutation.isPending,

View File

@@ -767,6 +767,7 @@ export enum I18nKey {
AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR = "AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
AUTH$NO_PROVIDERS_CONFIGURED = "AUTH$NO_PROVIDERS_CONFIGURED",
AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY = "AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY",
AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL = "AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL",
AUTH$EMAIL_VERIFIED_PLEASE_LOGIN = "AUTH$EMAIL_VERIFIED_PLEASE_LOGIN",
AUTH$DUPLICATE_EMAIL_ERROR = "AUTH$DUPLICATE_EMAIL_ERROR",
AUTH$RECAPTCHA_BLOCKED = "AUTH$RECAPTCHA_BLOCKED",

View File

@@ -12275,6 +12275,22 @@
"de": "Bitte überprüfen Sie Ihre E-Mail, um Ihr Konto zu verifizieren.",
"uk": "Будь ласка, перевірте свою електронну пошту, щоб підтвердити свій обліковий запис."
},
"AUTH$CHECK_INBOX_FOR_VERIFICATION_EMAIL": {
"en": "Please check your inbox for the verification email we sent earlier.",
"ja": "先ほど送信した確認メールを受信トレイでご確認ください。",
"zh-CN": "请检查您的收件箱,查收我们之前发送的验证邮件。",
"zh-TW": "請檢查您的收件箱,查收我們之前發送的驗證郵件。",
"ko-KR": "이전에 보내드린 인증 이메일을 받은 편지함에서 확인해 주세요.",
"no": "Vennligst sjekk innboksen din for bekreftelsese-posten vi sendte tidligere.",
"it": "Controlla la tua casella di posta per l'email di verifica che ti abbiamo inviato in precedenza.",
"pt": "Por favor, verifique sua caixa de entrada para o e-mail de verificação que enviamos anteriormente.",
"es": "Por favor, revisa tu bandeja de entrada para el correo de verificación que te enviamos anteriormente.",
"ar": "يرجى التحقق من صندوق الوارد للبريد الإلكتروني الذي أرسلناه لك سابقًا.",
"fr": "Veuillez vérifier votre boîte de réception pour l'e-mail de vérification que nous vous avons envoyé précédemment.",
"tr": "Lütfen daha önce gönderdiğimiz doğrulama e-postası için gelen kutunuzu kontrol edin.",
"de": "Bitte überprüfen Sie Ihren Posteingang auf die Bestätigungs-E-Mail, die wir Ihnen zuvor gesendet haben.",
"uk": "Будь ласка, перевірте вашу поштову скриньку на наявність листа підтвердження, який ми надіслали раніше."
},
"AUTH$EMAIL_VERIFIED_PLEASE_LOGIN": {
"en": "Your email has been verified. Please login below.",
"ja": "メールアドレスが確認されました。下記からログインしてください。",

View File

@@ -19,6 +19,7 @@ export default function LoginPage() {
emailVerified,
hasDuplicatedEmail,
recaptchaBlocked,
wasRateLimited,
emailVerificationModalOpen,
setEmailVerificationModalOpen,
userId,
@@ -83,6 +84,7 @@ export default function LoginPage() {
setEmailVerificationModalOpen(false);
}}
userId={userId}
wasRateLimited={wasRateLimited}
/>
)}
</>