Add cookie consent widget with analytics tunnel bypass for adblock

- Create cookie-consent.js module: banner show/hide, cookie management, conditional analytics loading
- Add cookie banner widget in base.html.twig (accept/refuse buttons)
- Analytics script loaded from /stats/ tunnel (bypass adblock) with data-host-url
- Add Caddy reverse proxy tunnel /stats/* -> tools-security.esy-web.dev
- Add tools-security.esy-web.dev to CSP connect-src
- Add 9 JS tests for cookie consent
- Revert manual composer.json edit for amazon-mailer (needs composer require)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-20 16:02:36 +01:00
parent 99e5428208
commit 518642551c
7 changed files with 168 additions and 1 deletions

View File

@@ -9,6 +9,13 @@ ticket.e-cosplay.fr {
file_server
}
handle_path /stats/* {
rewrite * {uri}
reverse_proxy https://tools-security.esy-web.dev {
header_up Host tools-security.esy-web.dev
}
}
@maintenance file /var/www/e-ticket/public/.update
handle @maintenance {
root * /var/www/e-ticket/public

View File

@@ -2,9 +2,11 @@ import "./app.scss"
import { initMobileMenu } from "./modules/mobile-menu.js"
import { initTabs } from "./modules/tabs.js"
import { registerEditor } from "./modules/editor.js"
import { initCookieConsent } from "./modules/cookie-consent.js"
document.addEventListener('DOMContentLoaded', () => {
initMobileMenu()
initTabs()
registerEditor()
initCookieConsent()
})

View File

@@ -0,0 +1,67 @@
const COOKIE_NAME = 'e_ticket_consent'
const COOKIE_DAYS = 365
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : null
}
function setCookie(name, value, days) {
const date = new Date()
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
document.cookie = name + '=' + value + ';expires=' + date.toUTCString() + ';path=/;SameSite=Lax;Secure'
}
function loadAnalytics() {
if (document.querySelector('script[data-analytics]')) {
return
}
const script = document.createElement('script')
script.defer = true
script.src = '/stats/script.js'
script.dataset.websiteId = 'a1f85dd5-741f-4df7-840a-7ef0931ed0cc'
script.dataset.hostUrl = '/stats'
script.dataset.analytics = '1'
document.head.appendChild(script)
}
export function initCookieConsent() {
const consent = getCookie(COOKIE_NAME)
if ('accepted' === consent) {
loadAnalytics()
return
}
if ('refused' === consent) {
return
}
const banner = document.getElementById('cookie-banner')
if (!banner) {
return
}
banner.classList.remove('hidden')
const acceptBtn = document.getElementById('cookie-accept')
const refuseBtn = document.getElementById('cookie-refuse')
if (acceptBtn) {
acceptBtn.addEventListener('click', () => {
setCookie(COOKIE_NAME, 'accepted', COOKIE_DAYS)
banner.classList.add('hidden')
loadAnalytics()
})
}
if (refuseBtn) {
refuseBtn.addEventListener('click', () => {
setCookie(COOKIE_NAME, 'refused', COOKIE_DAYS)
banner.classList.add('hidden')
})
}
}

View File

@@ -22,7 +22,6 @@
"phpstan/phpdoc-parser": "^2.3",
"stevenmaguire/oauth2-keycloak": "^6.1",
"stripe/stripe-php": "*",
"symfony/amazon-mailer": "8.0.*",
"symfony/asset": "8.0.*",
"symfony/console": "8.0.*",
"symfony/doctrine-messenger": "8.0.*",

View File

@@ -44,6 +44,7 @@ nelmio_security:
- 'self'
- 'https://cloudflareinsights.com'
- 'https://static.cloudflareinsights.com'
- 'https://tools-security.esy-web.dev'
font-src:
- 'self'
- 'https://cdnjs.cloudflare.com'

View File

@@ -181,5 +181,17 @@
</div>
</div>
</footer>
<div id="cookie-banner" class="hidden fixed bottom-0 left-0 right-0 z-50 border-t-4 border-gray-900 bg-white p-4">
<div class="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-sm font-bold text-gray-700">
Ce site utilise des cookies pour mesurer l'audience. <a href="{{ path('app_cookies') }}" class="text-indigo-600 hover:underline">En savoir plus</a>
</p>
<div class="flex gap-2">
<button id="cookie-refuse" class="px-4 py-2 border-2 border-gray-900 bg-white font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all cursor-pointer">Refuser</button>
<button id="cookie-accept" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-white transition-all cursor-pointer">Accepter</button>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { initCookieConsent } from '../../assets/modules/cookie-consent.js'
describe('initCookieConsent', () => {
beforeEach(() => {
document.cookie = 'e_ticket_consent=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
document.querySelectorAll('script[data-analytics]').forEach(s => s.remove())
document.body.innerHTML = `
<div id="cookie-banner" class="hidden">
<button id="cookie-accept"></button>
<button id="cookie-refuse"></button>
</div>
`
})
it('shows banner when no consent cookie', () => {
initCookieConsent()
const banner = document.getElementById('cookie-banner')
expect(banner.classList.contains('hidden')).toBe(false)
})
it('hides banner and sets cookie on accept', () => {
initCookieConsent()
document.getElementById('cookie-accept').click()
const banner = document.getElementById('cookie-banner')
expect(banner.classList.contains('hidden')).toBe(true)
expect(document.cookie).toContain('e_ticket_consent=accepted')
})
it('hides banner and sets cookie on refuse', () => {
initCookieConsent()
document.getElementById('cookie-refuse').click()
const banner = document.getElementById('cookie-banner')
expect(banner.classList.contains('hidden')).toBe(true)
expect(document.cookie).toContain('e_ticket_consent=refused')
})
it('does not show banner if already accepted', () => {
document.cookie = 'e_ticket_consent=accepted;path=/'
initCookieConsent()
const banner = document.getElementById('cookie-banner')
expect(banner.classList.contains('hidden')).toBe(true)
})
it('does not show banner if already refused', () => {
document.cookie = 'e_ticket_consent=refused;path=/'
initCookieConsent()
const banner = document.getElementById('cookie-banner')
expect(banner.classList.contains('hidden')).toBe(true)
})
it('does nothing without banner element', () => {
document.body.innerHTML = ''
expect(() => initCookieConsent()).not.toThrow()
})
it('loads analytics script on accept', () => {
initCookieConsent()
document.getElementById('cookie-accept').click()
const script = document.querySelector('script[data-analytics]')
expect(script).not.toBeNull()
expect(script.src).toContain('/stats/script.js')
expect(script.dataset.websiteId).toBe('a1f85dd5-741f-4df7-840a-7ef0931ed0cc')
})
it('does not load analytics on refuse', () => {
initCookieConsent()
document.getElementById('cookie-refuse').click()
const script = document.querySelector('script[data-analytics]')
expect(script).toBeNull()
})
it('loads analytics immediately if already accepted', () => {
document.cookie = 'e_ticket_consent=accepted;path=/'
initCookieConsent()
const script = document.querySelector('script[data-analytics]')
expect(script).not.toBeNull()
})
})