fix: Remove client-side auth redirects causing dashboard flashing

- Removed checkAuth() function and redirects from dashboard.astro
- Removed checkAuth() function and redirects from events/new.astro
- Updated to use Astro.cookies for better SSR compatibility
- Client-side code now focuses on data loading, not authentication
- Server-side unified auth system handles all protection

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-12 20:40:11 -06:00
parent 76d27590fb
commit 425dfc9348
3 changed files with 322 additions and 29 deletions

298
AUTH.md Normal file
View File

@@ -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');
}
---
<Layout title="Protected Page">
<h1>Welcome, {auth.user.email}!</h1>
</Layout>
```
### 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

View File

@@ -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 = `
<div class="rounded-xl p-6 max-w-md mx-auto" style="background: var(--error-bg); border: 1px solid var(--error-border);">
<p class="font-medium" style="color: var(--error-color);">Session error</p>
<p class="text-sm mt-2" style="color: var(--error-color); opacity: 0.8;">Please refresh the page</p>
</div>
`;
return;
}
@@ -830,9 +827,6 @@ if (!auth) {
// Handle onboarding success on page load
handleOnboardingSuccess();
checkAuth().then(session => {
if (session) {
// Load events directly (auth already verified server-side)
loadEvents();
}
});
</script>

View File

@@ -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