Files
xeewy.be/frontend/src/contexts/CookieContext.tsx
Van Leemput Dayron 46e6edcd9c feat: add profile setup, verification, and authentication context
- Implement ProfileSetup component for user profile creation with username validation.
- Create ProtectedRoute component to guard routes based on authentication status.
- Add VerifyAuthCode component for email verification with resend functionality.
- Establish AuthContext for managing user authentication state and actions.
- Enhance CookieContext for consent management with server-side verification.
- Set up Firebase configuration for authentication and analytics.
- Update styles for authentication components and header layout.
- Configure Vite proxy for API requests during development.
2026-02-24 08:25:37 +01:00

180 lines
5.4 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from 'react';
export interface CookieConsent {
essential: boolean; // Always true
preferences: boolean; // Theme, UI preferences
analytics: boolean; // Measurement / analytics
}
interface CookieContextType {
consent: CookieConsent;
updateConsent: (newConsent: CookieConsent) => void;
acceptAll: () => void;
rejectAll: () => void;
hasInteracted: boolean; // True if user has made a choice
showBanner: boolean;
closeBanner: () => void;
}
const CONSENT_COOKIE_NAME = 'xeewy_cookie_consent';
const CONSENT_COOKIE_MAX_AGE_DAYS = 180;
const defaultConsent: CookieConsent = {
essential: true,
preferences: false,
analytics: false,
};
const getCookie = (name: string): string | null => {
const prefix = `${name}=`;
const parts = document.cookie.split(';').map((part) => part.trim());
const found = parts.find((part) => part.startsWith(prefix));
if (!found) return null;
return found.slice(prefix.length);
};
const setCookie = (name: string, value: string, days: number) => {
const maxAge = days * 24 * 60 * 60;
const securePart = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${name}=${value}; Max-Age=${maxAge}; Path=/; SameSite=Lax${securePart}`;
};
const removeCookie = (name: string) => {
const securePart = window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax${securePart}`;
};
const CookieContext = createContext<CookieContextType | undefined>(undefined);
export const CookieProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [consent, setConsent] = useState<CookieConsent>(defaultConsent);
const [hasInteracted, setHasInteracted] = useState(false);
const [showBanner, setShowBanner] = useState(false);
const signConsent = async (rawConsent: CookieConsent) => {
const response = await fetch('/api/cookies/sign-consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
consent: {
essential: true,
preferences: Boolean(rawConsent.preferences),
analytics: Boolean(rawConsent.analytics)
}
})
});
const payload = (await response.json().catch(() => null)) as { token?: string; message?: string } | null;
if (!response.ok || !payload?.token) {
throw new Error(payload?.message || 'Unable to sign consent.');
}
return payload.token;
};
const verifyConsentToken = async (token: string) => {
const response = await fetch('/api/cookies/verify-consent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const payload = (await response.json().catch(() => null)) as {
consent?: Partial<CookieConsent>;
message?: string;
} | null;
if (!response.ok || !payload?.consent) {
return null;
}
return {
...defaultConsent,
...payload.consent,
essential: true
} as CookieConsent;
};
useEffect(() => {
const savedConsent = getCookie(CONSENT_COOKIE_NAME);
const bootstrapConsent = async () => {
if (!savedConsent) {
setShowBanner(true);
return;
}
const verified = await verifyConsentToken(savedConsent);
if (!verified) {
removeCookie(CONSENT_COOKIE_NAME);
setConsent(defaultConsent);
setHasInteracted(false);
setShowBanner(true);
return;
}
setConsent(verified);
setHasInteracted(true);
setShowBanner(false);
};
void bootstrapConsent();
}, []);
const saveConsent = async (newConsent: CookieConsent) => {
try {
const token = await signConsent(newConsent);
setCookie(CONSENT_COOKIE_NAME, token, CONSENT_COOKIE_MAX_AGE_DAYS);
setConsent(newConsent);
setHasInteracted(true);
setShowBanner(false);
} catch {
// Keep banner visible if signing fails
setShowBanner(true);
}
};
const updateConsent = (newConsent: CookieConsent) => {
void saveConsent({ ...newConsent, essential: true });
};
const acceptAll = () => {
void saveConsent({
essential: true,
preferences: true,
analytics: true,
});
};
const rejectAll = () => {
void saveConsent({
essential: true,
preferences: false,
analytics: false,
});
};
const closeBanner = () => {
setShowBanner(false);
};
return (
<CookieContext.Provider value={{
consent,
updateConsent,
acceptAll,
rejectAll,
hasInteracted,
showBanner,
closeBanner
}}>
{children}
</CookieContext.Provider>
);
};
export const useCookie = () => {
const context = useContext(CookieContext);
if (!context) {
throw new Error('useCookie must be used within a CookieProvider');
}
return context;
};