Reverted to commit 0091584677

This commit is contained in:
gpt-engineer-app[bot]
2025-11-01 15:22:30 +00:00
parent 26e5753807
commit 133141d474
125 changed files with 2316 additions and 9102 deletions

View File

@@ -1,32 +0,0 @@
/**
* Auth0 Configuration
*
* Centralized configuration for Auth0 authentication
*/
export const auth0Config = {
domain: import.meta.env.VITE_AUTH0_DOMAIN || '',
clientId: import.meta.env.VITE_AUTH0_CLIENT_ID || '',
authorizationParams: {
redirect_uri: typeof window !== 'undefined' ? `${window.location.origin}/auth/callback` : '',
audience: import.meta.env.VITE_AUTH0_DOMAIN ? `https://${import.meta.env.VITE_AUTH0_DOMAIN}/api/v2/` : '',
scope: 'openid profile email'
},
cacheLocation: 'localstorage' as const,
useRefreshTokens: true,
useRefreshTokensFallback: true,
};
/**
* Check if Auth0 is properly configured
*/
export function isAuth0Configured(): boolean {
return !!(auth0Config.domain && auth0Config.clientId);
}
/**
* Get Auth0 Management API audience
*/
export function getManagementAudience(): string {
return `https://${auth0Config.domain}/api/v2/`;
}

View File

@@ -1,138 +0,0 @@
/**
* Auth0 Management API Helper
*
* Provides helper functions to interact with Auth0 Management API
*/
import { supabase } from '@/integrations/supabase/client';
import type { Auth0MFAStatus, Auth0RoleInfo, ManagementTokenResponse } from '@/types/auth0';
/**
* Get Auth0 Management API access token via edge function
*/
export async function getManagementToken(): Promise<string> {
const { data, error } = await supabase.functions.invoke<ManagementTokenResponse>(
'auth0-get-management-token',
{ method: 'POST' }
);
if (error || !data) {
throw new Error('Failed to get management token: ' + (error?.message || 'Unknown error'));
}
return data.access_token;
}
/**
* Get user's MFA enrollment status
*/
export async function getMFAStatus(userId: string): Promise<Auth0MFAStatus> {
try {
const token = await getManagementToken();
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
const response = await fetch(`https://${domain}/api/v2/users/${userId}/enrollments`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch MFA status');
}
const enrollments = await response.json();
return {
enrolled: enrollments.length > 0,
methods: enrollments.map((e: any) => ({
id: e.id,
type: e.type,
name: e.name,
confirmed: e.status === 'confirmed',
})),
};
} catch (error) {
console.error('Error fetching MFA status:', error);
return { enrolled: false, methods: [] };
}
}
/**
* Get user's roles from Auth0
*/
export async function getUserRoles(userId: string): Promise<Auth0RoleInfo[]> {
try {
const token = await getManagementToken();
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
const response = await fetch(`https://${domain}/api/v2/users/${userId}/roles`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch user roles');
}
const roles = await response.json();
return roles.map((role: any) => ({
id: role.id,
name: role.name,
description: role.description,
}));
} catch (error) {
console.error('Error fetching user roles:', error);
return [];
}
}
/**
* Update user metadata
*/
export async function updateUserMetadata(
userId: string,
metadata: Record<string, any>
): Promise<boolean> {
try {
const token = await getManagementToken();
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
const response = await fetch(`https://${domain}/api/v2/users/${userId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_metadata: metadata,
}),
});
return response.ok;
} catch (error) {
console.error('Error updating user metadata:', error);
return false;
}
}
/**
* Trigger MFA enrollment for user
*/
export async function triggerMFAEnrollment(redirectUri?: string): Promise<void> {
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
const redirect = redirectUri || `${window.location.origin}/settings`;
// Redirect to Auth0 MFA enrollment page
window.location.href = `https://${domain}/authorize?` +
`client_id=${clientId}&` +
`response_type=code&` +
`redirect_uri=${encodeURIComponent(redirect)}&` +
`scope=openid profile email&` +
`prompt=enroll`;
}

View File

@@ -1,387 +0,0 @@
/**
* Cache Performance Monitoring Utilities
*
* Provides tools to monitor React Query cache performance in production.
* Use sparingly - only enable when debugging performance issues.
*
* Features:
* - Cache hit/miss tracking
* - Query duration monitoring
* - Slow query detection
* - Invalidation frequency tracking
*
* @example
* import { cacheMonitor } from '@/lib/cacheMonitoring';
*
* // Start monitoring (development only)
* if (process.env.NODE_ENV === 'development') {
* cacheMonitor.start();
* }
*
* // Get metrics
* const metrics = cacheMonitor.getMetrics();
* console.log('Cache hit rate:', metrics.hitRate);
*/
import { QueryClient } from '@tanstack/react-query';
import { logger } from '@/lib/logger';
interface CacheMetrics {
hits: number;
misses: number;
hitRate: number;
totalQueries: number;
avgQueryTime: number;
slowQueries: number;
invalidations: number;
lastReset: Date;
}
interface QueryTiming {
queryKey: string;
startTime: number;
endTime?: number;
duration?: number;
status: 'pending' | 'success' | 'error';
}
class CacheMonitor {
private metrics: CacheMetrics;
private queryTimings: Map<string, QueryTiming>;
private slowQueryThreshold: number = 500; // ms
private enabled: boolean = false;
private listeners: {
onSlowQuery?: (queryKey: string, duration: number) => void;
onCacheMiss?: (queryKey: string) => void;
onInvalidation?: (queryKey: string) => void;
} = {};
constructor() {
this.metrics = this.resetMetrics();
this.queryTimings = new Map();
}
/**
* Start monitoring cache performance
* Should only be used in development or for debugging
*/
start(queryClient?: QueryClient) {
if (this.enabled) {
logger.warn('Cache monitor already started');
return;
}
this.enabled = true;
this.metrics = this.resetMetrics();
logger.info('Cache monitor started', {
slowQueryThreshold: this.slowQueryThreshold
});
// If queryClient provided, set up automatic tracking
if (queryClient) {
this.setupQueryClientTracking(queryClient);
}
}
/**
* Stop monitoring
*/
stop() {
this.enabled = false;
this.queryTimings.clear();
logger.info('Cache monitor stopped');
}
/**
* Reset all metrics
*/
reset() {
this.metrics = this.resetMetrics();
this.queryTimings.clear();
logger.info('Cache metrics reset');
}
/**
* Record a cache hit (data served from cache)
*/
recordHit(queryKey: string) {
if (!this.enabled) return;
this.metrics.hits++;
this.metrics.totalQueries++;
this.updateHitRate();
logger.debug('Cache hit', { queryKey });
}
/**
* Record a cache miss (data fetched from server)
*/
recordMiss(queryKey: string) {
if (!this.enabled) return;
this.metrics.misses++;
this.metrics.totalQueries++;
this.updateHitRate();
logger.debug('Cache miss', { queryKey });
if (this.listeners.onCacheMiss) {
this.listeners.onCacheMiss(queryKey);
}
}
/**
* Start timing a query
*/
startQuery(queryKey: string) {
if (!this.enabled) return;
const key = this.normalizeQueryKey(queryKey);
this.queryTimings.set(key, {
queryKey: key,
startTime: performance.now(),
status: 'pending'
});
}
/**
* End timing a query
*/
endQuery(queryKey: string, status: 'success' | 'error') {
if (!this.enabled) return;
const key = this.normalizeQueryKey(queryKey);
const timing = this.queryTimings.get(key);
if (!timing) {
logger.warn('Query timing not found', { queryKey: key });
return;
}
const endTime = performance.now();
const duration = endTime - timing.startTime;
timing.endTime = endTime;
timing.duration = duration;
timing.status = status;
// Update average query time
const totalTime = this.metrics.avgQueryTime * (this.metrics.totalQueries - 1) + duration;
this.metrics.avgQueryTime = totalTime / this.metrics.totalQueries;
// Check for slow query
if (duration > this.slowQueryThreshold) {
this.metrics.slowQueries++;
logger.warn('Slow query detected', {
queryKey: key,
duration: Math.round(duration),
threshold: this.slowQueryThreshold
});
if (this.listeners.onSlowQuery) {
this.listeners.onSlowQuery(key, duration);
}
}
// Clean up
this.queryTimings.delete(key);
}
/**
* Record a cache invalidation
*/
recordInvalidation(queryKey: string) {
if (!this.enabled) return;
this.metrics.invalidations++;
logger.debug('Cache invalidated', { queryKey });
if (this.listeners.onInvalidation) {
this.listeners.onInvalidation(queryKey);
}
}
/**
* Get current metrics
*/
getMetrics(): Readonly<CacheMetrics> {
return { ...this.metrics };
}
/**
* Get metrics as formatted string
*/
getMetricsReport(): string {
const m = this.metrics;
const uptimeMinutes = Math.round((Date.now() - m.lastReset.getTime()) / 1000 / 60);
return `
Cache Performance Report
========================
Uptime: ${uptimeMinutes} minutes
Total Queries: ${m.totalQueries}
Cache Hits: ${m.hits} (${(m.hitRate * 100).toFixed(1)}%)
Cache Misses: ${m.misses}
Avg Query Time: ${Math.round(m.avgQueryTime)}ms
Slow Queries: ${m.slowQueries}
Invalidations: ${m.invalidations}
`.trim();
}
/**
* Log current metrics to console
*/
logMetrics() {
console.log(this.getMetricsReport());
}
/**
* Set slow query threshold (in milliseconds)
*/
setSlowQueryThreshold(ms: number) {
this.slowQueryThreshold = ms;
logger.info('Slow query threshold updated', { threshold: ms });
}
/**
* Register event listeners
*/
on(event: 'slowQuery', callback: (queryKey: string, duration: number) => void): void;
on(event: 'cacheMiss', callback: (queryKey: string) => void): void;
on(event: 'invalidation', callback: (queryKey: string) => void): void;
on(event: string, callback: (...args: any[]) => void): void {
if (event === 'slowQuery') {
this.listeners.onSlowQuery = callback;
} else if (event === 'cacheMiss') {
this.listeners.onCacheMiss = callback;
} else if (event === 'invalidation') {
this.listeners.onInvalidation = callback;
}
}
/**
* Setup automatic tracking with QueryClient
* @private
*/
private setupQueryClientTracking(queryClient: QueryClient) {
const cache = queryClient.getQueryCache();
// Subscribe to cache updates
const unsubscribe = cache.subscribe((event) => {
if (!this.enabled) return;
const queryKey = this.normalizeQueryKey(event.query.queryKey);
if (event.type === 'updated') {
const query = event.query;
// Check if this is a cache hit or miss
if (query.state.dataUpdatedAt > 0) {
const isCacheHit = query.state.fetchStatus !== 'fetching';
if (isCacheHit) {
this.recordHit(queryKey);
} else {
this.recordMiss(queryKey);
this.startQuery(queryKey);
}
}
// Record when fetch completes
if (query.state.status === 'success' || query.state.status === 'error') {
this.endQuery(queryKey, query.state.status);
}
}
});
// Store unsubscribe function
(this as any)._unsubscribe = unsubscribe;
}
/**
* Normalize query key to string for tracking
* @private
*/
private normalizeQueryKey(queryKey: string | readonly unknown[]): string {
if (typeof queryKey === 'string') {
return queryKey;
}
return JSON.stringify(queryKey);
}
/**
* Update hit rate percentage
* @private
*/
private updateHitRate() {
if (this.metrics.totalQueries === 0) {
this.metrics.hitRate = 0;
} else {
this.metrics.hitRate = this.metrics.hits / this.metrics.totalQueries;
}
}
/**
* Reset metrics to initial state
* @private
*/
private resetMetrics(): CacheMetrics {
return {
hits: 0,
misses: 0,
hitRate: 0,
totalQueries: 0,
avgQueryTime: 0,
slowQueries: 0,
invalidations: 0,
lastReset: new Date()
};
}
}
// Singleton instance
export const cacheMonitor = new CacheMonitor();
/**
* Hook to use cache monitoring in React components
* Only use for debugging - do not leave in production code
*
* @example
* function DebugPanel() {
* const metrics = useCacheMonitoring();
*
* return (
* <div>
* <h3>Cache Stats</h3>
* <p>Hit Rate: {(metrics.hitRate * 100).toFixed(1)}%</p>
* <p>Avg Query Time: {Math.round(metrics.avgQueryTime)}ms</p>
* </div>
* );
* }
*/
export function useCacheMonitoring() {
// Re-render when metrics change (simple polling)
const [metrics, setMetrics] = React.useState(cacheMonitor.getMetrics());
React.useEffect(() => {
const interval = setInterval(() => {
setMetrics(cacheMonitor.getMetrics());
}, 1000);
return () => clearInterval(interval);
}, []);
return metrics;
}
// Only import React if using the hook
let React: any;
try {
React = require('react');
} catch {
// React not available, hook won't work but main exports still functional
}

View File

@@ -223,76 +223,6 @@ export async function connectIdentity(
}
}
/**
* Link an OAuth identity to the logged-in user's account (Manual Linking)
* Requires user to be authenticated
*/
export async function linkOAuthIdentity(
provider: OAuthProvider
): Promise<IdentityOperationResult> {
try {
const { data, error } = await supabase.auth.linkIdentity({
provider
});
if (error) throw error;
// Log audit event
const { data: { user } } = await supabase.auth.getUser();
if (user) {
await logIdentityChange(user.id, 'identity_linked', {
provider,
method: 'manual',
timestamp: new Date().toISOString()
});
}
return { success: true };
} catch (error) {
const errorMsg = getErrorMessage(error);
logger.error('Failed to link identity', {
action: 'identity_link',
provider,
error: errorMsg
});
return {
success: false,
error: errorMsg
};
}
}
/**
* Log when automatic identity linking occurs
* Called internally when Supabase automatically links identities
*/
export async function logAutomaticIdentityLinking(
userId: string,
provider: OAuthProvider,
email: string
): Promise<void> {
try {
await logIdentityChange(userId, 'identity_auto_linked', {
provider,
email,
method: 'automatic',
timestamp: new Date().toISOString()
});
logger.info('Automatic identity linking logged', {
userId,
provider,
action: 'identity_auto_linked'
});
} catch (error) {
logger.error('Failed to log automatic identity linking', {
userId,
provider,
error: getErrorMessage(error)
});
}
}
/**
* Add password authentication to an OAuth-only account

View File

@@ -95,70 +95,6 @@ export function useQueryInvalidation() {
}
},
/**
* Invalidate user profile cache
* Call this after profile updates
*/
invalidateUserProfile: (userId: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.profile.detail(userId) });
},
/**
* Invalidate profile stats cache
* Call this after profile-related changes
*/
invalidateProfileStats: (userId: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.profile.stats(userId) });
},
/**
* Invalidate profile activity cache
* Call this after activity changes
*/
invalidateProfileActivity: (userId: string) => {
queryClient.invalidateQueries({ queryKey: ['profile', 'activity', userId] });
},
/**
* Invalidate user search results
* Call this when display names change
*/
invalidateUserSearch: () => {
queryClient.invalidateQueries({ queryKey: ['users', 'search'] });
},
/**
* Invalidate admin settings cache
* Call this after updating admin settings
*/
invalidateAdminSettings: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.admin.settings() });
},
/**
* Invalidate audit logs cache
* Call this after inserting audit log entries
*/
invalidateAuditLogs: (userId?: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.admin.auditLogs(userId) });
},
/**
* Invalidate contact submissions cache
* Call this after updating contact submissions
*/
invalidateContactSubmissions: () => {
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
},
/**
* Invalidate blog posts cache
* Call this after creating/updating/deleting blog posts
*/
invalidateBlogPosts: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.admin.blogPosts() });
},
/**
* Invalidate parks listing cache
* Call this after creating/updating/deleting parks
@@ -250,147 +186,5 @@ export function useQueryInvalidation() {
queryKey: ['homepage', 'featured-parks']
});
},
/**
* Invalidate company detail cache
* Call this after updating a company
*/
invalidateCompanyDetail: (slug: string, type: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.companies.detail(slug, type) });
},
/**
* Invalidate company statistics cache
* Call this after changes affecting company stats
*/
invalidateCompanyStatistics: (id: string, type: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.companies.statistics(id, type) });
},
/**
* Invalidate company parks cache
* Call this after park changes
*/
invalidateCompanyParks: (id: string, type: 'operator' | 'property_owner') => {
queryClient.invalidateQueries({ queryKey: ['companies', 'parks', id, type] });
},
/**
* Invalidate ride model detail cache
* Call this after updating a ride model
*/
invalidateRideModelDetail: (manufacturerSlug: string, modelSlug: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.rideModels.detail(manufacturerSlug, modelSlug) });
},
/**
* Invalidate ride model statistics cache
* Call this after changes affecting model stats
*/
invalidateRideModelStatistics: (modelId: string) => {
queryClient.invalidateQueries({ queryKey: queryKeys.rideModels.statistics(modelId) });
},
/**
* Invalidate model rides cache
* Call this after ride changes
*/
invalidateModelRides: (modelId: string, limit?: number) => {
queryClient.invalidateQueries({
queryKey: queryKeys.rideModels.rides(modelId, limit),
});
},
/**
* Invalidate entity name cache
* Call this after updating an entity's name
*/
invalidateEntityName: (entityType: string, entityId: string) => {
queryClient.invalidateQueries({
queryKey: queryKeys.entities.name(entityType, entityId)
});
},
/**
* Invalidate blog post cache
* Call this after updating a blog post
*/
invalidateBlogPost: (slug: string) => {
queryClient.invalidateQueries({
queryKey: queryKeys.blog.post(slug)
});
},
/**
* Invalidate coaster stats cache
* Call this after updating ride statistics
*/
invalidateCoasterStats: (rideId: string) => {
queryClient.invalidateQueries({
queryKey: queryKeys.stats.coaster(rideId)
});
},
/**
* Invalidate email change status cache
* Call this after email change operations
*/
invalidateEmailChangeStatus: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.security.emailChangeStatus()
});
},
/**
* Invalidate sessions cache
* Call this after session operations (login, logout, revoke)
*/
invalidateSessions: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.security.sessions()
});
},
/**
* Invalidate security queries
* Call this after security-related changes (email, sessions)
*/
invalidateSecurityQueries: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.security.emailChangeStatus()
});
queryClient.invalidateQueries({
queryKey: queryKeys.security.sessions()
});
},
/**
* Smart invalidation for related entities
* Invalidates entity detail, photos, reviews, and name cache
* Call this after any entity update
*/
invalidateRelatedEntities: (entityType: string, entityId: string) => {
// Invalidate the entity itself
if (entityType === 'park') {
queryClient.invalidateQueries({
queryKey: queryKeys.parks.detail(entityId)
});
} else if (entityType === 'ride') {
queryClient.invalidateQueries({
queryKey: queryKeys.rides.detail('', entityId)
});
}
// Invalidate photos, reviews, and entity name
queryClient.invalidateQueries({
queryKey: queryKeys.photos.entity(entityType, entityId)
});
queryClient.invalidateQueries({
queryKey: queryKeys.reviews.entity(entityType as 'park' | 'ride', entityId)
});
queryClient.invalidateQueries({
queryKey: queryKeys.entities.name(entityType, entityId)
});
},
};
}

View File

@@ -62,8 +62,8 @@ export const queryKeys = {
// Photos queries
photos: {
entity: (entityType: string, entityId: string, sortBy?: string) =>
['photos', entityType, entityId, sortBy] as const,
entity: (entityType: string, entityId: string) =>
['photos', entityType, entityId] as const,
count: (entityType: string, entityId: string) =>
['photos', 'count', entityType, entityId] as const,
},
@@ -76,81 +76,5 @@ export const queryKeys = {
// Lists queries
lists: {
items: (listId: string) => ['list-items', listId] as const,
user: (userId?: string) => ['lists', 'user', userId] as const,
},
// Users queries
users: {
roles: (userId?: string) => ['users', 'roles', userId] as const,
search: (searchTerm: string) => ['users', 'search', searchTerm] as const,
},
// Admin queries
admin: {
versionAudit: ['admin', 'version-audit'] as const,
settings: () => ['admin-settings'] as const,
blogPosts: () => ['admin-blog-posts'] as const,
contactSubmissions: (statusFilter?: string, categoryFilter?: string, searchQuery?: string, showArchived?: boolean) =>
['admin-contact-submissions', statusFilter, categoryFilter, searchQuery, showArchived] as const,
auditLogs: (userId?: string) => ['admin', 'audit-logs', userId] as const,
},
// Moderation queries
moderation: {
photoSubmission: (submissionId?: string) => ['moderation', 'photo-submission', submissionId] as const,
recentActivity: ['moderation', 'recent-activity'] as const,
},
// Company queries
companies: {
all: (type: string) => ['companies', 'all', type] as const,
detail: (slug: string, type: string) => ['companies', 'detail', slug, type] as const,
statistics: (id: string, type: string) => ['companies', 'statistics', id, type] as const,
parks: (id: string, type: string, limit: number) => ['companies', 'parks', id, type, limit] as const,
},
// Profile queries
profile: {
detail: (userId: string) => ['profile', userId] as const,
activity: (userId: string, isOwn: boolean, isMod: boolean) =>
['profile', 'activity', userId, isOwn, isMod] as const,
stats: (userId: string) => ['profile', 'stats', userId] as const,
},
// Ride Models queries
rideModels: {
all: (manufacturerId: string) => ['ride-models', 'all', manufacturerId] as const,
detail: (manufacturerSlug: string, modelSlug: string) =>
['ride-models', 'detail', manufacturerSlug, modelSlug] as const,
rides: (modelId: string, limit?: number) =>
['ride-models', 'rides', modelId, limit] as const,
statistics: (modelId: string) => ['ride-models', 'statistics', modelId] as const,
},
// Settings queries
settings: {
publicNovu: () => ['public-novu-settings'] as const,
},
// Stats queries
stats: {
coaster: (rideId: string) => ['coaster-stats', rideId] as const,
},
// Blog queries
blog: {
post: (slug: string) => ['blog-post', slug] as const,
viewIncrement: (slug: string) => ['blog-view-increment', slug] as const,
},
// Entity name queries (for PhotoManagementDialog)
entities: {
name: (entityType: string, entityId: string) => ['entity-name', entityType, entityId] as const,
},
// Security queries
security: {
emailChangeStatus: () => ['email-change-status'] as const,
sessions: () => ['my-sessions'] as const,
},
} as const;