# Clean Authentication & Authorization Solution ## Overview This document describes the **clean, predictable, and maintainable** authentication and authorization architecture that replaces the previous "fucking unpredictable mess." ## The Problem **Before:** - Multiple overlapping protection layers (middleware, RouteGuard, AuthGuard, Blockers, Gateways) - Hardcoded paths scattered throughout codebase - Mixed responsibilities between server and client - Inconsistent patterns across routes - Role logic in both client and server - Debugging nightmare with unclear flow ## The Solution ### Core Principle: **Single Source of Truth** All routing decisions flow through **one centralized configuration system**: ```typescript // apps/website/lib/routing/RouteConfig.ts export const routes = { auth: { login: '/auth/login', signup: '/auth/signup', // ... all auth routes }, public: { home: '/', leagues: '/leagues', // ... all public routes }, protected: { dashboard: '/dashboard', // ... all protected routes }, sponsor: { dashboard: '/sponsor/dashboard', // ... sponsor routes }, admin: { root: '/admin', users: '/admin/users', }, league: { detail: (id: string) => `/leagues/${id}`, // ... parameterized routes }, // ... etc }; ``` ### Architecture Layers #### 1. **Edge Middleware** (Simple & Clean) ```typescript // apps/website/middleware.ts export function middleware(request: NextRequest) { const hasAuthCookie = request.cookies.has('gp_session'); // Public routes from config const publicRoutes = [ routes.public.home, routes.public.leagues, routes.auth.login, // ... etc ]; if (publicRoutes.includes(pathname)) { // Handle auth route redirects return NextResponse.next(); } // Protected routes if (!hasAuthCookie) { const loginUrl = new URL(routes.auth.login, request.url); loginUrl.searchParams.set('returnTo', pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } ``` **Responsibilities:** - ✅ Check session cookie - ✅ Allow public routes - ✅ Redirect to login if no cookie - ❌ No role checking - ❌ No hardcoded paths #### 2. **Client Guards** (UX Enhancement) ```typescript // apps/website/lib/guards/AuthGuard.tsx export function AuthGuard({ children, requireAuth = true }: AuthGuardProps) { const { session, loading } = useAuth(); const router = useRouter(); useEffect(() => { if (requireAuth && !loading && !session) { const url = new URL(routes.auth.login, window.location.origin); url.searchParams.set('returnTo', window.location.pathname); router.push(url.toString()); } }, [session, loading]); if (loading) return ; if (!session && requireAuth) return null; return <>{children}; } // apps/website/lib/guards/RoleGuard.tsx export function RoleGuard({ children, requiredRoles }: RoleGuardProps) { const { session, loading } = useAuth(); const router = useRouter(); useEffect(() => { if (!loading && session && !requiredRoles.includes(session.role)) { router.push(routes.protected.dashboard); } }, [session, loading]); if (loading) return ; if (!session || !requiredRoles.includes(session.role)) return null; return <>{children}; } ``` **Responsibilities:** - ✅ Verify session exists - ✅ Show loading states - ✅ Redirect if unauthorized - ✅ Hide UI elements - ❌ Make security decisions #### 3. **API Guards** (Source of Truth) ```typescript // apps/api/src/domain/auth/AuthorizationGuard.ts @Injectable() export class AuthorizationGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const isPublic = this.reflector.getMetadata('public', handler); if (isPublic) return true; const request = context.switchToHttp().getRequest(); const userId = request.user?.userId; if (!userId) { throw new UnauthorizedException('Authentication required'); } const rolesMetadata = this.reflector.getMetadata('roles', handler); if (rolesMetadata) { const userRoles = this.authorizationService.getRolesForUser(userId); const hasRole = rolesMetadata.some(r => userRoles.includes(r)); if (!hasRole) { throw new ForbiddenException('Access denied'); } } return true; } } ``` **Responsibilities:** - ✅ Verify authentication - ✅ Check permissions - ✅ Return 401/403 - ❌ Redirect - ❌ Trust client ### Usage Examples #### Public Route ```typescript // app/leagues/page.tsx export default function LeaguesPage() { return ; } // No protection needed ``` #### Authenticated Route ```typescript // app/dashboard/layout.tsx import { AuthGuard } from '@/lib/guards/AuthGuard'; export default function DashboardLayout({ children }) { return (
{children}
); } // app/dashboard/page.tsx export default function DashboardPage() { return ; } ``` #### Role-Protected Route ```typescript // app/admin/layout.tsx import { AuthGuard } from '@/lib/guards/AuthGuard'; import { RoleGuard } from '@/lib/guards/RoleGuard'; export default function AdminLayout({ children }) { return (
{children}
); } ``` #### Scoped Route (League Admin) ```typescript // app/leagues/[id]/settings/layout.tsx import { AuthGuard } from '@/lib/guards/AuthGuard'; import { LeagueAccessGuard } from '@/components/leagues/LeagueAccessGuard'; export default function LeagueSettingsLayout({ children, params }) { return (
{children}
); } ``` #### API Endpoint ```typescript // apps/api/src/domain/league/LeagueController.ts @Controller('leagues') export class LeagueController { @Get(':leagueId/admin') @RequireAuthenticatedUser() @RequireRoles('admin') getLeagueAdmin(@Param('leagueId') leagueId: string) { // Service verifies league-specific permissions return this.leagueService.getAdminData(leagueId); } } ``` ## Benefits ### 1. **Predictable Flow** ``` User Request → Middleware (check cookie) → API (auth + authz) → Controller → Response → Client (handle errors) ``` ### 2. **Easy Debugging** ```bash # Check middleware curl -I http://localhost:3000/dashboard # Check API auth curl -I http://localhost:3000/api/admin/users \ -H "Cookie: gp_session=token" # Check client session # Browser console: console.log(useAuth().session) ``` ### 3. **i18n Ready** ```typescript // Future: Switch locales by changing config const routesDe = { ...routes, auth: { login: '/de/auth/login' } }; const routesEs = { ...routes, auth: { login: '/es/auth/login' } }; // All code uses routes.auth.login, so switching is trivial ``` ### 4. **Type Safety** ```typescript // Compile-time checking routes.league.detail('123'); // ✅ Works routes.league.detail(); // ❌ Error: requires string // Parameter validation const path = buildPath('league.detail', { id: '123' }); // ✅ const path = buildPath('league.detail', {}); // ❌ Error ``` ### 5. **Maintainable** - **One file** to change all routes - **No hardcoded paths** anywhere else - **Clear separation** of concerns - **Easy to test** each layer independently ## Migration Checklist ### Phase 1: Foundation (1 day) - [x] Create `RouteConfig.ts` with all routes - [x] Update `middleware.ts` to use route config - [x] Remove hardcoded paths from middleware ### Phase 2: Guards (2 days) - [x] Create `AuthGuard.tsx` with route config - [x] Create `RoleGuard.tsx` with route config - [x] Remove old `RouteGuard` and `AuthGuard` files - [x] Remove `AuthGateway` and `AuthorizationBlocker` ### Phase 3: Route Updates (2 days) - [ ] Update all route layouts to use new guards - [ ] Remove redundant page-level checks - [ ] Test all redirect flows ### Phase 4: API Verification (1 day) - [ ] Ensure all endpoints have proper decorators - [ ] Add missing `@Public()` or `@RequireRoles()` - [ ] Test 401/403 responses ### Phase 5: Documentation & Testing (1 day) - [ ] Update all route protection docs - [ ] Create testing checklist - [ ] Verify all scenarios work ## Testing Checklist ### Unauthenticated User - [ ] `/dashboard` → Redirects to `/auth/login?returnTo=/dashboard` - [ ] `/admin` → Redirects to `/auth/login?returnTo=/admin` - [ ] `/leagues` → Works (public) - [ ] `/auth/login` → Works (public) ### Authenticated User (Regular) - [ ] `/dashboard` → Works - [ ] `/admin` → Redirects to `/dashboard` (no role) - [ ] `/leagues` → Works (public) - [ ] `/auth/login` → Redirects to `/dashboard` ### Authenticated User (Admin) - [ ] `/dashboard` → Works - [ ] `/admin` → Works - [ ] `/admin/users` → Works ### Session Expiry - [ ] Navigate to protected route with expired session → Redirect to login - [ ] Return to original route after login → Works ### API Direct Calls - [ ] Call protected endpoint without auth → 401 - [ ] Call admin endpoint without role → 403 - [ ] Call public endpoint → 200 ## Summary This architecture eliminates the chaos by: 1. **One Source of Truth**: All routes in `RouteConfig.ts` 2. **Clear Layers**: Middleware → API → Guards → Controller 3. **No Hardcoded Paths**: Everything uses the config 4. **i18n Ready**: Easy to add localized routes 5. **Type Safe**: Compile-time route validation 6. **Easy to Debug**: Each layer has one job **Result**: Clean, predictable, secure authentication that just works.