Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 41 additions & 15 deletions api/ai/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ if (SUPABASE_URL && SUPABASE_KEY) {
}

// Accept multiple env var names so it works on Vercel, local dev, or when the
// key was mistakenly added with a VITE_/NEXT_PUBLIC_ prefix.
const GEMINI_API_KEY = (process.env.GEMINI_API_KEY ||
// key was added with a different name. Also allow an OAuth-style bearer token
// (for service accounts or short-lived credentials). If a bearer token is
// provided we will call the endpoint without a ?key= parameter and send
// Authorization: Bearer <token>.
const GEMINI_API_KEY = (
process.env.GEMINI_API_KEY ||
process.env.VITE_GEMINI_API_KEY ||
process.env.NEXT_PUBLIC_GEMINI_API_KEY) as string | undefined;
const GEMINI_URL = GEMINI_API_KEY
? `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`
: null;
process.env.NEXT_PUBLIC_GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY ||
process.env.GOOGLE_CLOUD_API_KEY ||
process.env.GENERATIVE_API_KEY
) as string | undefined;
const GEMINI_BEARER = (process.env.GEMINI_BEARER_TOKEN || process.env.GOOGLE_BEARER_TOKEN) as string | undefined;
const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
const GEMINI_URL = GEMINI_BEARER ? GEMINI_BASE : (GEMINI_API_KEY ? `${GEMINI_BASE}?key=${encodeURIComponent(GEMINI_API_KEY)}` : null);

// Lightweight CORS helper so static builds and alternate origins can call this function
function applyCors(req: any, res: any) {
Expand Down Expand Up @@ -245,15 +253,33 @@ export default async function handler(req: any, res: any) {
// rather than a fabricated plan JSON so the UI shows a clear status.
generatedText = 'Sorry, this feature is currently under development.';
} else {
const response = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
});
const data = await response.json();
generatedText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
if (!generatedText || String(generatedText).trim().length === 0) {
generatedText = 'Sorry, this feature is currently under development.';
// Call Gemini/Generative API. Support either API key query param or
// Authorization: Bearer header when a bearer token is present.
const headers: any = { 'Content-Type': 'application/json' };
if (GEMINI_BEARER) headers['Authorization'] = `Bearer ${GEMINI_BEARER}`;
try {
const response = await fetch(GEMINI_URL as string, {
method: 'POST',
headers,
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
});
const text = await response.text();
let data: any = null;
try { data = JSON.parse(text); } catch (e) { data = null; }
if (!response.ok) {
console.error('[api/ai/generate] Gemini provider returned non-OK', response.status, text);
// Surface a helpful diagnostic to client (non-sensitive) so the UI
// can show a useful message while ops fixes the API keys/config.
return res.status(502).json({ message: 'AI provider error', status: response.status, body: data || text });
}
generatedText = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
if (!generatedText || String(generatedText).trim().length === 0) {
// If provider returned empty text, return a short friendly message
generatedText = 'Sorry, this feature is currently under development.';
}
} catch (e: any) {
console.error('[api/ai/generate] failed to call Gemini provider', String(e));
return res.status(502).json({ message: 'AI provider request failed', error: String(e) });
}
}

Expand Down
31 changes: 31 additions & 0 deletions src/components/GoogleIdentityButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> => {
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.
Expand All @@ -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);
Expand Down
33 changes: 30 additions & 3 deletions src/services/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ export async function fetchUserById(id: string): Promise<User | null> {
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<boolean> {
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;
Expand All @@ -91,7 +115,8 @@ export interface User {
export async function signIn(email: string, password: string): Promise<User> {
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) {
Expand Down Expand Up @@ -166,7 +191,8 @@ export async function signIn(email: string, password: string): Promise<User> {
export async function signUp(email: string, username: string, password: string): Promise<User> {
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
Expand Down Expand Up @@ -227,7 +253,8 @@ export async function signUp(email: string, username: string, password: string):
// unsupported.
export async function signInWithGoogle(): Promise<void> {
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
Expand Down
Loading