diff --git a/AUTH.md b/AUTH.md new file mode 100644 index 0000000..f1f5d1a --- /dev/null +++ b/AUTH.md @@ -0,0 +1,298 @@ +# Authentication System Documentation + +## Overview + +Black Canyon Tickets uses a unified authentication system built on top of Supabase Auth with server-side rendering (SSR) support. All authentication functionality is centralized in a single module (`src/lib/auth-unified.ts`) to ensure consistency and maintainability. + +## Architecture + +### Core Module: `auth-unified.ts` + +The unified auth module is the single source of truth for all authentication in the application. It provides: + +- 🔐 **Session-based authentication** using Supabase cookies +- 🎭 **Role-based access control** (User, Admin, Super Admin) +- 🏢 **Organization-based access control** +- 🛡️ **Security logging and rate limiting** +- 📝 **Type-safe authentication context** +- 🐳 **Docker-friendly cookie handling** + +### Key Features + +1. **Universal Compatibility**: Works with both `Request` objects (API routes) and `AstroCookies` (Astro pages) +2. **SSR Support**: Full server-side rendering support with proper cookie handling +3. **Bearer Token Fallback**: Supports Authorization header for API clients +4. **Security Hardening**: Built-in CSRF protection, rate limiting, and security logging +5. **Type Safety**: Full TypeScript support with proper types + +## Usage Guide + +### Basic Authentication Check + +```typescript +// In Astro pages (.astro files) +import { verifyAuth } from '../lib/auth-unified'; + +// Check if user is authenticated (returns null if not) +const auth = await verifyAuth(Astro.cookies); + +// In API routes (.ts files) +const auth = await verifyAuth(request); +``` + +### Requiring Authentication + +```typescript +// Throws AuthError if not authenticated +const auth = await requireAuth(Astro.cookies); + +// The auth object contains: +// - user: Supabase user object +// - session: Session information +// - isAdmin: boolean +// - isSuperAdmin: boolean +// - organizationId: string | null +``` + +### Role-Based Access Control + +```typescript +// Require admin access +const auth = await requireAdmin(Astro.cookies); + +// Require super admin access +const auth = await requireSuperAdmin(Astro.cookies); + +// Check organization access +const auth = await requireOrganizationAccess(Astro.cookies, organizationId); +``` + +### Protected Page Pattern + +```typescript +--- +import Layout from '../layouts/Layout.astro'; +import { verifyAuth } from '../lib/auth-unified'; + +export const prerender = false; // Enable SSR + +const auth = await verifyAuth(Astro.cookies); +if (!auth) { + return Astro.redirect('/login'); +} +--- + + +

Welcome, {auth.user.email}!

+
+``` + +### Protected API Route Pattern + +```typescript +import type { APIRoute } from 'astro'; +import { requireAuth, createAuthResponse } from '../lib/auth-unified'; + +export const GET: APIRoute = async ({ request }) => { + try { + const auth = await requireAuth(request); + + // Your protected logic here + const data = { userId: auth.user.id }; + + return createAuthResponse(data); + } catch (error) { + return createAuthResponse( + { error: error.message }, + error.statusCode || 401 + ); + } +}; +``` + +### Using the Middleware Pattern + +```typescript +import { withAuth } from '../lib/auth-unified'; + +// Wrap your handler with authentication +export const GET: APIRoute = async ({ request }) => { + return withAuth(request, async (auth) => { + // This code only runs if authenticated + return new Response(JSON.stringify({ + message: `Hello ${auth.user.email}` + })); + }); +}; +``` + +## Authentication Flow + +1. **Login**: User submits credentials → `/api/auth/login` → Supabase sets cookies → Redirect to dashboard +2. **Session Check**: Every request → `verifyAuth()` checks cookies → Returns auth context or null +3. **Protected Routes**: Page/API checks auth → Redirects to login or returns 401 if not authenticated +4. **Logout**: Clear session → `/api/auth/logout` → Supabase clears cookies → Redirect to login + +## Error Handling + +The auth system uses a custom `AuthError` class with specific error codes: + +```typescript +try { + const auth = await requireAuth(request); +} catch (error) { + if (error instanceof AuthError) { + switch (error.code) { + case 'NO_SESSION': + // User not logged in + break; + case 'NO_PERMISSION': + // User lacks required role + break; + case 'EXPIRED': + // Session expired + break; + } + } +} +``` + +## Security Features + +### CSRF Protection + +```typescript +// Generate token for forms +const csrfToken = generateCSRFToken(); + +// Verify token on submission +if (!verifyCSRFToken(request, sessionToken)) { + throw new Error('Invalid CSRF token'); +} +``` + +### Rate Limiting + +```typescript +// Check rate limit (10 requests per minute by default) +const identifier = `login:${email}`; +if (!checkRateLimit(identifier, 5, 60000)) { + throw new Error('Too many attempts'); +} +``` + +### Security Logging + +All authentication events are automatically logged: +- Failed login attempts +- Successful authentications +- Permission denied events +- Rate limit violations + +## Testing + +Visit `/auth-test-unified` to test the authentication system. This page shows: +- Current authentication status +- Session information +- Request headers and cookies +- Links to test protected routes + +## Migration Guide + +### From Old Auth System + +1. **Update imports**: + ```typescript + // Old + import { verifyAuth } from '../lib/auth'; + + // New (but old path still works via proxy) + import { verifyAuth } from '../lib/auth-unified'; + ``` + +2. **Use Astro.cookies instead of Astro.request**: + ```typescript + // Old + const auth = await verifyAuth(Astro.request); + + // New (better SSR support) + const auth = await verifyAuth(Astro.cookies); + ``` + +3. **Handle new error types**: + ```typescript + // Old + if (!auth) throw new Error('Not authenticated'); + + // New + import { AuthError } from '../lib/auth-unified'; + if (!auth) throw new AuthError('Not authenticated', 'NO_SESSION'); + ``` + +## Docker Considerations + +The unified auth system is designed to work seamlessly in Docker environments: + +1. **Cookie Domain**: Automatically handles cookie domain settings +2. **Secure Cookies**: Uses secure cookies in production +3. **Proxy Support**: Correctly reads IP addresses behind proxies + +## Environment Variables + +```bash +# Required for auth to work +PUBLIC_SUPABASE_URL=https://your-project.supabase.co +PUBLIC_SUPABASE_ANON_KEY=your-anon-key +SUPABASE_SERVICE_ROLE_KEY=your-service-key + +# Optional for enhanced security +COOKIE_DOMAIN=.yourdomain.com # For subdomain support +NODE_ENV=production # Enables secure cookies +``` + +## Troubleshooting + +### Common Issues + +1. **"No session found" after login** + - Check cookie settings in browser + - Verify Supabase URL and keys are correct + - Ensure cookies are not blocked + +2. **Dashboard flashing before redirect** + - Ensure using `Astro.cookies` not `Astro.request` + - Verify `prerender = false` is set + - Check server-side auth is enabled + +3. **Auth works locally but not in Docker** + - Check `COOKIE_DOMAIN` environment variable + - Verify proxy headers are being forwarded + - Ensure secure cookie settings match environment + +### Debug Mode + +Enable debug logging in development: + +```typescript +// In your page or API route +import { authDebug } from '../lib/auth-unified'; + +authDebug.logCookies(request); +authDebug.logSession(session); +``` + +## Best Practices + +1. **Always use the unified auth module** - Don't create separate auth implementations +2. **Use `Astro.cookies` for pages** - Better SSR support than `Astro.request` +3. **Handle errors gracefully** - Show user-friendly messages, not technical errors +4. **Test auth flows regularly** - Use `/auth-test-unified` to verify functionality +5. **Keep sessions secure** - Use HTTPS in production, set proper cookie flags + +## Future Enhancements + +- [ ] Refresh token rotation +- [ ] Remember me functionality +- [ ] Two-factor authentication +- [ ] Session activity tracking +- [ ] IP-based session validation \ No newline at end of file diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index f329b3b..9f2d7e1 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -351,26 +351,23 @@ if (!auth) { }, 8000); } - // Check authentication and redirect immediately if no session - async function checkAuth() { - const { data: { session } } = await supabase.auth.getSession(); - if (!session) { - // No session found, redirecting to login - window.location.href = '/login'; - return null; - } - return session; - } + // Note: Authentication is now handled server-side by unified auth system // Load events async function loadEvents() { try { - // Check if user has organization_id or is admin + // Get current user (auth already verified server-side) const { data: { user } } = await supabase.auth.getUser(); if (!user) { - // User is null, redirecting to login - window.location.href = '/login'; + // This shouldn't happen due to server-side auth, but handle gracefully + console.error('No user found despite server-side auth'); + loading.innerHTML = ` +
+

Session error

+

Please refresh the page

+
+ `; return; } @@ -830,9 +827,6 @@ if (!auth) { // Handle onboarding success on page load handleOnboardingSuccess(); - checkAuth().then(session => { - if (session) { - loadEvents(); - } - }); + // Load events directly (auth already verified server-side) + loadEvents(); \ No newline at end of file diff --git a/src/pages/events/new.astro b/src/pages/events/new.astro index 146683d..93c1843 100644 --- a/src/pages/events/new.astro +++ b/src/pages/events/new.astro @@ -7,7 +7,7 @@ import { verifyAuth } from '../../lib/auth'; export const prerender = false; // Server-side authentication check -const auth = await verifyAuth(Astro.request); +const auth = await verifyAuth(Astro.cookies); if (!auth) { return Astro.redirect('/login'); } @@ -322,11 +322,12 @@ if (!auth) { // let selectedAddons: any[] = []; // TODO: Implement addons functionality let eventImageUrl: string | null = null; - // Check authentication - async function checkAuth() { - const { data: { session } } = await supabase.auth.getSession(); - if (!session) { - window.location.href = '/'; + // Load user data (auth already verified server-side) + async function loadUserData() { + const { data: { user: authUser } } = await supabase.auth.getUser(); + + if (!authUser) { + console.error('No user found despite server-side auth'); return null; } @@ -334,14 +335,14 @@ if (!auth) { const { data: user } = await supabase .from('users') .select('name, email, organization_id, role') - .eq('id', session.user.id) + .eq('id', authUser.id) .single(); if (user) { currentOrganizationId = user.organization_id; } - return session; + return authUser; } // Generate slug from title @@ -552,9 +553,9 @@ if (!auth) { } } - // Initialize - checkAuth().then(session => { - if (session && currentOrganizationId) { + // Initialize (auth already verified server-side) + loadUserData().then(user => { + if (user && currentOrganizationId) { loadVenues(); } handleVenueOptionChange(); // Set initial state