diff --git a/src/app/api/automations/[id]/duplicate/route.ts b/src/app/api/automations/[id]/duplicate/route.ts index 0b76852..401951b 100644 --- a/src/app/api/automations/[id]/duplicate/route.ts +++ b/src/app/api/automations/[id]/duplicate/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/automations/admin-client' export async function POST( @@ -7,6 +8,16 @@ export async function POST( { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params + + // Duplicating creates a new automation row — a write. Enforce `agent` + // (the service-role client below bypasses the agent-gated + // automations_insert RLS). + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const supabase = await createClient() const { data: { user }, diff --git a/src/app/api/automations/[id]/route.ts b/src/app/api/automations/[id]/route.ts index 5ca0ab9..6f3c53c 100644 --- a/src/app/api/automations/[id]/route.ts +++ b/src/app/api/automations/[id]/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/automations/admin-client' import { loadStepsTree, @@ -47,6 +48,16 @@ export async function PATCH( { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params + + // Editing an automation is a write — the RLS automations_update policy + // requires `agent`, but this route mutates via the service-role client + // which bypasses RLS, so enforce the role here. + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const user = await requireUser() if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) @@ -125,6 +136,15 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params + + // Deleting an automation is a write — enforce `agent` (the service-role + // client below bypasses the agent-gated automations_delete RLS). + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const user = await requireUser() if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) diff --git a/src/app/api/automations/engine/route.ts b/src/app/api/automations/engine/route.ts index a66e695..1590de4 100644 --- a/src/app/api/automations/engine/route.ts +++ b/src/app/api/automations/engine/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server' -import { getCurrentAccount, toErrorResponse } from '@/lib/auth/account' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { runAutomationsForTrigger } from '@/lib/automations/engine' import type { AutomationTriggerType } from '@/types' @@ -9,9 +9,11 @@ import type { AutomationTriggerType } from '@/types' * account_id and dispatch over the account's automations. */ export async function POST(request: Request) { + // Firing automations sends outbound WhatsApp — a write action. Require + // at least `agent`; a viewer must not be able to trigger sends. let accountId: string try { - const ctx = await getCurrentAccount() + const ctx = await requireRole('agent') accountId = ctx.accountId } catch (err) { return toErrorResponse(err) diff --git a/src/app/api/automations/route.ts b/src/app/api/automations/route.ts index d4c5dc4..7c21147 100644 --- a/src/app/api/automations/route.ts +++ b/src/app/api/automations/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/automations/admin-client' import { getTemplate } from '@/lib/automations/templates' import { insertSteps, type BuilderStepInput } from '@/lib/automations/steps-tree' @@ -24,6 +25,15 @@ export async function GET() { } export async function POST(request: Request) { + // Creating an automation is a write — the RLS automations_insert policy + // requires `agent`, but this route inserts via the service-role client + // which bypasses RLS, so the role must be enforced here. + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const supabase = await createClient() const { data: { user }, diff --git a/src/app/api/flows/[id]/activate/route.ts b/src/app/api/flows/[id]/activate/route.ts index a2915c1..263addf 100644 --- a/src/app/api/flows/[id]/activate/route.ts +++ b/src/app/api/flows/[id]/activate/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/flows/admin-client' import { validateFlowForActivation } from '@/lib/flows/validate' @@ -23,6 +24,16 @@ export async function POST( ) { const { id } = await context.params + // Changing status (activate / draft / archive) is a write — the RLS + // flows_update policy requires `agent`, but the service-role client + // below bypasses RLS, so enforce the role here (a viewer passes the + // membership-only ownership check). + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const supabase = await createClient() const { data: { user }, diff --git a/src/app/api/flows/[id]/route.ts b/src/app/api/flows/[id]/route.ts index 5119dca..52e3b6f 100644 --- a/src/app/api/flows/[id]/route.ts +++ b/src/app/api/flows/[id]/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/flows/admin-client' /** @@ -91,6 +92,16 @@ export async function PUT( context: { params: Promise<{ id: string }> }, ) { const { id } = await context.params + + // Writes require at least `agent` — the RLS flows_update policy demands + // it, but this route mutates via the service-role client which bypasses + // RLS, so the role must be enforced here (a viewer passes ownership). + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const guard = await requireOwnership(id) if (!guard.ok) return NextResponse.json(guard.body, { status: guard.status }) @@ -177,6 +188,15 @@ export async function DELETE( context: { params: Promise<{ id: string }> }, ) { const { id } = await context.params + + // Writes require at least `agent` — see the PUT handler note. The + // service-role client below bypasses the agent-gated flows_delete RLS. + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const guard = await requireOwnership(id) if (!guard.ok) return NextResponse.json(guard.body, { status: guard.status }) diff --git a/src/app/api/flows/route.ts b/src/app/api/flows/route.ts index 94e7118..9fdac0d 100644 --- a/src/app/api/flows/route.ts +++ b/src/app/api/flows/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' +import { requireRole, toErrorResponse } from '@/lib/auth/account' import { supabaseAdmin } from '@/lib/flows/admin-client' import { getFlowTemplate } from '@/lib/flows/templates' @@ -45,6 +46,15 @@ export async function GET() { } export async function POST(request: Request) { + // Creating a flow is a write — the RLS flows_insert policy requires + // `agent`, but this route inserts via the service-role client which + // bypasses RLS, so the role must be enforced here. + try { + await requireRole('agent') + } catch (err) { + return toErrorResponse(err) + } + const guard = await requireUser() if (!guard.ok) { return NextResponse.json(guard.body, { status: guard.status })