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, sanitizeFolder, serviceSupabase, } from '@/lib/assets'; import { createHash, randomUUID } from 'crypto'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; async function gate() { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }; } const ctx = await resolveActiveTenant(user); if (ctx.portfolioWide || !ctx.tenantId) { return { error: NextResponse.json( { error: 'Selecciona un cliente en Contexto antes de gestionar activos.' }, { status: 400 } ), }; } return { user, ctx, agency: isAgencyRole(userRole(user)) }; } /** GET — list approved files for active tenant */ export async function GET() { const g = await gate(); if ('error' in g && g.error) return g.error; const { ctx, agency } = g as Awaited> & { ctx: { tenantId: string }; agency: boolean; }; const headers = serviceHeaders(); if (agency && headers) { const r = await fetch( `${supabaseUrl()}/rest/v1/asset_files?tenant_id=eq.${encodeURIComponent(ctx.tenantId!)}&status=eq.approved&select=*&order=created_at.desc`, { headers, cache: 'no-store' } ); if (!r.ok) { const msg = await r.text(); return NextResponse.json({ assets: [], warning: msg.includes('does not exist') ? 'Aplicar sql/asset_files.sql' : msg.slice(0, 200), }); } return NextResponse.json({ assets: await r.json() }); } const supabase = await createClient(); const { data, error } = await supabase .from('asset_files') .select('*') .eq('status', 'approved') .order('created_at', { ascending: false }); if (error) { return NextResponse.json({ assets: [], warning: error.message, }); } return NextResponse.json({ assets: data ?? [] }); } /** POST multipart: files[] + optional folder */ export async function POST(req: NextRequest) { const g = await gate(); if ('error' in g && g.error) return g.error; const { user, ctx } = g as Awaited> & { user: { id: string }; ctx: { tenantId: string }; }; const bucketOk = await ensureAssetsBucket(); if (!bucketOk.ok) { return NextResponse.json( { error: `Storage bucket: ${bucketOk.error}` }, { status: 503 } ); } const svc = serviceSupabase(); const headers = serviceHeaders(); if (!svc || !headers) { return NextResponse.json( { error: 'Falta SUPABASE_SERVICE_ROLE_KEY' }, { status: 503 } ); } const form = await req.formData(); const folder = sanitizeFolder(String(form.get('folder') || '')); const files = form.getAll('files').filter((f): f is File => f instanceof File); if (!files.length) { return NextResponse.json({ error: 'Ningún archivo' }, { status: 400 }); } const uploaded: unknown[] = []; const errors: string[] = []; for (const file of files) { if (file.size <= 0) { errors.push(`${file.name}: vacío`); continue; } if (file.size > MAX_ASSET_BYTES) { errors.push(`${file.name}: supera 80 MB`); continue; } const safeName = sanitizeFileName(file.name); const id = randomUUID(); const pathParts = [ctx.tenantId!, folder, `${id}_${safeName}`].filter( Boolean ); const storagePath = pathParts.join('/'); const buf = Buffer.from(await file.arrayBuffer()); const sha256 = createHash('sha256').update(buf).digest('hex'); const up = await svc.storage.from(ASSETS_BUCKET).upload(storagePath, buf, { contentType: file.type || 'application/octet-stream', upsert: false, }); if (up.error) { errors.push(`${file.name}: ${up.error.message}`); continue; } const row = { id, tenant_id: ctx.tenantId, title: safeName, folder, file_name: safeName, mime_type: file.type || 'application/octet-stream', bytes: file.size, storage_path: storagePath, sha256, status: 'approved', created_by: user.id, }; const ins = await fetch(`${supabaseUrl()}/rest/v1/asset_files`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify(row), }); if (!ins.ok) { await svc.storage.from(ASSETS_BUCKET).remove([storagePath]); errors.push(`${file.name}: ${await ins.text()}`); continue; } const created = await ins.json(); uploaded.push(Array.isArray(created) ? created[0] : created); } return NextResponse.json( { uploaded, errors }, { status: uploaded.length ? 201 : 400 } ); } /** DELETE ?id= */ export async function DELETE(req: NextRequest) { const g = await gate(); if ('error' in g && g.error) return g.error; const { ctx } = g as Awaited> & { ctx: { tenantId: string }; }; const id = req.nextUrl.searchParams.get('id')?.trim(); if (!id) { return NextResponse.json({ error: 'id requerido' }, { status: 400 }); } const headers = serviceHeaders(); const svc = serviceSupabase(); if (!headers || !svc) { return NextResponse.json({ error: 'no service role' }, { status: 503 }); } const get = await fetch( `${supabaseUrl()}/rest/v1/asset_files?id=eq.${encodeURIComponent(id)}&tenant_id=eq.${encodeURIComponent(ctx.tenantId!)}&select=id,storage_path`, { headers, cache: 'no-store' } ); if (!get.ok) { return NextResponse.json({ error: await get.text() }, { status: 500 }); } const rows = await get.json(); const row = rows[0]; if (!row) { return NextResponse.json({ error: 'No encontrado' }, { status: 404 }); } await svc.storage.from(ASSETS_BUCKET).remove([row.storage_path]); const del = await fetch( `${supabaseUrl()}/rest/v1/asset_files?id=eq.${encodeURIComponent(id)}`, { method: 'DELETE', headers } ); if (!del.ok) { return NextResponse.json({ error: await del.text() }, { status: 500 }); } return NextResponse.json({ ok: true }); }