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