fix: resolve Google login hydration mismatch and dynamic env load
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m42s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m42s
This commit is contained in:
@@ -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() {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{googleClientId && (
|
||||
{googleEnabled && googleClientId && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
{googleLoading ? (
|
||||
|
||||
@@ -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() {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-5">
|
||||
{googleClientId && (
|
||||
{googleEnabled && googleClientId && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
{googleLoading ? (
|
||||
|
||||
@@ -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({
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem={true} disableTransitionOnChange={false}>
|
||||
<I18nProvider>
|
||||
<QueryProvider>
|
||||
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ""}>
|
||||
<ClientGoogleProvider>
|
||||
<NotificationProvider>
|
||||
{children}
|
||||
<CookieConsent />
|
||||
</NotificationProvider>
|
||||
</GoogleOAuthProvider>
|
||||
</ClientGoogleProvider>
|
||||
</QueryProvider>
|
||||
</I18nProvider>
|
||||
{process.env.NODE_ENV === "development" && <Agentation />}
|
||||
|
||||
79
frontend/src/providers/ClientGoogleProvider.tsx
Normal file
79
frontend/src/providers/ClientGoogleProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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."""
|
||||
|
||||
23
tests/test_google_auth.py
Normal file
23
tests/test_google_auth.py
Normal 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
|
||||
Reference in New Issue
Block a user