feat(creatives): implement WF4 integrations, GET include=client, asset_final check and HMAC webhook
This commit is contained in:
@@ -2,6 +2,64 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
|
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const { id } = await params;
|
||||||
|
const auth = await authenticateIntegrationRequest(req, { requireTenant: true });
|
||||||
|
if (!auth.ok) {
|
||||||
|
return auth.errorResponse!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = auth.tenantId!;
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||||
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||||
|
const adminClient = createClient(supabaseUrl, serviceRoleKey);
|
||||||
|
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const include = searchParams.get('include');
|
||||||
|
|
||||||
|
const { data: creative, error: creativeErr } = await adminClient
|
||||||
|
.from('creatives')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', id)
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (creativeErr || !creative) {
|
||||||
|
return NextResponse.json({ error: 'Creatividad no encontrada' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include === 'client') {
|
||||||
|
const { data: client } = await adminClient
|
||||||
|
.from('tenants')
|
||||||
|
.select('id, name, slug, meta_ad_account_id, meta_page_id, meta_ig_user_id, default_landing_url, meta_publish_status, slack_channel_id, website')
|
||||||
|
.eq('id', tenantId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: {
|
||||||
|
creative,
|
||||||
|
client: {
|
||||||
|
id: client?.id,
|
||||||
|
name: client?.name,
|
||||||
|
slug: client?.slug,
|
||||||
|
meta_ad_account_id: client?.meta_ad_account_id,
|
||||||
|
meta_page_id: client?.meta_page_id,
|
||||||
|
meta_ig_user_id: client?.meta_ig_user_id,
|
||||||
|
default_landing_url: client?.default_landing_url,
|
||||||
|
meta_publish_status: client?.meta_publish_status || 'PAUSED',
|
||||||
|
slack_channel_id: client?.slack_channel_id,
|
||||||
|
website: client?.website,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ data: creative });
|
||||||
|
}
|
||||||
|
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
@@ -48,6 +106,8 @@ export async function PATCH(
|
|||||||
if (body.campaign_name !== undefined) updates.campaign_name = body.campaign_name;
|
if (body.campaign_name !== undefined) updates.campaign_name = body.campaign_name;
|
||||||
if (body.asset_url !== undefined) updates.asset_url = body.asset_url;
|
if (body.asset_url !== undefined) updates.asset_url = body.asset_url;
|
||||||
if (body.thumbnail_url !== undefined) updates.thumbnail_url = body.thumbnail_url;
|
if (body.thumbnail_url !== undefined) updates.thumbnail_url = body.thumbnail_url;
|
||||||
|
if (body.asset_final !== undefined) updates.asset_final = body.asset_final;
|
||||||
|
if (body.meta !== undefined) updates.meta = body.meta;
|
||||||
|
|
||||||
const { data: updated, error: updateErr } = await adminClient
|
const { data: updated, error: updateErr } = await adminClient
|
||||||
.from('creatives')
|
.from('creatives')
|
||||||
@@ -73,6 +133,7 @@ export async function PATCH(
|
|||||||
payload: {
|
payload: {
|
||||||
meta_ad_id: updates.meta_ad_id || creative.meta_ad_id,
|
meta_ad_id: updates.meta_ad_id || creative.meta_ad_id,
|
||||||
adset_id: updates.adset_id,
|
adset_id: updates.adset_id,
|
||||||
|
meta: updates.meta,
|
||||||
},
|
},
|
||||||
visibility: 'client',
|
visibility: 'client',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,24 +33,25 @@ export function verifyWebhookSignature(payloadStr: string, signature: string, se
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Envía un webhook firmado hacia n8n (N8N_WEBHOOK_BASE/event)
|
* Envía un webhook firmado hacia n8n (/webhook/app4v/creative-event)
|
||||||
*/
|
*/
|
||||||
export async function dispatchN8nWebhook(
|
export async function dispatchN8nWebhook(
|
||||||
event: CreativeWebhookEvent,
|
event: CreativeWebhookEvent,
|
||||||
payload: Record<string, unknown>
|
payload: Record<string, unknown>
|
||||||
): Promise<WebhookDispatchResult> {
|
): Promise<WebhookDispatchResult> {
|
||||||
const baseUrl = process.env.N8N_WEBHOOK_BASE?.trim() || 'https://n8n-agencia.4ventures.es/webhook';
|
const baseUrl = process.env.N8N_WEBHOOK_BASE?.trim() || 'https://n8n.4ventures.es/webhook';
|
||||||
const secret = process.env.N8N_WEBHOOK_SECRET?.trim() || 'whsec_4v_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
|
const secret = process.env.N8N_WEBHOOK_SECRET?.trim() || 'whsec_4v_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
return { sent: false, error: 'N8N_WEBHOOK_BASE is not configured' };
|
return { sent: false, error: 'N8N_WEBHOOK_BASE is not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${baseUrl.replace(/\/$/, '')}/${event.replace('.', '-')}`;
|
// WF4 listens on /webhook/app4v/creative-event
|
||||||
|
const endpoint = `${baseUrl.replace(/\/$/, '')}/app4v/creative-event`;
|
||||||
const fullBody = {
|
const fullBody = {
|
||||||
event,
|
event,
|
||||||
|
payload,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
data: payload,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bodyStr = JSON.stringify(fullBody);
|
const bodyStr = JSON.stringify(fullBody);
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export async function sendCreativeToClient(creativeId: string, tenantId: string,
|
|||||||
throw new Error('Creatividad no encontrada');
|
throw new Error('Creatividad no encontrada');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!creative.asset_final) {
|
||||||
|
throw new Error('Solo se puede enviar al cliente si cuenta con imagen final compuesta (asset_final=true)');
|
||||||
|
}
|
||||||
|
|
||||||
const prevStatus = creative.status;
|
const prevStatus = creative.status;
|
||||||
|
|
||||||
const { data: updated, error: updateErr } = await adminClient
|
const { data: updated, error: updateErr } = await adminClient
|
||||||
|
|||||||
Reference in New Issue
Block a user