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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user