7 Commits

Author SHA1 Message Date
bdcdf1a4da merge: promote Google Analytics update to release-1.0.3
All checks were successful
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
2026-08-23 10:44:45 +00:00
79e10946b8 merge: integrate Google Analytics
All checks were successful
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
2026-08-23 10:35:40 +00:00
79f463d280 feat: integrate consent-based Google Analytics 2026-08-23 10:35:33 +00:00
a09e9f17b5 fix: load OAuth secrets from server env file
All checks were successful
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
2026-08-20 23:44:36 +00:00
a40a62d600 fix: use backend API URL for Google OAuth
All checks were successful
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
2026-08-20 23:36:54 +00:00
12685c1b22 fix: resolve deployment compose conflicts
All checks were successful
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
2026-08-20 23:27:51 +00:00
3a0dd25ed2 ci: seed test database before backend tests
Some checks failed
Tests / Backend - Tests skipped: 7, passed: 27
THE-TIP-TOP-MULTIBRANCH/pipeline/head There was a failure building this commit
2026-08-20 23:15:38 +00:00
9 changed files with 100 additions and 67 deletions

12
Jenkinsfile vendored
View File

@@ -49,6 +49,7 @@ pipeline {
env.DEPLOY_NETWORK = 'tiptop-dev' env.DEPLOY_NETWORK = 'tiptop-dev'
env.FRONTEND_API_URL = "https://api-dev.dsp5-archi-o24a-4-5-g4.fr" env.FRONTEND_API_URL = "https://api-dev.dsp5-archi-o24a-4-5-g4.fr"
env.FRONTEND_GA_ID = ""
} else if (env.BRANCH_NAME.startsWith('release-')) { } else if (env.BRANCH_NAME.startsWith('release-')) {
env.DEPLOY_ENABLED = 'true' env.DEPLOY_ENABLED = 'true'
@@ -59,6 +60,7 @@ pipeline {
env.DEPLOY_NETWORK = 'tiptop-preprod' env.DEPLOY_NETWORK = 'tiptop-preprod'
env.FRONTEND_API_URL = "https://api-preprod.dsp5-archi-o24a-4-5-g4.fr" env.FRONTEND_API_URL = "https://api-preprod.dsp5-archi-o24a-4-5-g4.fr"
env.FRONTEND_GA_ID = ""
} else if (env.BRANCH_NAME == 'main') { } else if (env.BRANCH_NAME == 'main') {
env.DEPLOY_ENABLED = 'true' env.DEPLOY_ENABLED = 'true'
@@ -69,6 +71,7 @@ pipeline {
env.DEPLOY_NETWORK = 'tiptop-prod' env.DEPLOY_NETWORK = 'tiptop-prod'
env.FRONTEND_API_URL = "https://api.dsp5-archi-o24a-4-5-g4.fr" env.FRONTEND_API_URL = "https://api.dsp5-archi-o24a-4-5-g4.fr"
env.FRONTEND_GA_ID = "G-4FTNTPJDRJ"
} else { } else {
env.DEPLOY_ENABLED = 'false' env.DEPLOY_ENABLED = 'false'
@@ -78,6 +81,7 @@ pipeline {
env.COMPOSE_PROJECT = '' env.COMPOSE_PROJECT = ''
env.DEPLOY_NETWORK = '' env.DEPLOY_NETWORK = ''
env.FRONTEND_API_URL = '' env.FRONTEND_API_URL = ''
env.FRONTEND_GA_ID = ""
} }
echo "Branche : ${env.BRANCH_NAME}" echo "Branche : ${env.BRANCH_NAME}"
@@ -163,6 +167,7 @@ pipeline {
npm ci --prefer-offline --no-audit --no-fund && npm ci --prefer-offline --no-audit --no-fund &&
npx prisma generate && npx prisma generate &&
npx prisma migrate deploy && npx prisma migrate deploy &&
npx prisma db seed &&
npm test npm test
" "
''' '''
@@ -228,9 +233,10 @@ pipeline {
backend backend
docker build \ docker build \
--build-arg VITE_API_URL="${FRONTEND_API_URL}" \ --build-arg VITE_API_URL="${FRONTEND_API_URL}" \
-t "${FRONTEND_IMAGE}:${IMAGE_TAG}" \ --build-arg VITE_GA_ID="${FRONTEND_GA_ID}" \
frontend -t "${FRONTEND_IMAGE}:${IMAGE_TAG}" \
frontend
''' '''
} }
} }

View File

@@ -36,25 +36,9 @@ services:
NODE_ENV: development NODE_ENV: development
DATABASE_URL: postgresql://tiptop:tiptop_dev_password@postgres:5432/the_tip_top_db DATABASE_URL: postgresql://tiptop:tiptop_dev_password@postgres:5432/the_tip_top_db
JWT_SECRET: dev_secret_change_me JWT_SECRET: dev_secret_change_me
<<<<<<< HEAD
APP_URL: https://api-dev.dsp5-archi-o24a-4-5-g4.fr APP_URL: https://api-dev.dsp5-archi-o24a-4-5-g4.fr
API_PUBLIC_URL: https://api-dev.dsp5-archi-o24a-4-5-g4.fr API_PUBLIC_URL: https://api-dev.dsp5-archi-o24a-4-5-g4.fr
======= FRONTEND_URL: https://dev.dsp5-archi-o24a-4-5-g4.fr
APP_URL: http://192.168.1.23:5001
FRONTEND_URL: http://192.168.1.23:5173
MAIL_HOST: smtp.gmail.com
MAIL_PORT: 587
MAIL_USER: ""
MAIL_PASS: ""
MAIL_FROM: "The Tip Top <no-reply@tiptop.local>"
>>>>>>> github/Emmanuel
# Injectés par le pipeline, jamais commités. Vides, les routes de
# connexion externe répondent 503 sans bloquer le démarrage.
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
FACEBOOK_APP_ID: "${FACEBOOK_APP_ID:-}"
FACEBOOK_APP_SECRET: "${FACEBOOK_APP_SECRET:-}"
command: > command: >
sh -c " sh -c "

View File

@@ -36,25 +36,9 @@ services:
NODE_ENV: preproduction NODE_ENV: preproduction
DATABASE_URL: postgresql://tiptop_preprod:tiptop_preprod_password@postgres:5432/the_tip_top_preprod DATABASE_URL: postgresql://tiptop_preprod:tiptop_preprod_password@postgres:5432/the_tip_top_preprod
JWT_SECRET: preprod_secret_change_me JWT_SECRET: preprod_secret_change_me
<<<<<<< HEAD
APP_URL: https://api-preprod.dsp5-archi-o24a-4-5-g4.fr APP_URL: https://api-preprod.dsp5-archi-o24a-4-5-g4.fr
API_PUBLIC_URL: https://api-preprod.dsp5-archi-o24a-4-5-g4.fr API_PUBLIC_URL: https://api-preprod.dsp5-archi-o24a-4-5-g4.fr
======= FRONTEND_URL: https://preprod.dsp5-archi-o24a-4-5-g4.fr
APP_URL: http://192.168.1.23:5002
FRONTEND_URL: http://192.168.1.23:5174
MAIL_HOST: smtp.gmail.com
MAIL_PORT: 587
MAIL_USER: ""
MAIL_PASS: ""
MAIL_FROM: "The Tip Top Preprod <no-reply@tiptop.local>"
>>>>>>> github/Emmanuel
# Injectés par le pipeline, jamais commités. Vides, les routes de
# connexion externe répondent 503 sans bloquer le démarrage.
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
FACEBOOK_APP_ID: "${FACEBOOK_APP_ID:-}"
FACEBOOK_APP_SECRET: "${FACEBOOK_APP_SECRET:-}"
command: > command: >
sh -c " sh -c "

View File

@@ -36,26 +36,9 @@ services:
NODE_ENV: production NODE_ENV: production
DATABASE_URL: postgresql://tiptop_prod:tiptop_prod_password@postgres:5432/the_tip_top_prod DATABASE_URL: postgresql://tiptop_prod:tiptop_prod_password@postgres:5432/the_tip_top_prod
JWT_SECRET: prod_secret_change_me JWT_SECRET: prod_secret_change_me
<<<<<<< HEAD
APP_URL: https://api.dsp5-archi-o24a-4-5-g4.fr APP_URL: https://api.dsp5-archi-o24a-4-5-g4.fr
API_PUBLIC_URL: https://api.dsp5-archi-o24a-4-5-g4.fr API_PUBLIC_URL: https://api.dsp5-archi-o24a-4-5-g4.fr
======= FRONTEND_URL: https://dsp5-archi-o24a-4-5-g4.fr
APP_URL: http://192.168.1.23:5003
FRONTEND_URL: http://192.168.1.23:5175
MAIL_HOST: smtp.gmail.com
MAIL_PORT: 587
MAIL_USER: ""
MAIL_PASS: ""
MAIL_FROM: "The Tip Top <no-reply@tiptop.local>"
>>>>>>> github/Emmanuel
# Injectés par le pipeline, jamais commités. Vides, les routes de
# connexion externe répondent 503 sans bloquer le démarrage.
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
FACEBOOK_APP_ID: "${FACEBOOK_APP_ID:-}"
FACEBOOK_APP_SECRET: "${FACEBOOK_APP_SECRET:-}"
command: > command: >
sh -c " sh -c "

View File

@@ -8,7 +8,10 @@ RUN npm ci
COPY . . COPY . .
ARG VITE_API_URL ARG VITE_API_URL
ARG VITE_GA_ID
ENV VITE_API_URL=${VITE_API_URL} ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_GA_ID=${VITE_GA_ID}
RUN npm run build RUN npm run build

View File

@@ -1,40 +1,78 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { initAnalytics, trackPageView } from '../services/analytics'
export default function CookieBanner() { export default function CookieBanner() {
const [visible, setVisible] = useState(() => !localStorage.getItem('cookieConsent')) const location = useLocation()
if (!visible) return null const [consent, setConsent] = useState(() =>
localStorage.getItem('cookieConsent')
)
const visible = consent === null
// Si le consentement avait déjà été donné lors d'une précédente visite,
// Analytics est initialisé au chargement de l'application.
useEffect(() => {
if (consent !== 'accepted') return
initAnalytics()
trackPageView(location.pathname + location.search)
}, [consent, location.pathname, location.search])
const handleChoice = (choice) => { const handleChoice = (choice) => {
localStorage.setItem('cookieConsent', choice) localStorage.setItem('cookieConsent', choice)
setVisible(false) setConsent(choice)
if (choice === 'accepted') {
initAnalytics()
trackPageView(location.pathname + location.search)
}
} }
if (!visible) return null
return ( return (
<div className="fixed bottom-0 left-0 right-0 z-50 px-4 py-4 bg-white border-t border-gray-200 shadow-lg"> <div className="fixed bottom-0 left-0 right-0 z-50 px-4 py-4 bg-white border-t border-gray-200 shadow-lg">
<div className="max-w-5xl mx-auto flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div className="max-w-5xl mx-auto flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-semibold text-gray-800 mb-1">Ce site utilise des cookies</p> <p className="text-sm font-semibold text-gray-800 mb-1">
Gestion des cookies
</p>
<p className="text-xs text-gray-500 leading-relaxed"> <p className="text-xs text-gray-500 leading-relaxed">
Nous utilisons des cookies essentiels au fonctionnement du site. En continuant votre navigation, vous acceptez leur utilisation conformément à notre{' '} Les cookies strictement nécessaires au fonctionnement du site sont
<Link to="/confidentialite" className="text-[#006B6B] hover:underline">politique de confidentialité</Link>. utilisés automatiquement. Avec votre accord, nous utilisons également
Google Analytics afin de mesurer l'audience et améliorer le site. Vous
pouvez accepter ou refuser ces cookies statistiques. Consultez notre{' '}
<Link
to="/confidentialite"
className="text-[#006B6B] hover:underline"
>
politique de confidentialité
</Link>.
</p> </p>
</div> </div>
<div className="flex gap-2 flex-shrink-0 w-full sm:w-auto"> <div className="flex gap-2 flex-shrink-0 w-full sm:w-auto">
<button <button
type="button"
onClick={() => handleChoice('refused')} onClick={() => handleChoice('refused')}
className="flex-1 sm:flex-none border border-gray-300 text-gray-600 hover:bg-gray-50 text-sm font-medium px-4 py-2.5 rounded-lg transition-colors" className="flex-1 sm:flex-none border border-gray-300 text-gray-600 hover:bg-gray-50 text-sm font-medium px-4 py-2.5 rounded-lg transition-colors"
> >
Refuser Refuser
</button> </button>
<button <button
type="button"
onClick={() => handleChoice('accepted')} onClick={() => handleChoice('accepted')}
className="flex-1 sm:flex-none bg-[#006B6B] hover:bg-[#005959] text-white text-sm font-medium px-4 py-2.5 rounded-lg transition-colors" className="flex-1 sm:flex-none bg-[#006B6B] hover:bg-[#005959] text-white text-sm font-medium px-4 py-2.5 rounded-lg transition-colors"
> >
Accepter Accepter
</button> </button>
</div> </div>
</div> </div>
</div> </div>
) )

View File

@@ -17,9 +17,11 @@ export default function Connexion() {
// Navigation pleine page (et non fetch) : le flux OAuth passe par des // Navigation pleine page (et non fetch) : le flux OAuth passe par des
// redirections vers Google puis retour sur /auth/callback. // redirections vers Google puis retour sur /auth/callback.
const handleGoogleLogin = () => { const API_URL = import.meta.env.VITE_API_URL || ''
window.location.href = '/api/auth/google'
} const handleGoogleLogin = () => {
window.location.href = `${API_URL}/api/auth/google`
}
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault() e.preventDefault()

View File

@@ -0,0 +1,33 @@
const GA_ID = import.meta.env.VITE_GA_ID
export function initAnalytics() {
if (!GA_ID || window.gtag) return
const script = document.createElement('script')
script.async = true
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`
document.head.appendChild(script)
window.dataLayer = window.dataLayer || []
window.gtag = function gtag() {
window.dataLayer.push(arguments)
}
window.gtag('js', new Date())
// Les page_view sont envoyées explicitement lors des changements de route React.
window.gtag('config', GA_ID, {
send_page_view: false,
})
}
export function trackPageView(path) {
if (!GA_ID || !window.gtag) return
window.gtag('event', 'page_view', {
page_path: path,
page_location: window.location.href,
page_title: document.title,
})
}

View File

@@ -8,7 +8,7 @@ describe('CookieBanner', () => {
it("s'affiche quand aucun choix n'a été fait", () => { it("s'affiche quand aucun choix n'a été fait", () => {
renderWithProviders(<CookieBanner />) renderWithProviders(<CookieBanner />)
expect(screen.getByText(/ce site utilise des cookies/i)).toBeInTheDocument() expect(screen.getByText(/gestion des cookies/i)).toBeInTheDocument()
}) })
it('enregistre le consentement et disparaît quand on accepte', async () => { it('enregistre le consentement et disparaît quand on accepte', async () => {