diff --git a/src/__test__/integration/auth.test.ts b/src/__test__/integration/auth.test.ts index efe65599c..5e3c6aaed 100644 --- a/src/__test__/integration/auth.test.ts +++ b/src/__test__/integration/auth.test.ts @@ -20,6 +20,13 @@ const { validateEmail, shouldWarnAboutAlternateEmail } = vi.hoisted(() => ({ const originalConsoleError = console.error console.error = vi.fn() +// Mock global fetch for health check +const originalFetch = global.fetch +global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ version: 'v2.60.7', name: 'GoTrue' }), +}) + // Mock Supabase client const mockSupabaseClient = { auth: { @@ -74,10 +81,14 @@ vi.mock('@/server/auth/validate-email', () => ({ describe('Auth Actions - Integration Tests', () => { beforeEach(() => { vi.resetAllMocks() + // Set up fetch mock for health check + ;(global.fetch as ReturnType).mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ version: 'v2.60.7', name: 'GoTrue' }), + }) }) afterEach(() => { - vi.resetAllMocks() // Restore original console.error after each test console.error = originalConsoleError }) diff --git a/src/server/auth/auth-actions.ts b/src/server/auth/auth-actions.ts index 6e5b88dac..5fa8d181f 100644 --- a/src/server/auth/auth-actions.ts +++ b/src/server/auth/auth-actions.ts @@ -18,6 +18,28 @@ import { redirect } from 'next/navigation' import { z } from 'zod' import { forgotPasswordSchema, signInSchema, signUpSchema } from './auth.types' +async function checkAuthProviderHealth(): Promise { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/health`, + { + method: 'GET', + headers: { + apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + }, + signal: AbortSignal.timeout(5000), + next: { revalidate: 30 }, + } + ) + return response.ok + } catch { + return false + } +} + +const AUTH_PROVIDER_ERROR_MESSAGE = + 'Our authentication provider is experiencing issues. Please try again later.' + const SignInWithOAuthInputSchema = z.object({ provider: z.union([z.literal('github'), z.literal('google')]), returnTo: relativeUrlSchema.optional(), @@ -29,6 +51,17 @@ export const signInWithOAuthAction = actionClient .action(async ({ parsedInput }) => { const { provider, returnTo } = parsedInput + const isHealthy = await checkAuthProviderHealth() + if (!isHealthy) { + const queryParams = returnTo ? { returnTo } : undefined + throw encodedRedirect( + 'error', + AUTH_URLS.SIGN_IN, + AUTH_PROVIDER_ERROR_MESSAGE, + queryParams + ) + } + const supabase = await createClient() const headerStore = await headers() @@ -86,6 +119,17 @@ export const signUpAction = actionClient .schema(signUpSchema) .metadata({ actionName: 'signUp' }) .action(async ({ parsedInput: { email, password, returnTo = '' } }) => { + const isHealthy = await checkAuthProviderHealth() + if (!isHealthy) { + const queryParams = returnTo ? { returnTo } : undefined + throw encodedRedirect( + 'error', + AUTH_URLS.SIGN_UP, + AUTH_PROVIDER_ERROR_MESSAGE, + queryParams + ) + } + const supabase = await createClient() const headerStore = await headers() @@ -147,6 +191,17 @@ export const signInAction = actionClient .schema(signInSchema) .metadata({ actionName: 'signInWithEmailAndPassword' }) .action(async ({ parsedInput: { email, password, returnTo = '' } }) => { + const isHealthy = await checkAuthProviderHealth() + if (!isHealthy) { + const queryParams = returnTo ? { returnTo } : undefined + throw encodedRedirect( + 'error', + AUTH_URLS.SIGN_IN, + AUTH_PROVIDER_ERROR_MESSAGE, + queryParams + ) + } + const supabase = await createClient() const headerStore = await headers() @@ -191,6 +246,15 @@ export const forgotPasswordAction = actionClient .schema(forgotPasswordSchema) .metadata({ actionName: 'forgotPassword' }) .action(async ({ parsedInput: { email } }) => { + const isHealthy = await checkAuthProviderHealth() + if (!isHealthy) { + throw encodedRedirect( + 'error', + AUTH_URLS.FORGOT_PASSWORD, + AUTH_PROVIDER_ERROR_MESSAGE + ) + } + const supabase = await createClient() const { error } = await supabase.auth.resetPasswordForEmail(email)