Files
gridpilot.gg/docs/architecture/QUICK_AUTH_REFERENCE.md
2026-01-03 02:42:47 +01:00

276 lines
6.2 KiB
Markdown

# Quick Reference: Clean Authentication & Authorization
## The Golden Rules
1. **API is the source of truth** - Never trust the client for security
2. **Client is UX only** - Redirect, show loading, hide buttons
3. **One clear flow** - Middleware → API → Guard → Controller
4. **Roles are server-side** - Client only knows "can access" or "can't"
## What Goes Where
### Server-Side (API)
```typescript
// ✅ DO: Check permissions
@RequireRoles('admin')
@Get('users')
getUsers() { ... }
// ✅ DO: Return 401/403
throw new UnauthorizedException('Auth required')
throw new ForbiddenException('No permission')
// ❌ DON'T: Redirect
res.redirect('/login') // Never do this
// ❌ DON'T: Trust client identity
const userId = req.body.userId // Wrong!
const userId = req.user.userId // Correct
```
### Client-Side (Website)
```typescript
// ✅ DO: Redirect unauthenticated users
if (!session && !loading) {
router.push('/auth/login')
}
// ✅ DO: Show loading states
if (loading) return <Loading />
// ✅ DO: Hide UI elements
{canAccess && <button>Delete</button>}
// ❌ DON'T: Make security decisions
if (user.role === 'admin') // Wrong! API decides
// ❌ DON'T: Trust your own checks
// Client checks are UX only, API is the gatekeeper
```
## Route Protection Patterns
### Public Route
```typescript
// app/leagues/page.tsx
export default function LeaguesPage() {
return <LeaguesList />;
}
// No protection needed - accessible by all
```
### Authenticated Route
```typescript
// app/dashboard/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
export default function DashboardLayout({ children }) {
return <AuthLayout>{children}</AuthLayout>;
}
// app/dashboard/page.tsx
export default function DashboardPage() {
return <DashboardContent />;
}
// Layout handles auth check, page is clean
```
### Role-Protected Route
```typescript
// app/admin/layout.tsx
import { RoleLayout } from '@/lib/guards/RoleLayout';
export default function AdminLayout({ children }) {
return (
<RoleLayout requiredRoles={['owner', 'admin']}>
{children}
</RoleLayout>
);
}
// app/admin/page.tsx
export default function AdminPage() {
return <AdminDashboard />;
}
// Layout handles role check
```
### Scoped Route (League Admin)
```typescript
// app/leagues/[id]/settings/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
import { LeagueAccessGuard } from '@/components/leagues/LeagueAccessGuard';
export default function LeagueSettingsLayout({ children, params }) {
return (
<AuthLayout>
<LeagueAccessGuard leagueId={params.id}>
{children}
</LeagueAccessGuard>
</AuthLayout>
);
}
// Multiple guards for complex scenarios
```
## API Endpoint Patterns
### Public Endpoint
```typescript
@Public()
@Get('pricing')
getPricing() { ... }
// No auth required
```
### Authenticated Endpoint
```typescript
@RequireAuthenticatedUser()
@Get('profile')
getProfile(@User() user: UserEntity) { ... }
// Any logged-in user
```
### Role-Protected Endpoint
```typescript
@RequireRoles('admin')
@Get('users')
getUsers() { ... }
// Only admins
```
### Scoped Endpoint
```typescript
@RequireAuthenticatedUser()
@Get('leagues/:leagueId/admin')
getLeagueAdmin(
@Param('leagueId') leagueId: string,
@User() user: UserEntity
) {
// Check if user is league admin
this.leagueService.verifyLeagueAdmin(leagueId, user.id);
...
}
// Check scope in service
```
## Error Handling
### API Returns
- **401 Unauthorized**: No/invalid session
- **403 Forbidden**: Has session but no permission
- **404 Not Found**: Resource doesn't exist OR non-disclosure
### Client Handles
```typescript
try {
const data = await apiFetch('/api/admin/users');
return data;
} catch (error) {
if (error.message.includes('401')) {
// Redirect to login
window.location.href = '/auth/login';
} else if (error.message.includes('403')) {
// Show access denied
toast.error('You need admin access');
router.push('/dashboard');
} else {
// Show error
toast.error(error.message);
}
}
```
## Common Mistakes
### ❌ Wrong
```typescript
// Client making security decisions
function AdminPage() {
const { session } = useAuth();
if (session?.role !== 'admin') return <AccessDenied />;
return <AdminDashboard />;
}
// API trusting client
@Post('delete')
deleteUser(@Body() body: { userId: string }) {
const userId = body.userId; // Could be anyone!
...
}
// Middleware doing too much
if (user.role === 'admin') { // Wrong place for this!
return NextResponse.next();
}
```
### ✅ Correct
```typescript
// Client handles UX only
function AdminPage() {
return (
<RoleLayout requiredRoles={['admin']}>
<AdminDashboard />
</RoleLayout>
);
}
// API is source of truth
@Post('delete')
@RequireRoles('admin')
deleteUser(@User() user: UserEntity, @Body() body: { userId: string }) {
// user.id is from session, body.userId is target
// Service verifies permissions
...
}
// Middleware only checks auth
if (!hasAuthCookie) {
return redirect('/login');
}
// Let API handle roles
```
## Testing Checklist
### Before Deploy
- [ ] Unauthenticated user can't access protected routes
- [ ] Authenticated user can access their routes
- [ ] Wrong role gets redirected/denied
- [ ] Session expiry redirects to login
- [ ] API returns proper 401/403 codes
- [ ] Public routes work without login
### Quick Test Commands
```bash
# Test API directly
curl -I http://localhost:3000/api/admin/users
# Should return 401 (no auth)
# Test with session
curl -I http://localhost:3000/api/admin/users \
-H "Cookie: gp_session=valid_token"
# Should return 200 or 403 depending on role
# Test public route
curl -I http://localhost:3000/api/leagues/all
# Should return 200
```
## Migration Steps
1. **Simplify middleware** - Remove role logic
2. **Create clean guards** - AuthLayout, RoleLayout
3. **Update layouts** - Replace old RouteGuard
4. **Test all routes** - Check redirects work
5. **Verify API** - All endpoints have proper decorators
## Remember
- **Server**: Security, permissions, data filtering
- **Client**: UX, loading states, redirects
- **Flow**: Always the same, always predictable
- **Debug**: Check each layer in order
**When in doubt**: The API decides. The client just shows what the API says.