mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
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:
commit
0a9e2db61a
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user