Files
imphnen-frontend-service/apps/hackathon/src/middleware.ts
T
Maulana SodiqinandClaude fa30849d03 feat(hackathon): improve auth flow and UX
- Add email activation requirement for signup (no auto-login)
- Add form validation with React Hook Form and Zod on signup page
- Update callback page to handle email confirmation and password reset redirects
- Fix token format in API interceptor (use access_token)
- Fix middleware to use SessionToken for auth check
- Add infinite scroll with IntersectionObserver on browse teams page
- Replace all internal <a href> with <Link> components
- Add useInfiniteTeams hook for paginated team browsing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 17:40:34 +07:00

137 lines
3.6 KiB
TypeScript

import { SessionUser } from '@imphnen-frontend-service/utils';
import { hackathonApi, SessionToken } from '@imphnen-frontend-service/service';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
];
const mappingOnboardingRoutes = [
'/onboarding/user',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/teams/browse',
permissions: [],
},
{
path: '/teams/create',
permissions: [],
},
];
const mappingPublicPrefixRoutes = [
'/hackathons',
];
// Cache to prevent redundant checks (cache for 5 seconds)
const onboardingCache = new Map<string, { hasLocation: boolean; timestamp: number }>();
const CACHE_DURATION = 5000; // 5 seconds
export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pathname = url.pathname;
// Get token from cookies and user from local storage
const tokenData = SessionToken.get();
const user = SessionUser.get();
const isAuthenticated = !!tokenData?.token?.access_token;
// Allow to access the hackathon pages without authentication
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
return null;
}
// Public routes - allow everyone to view the landing page
if (mappingPublicRoutes.includes(pathname)) {
return null;
}
// 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 (isAuthenticated) return redirect('/dashboard');
return null;
}
// Require authentication for all other routes
if (!isAuthenticated) {
return redirect('/auth/login');
}
// Check if user has completed onboarding
// Skip onboarding check for onboarding routes themselves
if (!mappingOnboardingRoutes.includes(pathname)) {
try {
const userId = user?.id;
if (!userId) {
return redirect('/auth/login');
}
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 {
// First check user data (faster)
if (user?.location) {
hasLocation = true;
} else {
// Fetch from backend API
try {
const response = await hackathonApi.get('/users/me');
hasLocation = !!response.data?.data?.location;
} catch {
// If API fails, check user data as fallback
hasLocation = !!user?.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 user data
const userPermissions =
user?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return redirect('/dashboard');
}
}
return null;
};