2025-11-25 01:47:56 +07:00
|
|
|
import { SessionUser } from '@imphnen-frontend-service/utils';
|
|
|
|
|
import { supabase } from '@imphnen-frontend-service/service';
|
2025-11-24 12:51:41 +07:00
|
|
|
import { LoaderFunctionArgs, redirect } from 'react-router';
|
|
|
|
|
|
|
|
|
|
const mappingPublicRoutes = [
|
|
|
|
|
'/',
|
|
|
|
|
];
|
|
|
|
|
|
2025-11-25 01:47:56 +07:00
|
|
|
const mappingOnboardingRoutes = [
|
|
|
|
|
'/onboarding/user',
|
|
|
|
|
];
|
|
|
|
|
|
2025-11-24 12:51:41 +07:00
|
|
|
const mappingRoutePermissions = [
|
|
|
|
|
{
|
|
|
|
|
path: '/dashboard',
|
|
|
|
|
permissions: [],
|
|
|
|
|
},
|
2025-11-25 01:47:56 +07:00
|
|
|
{
|
|
|
|
|
path: '/teams/browse',
|
|
|
|
|
permissions: [],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/teams/create',
|
|
|
|
|
permissions: [],
|
|
|
|
|
},
|
2025-11-24 12:51:41 +07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const mappingPublicPrefixRoutes = [
|
|
|
|
|
'/hackathons',
|
|
|
|
|
];
|
|
|
|
|
|
2025-11-25 01:47:56 +07:00
|
|
|
// Cache to prevent redundant checks (cache for 5 seconds)
|
|
|
|
|
const onboardingCache = new Map<string, { hasLocation: boolean; timestamp: number }>();
|
|
|
|
|
const CACHE_DURATION = 5000; // 5 seconds
|
|
|
|
|
|
2025-11-24 12:51:41 +07:00
|
|
|
export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|
|
|
|
const url = new URL(request.url);
|
|
|
|
|
const pathname = url.pathname;
|
2025-11-25 01:47:56 +07:00
|
|
|
|
|
|
|
|
console.log('[Middleware] Checking route:', pathname);
|
|
|
|
|
|
|
|
|
|
// Get session from Supabase (authoritative source)
|
|
|
|
|
const { data: { session: supabaseSession }, error: sessionError } = await supabase.auth.getSession();
|
|
|
|
|
|
|
|
|
|
// Handle session errors
|
|
|
|
|
if (sessionError) {
|
|
|
|
|
console.error('[Middleware] Session error:', sessionError);
|
|
|
|
|
// Don't redirect on session errors, let the app handle it
|
|
|
|
|
}
|
2025-11-24 12:51:41 +07:00
|
|
|
|
|
|
|
|
// Allow to access the hackathon pages without authentication
|
|
|
|
|
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 11:01:41 +07:00
|
|
|
// Public routes - allow everyone to view the landing page
|
2025-11-24 12:51:41 +07:00
|
|
|
if (mappingPublicRoutes.includes(pathname)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 01:47:56 +07:00
|
|
|
// Auth callback - allow without authentication check (for OAuth callback)
|
|
|
|
|
if (pathname === '/auth/callback') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auth routes (all /auth/* paths) - redirect to dashboard if already authenticated
|
|
|
|
|
if (pathname.startsWith('/auth')) {
|
|
|
|
|
if (supabaseSession) return redirect('/dashboard');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Require authentication for all other routes - ONLY check Supabase session
|
|
|
|
|
if (!supabaseSession) {
|
|
|
|
|
return redirect('/auth/login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if user has completed onboarding by querying database (not localStorage!)
|
|
|
|
|
// Skip onboarding check for onboarding routes themselves
|
|
|
|
|
if (!mappingOnboardingRoutes.includes(pathname)) {
|
|
|
|
|
try {
|
|
|
|
|
const userId = supabaseSession.user.id;
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
const cached = onboardingCache.get(userId);
|
|
|
|
|
let hasLocation = false;
|
|
|
|
|
|
|
|
|
|
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
|
|
|
|
console.log('[Middleware] Using cached onboarding status');
|
|
|
|
|
hasLocation = cached.hasLocation;
|
|
|
|
|
} else {
|
|
|
|
|
console.log('[Middleware] Fetching fresh onboarding status');
|
|
|
|
|
const { data: userData, error: userError } = await supabase
|
|
|
|
|
.from('users')
|
|
|
|
|
.select('location')
|
|
|
|
|
.eq('id', userId)
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
if (userError) {
|
|
|
|
|
console.error('[Middleware] Failed to fetch user data:', userError);
|
|
|
|
|
// If we can't fetch user data, allow access (don't break the app)
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasLocation = !!userData?.location;
|
|
|
|
|
|
|
|
|
|
// Update cache
|
|
|
|
|
onboardingCache.set(userId, { hasLocation, timestamp: now });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasLocation) {
|
|
|
|
|
return redirect('/onboarding/user');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[Middleware] Unexpected error checking onboarding:', error);
|
|
|
|
|
// On error, allow access (fail open)
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check route permissions using fresh user data from Zustand (for UI metadata)
|
|
|
|
|
const session = SessionUser.get();
|
|
|
|
|
const userPermissions =
|
|
|
|
|
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
2025-11-24 12:51:41 +07:00
|
|
|
|
|
|
|
|
const matchedRoute = mappingRoutePermissions.find(
|
|
|
|
|
(route) => route.path === pathname
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (matchedRoute) {
|
|
|
|
|
const hasPermission =
|
|
|
|
|
!matchedRoute.permissions ||
|
|
|
|
|
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
|
|
|
|
|
|
|
|
|
if (!hasPermission) {
|
2025-11-25 01:47:56 +07:00
|
|
|
return redirect('/dashboard');
|
2025-11-24 12:51:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|