diff --git a/.env b/.env deleted file mode 100644 index 2a3f72d4..00000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlueHVvdGJqZnhrY3JmdmtybHRyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgzODI2NjYsImV4cCI6MjA5Mzk1ODY2Nn0.NmGjLNtxcYfnmhpd6T7VwH5sO2R0K8D-4sv7ATvZezc" -SUPABASE_URL="https://ynxuotbjfxkcrfvkrltr.supabase.co" -VITE_SUPABASE_PROJECT_ID="ynxuotbjfxkcrfvkrltr" -VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlueHVvdGJqZnhrY3JmdmtybHRyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgzODI2NjYsImV4cCI6MjA5Mzk1ODY2Nn0.NmGjLNtxcYfnmhpd6T7VwH5sO2R0K8D-4sv7ATvZezc" -VITE_SUPABASE_URL="https://ynxuotbjfxkcrfvkrltr.supabase.co" diff --git a/.env.development b/.env.development deleted file mode 100644 index a83be55d..00000000 --- a/.env.development +++ /dev/null @@ -1 +0,0 @@ -VITE_PAYMENTS_CLIENT_TOKEN="pk_test_51TVtRMIaKlRpu2f0PAnMiD4SlLBnuVlP45ldYg3nMRkL0GzMBhzneAi6ffDR8SHm8VaAhuWQRW3AG4rRadwwukqR00TCwmWdvD" \ No newline at end of file diff --git a/.env.production b/.env.production deleted file mode 100644 index aef9c1c1..00000000 --- a/.env.production +++ /dev/null @@ -1 +0,0 @@ -VITE_PAYMENTS_CLIENT_TOKEN="pk_live_51TVxFtEY0e7r92kuZbgPk7JFoEL91eq4fdsC8XNY3h9Apr18wMrrmIizeVzB9jzoBParMr9LT3NF7HAToX4rjFi600tHSSgV2g" \ No newline at end of file diff --git a/.gitignore b/.gitignore index d24df8a7..40bc3055 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,12 @@ pnpm-debug.log* lerna-debug.log* node_modules + +# Local environment files (keep .env.example tracked) +.env +.env.* +!.env.example + dist dist-ssr .output diff --git a/src/lib/admin/accounts.functions.ts b/src/lib/admin/accounts.functions.ts index 541b8877..3b910beb 100644 --- a/src/lib/admin/accounts.functions.ts +++ b/src/lib/admin/accounts.functions.ts @@ -1,6 +1,7 @@ import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireSupabaseAuth } from "@/integrations/supabase/auth-middleware"; +import { supabaseAdmin } from "@/integrations/supabase/client.server"; import { requireAdmin } from "./middleware"; // Used by the admin layout guard. @@ -17,9 +18,12 @@ export const getMyAdminStatus = createServerFn({ method: "GET" }) export const claimAdminBootstrap = createServerFn({ method: "POST" }) .middleware([requireSupabaseAuth]) .handler(async ({ context }) => { - const { supabase: _sbCtx, userId } = context as any; - const supabase = _sbCtx as any; - const { count, error: cErr } = await supabase + const { userId } = context as any; + // Use the service-role client so the "is there any admin yet?" check is a + // true global invariant. The user-scoped client is blinded by RLS + // (`roles read self`) and would report 0 admins to any non-admin caller, + // letting any authenticated user claim admin even after one exists. + const { count, error: cErr } = await supabaseAdmin .from("user_roles") .select("user_id", { count: "exact", head: true }) .eq("role", "admin"); @@ -27,7 +31,9 @@ export const claimAdminBootstrap = createServerFn({ method: "POST" }) if ((count ?? 0) > 0) { throw new Response("Admin already exists", { status: 409 }); } - const { error } = await supabase.from("user_roles").insert({ user_id: userId, role: "admin" }); + const { error } = await supabaseAdmin + .from("user_roles") + .insert({ user_id: userId, role: "admin" }); if (error) throw new Response(error.message, { status: 500 }); return { ok: true }; }); @@ -46,7 +52,12 @@ export const listAccounts = createServerFn({ method: "GET" }) .order("created_at", { ascending: false }) .limit(200); if (data.search) { - q = q.or(`handle.ilike.%${data.search}%,display_name.ilike.%${data.search}%`); + // Strip PostgREST filter metacharacters to prevent filter injection + // through the interpolated `.or()` expression. + const term = data.search.replace(/[%,()*]/g, "").trim(); + if (term) { + q = q.or(`handle.ilike.%${term}%,display_name.ilike.%${term}%`); + } } const { data: profiles, error } = await q; if (error) throw new Response(error.message, { status: 500 });