diff --git a/apps/server/src/routes/forgot-password/index.test.tsx b/apps/server/src/routes/forgot-password/index.test.tsx
index 41913f04..149ece7c 100644
--- a/apps/server/src/routes/forgot-password/index.test.tsx
+++ b/apps/server/src/routes/forgot-password/index.test.tsx
@@ -3,7 +3,7 @@
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import type { InputHTMLAttributes, ReactNode } from 'react'
+import type { ButtonHTMLAttributes, InputHTMLAttributes, ReactNode } from 'react'
const routerState = vi.hoisted(() => ({
navigate: vi.fn(),
@@ -17,6 +17,11 @@ const mutationState = vi.hoisted(() => ({
}[],
}))
+const authConfigState = vi.hoisted(() => ({
+ data: undefined as { turnstileSiteKey: string | null } | undefined,
+ isPending: false,
+}))
+
vi.mock('@lingui/react/macro', () => ({
Trans: ({ children }: { children: ReactNode }) => <>{children}>,
useLingui: () => ({ t: (strings: TemplateStringsArray) => strings[0] }),
@@ -36,7 +41,11 @@ vi.mock('../../lib/router', () => ({
}))
vi.mock('@tanstack/react-query', () => ({
- useQuery: () => ({ data: undefined, isPending: false, error: null }),
+ useQuery: () => ({
+ data: authConfigState.data,
+ isPending: authConfigState.isPending,
+ error: null,
+ }),
useMutation: (options: (typeof mutationState.captured)[number]) => {
mutationState.captured.push(options)
return { mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false, isSuccess: false }
@@ -54,7 +63,11 @@ vi.mock('../../components/layout', () => ({
vi.mock('../../components/ui', () => ({
Alert: ({ children }: { children: ReactNode }) =>
{children}
,
- Button: ({ children }: { children: ReactNode }) => ,
+ Button: ({ children, ...props }: ButtonHTMLAttributes) => (
+
+ ),
Field: ({ children }: { children: ReactNode }) => {children}
,
Input: (props: InputHTMLAttributes) => ,
PageHeader: ({ title }: { title: ReactNode }) => {title}
,
@@ -126,6 +139,8 @@ describe('ForgotPasswordPage navigation links', () => {
mutationState.captured.length = 0
routerState.search = {}
routerState.pathname = '/forgot-password'
+ authConfigState.data = undefined
+ authConfigState.isPending = false
globalThis.sessionStorage.clear()
globalThis.history.replaceState({}, '', '/forgot-password')
})
@@ -139,6 +154,18 @@ describe('ForgotPasswordPage navigation links', () => {
await unmount(container, root)
})
+ it('disables reset-link delivery until Turnstile is ready', async () => {
+ authConfigState.data = { turnstileSiteKey: 'site-key' }
+
+ const { container, root } = await renderPage()
+
+ const submit = Array.from(container.querySelectorAll('button')).find((button) =>
+ button.textContent?.includes('Send reset link'),
+ )
+ expect(submit?.disabled).toBe(true)
+ await unmount(container, root)
+ })
+
it('keeps organization and locale context when returning to sign in', async () => {
routerState.search = { organization_id: 'org-1', locale: 'en' }
globalThis.history.replaceState({}, '', '/forgot-password?organization_id=org-1&locale=en')
diff --git a/apps/server/src/routes/forgot-password/index.tsx b/apps/server/src/routes/forgot-password/index.tsx
index 05c3a572..16c636ac 100644
--- a/apps/server/src/routes/forgot-password/index.tsx
+++ b/apps/server/src/routes/forgot-password/index.tsx
@@ -114,6 +114,8 @@ function RequestStep({ organizationId, onDone }: RequestStepProps): ReactNode {
turnstileToken,
setTurnstileToken,
)
+ const turnstileReady =
+ !authConfigQuery.isPending && (authConfig.turnstileSiteKey === null || Boolean(turnstileToken))
const requestMutation = useMutation({
mutationFn: (emailValue: string) =>
@@ -145,6 +147,7 @@ function RequestStep({ organizationId, onDone }: RequestStepProps): ReactNode {
setEmailError(t`Enter a valid email address`)
return
}
+ if (!turnstileReady) return
await requestMutation.mutateAsync(email)
}
@@ -171,10 +174,10 @@ function RequestStep({ organizationId, onDone }: RequestStepProps): ReactNode {
/>
-