From ecc2f8d2613866eea60c6ab1d22d461d04e99f04 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:06:38 +0000 Subject: [PATCH 1/3] fix: resolve Google Auth 401, remove simulation mode, round sign-in button, and strengthen CSP headers - Extracted Google Analytics and Service Worker registration inline scripts to dedicated external files in `public/`. - Removed simulation mode from the unsubscribe route entirely, maintaining backend test mail support. - Configured dynamic Google Client ID for both front-end and back-end integration. - Configured the Google One Tap / Sign-In button with a modern rounded pill shape. - Strengthened CSP headers on both dev/production servers and vercel.json configurations by removing 'unsafe-inline' and 'unsafe-eval' directives. Co-authored-by: Simonc44 <216070312+Simonc44@users.noreply.github.com> From f6182d965db13dafb953386556347d1dae0c2c77 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 00:08:24 +0000 Subject: [PATCH 2/3] fix: resolve Google OAuth 401, remove simulation mode, modernize Google login button, and implement secure dynamic CSP with nonces - Configured GOOGLE_CLIENT_ID to use process.env via secure TanStack server loader and function, fixing the 401 invalid_client error. - Removed simulation mode visual card and states from /desabonnement to clean up development leftovers. - Modernized Google login button layout using pill shape borders. - hardended Content Security Policy (CSP) headers with 'default-src none' and removed 'unsafe-inline' for scripts and styles. - Extracted inline Google Analytics and Service Worker scripts to standalone script files in public. - Implemented dynamic cryptographic nonces generated per-request and passed them cleanly to TanStack Start's SSR nonce configurations. - Rendered csp-nonce meta tags to support isomorphic client hydration. - Solved undici Request construction bug inside server proxy middleware. Co-authored-by: Simonc44 <216070312+Simonc44@users.noreply.github.com> --- .env.example | 4 ++ public/gtag-init.js | 24 +++++++ public/sw-register.js | 7 ++ src/lib/google-auth.server.ts | 2 +- src/lib/google-config.functions.ts | 10 +++ src/router.tsx | 25 +++++++ src/routes/__root.tsx | 49 ++----------- src/routes/desabonnement.tsx | 107 +++++++---------------------- src/server.ts | 58 ++++++++++++---- src/start.ts | 11 ++- vercel.json | 2 +- 11 files changed, 156 insertions(+), 143 deletions(-) create mode 100644 public/gtag-init.js create mode 100644 public/sw-register.js create mode 100644 src/lib/google-config.functions.ts diff --git a/.env.example b/.env.example index 0ab3b7a..c5709c8 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,10 @@ TURSO_DATABASE_URL=libsql://votre-base.turso.io TURSO_AUTH_TOKEN=votre_token_ici +# ─── Google Authentification (Optionnel) ────────────────────────────────────── +# Clé client Google OAuth pour l'authentification et gestion des abonnements +GOOGLE_CLIENT_ID=788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com + # ─── API CIVIX (optionnel en développement local) ───────────────────────────── # https://civix.fr CIVIX_API_KEY= diff --git a/public/gtag-init.js b/public/gtag-init.js new file mode 100644 index 0000000..39b740a --- /dev/null +++ b/public/gtag-init.js @@ -0,0 +1,24 @@ +window.dataLayer = window.dataLayer || []; +function gtag(){dataLayer.push(arguments);} +gtag('js', new Date()); +gtag('consent', 'default', { + 'analytics_storage': 'denied', + 'ad_storage': 'denied', + 'ad_user_data': 'denied', + 'ad_personalization': 'denied', + 'wait_for_update': 500 +}); +gtag('config', 'G-CMMWPQG5P6'); +(function() { + try { + var saved = localStorage.getItem('mandat_analytics_consent'); + if (saved === 'granted') { + gtag('consent', 'update', { + 'analytics_storage': 'granted', + 'ad_storage': 'denied', + 'ad_user_data': 'denied', + 'ad_personalization': 'denied' + }); + } + } catch(e) {} +})(); diff --git a/public/sw-register.js b/public/sw-register.js new file mode 100644 index 0000000..acba5f1 --- /dev/null +++ b/public/sw-register.js @@ -0,0 +1,7 @@ +if ('serviceWorker' in navigator) { + window.addEventListener('load', function() { + navigator.serviceWorker.register('/sw.js').catch(function(e) { + console.warn('[SW] Registration failed:', e); + }); + }); +} diff --git a/src/lib/google-auth.server.ts b/src/lib/google-auth.server.ts index d10f73b..e9a497c 100644 --- a/src/lib/google-auth.server.ts +++ b/src/lib/google-auth.server.ts @@ -1,7 +1,7 @@ // src/lib/google-auth.server.ts // Validation serveur du JWT de connexion Google -const GOOGLE_CLIENT_ID = "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com"; +const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com"; export interface GoogleUserSession { email: string; diff --git a/src/lib/google-config.functions.ts b/src/lib/google-config.functions.ts new file mode 100644 index 0000000..ebd5495 --- /dev/null +++ b/src/lib/google-config.functions.ts @@ -0,0 +1,10 @@ +// src/lib/google-config.functions.ts +import { createServerFn } from "@tanstack/react-start"; + +export const getGoogleClientId = createServerFn({ method: "GET" }).handler( + async () => { + return { + googleClientId: process.env.GOOGLE_CLIENT_ID || "788417855681-3g32890scg7on4tq0fksb5aocn9s6u10.apps.googleusercontent.com" + }; + } +); diff --git a/src/router.tsx b/src/router.tsx index e528df1..8269b36 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,6 +1,26 @@ import { QueryClient } from "@tanstack/react-query"; import { createRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; +import { createIsomorphicFn, getGlobalStartContext } from "@tanstack/react-start"; + +export const getCspNonce = createIsomorphicFn() + .server(() => { + try { + const ctx = getGlobalStartContext(); + return (ctx?.cspNonce as string) || ""; + } catch (e) { + return ""; + } + }) + .client(() => { + if (typeof document !== "undefined") { + const el = document.querySelector( + "meta[property=csp-nonce]", + ) as HTMLMetaElement | null; + return el?.content || ""; + } + return ""; + }); export const getRouter = () => { // Query client partagé (nouveau à chaque requête SSR, réutilisé côté client). @@ -17,6 +37,8 @@ export const getRouter = () => { }, }); + const nonce = getCspNonce(); + const router = createRouter({ routeTree, context: { queryClient }, @@ -27,6 +49,9 @@ export const getRouter = () => { defaultPreloadStaleTime: 0, // Cache le HTML rendu pendant 30s en mémoire pour retour arrière rapide. defaultStaleTime: 1000 * 30, + ssr: { + nonce, + }, }); return router; diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 5a5642a..f6c2fd7 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -18,6 +18,7 @@ import { SpeedInsights } from "@vercel/speed-insights/react"; import { Analytics } from "@vercel/analytics/react"; import { Header } from "../components/Header"; import { Footer } from "../components/Footer"; +import { getCspNonce } from "../router"; import { CookieBanner } from "../components/CookieBanner"; import { PWAInstallPrompt } from "../components/PWAInstallPrompt"; import { LoadingOverlay } from "../components/LoadingOverlay"; @@ -281,57 +282,19 @@ export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()( // ─── SHELL & COMPONENT ────────────────────────────────────────────────── function RootShell({ children }: { children: ReactNode }) { + const nonce = getCspNonce(); + return (