diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0ef322..f514e5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,6 @@ jobs: with: node-version: '20' - - name: Set up Docker - uses: docker/setup-docker-action@v3 - - uses: supabase/setup-cli@v1 with: version: latest @@ -59,51 +56,229 @@ jobs: - run: npm run seed shell: bash - - name: Start Edge Functions + - name: Wait for services to be ready after reset shell: bash run: | - # Start functions in background - supabase functions serve & - FUNCTIONS_PID=$! - echo "FUNCTIONS_PID=$FUNCTIONS_PID" >> $GITHUB_ENV + echo "Waiting for all services to stabilize after database reset..." + echo "Database reset restarts auth and other services, need to wait for them to be ready" + sleep 15 + + # Verify database is ready + echo "Checking database..." + docker exec supabase_db_supabase-template psql -U postgres -d postgres -c "SELECT 1" >/dev/null || { + echo "❌ Database not responding" + exit 1 + } + echo "✅ Database is ready" + + # Verify auth service is responding + echo "Checking auth service health..." + for i in {1..10}; do + if curl -sf http://127.0.0.1:54321/auth/v1/health >/dev/null 2>&1; then + echo "✅ Auth service is healthy" + break + fi + if [ $i -eq 10 ]; then + echo "❌ Auth service not responding after 10 attempts" + docker logs supabase_auth_supabase-template --tail 50 + exit 1 + fi + echo "Waiting for auth service... attempt $i/10" + sleep 3 + done + + # Verify seeded users exist and can be queried + echo "Verifying seeded data..." + USER_COUNT=$(docker exec supabase_db_supabase-template psql -U postgres -d postgres -t -c "SELECT COUNT(*) FROM auth.users WHERE email LIKE '%@example.com'") + echo "Found $USER_COUNT test users" + if [ "$USER_COUNT" -lt 3 ]; then + echo "❌ Expected at least 3 test users, found $USER_COUNT" + exit 1 + fi + echo "✅ All services ready and data seeded" + + - name: Deploy and verify Edge Functions + shell: bash + run: | + echo "Checking Edge Functions setup..." + ls -la supabase/functions/ + + # In modern Supabase CLI, functions should be auto-served by 'supabase start' + # However, let's verify the edge-runtime container is running + echo "" + echo "Checking if edge-runtime is running..." + docker ps | grep edge-runtime || echo "⚠️ edge-runtime container not found" + + # Check Supabase status for functions + echo "" + echo "Supabase status:" + supabase status - # Wait for functions to be ready - echo "Waiting for Edge Functions to start..." + echo "" + echo "Waiting 10 seconds for Edge Functions to initialize..." sleep 10 + - name: Debug - Check seeded data + shell: bash + run: | + echo "Checking seeded users and profiles..." + STATUS_OUTPUT=$(supabase status) + + API_URL=$(echo "$STATUS_OUTPUT" | grep -i "API URL" | head -n1 | awk '{print $NF}') + ANON_KEY=$(echo "$STATUS_OUTPUT" | grep -i "anon key\|publishable" | head -n1 | awk '{print $NF}') + + # Fallback + if [ -z "$API_URL" ]; then + API_URL="http://127.0.0.1:54321" + fi + + echo "API_URL: $API_URL" + echo "ANON_KEY length: ${#ANON_KEY}" + + echo "Checking auth.users count..." + docker exec supabase_db_supabase-template psql -U postgres -d postgres -c "SELECT COUNT(*) as user_count FROM auth.users WHERE email LIKE '%@example.com';" || echo "Failed to query auth.users" + + echo "Checking profiles..." + curl -s -H "apikey: $ANON_KEY" "$API_URL/rest/v1/profiles?select=username,is_admin" | jq '.' || echo "Failed to query profiles" + + echo "Checking auth.users details (email, confirmed, encrypted_password)..." + docker exec supabase_db_supabase-template psql -U postgres -d postgres -c "SELECT id, email, email_confirmed_at, encrypted_password IS NOT NULL as has_password, is_sso_user FROM auth.users WHERE email LIKE '%@example.com';" || echo "Failed to query user details" + + echo "Testing login manually..." + LOGIN_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "$API_URL/auth/v1/token?grant_type=password" \ + -H "apikey: $ANON_KEY" \ + -H "Content-Type: application/json" \ + -d '{"email":"alice@example.com","password":"password123"}') + + echo "Login response:" + echo "$LOGIN_RESPONSE" | head -n -1 | jq '.' || echo "$LOGIN_RESPONSE" | head -n -1 + echo "HTTP Status: $(echo "$LOGIN_RESPONSE" | tail -n 1 | cut -d: -f2)" + + - name: Verify Edge Functions are ready + shell: bash + run: | + echo "Verifying Edge Functions endpoint..." + # Edge Functions are automatically served by 'supabase start' (via npm run dev) + # Test the actual admin-create-user function + + for i in {1..10}; do + RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST http://127.0.0.1:54321/functions/v1/admin-create-user \ + -H "Content-Type: application/json" \ + -d '{}' 2>&1) + + HTTP_CODE=$(echo "$RESPONSE" | tail -n 1 | cut -d: -f2) + + if [ "$HTTP_CODE" == "401" ] || [ "$HTTP_CODE" == "400" ] || [ "$HTTP_CODE" == "403" ]; then + echo "✅ Edge Functions endpoint is accessible (HTTP $HTTP_CODE - expected for unauthenticated request)" + break + fi + + if [ $i -eq 10 ]; then + echo "❌ Edge Functions endpoint not responding after 10 attempts" + echo "Last response:" + echo "$RESPONSE" | head -n -1 + echo "HTTP Code: $HTTP_CODE" + exit 1 + else + echo "Checking Edge Functions endpoint... attempt $i/10 (HTTP $HTTP_CODE)" + sleep 3 + fi + done + - name: Test Edge Functions shell: bash run: | - # Get API keys from supabase status - export SUPABASE_URL=$(supabase status | grep "API URL" | awk '{print $3}') - export SUPABASE_ANON_KEY=$(supabase status | grep "anon key" | awk '{print $3}') + # Get API keys from supabase status with better parsing + echo "Getting configuration from Supabase..." + STATUS_OUTPUT=$(supabase status) + echo "$STATUS_OUTPUT" + echo "" + + # Extract values using different methods for reliability + export SUPABASE_URL=$(echo "$STATUS_OUTPUT" | grep -i "API URL" | head -n1 | awk '{print $NF}') + export SUPABASE_ANON_KEY=$(echo "$STATUS_OUTPUT" | grep -i "anon key\|publishable" | head -n1 | awk '{print $NF}') + + # Fallback: try direct format + if [ -z "$SUPABASE_URL" ]; then + export SUPABASE_URL="http://127.0.0.1:54321" + fi echo "Testing Edge Functions..." echo "SUPABASE_URL: $SUPABASE_URL" + echo "SUPABASE_ANON_KEY length: ${#SUPABASE_ANON_KEY}" + + # Verify we got the API key + if [ -z "$SUPABASE_ANON_KEY" ] || [ ${#SUPABASE_ANON_KEY} -lt 20 ]; then + echo "❌ Failed to get valid SUPABASE_ANON_KEY from status" + echo "Status output was:" + echo "$STATUS_OUTPUT" + exit 1 + fi - # Run tests - deno run --allow-net --allow-env supabase/functions/test-functions.ts + echo "✅ Environment configured, running tests..." + echo "" + echo "==========================================" + echo "Starting Edge Functions Tests" + echo "==========================================" + echo "" + + # Run tests with full output + deno run --allow-net --allow-env supabase/functions/test-functions.ts 2>&1 || TEST_EXIT_CODE=$? + + echo "" + echo "==========================================" + echo "Test execution completed with exit code: ${TEST_EXIT_CODE:-0}" + echo "==========================================" + + if [ ! -z "$TEST_EXIT_CODE" ] && [ "$TEST_EXIT_CODE" != "0" ]; then + echo "" + echo "Tests failed, checking database state..." + docker exec supabase_db_supabase-template psql -U postgres -d postgres -c "SELECT email, created_at, email_confirmed_at FROM auth.users WHERE email LIKE '%@example.com';" || echo "Could not query auth.users" + echo "" + echo "Checking auth logs..." + docker logs supabase_auth_supabase-template 2>&1 | tail -50 + echo "" + echo "Checking edge-runtime logs..." + docker logs $(docker ps -q -f name=edge-runtime) 2>&1 | tail -50 || echo "No edge-runtime logs available" + exit $TEST_EXIT_CODE + fi # Optional: Test database and services health (remove if not needed) - name: Test database connection and data shell: bash run: | - # Get the API URL + # Get the API URL and key API_URL=$(supabase status | grep "API URL" | awk '{print $3}') + ANON_KEY=$(supabase status | grep "Publishable key" | awk '{print $3}') # Test API is responding echo "Testing API endpoint..." - curl -f "$API_URL/rest/v1/" || exit 1 + curl -f -H "apikey: $ANON_KEY" "$API_URL/rest/v1/" || exit 1 # Test database has tables (check if profiles table exists) echo "Testing database tables..." - curl -f "$API_URL/rest/v1/profiles?select=count" || exit 1 + curl -f -H "apikey: $ANON_KEY" "$API_URL/rest/v1/profiles?select=count" || exit 1 echo "✅ All services responding and database accessible" - - run: npm run logs + - name: Show logs on failure if: failure() shell: bash + run: | + echo "==========================================" + echo "Showing Supabase container logs..." + echo "==========================================" + echo "" + echo "--- Database Logs (last 100 lines) ---" + docker logs supabase_db_supabase-template --tail 100 2>&1 || echo "Could not fetch database logs" + echo "" + echo "--- Kong API Gateway Logs (last 50 lines) ---" + docker logs supabase_kong_supabase-template --tail 50 2>&1 || echo "Could not fetch API gateway logs" + echo "" + echo "--- Auth GoTrue Logs (last 50 lines) ---" + docker logs supabase_auth_supabase-template --tail 50 2>&1 || echo "Could not fetch auth logs" + echo "" + echo "==========================================" - run: npm run stop if: always() diff --git a/package.json b/package.json index fa99548..ced42c9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "migrate:prod": "supabase db push", "diff": "supabase db diff", "status": "supabase status", - "logs": "supabase logs", + "logs": "docker logs supabase_db_supabase-template --tail 100", "shell": "supabase db shell", "types": "supabase gen types typescript --local > types/database.types.ts", "link": "supabase link" diff --git a/supabase/functions/README.md b/supabase/functions/README.md index e46c082..2c59e81 100644 --- a/supabase/functions/README.md +++ b/supabase/functions/README.md @@ -45,7 +45,7 @@ This will start all functions at: ### 3. Test Functions -**Get your anon key:** +**Get your publishable key:** ```bash npm run status ``` diff --git a/supabase/functions/TEST.md b/supabase/functions/TEST.md index 72b7e5b..be54586 100644 --- a/supabase/functions/TEST.md +++ b/supabase/functions/TEST.md @@ -65,7 +65,7 @@ Total: 6 | Passed: 6 | Failed: 0 # 1. Get your API key npm run status -# Copy the "anon key" +# Copy the "Publishable key" # 2. Login as admin curl -X POST 'http://localhost:54321/auth/v1/token?grant_type=password' \ @@ -198,7 +198,7 @@ See `.github/workflows/ci.yml`: - name: Test Edge Functions run: | export SUPABASE_URL=$(supabase status | grep "API URL" | awk '{print $3}') - export SUPABASE_ANON_KEY=$(supabase status | grep "anon key" | awk '{print $3}') + export SUPABASE_ANON_KEY=$(supabase status | grep "Publishable key" | awk '{print $3}') deno run --allow-net --allow-env supabase/functions/test-functions.ts ``` diff --git a/supabase/functions/import_map.json b/supabase/functions/import_map.json new file mode 100644 index 0000000..be812bd --- /dev/null +++ b/supabase/functions/import_map.json @@ -0,0 +1,6 @@ +{ + "imports": { + "supabase": "https://esm.sh/@supabase/supabase-js@2.39.3", + "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.39.3" + } +} diff --git a/supabase/functions/test-functions.ts b/supabase/functions/test-functions.ts index 78b3b07..cbfc325 100644 --- a/supabase/functions/test-functions.ts +++ b/supabase/functions/test-functions.ts @@ -41,13 +41,18 @@ async function login(email: string, password: string): Promise { if (!response.ok) { const error = await response.text() - throw new Error(`Login failed: ${error}`) + console.error(`Login failed with status ${response.status}:`, error) + throw new Error(`Login failed (${response.status}): ${error}`) } const data = await response.json() + if (!data.access_token) { + console.error('Login response missing access_token:', data) + throw new Error('Login response missing access_token') + } return data.access_token } catch (error) { - console.error('Login error:', error) + console.error('Login error for', email, ':', error.message) return null } } @@ -57,6 +62,7 @@ async function testCreateUser(token: string, testEmail: string): Promise { try { const response = await fetch(`${SUPABASE_URL}/functions/v1/admin-list-users`, { headers: { + 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${token}`, }, }) @@ -117,6 +124,7 @@ async function testUpdateUser(token: string, userId: string): Promise { const response = await fetch(`${SUPABASE_URL}/functions/v1/admin-update-user`, { method: 'POST', headers: { + 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, @@ -150,6 +158,7 @@ async function testDeleteUser(token: string, userId: string): Promise { const response = await fetch(`${SUPABASE_URL}/functions/v1/admin-delete-user`, { method: 'POST', headers: { + 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, @@ -179,6 +188,7 @@ async function testUnauthorizedAccess(): Promise { const response = await fetch(`${SUPABASE_URL}/functions/v1/admin-create-user`, { method: 'POST', headers: { + 'apikey': SUPABASE_ANON_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -187,13 +197,19 @@ async function testUnauthorizedAccess(): Promise { }), }) - const data = await response.json() + let data + try { + data = await response.json() + } catch (e) { + throw new Error(`Failed to parse response (status ${response.status}): ${e.message}`) + } if (response.status === 401 && data.error) { logTest('Unauthorized Access Prevention', true) return true } else { - throw new Error('Should have rejected unauthorized request') + console.error('Unexpected response:', { status: response.status, data }) + throw new Error(`Should have rejected unauthorized request (got status ${response.status}, data: ${JSON.stringify(data)})`) } } catch (error) { logTest('Unauthorized Access Prevention', false, error.message) @@ -212,6 +228,7 @@ async function testNonAdminAccess(email: string, password: string): Promise>'full_name', + new.raw_user_meta_data->>'avatar_url' + ) + ON CONFLICT (id) DO NOTHING; + RETURN new; +EXCEPTION + WHEN OTHERS THEN + -- Log error but don't fail user creation + RAISE WARNING 'Error creating profile for user %: %', new.id, SQLERRM; + RETURN new; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Comment explaining the fix +COMMENT ON FUNCTION public.handle_new_user() IS +'Automatically creates a profile when a new user signs up. Includes error handling to prevent auth failures.'; diff --git a/supabase/migrations/00006_use_auth_admin_for_seed.sql b/supabase/migrations/00006_use_auth_admin_for_seed.sql new file mode 100644 index 0000000..d3ef9df --- /dev/null +++ b/supabase/migrations/00006_use_auth_admin_for_seed.sql @@ -0,0 +1,9 @@ +-- This migration is intentionally empty +-- The seed data will be handled by seed.sql using proper auth functions + +-- Note: Direct insertion into auth.users is problematic because: +-- 1. Password hashing must match GoTrue's expectations exactly +-- 2. Multiple auth-related columns must have correct non-NULL values +-- 3. The trigger system must be properly coordinated + +-- Instead, we'll use Supabase's auth.admin API via the seed file diff --git a/supabase/seed.sql b/supabase/seed.sql index 745e040..4dfa067 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -1,6 +1,9 @@ -- Seed file for development and testing -- This file is safe to run multiple times (uses upserts/checks) +-- Enable pgcrypto extension for password hashing +CREATE EXTENSION IF NOT EXISTS pgcrypto; + -- ============================================ -- SEED USERS (via auth.users) -- ============================================ @@ -12,7 +15,8 @@ -- Insert test users into auth.users -- Password for all test users: "password123" --- Hashed with bcrypt: $2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG1vv..mRyS +-- Using PostgreSQL's crypt() to generate bcrypt hash at insert time +-- This ensures compatibility with GoTrue's password verification INSERT INTO auth.users ( id, @@ -24,50 +28,90 @@ INSERT INTO auth.users ( created_at, updated_at, confirmation_token, + email_change, + email_change_token_new, + email_change_token_current, + email_change_confirm_status, + recovery_token, + phone_change, + phone_change_token, + reauthentication_token, role, - aud + aud, + is_sso_user, + is_super_admin ) VALUES -- User 1: Alice ( 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid, '00000000-0000-0000-0000-000000000000'::uuid, 'alice@example.com', - '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG1vv..mRyS', + crypt('password123', gen_salt('bf')), NOW(), '{"full_name": "Alice Johnson", "avatar_url": "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice"}', NOW(), NOW(), '', + '', + '', + '', + 0, + '', + '', + '', + '', 'authenticated', - 'authenticated' + 'authenticated', + false, + false ), -- User 2: Bob ( 'b1ffbc99-9c0b-4ef8-bb6d-6bb9bd380a22'::uuid, '00000000-0000-0000-0000-000000000000'::uuid, 'bob@example.com', - '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG1vv..mRyS', + crypt('password123', gen_salt('bf')), NOW(), '{"full_name": "Bob Smith", "avatar_url": "https://api.dicebear.com/7.x/avataaars/svg?seed=Bob"}', NOW(), NOW(), '', + '', + '', + '', + 0, + '', + '', + '', + '', 'authenticated', - 'authenticated' + 'authenticated', + false, + false ), -- User 3: Carol ( 'c2ffbc99-9c0b-4ef8-bb6d-6bb9bd380a33'::uuid, '00000000-0000-0000-0000-000000000000'::uuid, 'carol@example.com', - '$2a$10$XOPbrlUPQdwdJUpSrIF6X.LbE14qsMmKGhM1A8W9iqaG1vv..mRyS', + crypt('password123', gen_salt('bf')), NOW(), '{"full_name": "Carol Williams", "avatar_url": "https://api.dicebear.com/7.x/avataaars/svg?seed=Carol"}', NOW(), NOW(), '', + '', + '', + '', + 0, + '', + '', + '', + '', + 'authenticated', 'authenticated', - 'authenticated' + false, + false ) ON CONFLICT (id) DO NOTHING;