From 48fc10d0849ff5a3e937e42bc11837eac0ffda38 Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Fri, 5 Jun 2026 16:06:47 +0300 Subject: [PATCH] fix(account-sharing): stamp account_id on flow and pipeline inserts (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `flows.account_id` and `pipelines.account_id` are NOT NULL post-017 with no DB default, so inserts omitting account_id fail with a not-null violation (same class as #206 / #205). - POST /api/flows: resolve the caller's profiles.account_id and stamp it on both insert paths (template clone + blank/manual). Closes #210. - Pipelines page: stamp account_id (from useAuth) on the auto-seed and the manual "create pipeline" inserts — same bug, found while auditing other account-scoped insert flows. Audited every insert into the 15 NOT-NULL-account_id tables; all other sites already stamp account_id. typecheck + 370 tests pass; lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(dashboard)/pipelines/page.tsx | 16 +++++++++++++--- src/app/api/flows/route.ts | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx index 534aa8a..898489c 100644 --- a/src/app/(dashboard)/pipelines/page.tsx +++ b/src/app/(dashboard)/pipelines/page.tsx @@ -27,6 +27,7 @@ import { Label } from "@/components/ui/label"; import { GitBranch, Plus, ChevronDown, Settings } from "lucide-react"; import { toast } from "sonner"; import { useCan } from "@/hooks/use-can"; +import { useAuth } from "@/hooks/use-auth"; import { GatedButton } from "@/components/ui/gated-button"; // Pipeline creation is admin-class (settings-tier write under @@ -47,6 +48,7 @@ export default function PipelinesPage() { const supabase = createClient(); const canEditSettings = useCan("edit-settings"); const canCreateDeals = useCan("send-messages"); + const { accountId } = useAuth(); const [pipelines, setPipelines] = useState([]); const [selectedPipelineId, setSelectedPipelineId] = useState(""); @@ -111,10 +113,12 @@ export default function PipelinesPage() { } = await supabase.auth.getSession(); const user = session?.user; if (!user) return null; + // pipelines.account_id is NOT NULL post-017 with no DB default. + if (!accountId) return null; const { data: pipeline, error } = await supabase .from("pipelines") - .insert({ user_id: user.id, name: "Sales Pipeline" }) + .insert({ user_id: user.id, account_id: accountId, name: "Sales Pipeline" }) .select() .single(); @@ -132,7 +136,7 @@ export default function PipelinesPage() { await supabase.from("pipeline_stages").insert(stagesPayload); return pipeline as Pipeline; - }, [supabase]); + }, [supabase, accountId]); // Initial load + seed-if-empty useEffect(() => { @@ -254,10 +258,16 @@ export default function PipelinesPage() { setCreating(false); return; } + // pipelines.account_id is NOT NULL post-017 with no DB default. + if (!accountId) { + toast.error("Your profile is not linked to an account."); + setCreating(false); + return; + } const { data: pipeline, error } = await supabase .from("pipelines") - .insert({ user_id: user.id, name }) + .insert({ user_id: user.id, account_id: accountId, name }) .select() .single(); diff --git a/src/app/api/flows/route.ts b/src/app/api/flows/route.ts index 3b77c38..94e7118 100644 --- a/src/app/api/flows/route.ts +++ b/src/app/api/flows/route.ts @@ -49,7 +49,23 @@ export async function POST(request: Request) { if (!guard.ok) { return NextResponse.json(guard.body, { status: guard.status }) } - const { userId } = guard + const { userId, supabase } = guard + + // Resolve the caller's account_id — `flows.account_id` is NOT NULL + // post-017, so an INSERT without it trips the not-null constraint + // even though the admin client below bypasses RLS. + const { data: profile } = await supabase + .from('profiles') + .select('account_id') + .eq('user_id', userId) + .single() + const accountId = profile?.account_id as string | undefined + if (!accountId) { + return NextResponse.json( + { error: 'Your profile is not linked to an account.' }, + { status: 403 }, + ) + } const body = (await request.json().catch(() => null)) as | { @@ -85,6 +101,7 @@ export async function POST(request: Request) { .from('flows') .insert({ user_id: userId, + account_id: accountId, name: body.name?.trim() || template.name, description: template.description, status: 'draft', @@ -133,6 +150,7 @@ export async function POST(request: Request) { .from('flows') .insert({ user_id: userId, + account_id: accountId, name: body.name.trim(), description: body.description ?? null, status: 'draft',