wacrm/src/app/api/automations/route.ts
Arnas Donauskas c5ee04ce9e
fix(account-sharing): stamp account_id on account-scoped inserts (#205)
Migration 017 made domain tables account-scoped: account_id is NOT NULL
and INSERT RLS is WITH CHECK (is_account_member(account_id, ...)). Several
client- and server-side create flows still inserted only user_id, so on a
fresh install they failed with 403 / "new row violates row-level security
policy" / not-null violations.

Resolve account_id from useAuth() (client) or the caller's profile (server)
and include it on every account-scoped insert:

- contacts: contact-form, import-modal, broadcast CSV upsert
- contact_notes: contact-sidebar, contact-detail-view
- tags: tag-manager
- broadcasts: broadcasts/new page, use-broadcast-sending
- deals: deal-form
- automations: POST /api/automations, POST /api/automations/[id]/duplicate

Each path also guards on a missing accountId with a clear message instead
of letting the insert fail opaquely.

pipelines/page.tsx and api/flows/route.ts are intentionally left to the
already-open PR #193, which fixes those same paths (and adds tests).

Verified: a full sweep of every account-scoped insert/upsert across src/
now includes account_id (except the two paths owned by #193). typecheck,
lint (0 errors), and 370 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:20:54 +03:00

126 lines
4.2 KiB
TypeScript

import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { supabaseAdmin } from '@/lib/automations/admin-client'
import { getTemplate } from '@/lib/automations/templates'
import { insertSteps, type BuilderStepInput } from '@/lib/automations/steps-tree'
import {
validateStepsForActivation,
validateTriggerForActivation,
} from '@/lib/automations/validate'
export async function GET() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { data, error } = await supabase
.from('automations')
.select('*')
.order('created_at', { ascending: false })
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json({ automations: data ?? [] })
}
export async function POST(request: Request) {
const supabase = await createClient()
const {
data: { user },
} = 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 })
const { name, description, trigger_type, trigger_config, is_active, steps, template } = body
let effectiveSteps: BuilderStepInput[] | undefined = steps
let effectiveName = name
let effectiveDescription = description
let effectiveTriggerType = trigger_type
let effectiveTriggerConfig = trigger_config
if (template && (!steps || steps.length === 0)) {
const t = getTemplate(template)
if (t) {
effectiveName = effectiveName ?? t.name
effectiveDescription = effectiveDescription ?? t.description
effectiveTriggerType = effectiveTriggerType ?? t.trigger_type
effectiveTriggerConfig = effectiveTriggerConfig ?? t.trigger_config
effectiveSteps = t.steps as unknown as BuilderStepInput[]
}
}
if (!effectiveName || !effectiveTriggerType) {
return NextResponse.json(
{ error: 'name and trigger_type are required' },
{ status: 400 },
)
}
// Block activation of a clearly broken automation up-front instead of
// letting every trigger silently produce a failed log row. Drafts
// (is_active=false) are allowed to be incomplete so users can save
// progress mid-build.
if (is_active) {
const issues = [
...validateTriggerForActivation(effectiveTriggerType, effectiveTriggerConfig ?? {}),
...validateStepsForActivation(
(effectiveSteps ?? []) as unknown as { step_type: string; step_config: Record<string, unknown> }[],
),
]
if (issues.length > 0) {
return NextResponse.json(
{ error: 'Cannot activate automation with invalid configuration', issues },
{ status: 400 },
)
}
}
const admin = supabaseAdmin()
const { data: automation, error: insertErr } = await admin
.from('automations')
.insert({
user_id: user.id,
account_id: accountId,
name: effectiveName,
description: effectiveDescription ?? null,
trigger_type: effectiveTriggerType,
trigger_config: effectiveTriggerConfig ?? {},
is_active: !!is_active,
})
.select()
.single()
if (insertErr || !automation) {
return NextResponse.json(
{ error: insertErr?.message ?? 'insert failed' },
{ status: 500 },
)
}
if (effectiveSteps && effectiveSteps.length > 0) {
const err = await insertSteps(automation.id, effectiveSteps)
if (err) return NextResponse.json({ error: err }, { status: 500 })
}
return NextResponse.json({ automation }, { status: 201 })
}