fix: resolve Google login hydration mismatch and dynamic env load
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m42s

This commit is contained in:
2026-06-07 12:21:06 +02:00
parent 5b8c29dae6
commit feea02033b
6 changed files with 127 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ import { useI18n } from '@/lib/i18n';
import { apiClient } from '@/lib/apiClient'; import { apiClient } from '@/lib/apiClient';
import { useLogin } from './useLogin'; import { useLogin } from './useLogin';
import type { GoogleAuthResponse } from './types'; import type { GoogleAuthResponse } from './types';
import { useGoogleConfig } from '@/providers/ClientGoogleProvider';
export function LoginForm() { export function LoginForm() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -28,7 +29,7 @@ export function LoginForm() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get('redirect') || '/dashboard'; const redirect = searchParams.get('redirect') || '/dashboard';
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''; const { clientId: googleClientId, enabled: googleEnabled } = useGoogleConfig();
useEffect(() => { useEffect(() => {
if (loginMutation.isError && loginMutation.error) { if (loginMutation.isError && loginMutation.error) {
@@ -97,7 +98,7 @@ export function LoginForm() {
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{googleClientId && ( {googleEnabled && googleClientId && (
<> <>
<div className="flex justify-center"> <div className="flex justify-center">
{googleLoading ? ( {googleLoading ? (

View File

@@ -27,6 +27,7 @@ import { apiClient } from '@/lib/apiClient';
import { useRegister } from './useRegister'; import { useRegister } from './useRegister';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { GoogleAuthResponse } from '../login/types'; import type { GoogleAuthResponse } from '../login/types';
import { useGoogleConfig } from '@/providers/ClientGoogleProvider';
function validateEmail(email: string) { function validateEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
@@ -85,7 +86,7 @@ export function RegisterForm() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get('redirect') || '/dashboard'; 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 const nameError = touched.name && name.length > 0 && name.length < 2
? t('register.name.error') ? t('register.name.error')
@@ -179,7 +180,7 @@ export function RegisterForm() {
</CardHeader> </CardHeader>
<CardContent className="space-y-5"> <CardContent className="space-y-5">
{googleClientId && ( {googleEnabled && googleClientId && (
<> <>
<div className="flex justify-center"> <div className="flex justify-center">
{googleLoading ? ( {googleLoading ? (

View File

@@ -6,7 +6,7 @@ import { ThemeProvider } from "@/providers/ThemeProvider";
import { NotificationProvider } from "@/components/ui/notification"; import { NotificationProvider } from "@/components/ui/notification";
import { I18nProvider } from "@/lib/i18n"; import { I18nProvider } from "@/lib/i18n";
import { Agentation } from "agentation"; import { Agentation } from "agentation";
import { GoogleOAuthProvider } from "@react-oauth/google"; import { ClientGoogleProvider } from "@/providers/ClientGoogleProvider";
import { CookieConsent } from "@/components/ui/cookie-consent"; import { CookieConsent } from "@/components/ui/cookie-consent";
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -37,12 +37,12 @@ export default function RootLayout({
<ThemeProvider attribute="class" defaultTheme="system" enableSystem={true} disableTransitionOnChange={false}> <ThemeProvider attribute="class" defaultTheme="system" enableSystem={true} disableTransitionOnChange={false}>
<I18nProvider> <I18nProvider>
<QueryProvider> <QueryProvider>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ""}> <ClientGoogleProvider>
<NotificationProvider> <NotificationProvider>
{children} {children}
<CookieConsent /> <CookieConsent />
</NotificationProvider> </NotificationProvider>
</GoogleOAuthProvider> </ClientGoogleProvider>
</QueryProvider> </QueryProvider>
</I18nProvider> </I18nProvider>
{process.env.NODE_ENV === "development" && <Agentation />} {process.env.NODE_ENV === "development" && <Agentation />}

View File

@@ -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<GoogleConfig>({
clientId: '',
enabled: false,
loading: true,
});
export function useGoogleConfig() {
return useContext(GoogleConfigContext);
}
export function ClientGoogleProvider({ children }: { children: React.ReactNode }) {
const [config, setConfig] = useState<GoogleConfig>({
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 (
<GoogleConfigContext.Provider value={config}>
{children}
</GoogleConfigContext.Provider>
);
}
if (config.enabled && config.clientId) {
return (
<GoogleConfigContext.Provider value={config}>
<GoogleOAuthProvider clientId={config.clientId}>
{children}
</GoogleOAuthProvider>
</GoogleConfigContext.Provider>
);
}
return (
<GoogleConfigContext.Provider value={config}>
{children}
</GoogleConfigContext.Provider>
);
}

View File

@@ -620,6 +620,7 @@ async def google_auth_v1(body: GoogleAuthRequest):
try: try:
user = get_or_create_google_user(email=email, name=name, avatar_url=avatar_url) user = get_or_create_google_user(email=email, name=name, avatar_url=avatar_url)
except Exception as exc: except Exception as exc:
logger.exception("Google authentication failed in get_or_create_google_user")
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
content={"error": "USER_CREATE_FAILED", "message": str(exc)}, 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") @router_v1.post("/refresh")
async def refresh_v1(request: Request): async def refresh_v1(request: Request):
"""Refresh tokens (API v1) — accepte refresh_token en corps, retourne nouvel access_token et refresh_token.""" """Refresh tokens (API v1) — accepte refresh_token en corps, retourne nouvel access_token et refresh_token."""

23
tests/test_google_auth.py Normal file
View File

@@ -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