diff --git a/frontend/src/app/auth/login/LoginForm.tsx b/frontend/src/app/auth/login/LoginForm.tsx index d5dd751..391e310 100644 --- a/frontend/src/app/auth/login/LoginForm.tsx +++ b/frontend/src/app/auth/login/LoginForm.tsx @@ -14,6 +14,7 @@ import { useI18n } from '@/lib/i18n'; import { apiClient } from '@/lib/apiClient'; import { useLogin } from './useLogin'; import type { GoogleAuthResponse } from './types'; +import { useGoogleConfig } from '@/providers/ClientGoogleProvider'; export function LoginForm() { const [email, setEmail] = useState(''); @@ -28,7 +29,7 @@ export function LoginForm() { const searchParams = useSearchParams(); const redirect = searchParams.get('redirect') || '/dashboard'; - const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''; + const { clientId: googleClientId, enabled: googleEnabled } = useGoogleConfig(); useEffect(() => { if (loginMutation.isError && loginMutation.error) { @@ -97,7 +98,7 @@ export function LoginForm() { - {googleClientId && ( + {googleEnabled && googleClientId && ( <>
{googleLoading ? ( diff --git a/frontend/src/app/auth/register/RegisterForm.tsx b/frontend/src/app/auth/register/RegisterForm.tsx index 2200344..2827e79 100644 --- a/frontend/src/app/auth/register/RegisterForm.tsx +++ b/frontend/src/app/auth/register/RegisterForm.tsx @@ -27,6 +27,7 @@ import { apiClient } from '@/lib/apiClient'; import { useRegister } from './useRegister'; import { cn } from '@/lib/utils'; import type { GoogleAuthResponse } from '../login/types'; +import { useGoogleConfig } from '@/providers/ClientGoogleProvider'; function validateEmail(email: string) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); @@ -85,7 +86,7 @@ export function RegisterForm() { const searchParams = useSearchParams(); const redirect = searchParams.get('redirect') || '/dashboard'; - const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''; + const { clientId: googleClientId, enabled: googleEnabled } = useGoogleConfig(); const nameError = touched.name && name.length > 0 && name.length < 2 ? t('register.name.error') @@ -179,7 +180,7 @@ export function RegisterForm() { - {googleClientId && ( + {googleEnabled && googleClientId && ( <>
{googleLoading ? ( diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 7520941..62f2404 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -6,7 +6,7 @@ import { ThemeProvider } from "@/providers/ThemeProvider"; import { NotificationProvider } from "@/components/ui/notification"; import { I18nProvider } from "@/lib/i18n"; import { Agentation } from "agentation"; -import { GoogleOAuthProvider } from "@react-oauth/google"; +import { ClientGoogleProvider } from "@/providers/ClientGoogleProvider"; import { CookieConsent } from "@/components/ui/cookie-consent"; export const dynamic = 'force-dynamic'; @@ -37,12 +37,12 @@ export default function RootLayout({ - + {children} - + {process.env.NODE_ENV === "development" && } diff --git a/frontend/src/providers/ClientGoogleProvider.tsx b/frontend/src/providers/ClientGoogleProvider.tsx new file mode 100644 index 0000000..770a161 --- /dev/null +++ b/frontend/src/providers/ClientGoogleProvider.tsx @@ -0,0 +1,79 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { GoogleOAuthProvider } from '@react-oauth/google'; +import { apiClient } from '@/lib/apiClient'; + +interface GoogleConfig { + clientId: string; + enabled: boolean; + loading: boolean; +} + +const GoogleConfigContext = createContext({ + clientId: '', + enabled: false, + loading: true, +}); + +export function useGoogleConfig() { + return useContext(GoogleConfigContext); +} + +export function ClientGoogleProvider({ children }: { children: React.ReactNode }) { + const [config, setConfig] = useState({ + clientId: '', + enabled: false, + loading: true, + }); + + useEffect(() => { + let active = true; + apiClient + .get<{ data: { google_client_id: string; google_auth_enabled: boolean } }>('/api/v1/auth/config') + .then((res) => { + if (!active) return; + setConfig({ + clientId: res.data.google_client_id, + enabled: res.data.google_auth_enabled && !!res.data.google_client_id, + loading: false, + }); + }) + .catch((err) => { + console.error('Failed to load Google client configuration:', err); + if (!active) return; + setConfig({ + clientId: '', + enabled: false, + loading: false, + }); + }); + return () => { + active = false; + }; + }, []); + + if (config.loading) { + return ( + + {children} + + ); + } + + if (config.enabled && config.clientId) { + return ( + + + {children} + + + ); + } + + return ( + + {children} + + ); +} diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 5b9c6d5..3f308a7 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -620,6 +620,7 @@ async def google_auth_v1(body: GoogleAuthRequest): try: user = get_or_create_google_user(email=email, name=name, avatar_url=avatar_url) except Exception as exc: + logger.exception("Google authentication failed in get_or_create_google_user") return JSONResponse( status_code=500, content={"error": "USER_CREATE_FAILED", "message": str(exc)}, @@ -641,6 +642,21 @@ async def google_auth_v1(body: GoogleAuthRequest): ) +@router_v1.get("/config") +async def get_auth_config(): + """Retrieve public configuration settings, such as Google Client ID.""" + return JSONResponse( + status_code=200, + content={ + "data": { + "google_client_id": os.getenv("GOOGLE_CLIENT_ID", ""), + "google_auth_enabled": bool(os.getenv("GOOGLE_CLIENT_ID", "").strip()), + }, + "meta": {}, + }, + ) + + @router_v1.post("/refresh") async def refresh_v1(request: Request): """Refresh tokens (API v1) — accepte refresh_token en corps, retourne nouvel access_token et refresh_token.""" diff --git a/tests/test_google_auth.py b/tests/test_google_auth.py new file mode 100644 index 0000000..0e21f7b --- /dev/null +++ b/tests/test_google_auth.py @@ -0,0 +1,23 @@ +import pytest +import services.auth_service as auth_svc +from services.auth_service import get_or_create_google_user + +def test_google_user_creation(monkeypatch): + # Enable database for testing + monkeypatch.setattr(auth_svc, "USE_DATABASE", True) + monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", True) + + email = "google_test_user@example.com" + name = "Google User" + avatar_url = "https://example.com/avatar.png" + + # Create the user + user = get_or_create_google_user(email=email, name=name, avatar_url=avatar_url) + assert user is not None + assert user.email == email + assert user.name == name + assert user.avatar_url == avatar_url + + # Retrieve again + user_again = get_or_create_google_user(email=email, name=name, avatar_url="https://new-avatar.com") + assert user_again.id == user.id