Skip to content

Commit d9b7a9a

Browse files
Improve: auth provider check before auth flow (#195)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Gate all auth actions behind a Supabase auth health check and update tests to mock the health endpoint. > > - **Backend** > - Add `checkAuthProviderHealth()` in `src/server/auth/auth-actions.ts` to call `GET /auth/v1/health` with timeout and caching; returns `response.ok`. > - Introduce `AUTH_PROVIDER_ERROR_MESSAGE` and use `encodedRedirect('error', ...)` when provider is unhealthy. > - Apply health pre-check to `signInWithOAuthAction`, `signUpAction`, `signInAction`, and `forgotPasswordAction` (preserving `returnTo` when applicable). > - **Tests** > - In `src/__test__/integration/auth.test.ts`, mock global `fetch` for the health check and reset per test; remove redundant `vi.resetAllMocks()` in `afterEach`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 17596e6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 32445cf commit d9b7a9a

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

src/__test__/integration/auth.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ const { validateEmail, shouldWarnAboutAlternateEmail } = vi.hoisted(() => ({
2020
const originalConsoleError = console.error
2121
console.error = vi.fn()
2222

23+
// Mock global fetch for health check
24+
const originalFetch = global.fetch
25+
global.fetch = vi.fn().mockResolvedValue({
26+
ok: true,
27+
json: () => Promise.resolve({ version: 'v2.60.7', name: 'GoTrue' }),
28+
})
29+
2330
// Mock Supabase client
2431
const mockSupabaseClient = {
2532
auth: {
@@ -74,10 +81,14 @@ vi.mock('@/server/auth/validate-email', () => ({
7481
describe('Auth Actions - Integration Tests', () => {
7582
beforeEach(() => {
7683
vi.resetAllMocks()
84+
// Set up fetch mock for health check
85+
;(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
86+
ok: true,
87+
json: () => Promise.resolve({ version: 'v2.60.7', name: 'GoTrue' }),
88+
})
7789
})
7890

7991
afterEach(() => {
80-
vi.resetAllMocks()
8192
// Restore original console.error after each test
8293
console.error = originalConsoleError
8394
})

src/server/auth/auth-actions.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ import { redirect } from 'next/navigation'
1818
import { z } from 'zod'
1919
import { forgotPasswordSchema, signInSchema, signUpSchema } from './auth.types'
2020

21+
async function checkAuthProviderHealth(): Promise<boolean> {
22+
try {
23+
const response = await fetch(
24+
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/health`,
25+
{
26+
method: 'GET',
27+
headers: {
28+
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
29+
},
30+
signal: AbortSignal.timeout(5000),
31+
next: { revalidate: 30 },
32+
}
33+
)
34+
return response.ok
35+
} catch {
36+
return false
37+
}
38+
}
39+
40+
const AUTH_PROVIDER_ERROR_MESSAGE =
41+
'Our authentication provider is experiencing issues. Please try again later.'
42+
2143
const SignInWithOAuthInputSchema = z.object({
2244
provider: z.union([z.literal('github'), z.literal('google')]),
2345
returnTo: relativeUrlSchema.optional(),
@@ -29,6 +51,17 @@ export const signInWithOAuthAction = actionClient
2951
.action(async ({ parsedInput }) => {
3052
const { provider, returnTo } = parsedInput
3153

54+
const isHealthy = await checkAuthProviderHealth()
55+
if (!isHealthy) {
56+
const queryParams = returnTo ? { returnTo } : undefined
57+
throw encodedRedirect(
58+
'error',
59+
AUTH_URLS.SIGN_IN,
60+
AUTH_PROVIDER_ERROR_MESSAGE,
61+
queryParams
62+
)
63+
}
64+
3265
const supabase = await createClient()
3366

3467
const headerStore = await headers()
@@ -86,6 +119,17 @@ export const signUpAction = actionClient
86119
.schema(signUpSchema)
87120
.metadata({ actionName: 'signUp' })
88121
.action(async ({ parsedInput: { email, password, returnTo = '' } }) => {
122+
const isHealthy = await checkAuthProviderHealth()
123+
if (!isHealthy) {
124+
const queryParams = returnTo ? { returnTo } : undefined
125+
throw encodedRedirect(
126+
'error',
127+
AUTH_URLS.SIGN_UP,
128+
AUTH_PROVIDER_ERROR_MESSAGE,
129+
queryParams
130+
)
131+
}
132+
89133
const supabase = await createClient()
90134
const headerStore = await headers()
91135

@@ -147,6 +191,17 @@ export const signInAction = actionClient
147191
.schema(signInSchema)
148192
.metadata({ actionName: 'signInWithEmailAndPassword' })
149193
.action(async ({ parsedInput: { email, password, returnTo = '' } }) => {
194+
const isHealthy = await checkAuthProviderHealth()
195+
if (!isHealthy) {
196+
const queryParams = returnTo ? { returnTo } : undefined
197+
throw encodedRedirect(
198+
'error',
199+
AUTH_URLS.SIGN_IN,
200+
AUTH_PROVIDER_ERROR_MESSAGE,
201+
queryParams
202+
)
203+
}
204+
150205
const supabase = await createClient()
151206

152207
const headerStore = await headers()
@@ -191,6 +246,15 @@ export const forgotPasswordAction = actionClient
191246
.schema(forgotPasswordSchema)
192247
.metadata({ actionName: 'forgotPassword' })
193248
.action(async ({ parsedInput: { email } }) => {
249+
const isHealthy = await checkAuthProviderHealth()
250+
if (!isHealthy) {
251+
throw encodedRedirect(
252+
'error',
253+
AUTH_URLS.FORGOT_PASSWORD,
254+
AUTH_PROVIDER_ERROR_MESSAGE
255+
)
256+
}
257+
194258
const supabase = await createClient()
195259

196260
const { error } = await supabase.auth.resetPasswordForEmail(email)

0 commit comments

Comments
 (0)