diff --git a/scripts/apply-admin-create-stop.js b/scripts/apply-admin-create-stop.js deleted file mode 100755 index 9083a55..0000000 --- a/scripts/apply-admin-create-stop.js +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env node -/** - * apply-admin-create-stop.js - * - * Applies the admin_create_stop RPC using ONLY the keys in .env.local. - * You do NOT need Supabase dashboard / SQL editor access. - * - * This is the fix for the "could not find the function public.admin_create_stop(...)" - * error when you click "Add New Stop". - * - * Usage (run from the repo root on a machine that can reach the DB): - * node scripts/apply-admin-create-stop.js - * - * The script connects as the real postgres user (using your SUPABASE_SERVICE_ROLE_KEY - * as the password) and runs the CREATE OR REPLACE + GRANT + NOTIFY. - * This is the only reliable way to create the SECURITY DEFINER function because - * direct REST inserts are blocked by the `block_stops_mutations` policy. - * - * After it prints success: - * - Restart your `npm run dev` - * - "Add New Stop" (AddStopModal / NewStopForm) will now call the RPC successfully. - */ - -const { Client } = require("pg"); -const fs = require("fs"); -const path = require("path"); - -// Load .env.local (same as the rest of the project) -try { - require("dotenv").config({ path: path.resolve(__dirname, "../.env.local") }); -} catch (e) { - // dotenv might not be hoisted in some setups; the keys may already be in process.env -} - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - -if (!supabaseUrl || !serviceKey) { - console.error("❌ Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env.local"); - console.error(" Make sure you are running from the repo root and .env.local exists with the keys."); - process.exit(1); -} - -// Derive the direct Postgres URL (same logic as supabase/push-migrations.js) -const projectRef = supabaseUrl.replace("https://", "").split(".")[0]; -const pgHost = `db.${projectRef}.supabase.co`; -const dbUrl = `postgresql://postgres:${encodeURIComponent(serviceKey)}@${pgHost}:5432/postgres`; - -console.log("πŸ”§ apply-admin-create-stop RPC installer"); -console.log(" Project:", projectRef); -console.log(" Using service role key from .env.local (full Postgres access)"); -console.log(""); - -async function main() { - const migrationPath = path.resolve(__dirname, "../supabase/migrations/202_fix_admin_create_stop.sql"); - - if (!fs.existsSync(migrationPath)) { - console.error("❌ Cannot find migration file:", migrationPath); - console.error(" The file should have been created by the previous fix."); - process.exit(1); - } - - const sql = fs.readFileSync(migrationPath, "utf8"); - - const client = new Client({ - connectionString: dbUrl, - ssl: { rejectUnauthorized: false }, - // Some environments need a longer timeout - connectionTimeoutMillis: 15000, - }); - - try { - console.log("⏳ Connecting to Postgres (this uses port 5432 outbound)..."); - await client.connect(); - console.log("βœ… Connected."); - - console.log("⏳ Executing 202_fix_admin_create_stop.sql (CREATE OR REPLACE + GRANT + NOTIFY)..."); - const result = await client.query(sql); - - console.log("βœ… Function installed / updated successfully."); - console.log(" Rows affected in last statement:", result.rowCount ?? "(see output above if any)"); - console.log(""); - console.log("πŸŽ‰ admin_create_stop is now available via the anon key (SECURITY DEFINER)."); - - // Quick verification using the anon key over HTTPS (this is what the app uses) - try { - const anon = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - const probeBody = { - p_active: false, - p_address: null, - p_brand_id: "00000000-0000-0000-0000-000000000000", // will FK fail, but proves the function exists - p_city: "verify", - p_cutoff_time: null, - p_date: "2099-12-31", - p_location: "verify", - p_state: "TS", - p_time: "10:00", - p_zip: null, - }; - const probe = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/rpc/admin_create_stop`, { - method: "POST", - headers: { apikey: anon, "Content-Type": "application/json" }, - body: JSON.stringify(probeBody), - }); - const txt = await probe.text(); - if (probe.status === 404) { - console.log("⚠️ Post-apply probe still got 404 β€” you may need to wait a few seconds or the NOTIFY didn't take."); - } else if (/foreign key|violates|brand_id/i.test(txt)) { - console.log("βœ… Post-apply probe: function exists (got expected FK error on the zero-UUID brand β€” this is success)."); - } else { - console.log("βœ… Post-apply probe returned:", txt.slice(0, 200)); - } - } catch (e) { - console.log(" (verification fetch skipped:", e.message, ")"); - } - - console.log(""); - console.log("Next:"); - console.log(" β€’ Restart your dev server (`npm run dev`) if it was already running."); - console.log(" β€’ Go to the stops admin page and try \"Add New Stop\"."); - console.log(" β€’ (Optional but recommended) node scripts/verify-stop-fns.js"); - } catch (err) { - const msg = (err && err.message) ? err.message : String(err); - console.error("❌ Failed to apply the function."); - console.error(" Error:", msg.slice(0, 500)); - - if (/ENOTFOUND|EAI_AGAIN|getaddrinfo|network is unreachable|ECONNREFUSED|timeout/i.test(msg)) { - console.log(""); - console.log("πŸ’‘ Your current environment cannot reach the database on port 5432."); - console.log(" This is common inside some containers/CI sandboxes."); - console.log(""); - console.log(" Copy and run ONE of the following on a *normal* laptop/terminal that has"); - console.log(" outbound access to Supabase Postgres (most developer machines can reach it):"); - console.log(""); - console.log(" Option A (easiest, uses exactly the same code as this script):"); - console.log(" npm run migrate:one 202"); - console.log(""); - console.log(" Option B (psql):"); - console.log(" # Build the URL using the key from your .env.local (same as this script does):"); - console.log(" # psql \"postgresql://postgres:PUT_YOUR_SERVICE_ROLE_KEY_HERE@db." + projectRef + ".supabase.co:5432/postgres\" \\"); - console.log(" # -f supabase/migrations/202_fix_admin_create_stop.sql"); - console.log(""); - console.log(" Option C (Supabase CLI):"); - console.log(" supabase db push --db-url \"postgresql://postgres:YOUR_SERVICE_ROLE_KEY@db." + projectRef + ".supabase.co:5432/postgres\" \\"); - console.log(" supabase/migrations/202_fix_admin_create_stop.sql"); - console.log(""); - console.log(" After the command succeeds you will see the NOTIFY and the function will exist."); - console.log(" Then restart `npm run dev` and \"Add New Stop\" will stop showing the function-not-found error."); - } - - process.exit(1); - } finally { - try { await client.end(); } catch {} - } -} - -main().catch((e) => { - console.error("Unexpected crash:", e); - process.exit(1); -}); diff --git a/scripts/check-stop-fns.js b/scripts/check-stop-fns.js deleted file mode 100644 index 7745a5c..0000000 --- a/scripts/check-stop-fns.js +++ /dev/null @@ -1,49 +0,0 @@ -require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") }); - -const url = process.env.NEXT_PUBLIC_SUPABASE_URL; -const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; -const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - -const headers = { - apikey: serviceKey, - Authorization: `Bearer ${serviceKey}`, - "Content-Type": "application/json", -}; - -async function rpc(name, body = {}) { - const r = await fetch(`${url}/rest/v1/rpc/${name}`, { - method: "POST", headers, body: JSON.stringify(body), - }); - return { status: r.status, text: (await r.text()).slice(0, 500) }; -} - -(async () => { - // 1. Try calling admin_create_stop with service role - console.log("=== 1. Call admin_create_stop (service role) ==="); - console.log(await rpc("admin_create_stop", { p_brand_id: null })); - - // 2. Try with body - console.log("\n=== 2. Call admin_create_stop with stub body ==="); - console.log(await rpc("admin_create_stop", { - p_brand_id: "00000000-0000-0000-0000-000000000000", - p_city: "test", p_state: "CO", p_location: "x", p_date: "2025-01-01", p_time: "08:00" - })); - - // 3. Try with a name that obviously doesn't exist to see the error format - console.log("\n=== 3. Call a non-existent function (control) ==="); - console.log(await rpc("definitely_does_not_exist_xyz", {})); - - // 4. Use the PostgREST introspection β€” query information_schema via rpc - // PostgREST exposes pg_proc through /rest/v1/ for system tables is not allowed - // But we can use the OpenAPI spec endpoint - console.log("\n=== 4. OpenAPI spec β€” looking for admin_create_stop ==="); - const openApi = await fetch(`${url}/rest/v1/`, { headers: { apikey: anonKey, Authorization: `Bearer ${anonKey}` } }); - const text = await openApi.text(); - const matches = text.match(/admin_create_stop[a-z_]*/g) || []; - console.log("Matches in OpenAPI:", matches); - - // 5. Get all function names from OpenAPI - const funcMatches = [...text.matchAll(/"([a-z][a-z0-9_]*)\s*\{/g)].map(m => m[1]); - const stopFns = funcMatches.filter(n => n.includes("stop")); - console.log("\nStop-related functions in OpenAPI:", stopFns); -})(); diff --git a/scripts/check-stop-fns2.js b/scripts/check-stop-fns2.js deleted file mode 100644 index 2117388..0000000 --- a/scripts/check-stop-fns2.js +++ /dev/null @@ -1,52 +0,0 @@ -require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") }); -const { Client } = require("pg"); -const dns = require("dns"); - -const url = process.env.NEXT_PUBLIC_SUPABASE_URL; -const projectRef = url.replace("https://", "").split(".")[0]; -const candidates = [ - `db.${projectRef}.supabase.co`, - `aws-0-us-east-1.pooler.supabase.com`, - `aws-0-us-west-1.pooler.supabase.com`, -]; - -(async () => { - for (const host of candidates) { - try { - const ip = await new Promise((resolve, reject) => { - dns.resolve4(host, (err, addrs) => err ? reject(err) : resolve(addrs)); - }); - console.log(`${host} -> ${ip.join(", ")}`); - } catch (e) { - console.log(`${host} -> DNS FAIL: ${e.code}`); - } - } - - // Try direct DB with port 6543 (pooler) using supabase format - // Username pattern: postgres. - const client = new Client({ - host: "aws-0-us-east-1.pooler.supabase.com", - port: 6543, database: "postgres", - user: `postgres.${projectRef}`, - password: process.env.SUPABASE_SERVICE_ROLE_KEY, - ssl: { rejectUnauthorized: false }, - }); - try { - await client.connect(); - const r = await client.query(` - SELECT p.proname, - pg_get_function_identity_arguments(p.oid) AS id_args, - p.prosecdef AS secdef - FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid - WHERE n.nspname='public' AND p.proname IN - ('admin_create_stop','admin_create_stops_batch','delete_stop') - ORDER BY p.proname; - `); - console.log("\nFUNCTIONS IN DB:"); - console.log(JSON.stringify(r.rows, null, 2)); - } catch (e) { - console.error("POOLER ERR:", e.message); - } finally { - try { await client.end(); } catch {} - } -})(); diff --git a/scripts/check-stop-fns3.js b/scripts/check-stop-fns3.js deleted file mode 100644 index 52a2732..0000000 --- a/scripts/check-stop-fns3.js +++ /dev/null @@ -1,44 +0,0 @@ -require("dotenv").config({ path: require("path").resolve(__dirname, "../.env.local") }); -const { Client } = require("pg"); - -const url = process.env.NEXT_PUBLIC_SUPABASE_URL; -const projectRef = url.replace("https://", "").split(".")[0]; -const pw = process.env.SUPABASE_SERVICE_ROLE_KEY; - -const tries = [ - { host: "aws-0-us-east-1.pooler.supabase.com", port: 6543, user: `postgres.${projectRef}`, database: "postgres" }, - { host: "aws-0-us-east-1.pooler.supabase.com", port: 5432, user: `postgres.${projectRef}`, database: "postgres" }, - { host: "aws-0-us-east-1.pooler.supabase.com", port: 6543, user: "postgres", database: "postgres" }, -]; - -(async () => { - for (const cfg of tries) { - const client = new Client({ ...cfg, password: pw, ssl: { rejectUnauthorized: false } }); - try { - await client.connect(); - const r = await client.query(` - SELECT p.proname, - pg_get_function_identity_arguments(p.oid) AS id_args, - p.prosecdef AS secdef - FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid - WHERE n.nspname='public' AND p.proname IN - ('admin_create_stop','admin_create_stops_batch','delete_stop') - ORDER BY p.proname; - `); - console.log(`\nβœ“ ${cfg.host}:${cfg.port} user=${cfg.user}`); - console.log(JSON.stringify(r.rows, null, 2)); - - // Also check if migration tracking table exists - const trk = await client.query(` - SELECT version, name FROM supabase_migrations.schema_migrations - WHERE version >= 145 ORDER BY version; - `).catch(() => ({ rows: [] })); - console.log("Recent migrations applied:", trk.rows); - await client.end(); - return; - } catch (e) { - console.log(`βœ— ${cfg.host}:${cfg.port} user=${cfg.user} β†’ ${e.message}`); - try { await client.end(); } catch {} - } - } -})(); diff --git a/scripts/cleanup-duplicate-stops.ts b/scripts/cleanup-duplicate-stops.ts deleted file mode 100644 index af09cd1..0000000 --- a/scripts/cleanup-duplicate-stops.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Cleanup script: remove duplicate stops from the Tuxedo Corn 2026 tour. - * Keeps the row with the latest created_at per (brand_id, date, city, state, location, time). - * Run: DATABASE_URL="postgresql://..." npx tsx scripts/cleanup-duplicate-stops.ts - */ -import { Pool } from "pg"; - -const pool = new Pool({ - connectionString: - process.env.DATABASE_ADMIN_URL ?? - process.env.DATABASE_URL ?? - "postgres://postgres:postgres@localhost:5432/route_commerce", -}); - -async function main() { - const client = await pool.connect(); - try { - await client.query("BEGIN"); - - // Find duplicate groups (same brand_id, date, city, state, location, time) - const dupes = await client.query(` - SELECT brand_id::text, date, city, state, location, time, count(*) as cnt, min(created_at::text) as oldest, max(created_at::text) as newest - FROM stops - WHERE deleted_at IS NULL - GROUP BY brand_id, date, city, state, location, time - HAVING count(*) > 1 - ORDER BY date, city - `); - - console.log(`Found ${dupes.rowCount} duplicate groups`); - if (dupes.rowCount === 0) { - await client.query("COMMIT"); - console.log("No duplicates found."); - return; - } - - let totalDeleted = 0; - for (const row of dupes.rows) { - // Keep the row with the latest created_at, delete the rest - const del = await client.query(` - DELETE FROM stops - WHERE brand_id = $1 - AND date = $2 - AND city = $3 - AND state = $4 - AND location = $5 - AND time IS NOT DISTINCT FROM $6 - AND deleted_at IS NULL - AND created_at < ( - SELECT max(created_at) FROM stops s2 - WHERE s2.brand_id = stops.brand_id - AND s2.date = stops.date - AND s2.city = stops.city - AND s2.state = stops.state - AND s2.location = stops.location - AND (s2.time IS NOT DISTINCT FROM stops.time) - AND s2.deleted_at IS NULL - ) - `, [row.brand_id, row.date, row.city, row.state, row.location, row.time]); - totalDeleted += del.rowCount ?? 0; - console.log(` Deleted ${del.rowCount} duplicate(s) for ${row.city}, ${row.state} on ${row.date}`); - } - - await client.query("COMMIT"); - console.log(`\nDone. Total duplicates removed: ${totalDeleted}`); - } catch (e) { - await client.query("ROLLBACK"); - throw e; - } finally { - client.release(); - await pool.end(); - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/scripts/create-admin-user.ts b/scripts/create-admin-user.ts deleted file mode 100644 index 676f88a..0000000 --- a/scripts/create-admin-user.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Create an admin user in Neon Auth via direct HTTP call. - * Run: npx tsx scripts/create-admin-user.ts [email] [password] [name] - * - * Makes a direct HTTP request to the Neon Auth API so we don't need - * a Next.js request context, then updates email_verified in the DB. - */ -import { config } from "dotenv"; -config({ path: ".env.local" }); -import pg from "pg"; - -const { Pool } = pg; - -async function main() { - const email = process.argv[2] ?? "admin@example.com"; - const password = process.argv[3] ?? "Admin1234!"; - const name = process.argv[4] ?? "Admin"; - - const baseUrl = process.env.NEON_AUTH_BASE_URL!; - - console.log(`Creating user: ${email}`); - - const res = await fetch(`${baseUrl}/sign-up/email`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Origin": "http://localhost:4000", - "x-neon-auth-proxy": "node", - }, - body: JSON.stringify({ email, password, name, callbackURL: "/admin" }), - }); - - const data = await res.json().catch(() => ({})); - console.log(`Sign-up status: ${res.status}`); - - if (!res.ok) { - console.error("Failed to create user:", JSON.stringify(data)); - process.exit(1); - } - - const userId = data.user?.id; - console.log("User created:", data.user?.email, "| ID:", userId); - - // Mark email verified in DB so they can log in immediately - if (userId) { - const pool = new Pool({ connectionString: process.env.DATABASE_URL }); - try { - await pool.query( - "UPDATE neon_auth.user SET email_verified = true WHERE id = $1", - [userId] - ); - console.log("Email verified in DB."); - } finally { - await pool.end(); - } - } - - console.log(`\nLog in at http://localhost:4000/login with ${email} / ${password}`); -} - -main(); diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh deleted file mode 100755 index a526e00..0000000 --- a/scripts/e2e-test.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -# End-to-end validation test for the local Postgres + PostgREST + MinIO + Next.js stack -# Exit 0 = all green, exit 1 = at least one failure. - -set -e -API="http://localhost:3001" -WEB="http://localhost:4000" - -pass=0 -fail=0 -declare -a FAILURES - -check() { - local name="$1" url="$2" expected="${3:-200}" cookies="${4:-}" - local cmd="curl -s -o /dev/null -w '%{http_code}'" - if [ -n "$cookies" ]; then cmd="$cmd -b \"$cookies\""; fi - local code=$(eval "$cmd $url") - if [ "$code" = "$expected" ]; then - echo " PASS $name ($code)" - pass=$((pass+1)) - else - echo " FAIL $name expected=$expected got=$code url=$url" - fail=$((fail+1)) - FAILURES+=("$name") - fi -} - -echo "=== Postgres connection ===" -PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT 'ok';" >/dev/null -echo " PASS postgres responds" -pass=$((pass+1)) - -echo "=== DB integrity ===" -TABLE_COUNT=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';") -[ "$TABLE_COUNT" -ge 65 ] && echo " PASS $TABLE_COUNT public tables (>=65)" && pass=$((pass+1)) || { echo " FAIL $TABLE_COUNT tables"; fail=$((fail+1)); } - -FN_COUNT=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public';") -[ "$FN_COUNT" -ge 250 ] && echo " PASS $FN_COUNT functions (>=250)" && pass=$((pass+1)) || { echo " FAIL $FN_COUNT functions"; fail=$((fail+1)); } - -BRANDS=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM brands;") -[ "$BRANDS" -ge 2 ] && echo " PASS $BRANDS brands" && pass=$((pass+1)) || { echo " FAIL $BRANDS brands"; fail=$((fail+1)); } - -STOPS=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM stops WHERE active=true AND deleted_at IS NULL;") -[ "$STOPS" -ge 200 ] && echo " PASS $STOPS active stops" && pass=$((pass+1)) || { echo " FAIL $STOPS stops"; fail=$((fail+1)); } - -# No test brands -TEST_BRANDS=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM brands WHERE slug IN ('sunrise-farms','green-valley','orchard-fresh');") -[ "$TEST_BRANDS" = "0" ] && echo " PASS no test brands" && pass=$((pass+1)) || { echo " FAIL test brands found"; fail=$((fail+1)); } - -echo "=== Tuxedo Corn data ===" -PHONE=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT phone FROM brand_settings WHERE brand_id='64294306-5f42-463d-a5e8-2ad6c81a96de';") -[ "$PHONE" = "970-323-6874" ] && echo " PASS phone=$PHONE" && pass=$((pass+1)) || { echo " FAIL phone=$PHONE (expected 970-323-6874)"; fail=$((fail+1)); } - -LOGOS=$(PGPASSWORD=routecommerce_dev_password psql -U routecommerce -h 127.0.0.1 -d route_commerce -tA -c "SELECT count(*) FROM brand_settings WHERE brand_id='64294306-5f42-463d-a5e8-2ad6c81a96de' AND logo_url LIKE '/storage/%';") -[ "$LOGOS" = "1" ] && echo " PASS logo_url is /storage/ path" && pass=$((pass+1)) || { echo " FAIL logo_url not /storage/"; fail=$((fail+1)); } - -echo "=== PostgREST ===" -check "GET /" "$API/" -check "GET /brands" "$API/brands?select=id,name,slug&order=name" -check "GET /brands?slug=eq.tuxedo" "$API/brands?slug=eq.tuxedo&select=*" -check "GET /stops" "$API/stops?select=id,city&limit=5" -check "GET /products" "$API/products?select=id,name&limit=5" -check "POST rpc get_brand_settings_by_slug" "$API/rpc/get_brand_settings_by_slug" 200 \ - "-X POST -H Content-Type:application/json -d {\"p_brand_slug\":\"tuxedo\"}" -check "POST rpc get_brand_plan_info" "$API/rpc/get_brand_plan_info" 200 \ - "-X POST -H Content-Type:application/json -d {\"p_brand_id\":\"64294306-5f42-463d-a5e8-2ad6c81a96de\"}" -check "POST rpc get_public_stops_for_brand" "$API/rpc/get_public_stops_for_brand" 200 \ - "-X POST -H Content-Type:application/json -d {\"p_brand_slug\":\"tuxedo\"}" -check "GET /admin_users with brand join" "$API/admin_users?select=id,user_id,role,brand_id,brands(name)&limit=5" -check "GET /stops with brand join" "$API/stops?select=id,city,brand_id,brands(name)&limit=5" - -echo "=== MinIO ===" -check "Storage logo.png" "$WEB/storage/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/logo.png" -check "Storage olathe-sweet-logo.png" "$WEB/storage/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png" -check "Storage olathe-sweet-logo-dark.png" "$WEB/storage/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png" - -echo "=== Public storefronts ===" -check "GET /" "$WEB/" -check "GET /tuxedo" "$WEB/tuxedo" -check "GET /tuxedo/about" "$WEB/tuxedo/about" -check "GET /tuxedo/stops" "$WEB/tuxedo/stops" -check "GET /indian-river-direct" "$WEB/indian-river-direct" -check "GET /login" "$WEB/login" - -echo "=== Admin pages (dev_session=platform_admin) ===" -for p in /admin /admin/products /admin/stops /admin/orders /admin/users /admin/settings /admin/settings/billing /admin/settings/apps /admin/settings/payments /admin/communications /admin/communications/compose /admin/time-tracking /admin/wholesale /admin/water-log /admin/analytics /admin/reports; do - check "GET $p" "$WEB$p" 200 "dev_session=platform_admin" -done - -echo "" -echo "=== Summary ===" -echo " PASS: $pass" -echo " FAIL: $fail" -if [ $fail -gt 0 ]; then - echo " Failures:" - for f in "${FAILURES[@]}"; do echo " - $f"; done - exit 1 -fi -echo " ALL GREEN" diff --git a/scripts/fix-archived-rls.js b/scripts/fix-archived-rls.js deleted file mode 100644 index 2052431..0000000 --- a/scripts/fix-archived-rls.js +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env node -/** - * fix-archived-rls.js - * - * Add proper RLS enables + policies to tables created in - * supabase/migrations/.archived/*.sql files. These files are - * historical/archived migrations (per commit 916ad39 "Supabase removal") - * but react-doctor scans them. We satisfy the rule with real, correct - * policies scoped by user_id (auth.uid()) or brand_id (current_brand_id()) - * where possible; deny-all otherwise. - * - * Tables are detected by parsing CREATE TABLE blocks. The list of - * target files comes from the react-doctor scan output: - * - supabase-table-missing-rls (22 errors) - * - supabase-rls-policy-risk (9 errors in archived files; 1 in init.sql fixed separately) - * - * Idempotent: skips tables that already have RLS enabled. - */ - -const fs = require("node:fs"); -const path = require("node:path"); - -const ARCHIVE_DIR = path.join(__dirname, "..", "supabase", "migrations", ".archived"); - -// Helper policy templates. -// `brand_id`-scoped (most common in archived files): -// USING (brand_id = current_brand_id() OR is_platform_admin()) -// WITH CHECK (brand_id = current_brand_id() OR is_platform_admin()) -// `user_id`-scoped: -// USING (user_id = auth.uid() OR is_platform_admin()) -// WITH CHECK (user_id = auth.uid() OR is_platform_admin()) -// Public-read (e.g. blog_posts): -// SELECT to anon/authenticated USING (true) -// INSERT/UPDATE/DELETE: USING (false) WITH CHECK (false) - -function policyFor(tableName, columns) { - // Heuristics based on columns present: - const hasBrandId = columns.some(c => /^brand_id\b/i.test(c)); - const hasUserId = columns.some(c => /^user_id\b/i.test(c) && !/"userId"/i.test(c)); - const hasUserIdCamel = columns.some(c => /"userId"/i.test(c)); - const hasContactEmail = columns.some(c => /^contact_email\b/i.test(c)); - const hasEmail = columns.some(c => /^email\b/i.test(c) && !/email_sent/i.test(c)); - - // Public-read-only with deny write: blog_posts / blog_resources / roadmap_items (public-facing) - const publicReadOnly = new Set([ - "blog_posts", "blog_resources", "roadmap_items", - "newsletter_subscribers", "waitlist", - ]); - if (publicReadOnly.has(tableName)) { - return [ - `-- RLS: ${tableName} is public-read; deny writes by default`, - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS public_read ON ${tableName};`, - `CREATE POLICY public_read ON ${tableName} FOR SELECT USING (true);`, - `DROP POLICY IF EXISTS deny_write ON ${tableName};`, - `CREATE POLICY deny_write ON ${tableName} FOR INSERT WITH CHECK (false);`, - `DROP POLICY IF EXISTS deny_update ON ${tableName};`, - `CREATE POLICY deny_update ON ${tableName} FOR UPDATE USING (false) WITH CHECK (false);`, - `DROP POLICY IF EXISTS deny_delete ON ${tableName};`, - `CREATE POLICY deny_delete ON ${tableName} FOR DELETE USING (false);`, - ]; - } - - // verification_token (Auth.js): no RLS needed since it's used as the - // session lookup, but rule requires it. Lock to service role only. - if (tableName === "verification_token") { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS deny_all ON ${tableName};`, - `CREATE POLICY deny_all ON ${tableName} FOR ALL USING (false) WITH CHECK (false);`, - ]; - } - - // users / accounts / sessions: Auth.js tables, scoped by userId. - if (hasUserIdCamel) { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS self_access ON ${tableName};`, - `CREATE POLICY self_access ON ${tableName} FOR ALL USING ("userId" = auth.uid() OR is_platform_admin()) WITH CHECK ("userId" = auth.uid() OR is_platform_admin());`, - ]; - } - - // users (Auth.js): self-read own row. - if (tableName === "users") { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS self_read ON ${tableName};`, - `CREATE POLICY self_read ON ${tableName} FOR SELECT USING (id = auth.uid() OR is_platform_admin());`, - `DROP POLICY IF EXISTS deny_write ON ${tableName};`, - `CREATE POLICY deny_write ON ${tableName} FOR INSERT WITH CHECK (false);`, - `DROP POLICY IF EXISTS deny_update ON ${tableName};`, - `CREATE POLICY deny_update ON ${tableName} FOR UPDATE USING (id = auth.uid() OR is_platform_admin()) WITH CHECK (id = auth.uid() OR is_platform_admin());`, - `DROP POLICY IF EXISTS deny_delete ON ${tableName};`, - `CREATE POLICY deny_delete ON ${tableName} FOR DELETE USING (id = auth.uid() OR is_platform_admin());`, - ]; - } - - // sessions: self-access by userId column. - if (tableName === "sessions") { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS self_access ON ${tableName};`, - `CREATE POLICY self_access ON ${tableName} FOR ALL USING ("userId" = auth.uid() OR is_platform_admin()) WITH CHECK ("userId" = auth.uid() OR is_platform_admin());`, - ]; - } - - // launch_checklist_progress: user-scoped. - if (tableName === "launch_checklist_progress" && hasUserId) { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS self_access ON ${tableName};`, - `CREATE POLICY self_access ON ${tableName} FOR ALL USING (user_id = auth.uid()::text OR is_platform_admin()) WITH CHECK (user_id = auth.uid()::text OR is_platform_admin());`, - ]; - } - - // brand-scoped tables: most common. - if (hasBrandId) { - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS tenant_isolation ON ${tableName};`, - `CREATE POLICY tenant_isolation ON ${tableName} FOR ALL USING (brand_id = current_brand_id() OR is_platform_admin()) WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());`, - ]; - } - - // Fallback: deny all (safest default). - return [ - `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`, - `ALTER TABLE ${tableName} FORCE ROW LEVEL SECURITY;`, - `DROP POLICY IF EXISTS deny_all ON ${tableName};`, - `CREATE POLICY deny_all ON ${tableName} FOR ALL USING (false) WITH CHECK (false);`, - ]; -} - -// Parse a file and return [{name, columns}] for each CREATE TABLE block. -function parseTables(sql) { - const tables = []; - // Match CREATE TABLE ... (...); - const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([\s\S]*?)\)\s*;/gi; - let match; - while ((match = re.exec(sql)) !== null) { - const name = match[1]; - const body = match[2]; - // Extract column names (first identifier on each comma-separated line) - const columns = []; - body.split(",").forEach(line => { - const trimmed = line.trim(); - if (!trimmed) return; - if (/^(?:CONSTRAINT|UNIQUE|PRIMARY\s+KEY|FOREIGN\s+KEY|CHECK|INDEX)\b/i.test(trimmed)) return; - const colMatch = trimmed.match(/^(?:"[^"]+"|`[^`]+`|[a-zA-Z_][a-zA-Z0-9_]*)/); - if (colMatch) columns.push(colMatch[0]); - }); - tables.push({ name, columns }); - } - return tables; -} - -const files = [ - "005_water_log.sql", - "044_square_sync_log.sql", - "066_auto_square_sync.sql", - "069_brand_scoping_phase2.sql", - "070_rls_policy_audit.sql", - "080_communication_segments.sql", - "088_brand_features.sql", - "120_water_alerts.sql", - "126_founder_pain_log.sql", - "129_worker_time_tracking.sql", - "130_time_tracking_pay_period_overtime.sql", - "133_time_tracking_notifications.sql", - "134_abandoned_cart_recovery.sql", - "137_time_tracking_workers_and_tasks.sql", - "138_time_tracking_logs.sql", - "204_authjs_tables.sql", - "XXX_blog_tables.sql", - "XXX_launch_checklist.sql", - "XXX_roadmap_tables.sql", - "XXX_waitlist_table.sql", -]; - -let totalAdded = 0; -let totalSkipped = 0; - -for (const file of files) { - const filePath = path.join(ARCHIVE_DIR, file); - if (!fs.existsSync(filePath)) { - console.log(`skip (not found): ${file}`); - continue; - } - const original = fs.readFileSync(filePath, "utf8"); - const tables = parseTables(original); - if (tables.length === 0) { - console.log(`skip (no tables): ${file}`); - continue; - } - - // Idempotency check: skip if "ENABLE ROW LEVEL SECURITY" already present for any of the tables. - const newTables = tables.filter(t => !new RegExp(`ALTER\\s+TABLE\\s+${t.name}\\s+ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY`, "i").test(original)); - if (newTables.length === 0) { - console.log(`skip (already done): ${file} β€” ${tables.length} tables`); - totalSkipped += tables.length; - continue; - } - - const blocks = newTables.map(t => { - const policy = policyFor(t.name, t.columns); - return [ - `-- ============================================================`, - `-- RLS for ${t.name} (added by scripts/fix-archived-rls.js)`, - `-- Columns: ${t.columns.join(", ")}`, - `-- ============================================================`, - ...policy, - ].join("\n"); - }); - - const appended = blocks.join("\n\n") + "\n"; - fs.writeFileSync(filePath, original + "\n" + appended); - console.log(`patched: ${file} β€” added RLS for ${newTables.length} table(s): ${newTables.map(t => t.name).join(", ")}`); - totalAdded += newTables.length; -} - -console.log(`\nTotal: added ${totalAdded} RLS block(s), skipped ${totalSkipped} already-done.`); diff --git a/scripts/fix-button-has-type.js b/scripts/fix-button-has-type.js deleted file mode 100644 index 887013e..0000000 --- a/scripts/fix-button-has-type.js +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env node -/** - * fix-button-has-type.js - * - * Bulk-fix the react-doctor/button-has-type warning by adding - * `type="button"` to every `