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:
+
+
+ - • The authorization code was invalid or expired
+ - • There was a network issue during sign-in
+ - • The OAuth flow was interrupted
+
+
+
+ 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