From f2351f360ec4093b3b304bbb494070e896b5581b Mon Sep 17 00:00:00 2001 From: zman-789 Date: Mon, 12 Jan 2026 08:24:08 -0500 Subject: [PATCH 1/2] feat(auth): fallback when Supabase unreachable; add reachability check --- src/services/authService.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/services/authService.ts b/src/services/authService.ts index 3065d19..88fae2a 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -73,6 +73,30 @@ export async function fetchUserById(id: string): Promise { return null; } } + +// Quick reachability test for the configured Supabase URL. We attempt a +// simple fetch to the Supabase auth root so we can detect DNS failures +// or network issues early and fall back to server-side auth endpoints. +async function isSupabaseReachable(): Promise { + try { + const rawUrl = (import.meta.env.VITE_SUPABASE_URL || (process && process.env && process.env.SUPABASE_URL) || ''); + if (!rawUrl) return false; + // Trim trailing slash and hit the auth endpoint; DNS failures surface as fetch errors. + const url = rawUrl.replace(/\/$/, '') + '/auth/v1'; + // Use a short timeout to avoid blocking the UI for long + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), 3000); + try { + await fetch(url, { method: 'GET', signal: controller.signal, cache: 'no-store' }); + clearTimeout(id); + return true; + } finally { + clearTimeout(id); + } + } catch (e) { + return false; + } +} // src/services/authService.ts export interface User { id: string; @@ -91,7 +115,8 @@ export interface User { export async function signIn(email: string, password: string): Promise { const normalizedEmail = String(email).trim().toLowerCase(); const useSupabase = Boolean(import.meta.env.VITE_LOCAL_USE_SUPABASE || import.meta.env.VITE_SUPABASE_URL); - if (useSupabase) { + const reachable = useSupabase ? await isSupabaseReachable() : false; + if (useSupabase && reachable) { const result = await supabase.auth.signInWithPassword({ email: normalizedEmail, password }); // Supabase may return error with status 400 and message indicating "User is not confirmed" or similar. if (result.error || !result.data?.session) { @@ -166,7 +191,8 @@ export async function signIn(email: string, password: string): Promise { export async function signUp(email: string, username: string, password: string): Promise { const normalizedEmail = String(email).trim().toLowerCase(); const useSupabase = Boolean(import.meta.env.VITE_LOCAL_USE_SUPABASE || import.meta.env.VITE_SUPABASE_URL); - if (useSupabase) { + const reachable = useSupabase ? await isSupabaseReachable() : false; + if (useSupabase && reachable) { const result = await supabase.auth.signUp({ email: normalizedEmail, password, options: { data: { username, energy: DEFAULT_ENERGY } } }); if (result.error) throw new Error(result.error.message || 'Sign up failed'); // Supabase may not return a session depending on config; if a session exists save token @@ -227,7 +253,8 @@ export async function signUp(email: string, username: string, password: string): // unsupported. export async function signInWithGoogle(): Promise { const useSupabase = Boolean(import.meta.env.VITE_LOCAL_USE_SUPABASE || import.meta.env.VITE_SUPABASE_URL); - if (useSupabase) { + const reachable = useSupabase ? await isSupabaseReachable() : false; + if (useSupabase && reachable) { try { // Prefer an explicit public app URL (so Supabase redirects back to your // branded domain after it finishes the provider exchange). If you set From 8370dca6aca1c4b77da32284a3fd0c88d7281a74 Mon Sep 17 00:00:00 2001 From: zman-789 Date: Mon, 26 Jan 2026 11:21:52 -0500 Subject: [PATCH 2/2] feat(ui): show friendly message when Supabase unreachable for Google sign-in --- src/components/GoogleIdentityButton.tsx | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/components/GoogleIdentityButton.tsx b/src/components/GoogleIdentityButton.tsx index 59a5ba4..afe70b1 100644 --- a/src/components/GoogleIdentityButton.tsx +++ b/src/components/GoogleIdentityButton.tsx @@ -31,6 +31,27 @@ const GoogleIdentityButton: React.FC = () => { const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID || ''; const useSupabase = Boolean(import.meta.env.VITE_LOCAL_USE_SUPABASE || import.meta.env.VITE_SUPABASE_URL); + // Quick reachability check for the configured Supabase URL. Returns false + // when the domain doesn't resolve or the request times out. + const checkSupabaseReachable = async (): Promise => { + try { + const rawUrl = (import.meta.env.VITE_SUPABASE_URL && String(import.meta.env.VITE_SUPABASE_URL).trim()) || ''; + if (!rawUrl) return false; + const url = rawUrl.replace(/\/$/, '') + '/auth/v1'; + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), 3000); + try { + await fetch(url, { method: 'GET', signal: controller.signal, cache: 'no-store' }); + clearTimeout(id); + return true; + } finally { + clearTimeout(id); + } + } catch (e) { + return false; + } + }; + // If Supabase is available prefer the Supabase OAuth redirect flow. This // ensures sessions are created/managed by Supabase rather than doing local // ID token verification. @@ -41,6 +62,16 @@ const GoogleIdentityButton: React.FC = () => { className="btn btn-google" onClick={async () => { try { + const reachable = await checkSupabaseReachable(); + if (!reachable) { + window.showFitBuddyNotification?.({ + title: 'Sign-in Unavailable', + message: + 'Google sign-in is temporarily unavailable because the authentication service cannot be reached. Please try again later or contact the site administrator.', + variant: 'warning' + }); + return; + } await signInWithGoogle(); } catch (e) { console.warn('[GoogleIdentityButton] signInWithGoogle failed', e);