import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { ASSETS_BUCKET, MAX_ASSET_BYTES, ensureAssetsBucket, sanitizeFileName, serviceSupabase, } from '@/lib/assets'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; const ALLOWED_MIME = new Set(['image/jpeg', 'image/png', 'image/webp']); /** * POST multipart: sube portada o slide de una pieza del pipeline. * fields: file, itemId (requerido), slot ('cover' | 'slide'), index? (para slide, 0-based) */ export async function POST(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } if (!isAgencyRole(userRole(user))) { return NextResponse.json( { error: 'Solo el equipo de la agencia puede subir imágenes' }, { status: 403 } ); } const ctx = await resolveActiveTenant(user); if (!ctx.tenantId || ctx.portfolioWide) { return NextResponse.json({ error: 'Selecciona un cliente en Contexto' }, { status: 400 }); } const form = await req.formData(); const file = form.get('file'); const itemId = String(form.get('itemId') || '').trim(); const slot = String(form.get('slot') || 'cover').trim(); const index = Math.max(0, parseInt(String(form.get('index') || '0'), 10) || 0); if (!(file instanceof File)) { return NextResponse.json({ error: 'file requerido' }, { status: 400 }); } if (!itemId) { return NextResponse.json({ error: 'itemId requerido' }, { status: 400 }); } if (file.size <= 0 || file.size > MAX_ASSET_BYTES) { return NextResponse.json( { error: `Archivo vacío o > ${MAX_ASSET_BYTES / (1024 * 1024)} MB` }, { status: 400 } ); } const mime = file.type || 'application/octet-stream'; if (!ALLOWED_MIME.has(mime)) { return NextResponse.json({ error: 'Solo jpg/png/webp' }, { status: 400 }); } const bucketOk = await ensureAssetsBucket(); if (!bucketOk.ok) { return NextResponse.json( { error: bucketOk.error || 'No se pudo asegurar el bucket' }, { status: 500 } ); } const svc = serviceSupabase(); if (!svc) { return NextResponse.json({ error: 'Service role required' }, { status: 503 }); } const headers = serviceHeaders(); if (!headers) { return NextResponse.json({ error: 'Service role required' }, { status: 503 }); } // Verifica que la pieza existe y pertenece al tenant activo. const curRes = await fetch( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent( itemId )}&tenant_id=eq.${encodeURIComponent(ctx.tenantId)}&select=id,slide_paths`, { headers, cache: 'no-store' } ); const curRows = curRes.ok ? await curRes.json() : []; if (!curRows?.[0]) { return NextResponse.json({ error: 'Pieza no encontrada' }, { status: 404 }); } const ext = (sanitizeFileName(file.name || 'img.jpg').split('.').pop() || 'jpg').toLowerCase(); const fileName = slot === 'cover' ? `cover.${ext}` : `slide-${index + 1}.${ext}`; const storagePath = `${ctx.tenantId}/pipeline/${itemId}/${fileName}`; const buf = Buffer.from(await file.arrayBuffer()); const up = await svc.storage.from(ASSETS_BUCKET).upload(storagePath, buf, { contentType: mime, upsert: true, }); if (up.error) { return NextResponse.json({ error: up.error.message }, { status: 500 }); } const patch: Record = { updated_at: new Date().toISOString() }; if (slot === 'cover') { patch.cover_path = storagePath; patch.cover_mime = mime; } else { const slides: string[] = Array.isArray(curRows[0].slide_paths) ? [...curRows[0].slide_paths] : []; slides[index] = storagePath; patch.slide_paths = slides; } const patchRes = await fetch( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(itemId)}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify(patch), } ); if (!patchRes.ok) { await svc.storage.from(ASSETS_BUCKET).remove([storagePath]); return NextResponse.json({ error: await patchRes.text() }, { status: 500 }); } const data = await patchRes.json(); return NextResponse.json({ item: Array.isArray(data) ? data[0] : data, storage_path: storagePath, }); }