Tutorial 1: Building a Complete Cookie Consent System
Tutorial 1: Building a Complete Cookie Consent System
This tutorial walks through creating a production-ready cookie consent system that handles multiple jurisdictions, remembers user preferences, and integrates with popular analytics and advertising platforms. The implementation includes both frontend and backend components, providing a complete solution.
// Frontend: Cookie consent banner component
class CookieConsentBanner {
constructor() {
this.consentEndpoint = '/api/privacy/consent';
this.cookieName = 'privacy_consent';
this.cookieMaxAge = 365 * 24 * 60 * 60; // 1 year
this.jurisdiction = null;
this.consentState = null;
this.init();
}
async init() {
// Detect user jurisdiction
this.jurisdiction = await this.detectJurisdiction();
// Check existing consent
this.consentState = this.getStoredConsent();
// Show banner if needed
if (!this.consentState || this.isConsentExpired()) {
this.renderBanner();
} else {
this.applyConsent();
}
// Listen for consent events
this.setupEventListeners();
}
async detectJurisdiction() {
try {
const response = await fetch('/api/privacy/jurisdiction');
const data = await response.json();
return data.jurisdiction; // 'EU', 'CA', 'US', etc.
} catch {
return 'EU'; // Default to most restrictive
}
}
renderBanner() {
const template = this.getTemplate();
const banner = document.createElement('div');
banner.className = 'cookie-consent-banner';
banner.innerHTML = template;
// Add to page with animation
document.body.appendChild(banner);
requestAnimationFrame(() => {
banner.classList.add('visible');
});
// Setup form handlers
this.setupFormHandlers(banner);
// Trap focus for accessibility
this.trapFocus(banner);
}
getTemplate() {
const templates = {
EU: `
<div class="consent-content">
<h2>We value your privacy</h2>
<p>We use cookies and similar technologies to provide you with a better experience,
analyze site traffic, and serve targeted advertisements. By continuing to use
this site, you consent to our use of cookies.</p>
<div class="consent-options" id="detailed-options" style="display: none;">
<div class="consent-category">
<label>
<input type="checkbox" name="necessary" checked disabled>
<strong>Necessary</strong>
<p>Required for the website to function properly</p>
</label>
</div>
<div class="consent-category">
<label>
<input type="checkbox" name="analytics">
<strong>Analytics</strong>
<p>Help us understand how visitors interact with our website</p>
</label>
</div>
<div class="consent-category">
<label>
<input type="checkbox" name="marketing">
<strong>Marketing</strong>
<p>Used to deliver relevant advertisements</p>
</label>
</div>
<div class="consent-category">
<label>
<input type="checkbox" name="preferences">
<strong>Preferences</strong>
<p>Remember your settings and preferences</p>
</label>
</div>
</div>
<div class="consent-actions">
<button class="btn-primary" data-action="accept-all">Accept All</button>
<button class="btn-secondary" data-action="accept-selected">Accept Selected</button>
<button class="btn-text" data-action="show-details">Manage Preferences</button>
<a href="/privacy-policy" class="privacy-link">Privacy Policy</a>
</div>
</div>
`,
CA: `
<div class="consent-content">
<h2>Cookie Notice</h2>
<p>We use cookies to improve your experience. You can manage your preferences below.</p>
<div class="consent-actions">
<button class="btn-primary" data-action="accept-all">OK</button>
<button class="btn-text" data-action="show-details">Manage Cookies</button>
<a href="/privacy/do-not-sell" class="ccpa-link">Do Not Sell My Personal Information</a>
</div>
</div>
`
};
return templates[this.jurisdiction] || templates.EU;
}
setupFormHandlers(banner) {
// Accept all handler
banner.querySelector('[data-action="accept-all"]').addEventListener('click', () => {
this.saveConsent({
necessary: true,
analytics: true,
marketing: true,
preferences: true
});
this.closeBanner();
});
// Accept selected handler
const acceptSelected = banner.querySelector('[data-action="accept-selected"]');
if (acceptSelected) {
acceptSelected.addEventListener('click', () => {
const consent = this.getSelectedConsent(banner);
this.saveConsent(consent);
this.closeBanner();
});
}
// Show details handler
banner.querySelector('[data-action="show-details"]').addEventListener('click', () => {
const options = banner.querySelector('#detailed-options');
options.style.display = options.style.display === 'none' ? 'block' : 'none';
// Update button text
const btn = banner.querySelector('[data-action="show-details"]');
btn.textContent = options.style.display === 'none'
? 'Manage Preferences'
: 'Hide Preferences';
});
}
getSelectedConsent(banner) {
const consent = { necessary: true }; // Always true
const checkboxes = banner.querySelectorAll('input[type="checkbox"]:not([name="necessary"])');
checkboxes.forEach(checkbox => {
consent[checkbox.name] = checkbox.checked;
});
return consent;
}
async saveConsent(consent) {
const consentRecord = {
consent,
timestamp: new Date().toISOString(),
jurisdiction: this.jurisdiction,
consentVersion: '1.0',
userAgent: navigator.userAgent,
source: 'banner'
};
// Save locally
this.setCookie(this.cookieName, JSON.stringify(consentRecord));
this.consentState = consentRecord;
// Save to server
try {
await fetch(this.consentEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(consentRecord)
});
} catch (error) {
console.error('Failed to save consent to server:', error);
}
// Apply consent immediately
this.applyConsent();
}
applyConsent() {
if (!this.consentState) return;
const consent = this.consentState.consent;
// Google Analytics
if (window.gtag) {
gtag('consent', 'update', {
'analytics_storage': consent.analytics ? 'granted' : 'denied',
'ads_storage': consent.marketing ? 'granted' : 'denied'
});
}
// Facebook Pixel
if (window.fbq) {
if (!consent.marketing) {
fbq('dataProcessingOptions', ['LDU'], 1, 1000);
}
}
// Custom event for other scripts
window.dispatchEvent(new CustomEvent('consentUpdated', {
detail: consent
}));
}
setCookie(name, value) {
const date = new Date();
date.setTime(date.getTime() + (this.cookieMaxAge * 1000));
document.cookie = `${name}=${encodeURIComponent(value)};` +
`expires=${date.toUTCString()};` +
`path=/;` +
`SameSite=Lax;` +
`Secure`;
}
closeBanner() {
const banner = document.querySelector('.cookie-consent-banner');
if (banner) {
banner.classList.remove('visible');
setTimeout(() => banner.remove(), 300);
}
}
}
// Backend: Consent API endpoint
const express = require('express');
const router = express.Router();
// POST /api/privacy/consent
router.post('/consent', async (req, res) => {
try {
const consentData = req.body;
// Validate consent data
if (!isValidConsent(consentData)) {
return res.status(400).json({ error: 'Invalid consent data' });
}
// Add server-side metadata
const enrichedConsent = {
...consentData,
ipHash: hashIP(req.ip),
sessionId: req.session.id,
serverTimestamp: new Date().toISOString()
};
// Store in database
await storeConsent(enrichedConsent);
// Update user preferences
if (req.user) {
await updateUserPreferences(req.user.id, consentData.consent);
}
res.json({ success: true, consentId: enrichedConsent.id });
} catch (error) {
console.error('Consent storage error:', error);
res.status(500).json({ error: 'Failed to save consent' });
}
});
// GET /api/privacy/jurisdiction
router.get('/jurisdiction', async (req, res) => {
try {
// Use GeoIP to determine jurisdiction
const geoData = await geoip.lookup(req.ip);
const jurisdiction = mapCountryToJurisdiction(geoData.country);
res.json({
jurisdiction,
country: geoData.country,
region: geoData.region
});
} catch (error) {
// Default to EU (most restrictive)
res.json({ jurisdiction: 'EU' });
}
});
// Helper functions
function isValidConsent(data) {
return data.consent
&& typeof data.consent === 'object'
&& data.timestamp
&& data.jurisdiction
&& data.consentVersion;
}
function hashIP(ip) {
return crypto
.createHash('sha256')
.update(ip + process.env.IP_SALT)
.digest('hex')
.substring(0, 16);
}
async function storeConsent(consentData) {
const consent = await db.consent.create({
data: {
...consentData,
id: generateConsentId()
}
});
// Also store in time-series database for analytics
await timeseriesDB.insert('consent_events', {
timestamp: consentData.serverTimestamp,
jurisdiction: consentData.jurisdiction,
categories: consentData.consent,
source: consentData.source
});
return consent;
}