diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6ad95d5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,42 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(ls:*)", + "Bash(npm run dev:*)", + "Bash(curl:*)", + "Bash(tmux kill-session:*)", + "Bash(true)", + "Bash(tmux new-session:*)", + "Bash(tmux list-sessions:*)", + "Bash(brew install:*)", + "Bash(tmux capture-pane:*)", + "Bash(grep:*)", + "Bash(claude mcp add:*)", + "Bash(claude mcp:*)", + "Bash(tmux send-keys:*)", + "Bash(mkdir:*)", + "Bash(cp:*)", + "WebFetch(domain:supabase.com)", + "Bash(rm:*)", + "Bash(npm run build:*)", + "Bash(mv:*)", + "Bash(npm run rev --debug)", + "Bash(npm run:*)", + "Bash(tmux attach-session:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(gh pr create:*)", + "Bash(gh pr:*)", + "Bash(git fetch:*)", + "Bash(git rebase:*)", + "Bash(gh run list:*)", + "Bash(gh run view:*)", + "Bash(npm run lint)", + "Bash(git pull:*)", + "Bash(npm run type-check:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 89198bd..be0327b 100644 --- a/.gitignore +++ b/.gitignore @@ -113,4 +113,3 @@ lighthouse-report/ *.tsbuildinfo # Husky -.husky/_ \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..6d0a766 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run test:unit diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..505ca1b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,175 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Core Development +- `npm run dev` - Start development server +- `npm run build` - Production build with type checking +- `npm run lint` - ESLint checking +- `npm run type-check` - TypeScript validation + +### Testing Suite +- `npm test` - Unit tests with Jest +- `npm run test:ci` - CI testing with coverage +- `npm run test:e2e` - Playwright E2E tests (Chrome, Firefox, Safari, Mobile) +- `npm run test:cross-browser` - Multi-browser testing +- `npm run coverage` - Generate test coverage reports + +### Single Test Execution +- `npm test -- --testNamePattern="test name"` - Run specific Jest test +- `npx playwright test tests/specific-test.spec.ts` - Run specific E2E test + +### Testing Credentials +- **Email/Password**: `jackson_rhoden@outlook.com` / `numfIt-8rorpo-fumwym` +- **Google OAuth**: `jacksonrhoden64@googlemail.com` +- **Test Events**: `/events/75c8904e-671f-426c-916d-4e275806e277` + +## Architecture Overview + +### Tech Stack +- **Next.js 15** with App Router and React 19 +- **TypeScript** with strict mode +- **Supabase** for database, auth, and real-time features +- **Stripe** for payment processing with webhooks +- **Google Calendar API** integration (primary client requirement) +- **Tailwind CSS** + **Shadcn/UI** components + +### Key Directory Structure +``` +/app/ # Next.js App Router pages +├── api/ # API routes (auth, events, calendar, checkout, webhooks) +├── events/ # Event discovery and details +├── staff/ # Staff dashboard +└── auth/ # Authentication pages + +/components/ +├── ui/ # Shadcn/UI base components +├── auth/ # Authentication components +├── events/ # Event-specific components +└── dashboard/ # Dashboard components + +/lib/ +├── database/ # Schema, migrations, types +├── auth.ts # Authentication logic +├── google-calendar.ts # Google Calendar integration +├── stripe.ts # Stripe integration +└── supabase.ts # Supabase client configuration +``` + +### Authentication & Security +- **Supabase Auth** with Google OAuth 2.0 +- **Row Level Security (RLS)** policies on all database tables +- **Middleware protection** for `/dashboard`, `/profile`, `/admin` routes +- **Google Calendar tokens** encrypted and stored securely +- User roles: `user`, `organizer`, `admin` + +### Database Architecture +- **PostgreSQL** via Supabase with real-time subscriptions +- **Migrations** in `/lib/database/migrations/` +- **Type generation** from database schema +- **RLS policies** for multi-tenant security + +### Google Calendar Integration +This is a **primary client requirement**: +- **OAuth 2.0 flow** with PKCE for security +- **Token refresh** handling with encryption +- **Two-way sync**: create, update, delete events +- **Fallback to .ics files** when OAuth unavailable +- Implementation in `/lib/google-calendar.ts` + +### Payment Processing +- **Stripe Checkout** integration +- **Webhook handling** with signature verification at `/api/webhooks/stripe` +- **Multiple ticket types** with pricing and capacity +- **Refund system** with automated processing + +### Testing Strategy +- **Unit tests**: Jest with jsdom environment +- **E2E tests**: Playwright across Chrome, Firefox, Safari, Mobile +- **Load testing**: K6 configuration available +- **Coverage reporting**: HTML, LCOV, JSON formats +- Tests run in parallel for CI optimization + +#### Test Coverage Requirements +- **CRITICAL**: 100% coverage for payment flows (Stripe integration) +- **CRITICAL**: 100% coverage for RSVP flows (including Google Calendar) +- **Comprehensive E2E tests** with Playwright for complete user journeys +- **Unit tests** for all utility functions and API routes + +#### Test Patterns +```typescript +// E2E test structure +test('RSVP flow with Google Calendar integration', async ({ page }) => { + await page.goto('/events/test-event'); + await page.click('[data-testid="rsvp-button"]'); + await page.click('[data-testid="add-to-calendar"]'); + // Verify complete user journey +}); +``` + +### Development Rules & Patterns + +#### Code Style & Structure +- **TypeScript Standards**: Use strict mode, define interfaces for all data structures, implement proper error handling with typed errors +- **File Organization**: Use lowercase with dashes for directories (e.g., `components/auth-wizard`) +- **Component Patterns**: Use functional and declarative programming patterns, avoid classes, use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`) +- **Code Length**: Don't create files longer than 300 lines (split into modules) + +#### Next.js 15 App Router Patterns +- Use App Router (`app/` directory) exclusively +- Server Components by default, Client Components only when needed +- Minimize use of `'use client'`, `useEffect`, and `setState` +- Implement proper SEO with metadata API +- Use dynamic imports for code splitting and optimization + +#### Supabase Integration (CRITICAL) +- **ALWAYS use @supabase/ssr** for Next.js integration +- **NEVER use deprecated @supabase/auth-helpers-nextjs** +- **CRITICAL**: Use ONLY `getAll` and `setAll` cookie methods +- **NEVER use** individual cookie methods (`get`, `set`, `remove`) +- Use Row-Level Security (RLS) policies for all tables +- Implement proper database types with `supabase gen types typescript` + +#### Component Patterns +- Use Shadcn/ui for base components with semantic Tailwind classes +- Text color logic: `className={hasActiveFilter ? 'text-foreground' : 'text-muted-foreground'}` +- Implement proper loading states and optimistic updates +- Use react-hook-form with Zod validation for forms +- Design for touch-friendly mobile interaction (44px minimum) + +#### Error Handling & Security +- Use early returns for error conditions and guard clauses +- Implement custom error types for consistent error handling +- Never expose sensitive data in client code or logs +- Validate all API inputs and implement proper CORS policies +- Handle sensitive data (calendar tokens) with encryption + +#### Development Methodology +- **System 2 Thinking**: Approach problems with analytical rigor, break down requirements into manageable parts +- **Tree of Thoughts**: Evaluate multiple solutions and their consequences before implementation +- **Iterative Refinement**: Consider improvements, edge cases, and optimizations before finalizing code + +#### Performance Optimization +- **Next.js Optimization**: Use Server Components for data fetching, implement proper caching strategies, optimize images with Next.js Image component +- **Database Optimization**: Use proper indexing for Supabase queries, implement pagination for event listings, cache frequently accessed data +- **Mobile Performance**: Minimize JavaScript bundle size, use progressive loading, optimize for slow networks + +#### Business Logic Patterns +- **Event Management**: Support both free RSVP and paid ticketing, implement capacity management, handle event status (upcoming, in-progress, past, full) +- **User Flows**: Guest checkout must work seamlessly, account creation should be optional but encouraged, calendar integration should work for both guests and registered users +- **Mobile-First**: Design for mobile viewport first (320px+), use touch-friendly interactive elements, implement swipe gestures where appropriate + +### Environment Configuration +Required environment variables: +- `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` +- `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` +- `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` +- `RESEND_API_KEY` for transactional emails + +### Deployment +- **Platform**: Vercel with auto-deployment on main branch +- **Live URL**: https://local-loop-qa.vercel.app +- **CI/CD**: 6 active GitHub workflows including performance testing and monitoring +- **Database backups**: Automated daily backups configured \ No newline at end of file diff --git a/app/api/debug-oauth/route.ts b/app/api/debug-oauth/route.ts index 9784715..328cf5d 100644 --- a/app/api/debug-oauth/route.ts +++ b/app/api/debug-oauth/route.ts @@ -1,25 +1,79 @@ import { NextResponse } from 'next/server' import { createGoogleCalendarService } from '@/lib/google-calendar' +import { supabase } from '@/lib/supabase' export async function GET() { try { + console.log('🔍 [Debug] OAuth debug endpoint called') + + // Test Google Calendar service (this is working fine) const googleCalendarService = createGoogleCalendarService() const authUrl = googleCalendarService.getAuthUrl('debug-test') - return NextResponse.json({ + // Test Supabase Auth configuration + let supabaseAuthTest = null + let supabaseAuthError = null + + try { + // Test if we can initialize OAuth flow with Supabase + const { data: authData, error: authError } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/auth/callback`, + skipBrowserRedirect: true + } + }) + + supabaseAuthTest = { + hasData: !!authData, + hasUrl: !!authData?.url, + provider: authData?.provider, + hasError: !!authError + } + + if (authError) { + supabaseAuthError = authError as any // Type assertion to handle the error properly + } + } catch (err: any) { + supabaseAuthError = err + } + + const response = { success: true, - authUrl, + timestamp: new Date().toISOString(), + googleCalendar: { + authUrl, + configured: true + }, + supabaseAuth: { + test: supabaseAuthTest, + error: supabaseAuthError?.message || null, + config: { + NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL ? 'SET' : 'NOT SET', + NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ? 'SET' : 'NOT SET' + } + }, environment: { NODE_ENV: process.env.NODE_ENV, GOOGLE_REDIRECT_URI: process.env.GOOGLE_REDIRECT_URI || 'NOT SET', NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL || 'NOT SET', - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID ? 'SET' : 'NOT SET' + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID ? 'SET' : 'NOT SET', + NEXT_PUBLIC_ENABLE_GOOGLE_AUTH: process.env.NEXT_PUBLIC_ENABLE_GOOGLE_AUTH || 'default(true)' + }, + pkceInfo: { + note: 'PKCE is handled automatically by Supabase Auth', + checkClientSide: 'Check browser storage for supabase.auth.code_verifier' } - }) + } + + console.log('🔍 [Debug] OAuth debug response:', response) + return NextResponse.json(response) } catch (error) { + console.error('💥 [Debug] OAuth debug error:', error) return NextResponse.json({ success: false, error: String(error), + timestamp: new Date().toISOString(), environment: { NODE_ENV: process.env.NODE_ENV, GOOGLE_REDIRECT_URI: process.env.GOOGLE_REDIRECT_URI || 'NOT SET', diff --git a/app/auth/auth-code-error/page.tsx b/app/auth/auth-code-error/page.tsx new file mode 100644 index 0000000..7af1397 --- /dev/null +++ b/app/auth/auth-code-error/page.tsx @@ -0,0 +1,27 @@ +import Link from 'next/link' + +export default function AuthCodeError() { + return ( +
+
+

Authentication Error

+

+ Sorry, we couldn't sign you in. This could be due to: +

+ +
+ + Try Again + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx deleted file mode 100644 index 2698258..0000000 --- a/app/auth/callback/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client' - -import { useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { supabase } from '@/lib/supabase' - -// Force dynamic rendering to prevent pre-rendering issues -export const dynamic = 'force-dynamic' - -export default function AuthCallback() { - const router = useRouter() - - useEffect(() => { - const handleAuthCallback = async () => { - try { - const { data, error } = await supabase.auth.getSession() - - if (error) { - console.error('Auth callback error:', error) - router.push('/auth/login?error=callback_error') - return - } - - if (data.session) { - // Successful authentication, redirect to dashboard or home - router.push('/') - } else { - // No session, redirect to login - router.push('/auth/login') - } - } catch (error) { - console.error('Unexpected error in auth callback:', error) - router.push('/auth/login?error=unexpected_error') - } - } - - handleAuthCallback() - }, [router]) - - return ( -
-
-
-

Completing authentication...

-
-
- ) -} \ No newline at end of file diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts new file mode 100644 index 0000000..15659a1 --- /dev/null +++ b/app/auth/callback/route.ts @@ -0,0 +1,29 @@ +import { createClient } from '@/utils/supabase/server' +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url) + const code = searchParams.get('code') + // if "next" is in param, use it as the redirect URL + const next = searchParams.get('next') ?? '/' + + if (code) { + const supabase = await createClient() + const { error } = await supabase.auth.exchangeCodeForSession(code) + if (!error) { + const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer + const isLocalEnv = process.env.NODE_ENV === 'development' + if (isLocalEnv) { + // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host + return NextResponse.redirect(`${origin}${next}`) + } else if (forwardedHost) { + return NextResponse.redirect(`https://${forwardedHost}${next}`) + } else { + return NextResponse.redirect(`${origin}${next}`) + } + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/auth-code-error`) +} \ No newline at end of file diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 7810e3a..d7ecdae 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -23,6 +23,33 @@ export default function LoginPage() { } = useAuth() const router = useRouter() + // Handle OAuth callback errors from URL parameters + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search) + const oauthError = urlParams.get('error') + + if (oauthError) { + const errorMessages: Record = { + 'timeout': 'OAuth authentication timed out. Please try again.', + 'no_code': 'No authorization code received from Google. Please try again.', + 'invalid_code': 'Invalid authorization code. Please try again.', + 'no_session': 'Failed to create session. Please try again.', + 'auth_failed': 'Authentication failed. Please try again.', + 'callback_failed': 'OAuth callback failed. Please try again.', + 'exchange_failed': 'Failed to exchange authorization code. Please try again.', + 'access_denied': 'Access was denied. Please try again if you want to sign in.', + 'server_error': 'Server error during authentication. Please try again.' + } + + const errorMessage = errorMessages[oauthError] || `Authentication error: ${oauthError}. Please try again.` + setError(errorMessage) + + // Clear the error from URL without page reload + const newUrl = window.location.pathname + window.history.replaceState({}, '', newUrl) + } + }, []) + // Auto-redirect when user becomes authenticated useEffect(() => { if (user && !authLoading) { diff --git a/app/debug/oauth/page.tsx b/app/debug/oauth/page.tsx new file mode 100644 index 0000000..206f431 --- /dev/null +++ b/app/debug/oauth/page.tsx @@ -0,0 +1,132 @@ +'use client' + +import { useEffect, useState } from 'react' + +export default function OAuthDebugPage() { + const [debugInfo, setDebugInfo] = useState([]) + const [preOAuthDebug, setPreOAuthDebug] = useState(null) + const [postOAuthDebug, setPostOAuthDebug] = useState(null) + + useEffect(() => { + // Retrieve saved OAuth debug info from sessionStorage + try { + const savedDebugInfo = sessionStorage.getItem('oauth_debug_info') + if (savedDebugInfo) { + setDebugInfo(JSON.parse(savedDebugInfo)) + } + + const preOAuth = sessionStorage.getItem('pre_oauth_debug') + if (preOAuth) { + setPreOAuthDebug(JSON.parse(preOAuth)) + } + + const postOAuth = sessionStorage.getItem('post_oauth_debug') + if (postOAuth) { + setPostOAuthDebug(JSON.parse(postOAuth)) + } + } catch (e) { + console.error('Could not load debug info', e) + } + }, []) + + const clearDebugInfo = () => { + sessionStorage.removeItem('oauth_debug_info') + sessionStorage.removeItem('pre_oauth_debug') + sessionStorage.removeItem('post_oauth_debug') + setDebugInfo([]) + setPreOAuthDebug(null) + setPostOAuthDebug(null) + } + + return ( +
+
+
+

OAuth Debug Information

+ +
+ + {debugInfo.length === 0 ? ( +
+

No OAuth debug information available.

+

+ Debug info will appear here after an OAuth flow attempt. +

+
+ ) : ( +
+ {/* OAuth Initiation Debug */} + {(preOAuthDebug || postOAuthDebug) && ( +
+

OAuth Initiation Analysis

+ +
+ {preOAuthDebug && ( +
+

Before OAuth Call

+
+                                                {JSON.stringify(preOAuthDebug, null, 2)}
+                                            
+
+ )} + + {postOAuthDebug && ( +
+

After OAuth Call

+
+                                                {JSON.stringify(postOAuthDebug, null, 2)}
+                                            
+
+ )} +
+ + {preOAuthDebug && postOAuthDebug && ( +
+

Key Differences:

+
    +
  • Session keys added: {postOAuthDebug.sessionStorageKeys.length - preOAuthDebug.sessionStorageKeys.length}
  • +
  • Local keys added: {postOAuthDebug.localStorageKeys.length - preOAuthDebug.localStorageKeys.length}
  • +
  • PKCE code verifier stored: {postOAuthDebug.hasCodeVerifier ? '✅ Yes' : '❌ No'}
  • +
+
+ )} +
+ )} + +
+

OAuth Flow Steps ({debugInfo.length} steps)

+ +
+ {debugInfo.map((entry, index) => ( +
+
+

{entry.step}

+ + {new Date(entry.timestamp).toLocaleTimeString()} + +
+
+                                            {JSON.stringify(entry.data, null, 2)}
+                                        
+
+ ))} +
+
+ +
+

Raw Debug Data (JSON)

+
+                                {JSON.stringify(debugInfo, null, 2)}
+                            
+
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/app/debug/supabase/page.tsx b/app/debug/supabase/page.tsx new file mode 100644 index 0000000..7fe0f98 --- /dev/null +++ b/app/debug/supabase/page.tsx @@ -0,0 +1,117 @@ +'use client' + +import { useEffect, useState } from 'react' +import { supabase } from '@/lib/supabase' + +export default function SupabaseDebugPage() { + const [testResults, setTestResults] = useState([]) + + useEffect(() => { + const runDiagnostics = async () => { + const results: any[] = [] + + // Test 1: Basic client info + results.push({ + test: 'Client Configuration', + result: { + supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL, + hasAnonKey: !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, + anonKeyLength: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.length, + storageKey: 'sb-*-auth-token', + clientType: typeof supabase + } + }) + + // Test 2: Basic auth methods + try { + const session = await supabase.auth.getSession() + results.push({ + test: 'getSession() call', + result: { + success: true, + hasSession: !!session.data.session, + hasUser: !!session.data.session?.user, + error: session.error?.message + } + }) + } catch (error: any) { + results.push({ + test: 'getSession() call', + result: { + success: false, + error: error.message + } + }) + } + + // Test 3: OAuth URL generation (without redirect) + try { + const oauth = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + queryParams: { + access_type: 'offline', + prompt: 'select_account', + }, + skipBrowserRedirect: true // This prevents actual redirect + } + }) + + results.push({ + test: 'OAuth URL Generation', + result: { + success: !oauth.error, + hasUrl: !!oauth.data?.url, + urlPreview: oauth.data?.url?.substring(0, 100) + '...', + error: oauth.error?.message, + provider: oauth.data?.provider + } + }) + } catch (error: any) { + results.push({ + test: 'OAuth URL Generation', + result: { + success: false, + error: error.message + } + }) + } + + // Test 4: Storage state + results.push({ + test: 'Browser Storage State', + result: { + localStorageKeys: Object.keys(localStorage), + sessionStorageKeys: Object.keys(sessionStorage), + supabaseKeys: Object.keys(localStorage).filter(k => k.includes('supabase')), + sbKeys: Object.keys(localStorage).filter(k => k.startsWith('sb-')), + hasAuthToken: Object.keys(localStorage).some(k => k.includes('auth-token')) + } + }) + + setTestResults(results) + } + + runDiagnostics() + }, []) + + return ( +
+
+

Supabase Client Diagnostics

+ +
+ {testResults.map((test, index) => ( +
+

{test.test}

+
+                                {JSON.stringify(test.result, null, 2)}
+                            
+
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/components/auth/ProfileDropdown.tsx b/components/auth/ProfileDropdown.tsx index 92f49da..7ba52ba 100644 --- a/components/auth/ProfileDropdown.tsx +++ b/components/auth/ProfileDropdown.tsx @@ -3,7 +3,6 @@ import { useState, useRef, useEffect } from 'react' import { User, LogOut, ChevronDown, Settings, Calendar, BarChart3 } from 'lucide-react' import { useAuth } from '@/lib/auth-context' -import { useRouter } from 'next/navigation' import { useAuth as useAuthHook } from '@/lib/hooks/useAuth' import Link from 'next/link' @@ -12,7 +11,6 @@ export function ProfileDropdown() { const dropdownRef = useRef(null) const { user, signOut } = useAuth() const { user: userProfile, isStaff, isAdmin } = useAuthHook() - const router = useRouter() // Get user display name const getUserDisplayName = () => { @@ -26,11 +24,21 @@ export function ProfileDropdown() { // Handle sign out const handleSignOut = async () => { try { - await signOut() + console.log('🚪 ProfileDropdown: Starting sign out...') setIsOpen(false) - router.push('/') + + // Use the main auth context signOut method + await signOut() + + console.log('✅ ProfileDropdown: Sign out completed') } catch (error) { - console.error('Error signing out:', error) + console.error('❌ ProfileDropdown: Error signing out:', error) + + // Even if signOut fails, force a page reload to clear state + if (typeof window !== 'undefined') { + console.log('🔄 ProfileDropdown: Forcing page reload to clear auth state') + window.location.href = '/' + } } } diff --git a/components/events/RSVPTicketSection.tsx b/components/events/RSVPTicketSection.tsx index 440225e..7455ece 100644 --- a/components/events/RSVPTicketSection.tsx +++ b/components/events/RSVPTicketSection.tsx @@ -19,7 +19,7 @@ import { User, Loader2 } from 'lucide-react'; -import { createClient } from '@/lib/supabase'; +import { createClient } from '@/utils/supabase/client'; import { cn } from '@/lib/utils'; // Types diff --git a/components/ui/Navigation.tsx b/components/ui/Navigation.tsx index 35d8355..de84896 100644 --- a/components/ui/Navigation.tsx +++ b/components/ui/Navigation.tsx @@ -33,6 +33,7 @@ export function Navigation({ }, 100) } + return (
@@ -80,6 +81,7 @@ export function Navigation({ + {/* Auth state conditional rendering */} {authLoading ? (
@@ -153,6 +155,7 @@ export function Navigation({ + {/* Auth state conditional rendering for mobile */} {authLoading ? (
diff --git a/e2e/oauth-test.spec.ts b/e2e/oauth-test.spec.ts new file mode 100644 index 0000000..326791f --- /dev/null +++ b/e2e/oauth-test.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test' + +test.describe('OAuth Flow Tests', () => { + test('should handle OAuth callback without PKCE errors', async ({ page }) => { + // Navigate to the callback page with a mock OAuth code + // Note: This tests the callback processing logic without going through actual OAuth + await page.goto('/auth/callback?code=mock-oauth-code-for-testing') + + // Check that we don't get the PKCE error + await page.waitForTimeout(2000) // Wait for processing + + // Should not contain the PKCE error message + const pageContent = await page.textContent('body') + expect(pageContent).not.toContain('both auth code and code verifier should be non-empty') + + // Should show authentication processing or error handling + const statusMessage = await page.locator('h2').first() + const statusText = await statusMessage.textContent() + + // Should show either processing or a more specific error (not PKCE) + expect(statusText).toMatch(/(Processing|Authentication|Error)/) + + console.log('OAuth callback page status:', statusText) + }) + + test('should handle OAuth errors gracefully', async ({ page }) => { + // Test error handling + await page.goto('/auth/callback?error=access_denied&error_description=User+denied+access') + + await page.waitForTimeout(1000) + + // Should show error state + const errorMessage = await page.locator('h2').first() + const errorText = await errorMessage.textContent() + + expect(errorText).toContain('Error') + + console.log('OAuth error handling:', errorText) + }) + + test('should handle missing code parameter', async ({ page }) => { + // Test missing code parameter + await page.goto('/auth/callback') + + await page.waitForTimeout(1000) + + // Should show error for missing code + const statusMessage = await page.locator('h2').first() + const statusText = await statusMessage.textContent() + + expect(statusText).toMatch(/(Error|Authentication failed)/) + + console.log('Missing code handling:', statusText) + }) + + test('should have correct session storage key', async ({ page }) => { + // Test that the session storage key is consistent + await page.goto('/auth/login') + + // Set the return URL in session storage with the correct key + await page.evaluate(() => { + sessionStorage.setItem('auth_return_url', '/test-redirect') + }) + + // Check the key is correctly set + const returnUrl = await page.evaluate(() => { + return sessionStorage.getItem('auth_return_url') + }) + + expect(returnUrl).toBe('/test-redirect') + + console.log('Session storage key test passed') + }) +}) \ No newline at end of file diff --git a/lib/auth-context.tsx b/lib/auth-context.tsx index 30bb718..87e6ee5 100644 --- a/lib/auth-context.tsx +++ b/lib/auth-context.tsx @@ -2,190 +2,176 @@ import { createContext, useContext, useEffect, useState } from 'react' import { User, Session } from '@supabase/supabase-js' -import { supabase } from './supabase' +import { createClient } from '@/utils/supabase/client' // Feature toggles for authentication providers const ENABLE_GOOGLE_AUTH = process.env.NEXT_PUBLIC_ENABLE_GOOGLE_AUTH !== 'false' const ENABLE_APPLE_AUTH = process.env.NEXT_PUBLIC_ENABLE_APPLE_AUTH === 'true' interface AuthContextType { - user: User | null - session: Session | null - loading: boolean - signIn: (email: string, password: string) => Promise - signUp: (email: string, password: string) => Promise - signOut: () => Promise - signInWithGoogle: () => Promise - signInWithApple: () => Promise - resetPassword: (email: string) => Promise - updatePassword: (password: string) => Promise - // Feature flags - isGoogleAuthEnabled: boolean - isAppleAuthEnabled: boolean + user: User | null + session: Session | null + loading: boolean + signIn: (email: string, password: string) => Promise + signUp: (email: string, password: string) => Promise + signOut: () => Promise + signInWithGoogle: () => Promise + signInWithApple: () => Promise + resetPassword: (email: string) => Promise + updatePassword: (password: string) => Promise + // Feature flags + isGoogleAuthEnabled: boolean + isAppleAuthEnabled: boolean } const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null) - const [session, setSession] = useState(null) - const [loading, setLoading] = useState(true) - - useEffect(() => { - console.log('🔥 AuthProvider useEffect started') - - // Fallback timeout to ensure loading state is resolved - const timeoutId = setTimeout(() => { - if (loading) { - console.warn('⏰ Auth initialization timeout - resolving loading state') - setLoading(false) - } - }, 10000) // 10 second timeout - - // Get initial session - const getInitialSession = async () => { - console.log('🔍 Getting initial session...') - try { - const { data: { session }, error } = await supabase.auth.getSession() - - console.log('📊 Initial session result:', { - hasSession: !!session, - hasError: !!error, - error: error?.message, - user: session?.user?.email - }) - - if (error) { - console.error('❌ Error getting initial session:', error) - // Still set loading to false even if there's an error - setSession(null) - setUser(null) - setLoading(false) - return - } - - setSession(session) - setUser(session?.user ?? null) - setLoading(false) - console.log('✅ Initial session loaded successfully') - } catch (error) { - console.error('💥 Unexpected error getting initial session:', error) - // Ensure loading state is always resolved - setSession(null) - setUser(null) - setLoading(false) - } - } - - getInitialSession() - - // Listen for auth changes - const { data: { subscription } } = supabase.auth.onAuthStateChange( - async (event, session) => { - console.log('🔄 Auth state change:', { event, hasSession: !!session }) - try { - setSession(session) - setUser(session?.user ?? null) - setLoading(false) - } catch (error) { - console.error('❌ Error in auth state change:', error) - setLoading(false) - } - } - ) - - return () => { - console.log('🧹 AuthProvider cleanup') - clearTimeout(timeoutId) - subscription.unsubscribe() - } - }, []) - - const signIn = async (email: string, password: string) => { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - if (error) throw error - } - - const signUp = async (email: string, password: string) => { - const { error } = await supabase.auth.signUp({ - email, - password, + const [user, setUser] = useState(null) + const [session, setSession] = useState(null) + const [loading, setLoading] = useState(true) + const supabase = createClient() + + useEffect(() => { + console.log('🔥 AuthProvider useEffect started') + + // Reduced timeout to ensure loading state is resolved quickly for optimistic UI + const timeoutId = setTimeout(() => { + if (loading) { + console.warn('⏰ Auth initialization timeout - resolving loading state') + setLoading(false) + } + }, 1000) // Reduced timeout for faster optimistic UI response + + // Get initial session immediately + const getInitialSession = async () => { + console.log('🔍 Getting initial session...') + try { + const { data: { session }, error } = await supabase.auth.getSession() + + console.log('📊 Initial session result:', { + hasSession: !!session, + hasError: !!error, + error: error?.message, + user: session?.user?.email }) - if (error) throw error - } - - const signOut = async () => { - const { error } = await supabase.auth.signOut() - if (error) throw error - } - const signInWithGoogle = async () => { - if (!ENABLE_GOOGLE_AUTH) { - throw new Error('Google authentication is currently disabled') + if (error) { + console.error('❌ Error getting initial session:', error) } - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || window.location.origin}/auth/callback`, - }, - }) - if (error) throw error + setSession(session) + setUser(session?.user ?? null) + setLoading(false) + console.log('✅ Initial session loaded successfully') + } catch (error) { + console.error('💥 Unexpected error getting initial session:', error) + setSession(null) + setUser(null) + setLoading(false) + } } - const signInWithApple = async () => { - if (!ENABLE_APPLE_AUTH) { - throw new Error('Apple authentication is coming soon! An Apple Developer account is required.') + // Start session loading immediately + getInitialSession() + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + console.log('🔄 Auth state change:', { event, hasSession: !!session }) + try { + setSession(session) + setUser(session?.user ?? null) + setLoading(false) + } catch (error) { + console.error('❌ Error in auth state change:', error) + setLoading(false) } + } + ) - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'apple', - options: { - redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || window.location.origin}/auth/callback`, - }, - }) - if (error) throw error - } - - const resetPassword = async (email: string) => { - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || window.location.origin}/auth/update-password`, - }) - if (error) throw error - } - - const updatePassword = async (password: string) => { - const { error } = await supabase.auth.updateUser({ - password, - }) - if (error) throw error + return () => { + console.log('🧹 AuthProvider cleanup') + clearTimeout(timeoutId) + subscription.unsubscribe() } - - const value = { - user, - session, - loading, - signIn, - signUp, - signOut, - signInWithGoogle, - signInWithApple, - resetPassword, - updatePassword, - isGoogleAuthEnabled: ENABLE_GOOGLE_AUTH, - isAppleAuthEnabled: ENABLE_APPLE_AUTH, + }, [supabase.auth]) + + const signIn = async (email: string, password: string) => { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }) + if (error) throw error + } + + const signUp = async (email: string, password: string) => { + const { error } = await supabase.auth.signUp({ + email, + password, + }) + if (error) throw error + } + + const signOut = async () => { + const { error } = await supabase.auth.signOut() + if (error) throw error + } + + const signInWithGoogle = async () => { + if (!ENABLE_GOOGLE_AUTH) { + throw new Error('Google authentication is currently disabled') } - return {children} + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + }, + }) + if (error) throw error + } + + const signInWithApple = async () => { + throw new Error('Apple authentication is coming soon! An Apple Developer account is required.') + } + + const resetPassword = async (email: string) => { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/auth/update-password`, + }) + if (error) throw error + } + + const updatePassword = async (password: string) => { + const { error } = await supabase.auth.updateUser({ + password, + }) + if (error) throw error + } + + const value = { + user, + session, + loading, + signIn, + signUp, + signOut, + signInWithGoogle, + signInWithApple, + resetPassword, + updatePassword, + isGoogleAuthEnabled: ENABLE_GOOGLE_AUTH, + isAppleAuthEnabled: ENABLE_APPLE_AUTH, + } + + return {children} } export function useAuth() { - const context = useContext(AuthContext) - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider') - } - return context -} \ No newline at end of file + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} \ No newline at end of file diff --git a/lib/hooks/useAuth.ts b/lib/hooks/useAuth.ts index fe92d28..78968f2 100644 --- a/lib/hooks/useAuth.ts +++ b/lib/hooks/useAuth.ts @@ -2,7 +2,7 @@ 'use client' import { useState, useEffect, useCallback } from 'react' -import { supabase } from '@/lib/supabase' +import { createClient } from '@/utils/supabase/client' import type { User } from '@supabase/supabase-js' export type UserRole = 'user' | 'organizer' | 'admin' @@ -31,6 +31,7 @@ export function useAuth(): UseAuthReturn { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const supabase = createClient() const fetchUserProfile = useCallback(async (authUser: User) => { try { @@ -42,6 +43,17 @@ export function useAuth(): UseAuthReturn { if (error) { console.error('Error fetching user profile:', error) + // Don't treat missing user as a hard error - they might not be in the users table yet + if (error.code === 'PGRST116') { + // No rows returned - user doesn't exist in users table yet + return { + id: authUser.id, + email: authUser.email || '', + display_name: authUser.user_metadata?.display_name || authUser.user_metadata?.full_name, + role: 'user' as UserRole, // Default role + google_calendar_connected: false + } + } setError('Failed to load user profile') return null } @@ -58,7 +70,7 @@ export function useAuth(): UseAuthReturn { setError('Failed to load user profile') return null } - }, []) + }, [supabase]) const refresh = useCallback(async () => { try { diff --git a/lib/supabase-server.ts b/lib/supabase-server.ts index 9f66d84..35e8594 100644 --- a/lib/supabase-server.ts +++ b/lib/supabase-server.ts @@ -1,37 +1,2 @@ -import { createServerClient } from '@supabase/ssr' -import { cookies } from 'next/headers' -import type { CookieOptions } from '@supabase/ssr' - -// Fallback values for build time when environment variables might not be available -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'fallback-key' - -export async function createServerSupabaseClient() { - const cookieStore = await cookies() - - return createServerClient(supabaseUrl, supabaseAnonKey, { - cookies: { - get(name: string) { - return cookieStore.get(name)?.value - }, - set(name: string, value: string, options: CookieOptions) { - try { - cookieStore.set({ name, value, ...options }) - } catch { - // The `set` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - }, - remove(name: string, options: CookieOptions) { - try { - cookieStore.set({ name, value: '', ...options }) - } catch { - // The `delete` method was called from a Server Component. - // This can be ignored if you have middleware refreshing - // user sessions. - } - }, - }, - }) -} \ No newline at end of file +// This file is deprecated - use @/utils/supabase/server instead +export { createClient as createServerSupabaseClient } from '@/utils/supabase/server' \ No newline at end of file diff --git a/lib/supabase.ts b/lib/supabase.ts index b9e52fd..386cfb0 100644 --- a/lib/supabase.ts +++ b/lib/supabase.ts @@ -1,70 +1,4 @@ -import { createBrowserClient } from '@supabase/ssr' +import { createClient } from '@/utils/supabase/client' -// Fallback values for build time when environment variables might not be available -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:54321' -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'fallback-key' - -// Debug logging for production issues -if (typeof window !== 'undefined') { - console.log('🔍 Supabase client configuration:', { - url: supabaseUrl, - hasValidUrl: supabaseUrl !== 'http://localhost:54321', - hasValidKey: supabaseAnonKey !== 'fallback-key', - keyLength: supabaseAnonKey.length, - urlPrefix: supabaseUrl.substring(0, 30) + '...', - isProduction: process.env.NODE_ENV === 'production' - }) - - // Test if we can reach the Supabase URL - if (supabaseUrl !== 'http://localhost:54321') { - fetch(`${supabaseUrl}/rest/v1/`, { - method: 'HEAD', - headers: { - 'apikey': supabaseAnonKey, - 'Authorization': `Bearer ${supabaseAnonKey}` - } - }) - .then(response => { - console.log('✅ Supabase connection test:', { - status: response.status, - ok: response.ok, - headers: Object.fromEntries(response.headers.entries()) - }) - }) - .catch(error => { - console.error('❌ Supabase connection test failed:', error) - }) - } else { - console.warn('⚠️ Using fallback Supabase URL - environment variables not loaded!') - } -} - -export function createClient() { - const client = createBrowserClient(supabaseUrl, supabaseAnonKey) - - // Additional client validation - if (typeof window !== 'undefined') { - console.log('🚀 Supabase client created successfully') - - // Test basic auth functionality - client.auth.getSession() - .then(({ data, error }) => { - if (error) { - console.error('❌ Initial session check failed:', error) - } else { - console.log('✅ Initial session check successful:', { - hasSession: !!data.session, - user: data.session?.user?.email || 'no user' - }) - } - }) - .catch(error => { - console.error('❌ Session check threw error:', error) - }) - } - - return client -} - -// Default client for client-side usage -export const supabase = createClient() \ No newline at end of file +// Default client for client-side usage - using new structure +export const supabase = createClient() \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index 5f29564..3075a53 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,104 +1,8 @@ -import { createServerClient } from '@supabase/ssr' -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' +import { type NextRequest } from 'next/server' +import { updateSession } from './utils/supabase/middleware' -export async function middleware(req: NextRequest) { - // Skip middleware if environment variables are not available (build time) - if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { - return NextResponse.next() - } - - // Create a response object to pass to the supabase client - let response = NextResponse.next({ - request: { - headers: req.headers, - }, - }) - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, - { - cookies: { - get(name: string) { - return req.cookies.get(name)?.value - }, - set(name: string, value: string, options: any) { - req.cookies.set({ - name, - value, - ...options, - }) - response = NextResponse.next({ - request: { - headers: req.headers, - }, - }) - response.cookies.set({ - name, - value, - ...options, - }) - }, - remove(name: string, options: any) { - req.cookies.set({ - name, - value: '', - ...options, - }) - response = NextResponse.next({ - request: { - headers: req.headers, - }, - }) - response.cookies.set({ - name, - value: '', - ...options, - }) - }, - }, - } - ) - - // Try to refresh session if expired - handle gracefully if it fails - let session = null - try { - const { - data: { session: sessionData }, - } = await supabase.auth.getSession() - session = sessionData - } catch (error) { - // Log error but don't break the middleware - console.warn('Failed to get session in middleware:', error) - } - - // Protected routes that require authentication - const protectedRoutes = ['/dashboard', '/profile', '/admin'] - const isProtectedRoute = protectedRoutes.some(route => - req.nextUrl.pathname.startsWith(route) - ) - - // Auth routes that should redirect if already authenticated - const authRoutes = ['/auth/login', '/auth/signup'] - const isAuthRoute = authRoutes.includes(req.nextUrl.pathname) - - // Redirect to login if accessing protected route without session - if (isProtectedRoute && !session) { - const redirectUrl = req.nextUrl.clone() - redirectUrl.pathname = '/auth/login' - redirectUrl.searchParams.set('redirectTo', req.nextUrl.pathname) - return NextResponse.redirect(redirectUrl) - } - - // Redirect to home if accessing auth routes with valid session - if (isAuthRoute && session) { - const redirectUrl = req.nextUrl.clone() - redirectUrl.pathname = '/' - return NextResponse.redirect(redirectUrl) - } - - return response +export async function middleware(request: NextRequest) { + return await updateSession(request) } export const config = { diff --git a/package.json b/package.json index b8215cc..532d25a 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,6 @@ "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit", - "prepare": "husky install", - "test": "jest", "test:unit": "jest --testPathPattern='tests/unit|lib.*__tests__|components.*__tests__'", "test:unit:coverage": "jest --testPathPattern='tests/unit|lib.*__tests__|components.*__tests__' --coverage", "test:integration": "jest --testPathPattern='tests/integration'", diff --git a/public/favicon-16x16.svg b/public/favicon-16x16.svg new file mode 100644 index 0000000..46ce6f7 --- /dev/null +++ b/public/favicon-16x16.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/favicon-32x32.svg b/public/favicon-32x32.svg new file mode 100644 index 0000000..b9d4e1d --- /dev/null +++ b/public/favicon-32x32.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..b9d4e1d --- /dev/null +++ b/public/favicon.ico @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..b9d4e1d --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index ec6c948..bae0ec3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -13,5 +13,22 @@ "social", "lifestyle", "productivity" + ], + "icons": [ + { + "src": "/favicon-16x16.svg", + "sizes": "16x16", + "type": "image/svg+xml" + }, + { + "src": "/favicon-32x32.svg", + "sizes": "32x32", + "type": "image/svg+xml" + }, + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml" + } ] } \ No newline at end of file diff --git a/utils/supabase/client.ts b/utils/supabase/client.ts new file mode 100644 index 0000000..78ff395 --- /dev/null +++ b/utils/supabase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) +} \ No newline at end of file diff --git a/utils/supabase/middleware.ts b/utils/supabase/middleware.ts new file mode 100644 index 0000000..ff207ff --- /dev/null +++ b/utils/supabase/middleware.ts @@ -0,0 +1,67 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ) + + // IMPORTANT: Avoid writing any logic between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + const { + data: { user }, + } = await supabase.auth.getUser() + + // Protected routes + const protectedRoutes = ['/dashboard', '/profile', '/admin'] + const isProtectedRoute = protectedRoutes.some(route => + request.nextUrl.pathname.startsWith(route) + ) + + if (isProtectedRoute && !user) { + const url = request.nextUrl.clone() + url.pathname = '/auth/login' + url.searchParams.set('redirectTo', request.nextUrl.pathname) + return NextResponse.redirect(url) + } + + // Auth routes (redirect if already logged in) + const authRoutes = ['/auth/login', '/auth/signup'] + const isAuthRoute = authRoutes.includes(request.nextUrl.pathname) + + if (isAuthRoute && user) { + const url = request.nextUrl.clone() + url.pathname = '/' + return NextResponse.redirect(url) + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're + // creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: NextResponse.next({ request }) + // 2. Copy over the cookies, like so: response.cookies.setAll(supabaseResponse.cookies.getAll()) + + return supabaseResponse +} \ No newline at end of file diff --git a/utils/supabase/server.ts b/utils/supabase/server.ts new file mode 100644 index 0000000..881f173 --- /dev/null +++ b/utils/supabase/server.ts @@ -0,0 +1,27 @@ +import { createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +export async function createClient() { + const cookieStore = await cookies() + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options) + }) + } catch (error) { + // Ignore if running in middleware context + } + }, + }, + } + ) +} \ No newline at end of file