2025-11-25 01:47:56 +07:00
|
|
|
import { SessionUser } from '@imphnen-frontend-service/utils';
|
2025-11-28 17:40:34 +07:00
|
|
|
import { hackathonApi, SessionToken } 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
|
|
|
|
2025-11-28 17:40:34 +07:00
|
|
|
// Get token from cookies and user from local storage
|
|
|
|
|
const tokenData = SessionToken.get();
|
|
|
|
|
const user = SessionUser.get();
|
|
|
|
|
const isAuthenticated = !!tokenData?.token?.access_token;
|
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')) {
|
2025-11-28 15:57:30 +07:00
|
|
|
if (isAuthenticated) return redirect('/dashboard');
|
2025-11-25 01:47:56 +07:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 15:57:30 +07:00
|
|
|
// Require authentication for all other routes
|
|
|
|
|
if (!isAuthenticated) {
|
2025-11-25 01:47:56 +07:00
|
|
|
return redirect('/auth/login');
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 15:57:30 +07:00
|
|
|
// Check if user has completed onboarding
|
2025-11-25 01:47:56 +07:00
|
|
|
// Skip onboarding check for onboarding routes themselves
|
|
|
|
|
if (!mappingOnboardingRoutes.includes(pathname)) {
|
|
|
|
|
try {
|
2025-11-28 17:40:34 +07:00
|
|
|
const userId = user?.id;
|
2025-11-28 15:57:30 +07:00
|
|
|
if (!userId) {
|
|
|
|
|
return redirect('/auth/login');
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 01:47:56 +07:00
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
const cached = onboardingCache.get(userId);
|
|
|
|
|
let hasLocation = false;
|
|
|
|
|
|
|
|
|
|
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
|
|
|
|
|
hasLocation = cached.hasLocation;
|
|
|
|
|
} else {
|
2025-11-28 17:40:34 +07:00
|
|
|
// First check user data (faster)
|
|
|
|
|
if (user?.location) {
|
2025-11-28 15:57:30 +07:00
|
|
|
hasLocation = true;
|
|
|
|
|
} else {
|
|
|
|
|
// Fetch from backend API
|
|
|
|
|
try {
|
|
|
|
|
const response = await hackathonApi.get('/users/me');
|
|
|
|
|
hasLocation = !!response.data?.data?.location;
|
|
|
|
|
} catch {
|
2025-11-28 17:40:34 +07:00
|
|
|
// If API fails, check user data as fallback
|
|
|
|
|
hasLocation = !!user?.location;
|
2025-11-28 15:57:30 +07:00
|
|
|
}
|
2025-11-25 01:47:56 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-28 17:40:34 +07:00
|
|
|
// Check route permissions using user data
|
2025-11-25 01:47:56 +07:00
|
|
|
const userPermissions =
|
2025-11-28 17:40:34 +07:00
|
|
|
user?.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;
|
|
|
|
|
};
|