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.
This commit is contained in:
Van Leemput Dayron
2026-02-24 08:25:37 +01:00
parent d3e0570214
commit 46e6edcd9c
30 changed files with 3469 additions and 324 deletions

View File

@@ -2,8 +2,8 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
export interface CookieConsent {
essential: boolean; // Always true
preferences: boolean; // Language, Theme
analytics: boolean; // Optional future use
preferences: boolean; // Theme, UI preferences
analytics: boolean; // Measurement / analytics
}
interface CookieContextType {
@@ -16,12 +16,34 @@ interface CookieContextType {
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 }) => {
@@ -29,36 +51,92 @@ export const CookieProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const [hasInteracted, setHasInteracted] = useState(false);
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
const savedConsent = localStorage.getItem('cookie_consent');
if (savedConsent) {
try {
const parsed = JSON.parse(savedConsent);
setConsent({ ...defaultConsent, ...parsed, essential: true });
setHasInteracted(true);
setShowBanner(false);
} catch (e) {
// Invalid json, treat as no consent
setShowBanner(true);
}
} else {
setShowBanner(true);
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 = (newConsent: CookieConsent) => {
localStorage.setItem('cookie_consent', JSON.stringify(newConsent));
setConsent(newConsent);
setHasInteracted(true);
setShowBanner(false);
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) => {
saveConsent({ ...newConsent, essential: true });
void saveConsent({ ...newConsent, essential: true });
};
const acceptAll = () => {
saveConsent({
void saveConsent({
essential: true,
preferences: true,
analytics: true,
@@ -66,7 +144,7 @@ export const CookieProvider: React.FC<{ children: React.ReactNode }> = ({ childr
};
const rejectAll = () => {
saveConsent({
void saveConsent({
essential: true,
preferences: false,
analytics: false,