Merge pull request #206 from ArnasDon/fix/account-id-inserts

fix(account-sharing): stamp account_id on account-scoped inserts (#205)
This commit is contained in:
Arnas Donauskas 2026-06-05 10:25:18 +03:00 committed by GitHub
commit 0a9e2db61a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 65 additions and 5 deletions

View File

@ -3,6 +3,7 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { toast } from 'sonner';
import { MessageTemplate } from '@/types';
import { Step1ChooseTemplate } from '@/components/broadcasts/step1-choose-template';
@ -21,6 +22,7 @@ const steps = [
export default function NewBroadcastPage() {
const router = useRouter();
const { accountId } = useAuth();
const { createAndSendBroadcast, isProcessing, progress } = useBroadcastSending();
const [currentStep, setCurrentStep] = useState(0);
@ -90,9 +92,14 @@ export default function NewBroadcastPage() {
toast.error('Not signed in.');
return;
}
if (!accountId) {
toast.error('Your profile is not linked to an account.');
return;
}
const { error } = await supabase.from('broadcasts').insert({
user_id: user.id,
account_id: accountId,
name: name.trim(),
template_name: template.name,
template_language: template.language ?? 'en_US',

View File

@ -26,6 +26,9 @@ export async function POST(
const { data: copy, error: copyErr } = await admin
.from('automations')
.insert({
// Clone into the same account as the original. account_id is NOT
// NULL post-017, so the INSERT fails the constraint without it.
account_id: original.account_id,
user_id: user.id,
name: `${original.name} (Copy)`,
description: original.description,

View File

@ -30,6 +30,22 @@ export async function POST(request: Request) {
} = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// Resolve the caller's account_id — `automations.account_id` is NOT
// NULL post-017, so an INSERT without it trips the not-null constraint
// even though the admin client bypasses RLS.
const { data: profile } = await supabase
.from('profiles')
.select('account_id')
.eq('user_id', user.id)
.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)
if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
@ -83,6 +99,7 @@ export async function POST(request: Request) {
.from('automations')
.insert({
user_id: user.id,
account_id: accountId,
name: effectiveName,
description: effectiveDescription ?? null,
trigger_type: effectiveTriggerType,

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { toast } from 'sonner';
import type { Contact, Tag, ContactTag, ContactNote, CustomField, ContactCustomValue, Deal } from '@/types';
import {
@ -47,6 +48,7 @@ export function ContactDetailView({
onUpdated,
}: ContactDetailViewProps) {
const supabase = createClient();
const { accountId } = useAuth();
const [contact, setContact] = useState<Contact | null>(null);
const [loading, setLoading] = useState(false);
@ -244,7 +246,7 @@ export function ContactDetailView({
data: { session },
} = await supabase.auth.getSession();
const user = session?.user;
if (!user) {
if (!user || !accountId) {
toast.error('Not authenticated');
setSavingNote(false);
return;
@ -252,6 +254,7 @@ export function ContactDetailView({
const { error } = await supabase.from('contact_notes').insert({
contact_id: contactId,
account_id: accountId,
user_id: user.id,
note_text: newNote.trim(),
});

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { toast } from 'sonner';
import type { Contact, Tag, ContactTag } from '@/types';
import {
@ -34,6 +35,7 @@ export function ContactForm({
onSaved,
}: ContactFormProps) {
const supabase = createClient();
const { accountId } = useAuth();
const isEdit = !!contact;
const [name, setName] = useState('');
@ -91,6 +93,7 @@ export function ContactForm({
} = await supabase.auth.getSession();
const user = session?.user;
if (!user) throw new Error('Not authenticated');
if (!accountId) throw new Error('Your profile is not linked to an account.');
let contactId = contact?.id;
@ -111,6 +114,7 @@ export function ContactForm({
.from('contacts')
.insert({
user_id: user.id,
account_id: accountId,
name: name.trim() || null,
phone: phone.trim(),
email: email.trim() || null,

View File

@ -2,6 +2,7 @@
import { useState, useRef } from 'react';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { toast } from 'sonner';
import {
Dialog,
@ -79,6 +80,7 @@ function parseCSV(text: string): ParsedRow[] {
export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps) {
const supabase = createClient();
const { accountId } = useAuth();
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
@ -127,6 +129,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
} = await supabase.auth.getSession();
const user = session?.user;
if (!user) throw new Error('Not authenticated');
if (!accountId) throw new Error('Your profile is not linked to an account.');
let imported = 0;
let failed = 0;
@ -137,6 +140,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
const chunk = parsedRows.slice(i, i + chunkSize);
const rows = chunk.map((row) => ({
user_id: user.id,
account_id: accountId,
phone: row.phone,
name: row.name || null,
email: row.email || null,

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from "react";
import { createClient } from "@/lib/supabase/client";
import { useAuth } from "@/hooks/use-auth";
import { cn } from "@/lib/utils";
import type { Contact, Deal, ContactNote, Tag } from "@/types";
import {
@ -24,6 +25,7 @@ interface ContactSidebarProps {
}
export function ContactSidebar({ contact }: ContactSidebarProps) {
const { accountId } = useAuth();
const [copied, setCopied] = useState(false);
const [deals, setDeals] = useState<Deal[]>([]);
const [notes, setNotes] = useState<ContactNote[]>([]);
@ -86,6 +88,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
const handleAddNote = useCallback(async () => {
if (!contact || !newNote.trim()) return;
if (!accountId) return;
setAddingNote(true);
const supabase = createClient();
@ -98,6 +101,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
.from("contact_notes")
.insert({
contact_id: contact.id,
account_id: accountId,
user_id: user?.id,
note_text: newNote.trim(),
})
@ -109,7 +113,7 @@ export function ContactSidebar({ contact }: ContactSidebarProps) {
setNewNote("");
}
setAddingNote(false);
}, [contact, newNote]);
}, [contact, newNote, accountId]);
if (!contact) {
return (

View File

@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import { useAuth } from "@/hooks/use-auth";
import type {
Contact,
Conversation,
@ -51,6 +52,7 @@ export function DealForm({
onSaved,
}: DealFormProps) {
const supabase = createClient();
const { accountId } = useAuth();
const [title, setTitle] = useState("");
const [value, setValue] = useState("");
@ -185,9 +187,14 @@ export function DealForm({
setSaving(false);
return;
}
if (!accountId) {
toast.error("Your profile is not linked to an account.");
setSaving(false);
return;
}
const { error } = await supabase
.from("deals")
.insert({ ...payload, user_id: user.id, status: "open" });
.insert({ ...payload, user_id: user.id, account_id: accountId, status: "open" });
if (error) {
toast.error("Failed to create deal");
setSaving(false);

View File

@ -32,7 +32,7 @@ const PRESET_COLORS = [
export function TagManager() {
const supabase = createClient();
const { user, loading: authLoading } = useAuth();
const { user, accountId, loading: authLoading } = useAuth();
const [loading, setLoading] = useState(true);
const [tags, setTags] = useState<Tag[]>([]);
@ -82,7 +82,7 @@ export function TagManager() {
try {
setSaving(true);
if (!user) {
if (!user || !accountId) {
toast.error('Not authenticated');
return;
}
@ -91,6 +91,7 @@ export function TagManager() {
.from('tags')
.insert({
user_id: user.id,
account_id: accountId,
name: newTagName.trim(),
color: selectedColor,
});

View File

@ -2,6 +2,7 @@
import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth';
import { Contact, MessageTemplate } from '@/types';
export type CustomFieldOperator = 'is' | 'is_not' | 'contains';
@ -140,6 +141,7 @@ async function fetchCustomValueIndex(
}
export function useBroadcastSending(): UseBroadcastSendingReturn {
const { accountId } = useAuth();
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState(0);
@ -220,6 +222,9 @@ export function useBroadcastSending(): UseBroadcastSendingReturn {
if (!user) {
throw new Error('You are not signed in.');
}
if (!accountId) {
throw new Error('Your profile is not linked to an account.');
}
// De-duplicate by phone within the CSV (users can paste duplicates).
const uniqueByPhone = new Map<string, { phone: string; name?: string }>();
@ -249,6 +254,7 @@ export function useBroadcastSending(): UseBroadcastSendingReturn {
.filter((p) => !byPhone.has(p))
.map((phone) => ({
user_id: user.id,
account_id: accountId,
phone,
name: uniqueByPhone.get(phone)?.name ?? null,
}));
@ -326,6 +332,9 @@ export function useBroadcastSending(): UseBroadcastSendingReturn {
if (!user) {
throw new Error('You are not signed in.');
}
if (!accountId) {
throw new Error('Your profile is not linked to an account.');
}
// ── Step 1: Resolve audience contacts ─────────────────────────
setProgress(5);
@ -341,6 +350,7 @@ export function useBroadcastSending(): UseBroadcastSendingReturn {
.from('broadcasts')
.insert({
user_id: user.id,
account_id: accountId,
name: payload.name,
template_name: payload.template.name,
template_language: payload.template.language ?? 'en_US',