feat(creatives): task 2 - integrations auth and visibility scope with sanitization

This commit is contained in:
Marcos Fargas
2026-10-12 13:13:46 +02:00
parent f08bcf2bb8
commit 62d1f3a5f6
4 changed files with 326 additions and 1 deletions

View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
export type IntegrationCaller = 'n8n' | 'claude';
export interface IntegrationAuthResult {
ok: boolean;
caller?: IntegrationCaller;
tenantId?: string;
errorResponse?: NextResponse;
}
/**
* Valida la autenticación de llamadas a /api/integrations/*
* Revisa X-Api-Key contra N8N_API_KEY y CLAUDE_API_KEY.
* Valida que el tenant (obtenido por tenant_id o client_id en query/body) tenga creatives_enabled = true.
*/
export async function authenticateIntegrationRequest(
req: NextRequest,
options: { requireTenant?: boolean } = { requireTenant: true }
): Promise<IntegrationAuthResult> {
const apiKey = req.headers.get('x-api-key') || req.headers.get('X-Api-Key');
const n8nKey = process.env.N8N_API_KEY?.trim();
const claudeKey = process.env.CLAUDE_API_KEY?.trim();
let caller: IntegrationCaller | undefined;
if (apiKey && n8nKey && apiKey === n8nKey) {
caller = 'n8n';
} else if (apiKey && claudeKey && apiKey === claudeKey) {
caller = 'claude';
}
if (!caller) {
return {
ok: false,
errorResponse: NextResponse.json(
{ error: 'Unauthorized: clave de integración inválida o no proporcionada' },
{ status: 401 }
),
};
}
if (!options.requireTenant) {
return { ok: true, caller };
}
// Extraer tenant_id o client_id de query params o body
let tenantId =
req.nextUrl.searchParams.get('tenant_id') ||
req.nextUrl.searchParams.get('client_id');
if (!tenantId && req.method !== 'GET') {
try {
const cloned = req.clone();
const body = await cloned.json();
tenantId = body.tenant_id || body.client_id;
} catch {
// Ignorar error al parsear body si no viene JSON
}
}
if (!tenantId) {
return {
ok: false,
errorResponse: NextResponse.json(
{ error: 'Bad Request: client_id o tenant_id requerido' },
{ status: 400 }
),
};
}
// Verificar en base de datos si tiene creatives_enabled
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const adminClient = createClient(supabaseUrl, serviceRoleKey);
const { data: tenant, error } = await adminClient
.from('tenants')
.select('id, creatives_enabled')
.eq('id', tenantId)
.maybeSingle();
if (error || !tenant) {
return {
ok: false,
errorResponse: NextResponse.json(
{ error: 'Not Found: cliente no encontrado' },
{ status: 404 }
),
};
}
if (!tenant.creatives_enabled) {
return {
ok: false,
errorResponse: NextResponse.json(
{ error: 'Forbidden: el cliente no tiene habilitado el módulo de creatividades' },
{ status: 403 }
),
};
}
return { ok: true, caller, tenantId: tenant.id };
}