import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { createClient as createAdminClient } from '@supabase/supabase-js'; import { isAgencyRole, supabaseUrl, userRole } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { randomUUID } from 'crypto'; export const dynamic = 'force-dynamic'; export interface ClientUrlAsset { id: string; url: string; title: string; description: string; category?: string; created_at: string; updated_at?: string; created_by?: string; } function getAdmin() { const url = supabaseUrl(); const key = process.env.SUPABASE_SERVICE_ROLE_KEY!; return createAdminClient(url, key); } async function gate(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); // Permite autenticación por x-api-key en llamadas directas const apiKey = req.headers.get('x-api-key'); const validApiKey = process.env.CRON_SECRET || '4v-cron-sec-2026-mkt'; const isApiAuth = Boolean(apiKey && apiKey === validApiKey); if (!user && !isApiAuth) { return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }; } const queryTenant = req.nextUrl.searchParams.get('tenant_id') || req.nextUrl.searchParams.get('tenantId'); const role = user ? userRole(user) : 'admin'; const isAgency = user ? isAgencyRole(role) : true; let tenantId: string | null = null; let label = 'Cliente'; if ((isAgency || isApiAuth) && queryTenant) { tenantId = queryTenant; const admin = getAdmin(); const { data: t } = await admin .from('tenants') .select('name') .eq('id', queryTenant) .maybeSingle(); if (t?.name) label = t.name; } else if (user) { const ctx = await resolveActiveTenant(user); if (ctx.portfolioWide || !ctx.tenantId) { return { error: NextResponse.json( { error: 'Selecciona un cliente en Contexto antes de gestionar URLs.' }, { status: 400 } ), }; } tenantId = ctx.tenantId; label = ctx.label; } if (!tenantId) { return { error: NextResponse.json( { error: 'No se pudo identificar el tenant objetivo.' }, { status: 400 } ), }; } return { user, tenantId, label, isAgency }; } async function loadUrlsForTenant(tenantId: string): Promise<{ docId: string | null; urls: ClientUrlAsset[] }> { const admin = getAdmin(); const { data: row } = await admin .from('documents') .select('id, content') .eq('tenant_id', tenantId) .eq('type', 'client_urls') .order('updated_at', { ascending: false }) .limit(1) .maybeSingle(); if (!row) return { docId: null, urls: [] }; try { const parsed = typeof row.content === 'string' ? JSON.parse(row.content) : row.content; const list = Array.isArray(parsed) ? parsed : parsed?.urls || []; return { docId: row.id, urls: list }; } catch { return { docId: row.id, urls: [] }; } } async function saveUrlsForTenant(tenantId: string, docId: string | null, urls: ClientUrlAsset[]) { const admin = getAdmin(); const payload = { tenant_id: tenantId, type: 'client_urls', title: 'Enlaces y URLs del Cliente', content: JSON.stringify(urls), updated_at: new Date().toISOString(), }; if (docId) { await admin .from('documents') .update({ content: payload.content, updated_at: payload.updated_at, }) .eq('id', docId); } else { await admin .from('documents') .insert(payload); } } function normalizeUrl(raw: string): string { let s = (raw || '').trim(); if (!s) return ''; if (!/^https?:\/\//i.test(s)) { s = 'https://' + s; } return s; } export async function GET(req: NextRequest) { const g = await gate(req); if ('error' in g && g.error) return g.error; const { tenantId, label } = g as { tenantId: string; label: string }; const { urls } = await loadUrlsForTenant(tenantId); return NextResponse.json({ tenant_id: tenantId, tenant_label: label, urls, }); } export async function POST(req: NextRequest) { const g = await gate(req); if ('error' in g && g.error) return g.error; const { tenantId, user } = g as { tenantId: string; user?: any }; try { const body = await req.json(); const rawUrl = body.url?.trim(); const description = body.description?.trim(); const title = body.title?.trim() || ''; const category = body.category?.trim() || 'General'; if (!rawUrl) { return NextResponse.json({ error: 'La URL es obligatoria' }, { status: 400 }); } if (!description) { return NextResponse.json({ error: 'El texto descriptivo es obligatorio' }, { status: 400 }); } const cleanUrl = normalizeUrl(rawUrl); // Intentar derivar un título limpio si no se proporcionó let finalTitle = title; if (!finalTitle) { try { const parsed = new URL(cleanUrl); finalTitle = parsed.hostname.replace(/^www\./, ''); if (parsed.pathname && parsed.pathname !== '/') { const segment = parsed.pathname.split('/').filter(Boolean).pop(); if (segment) finalTitle += ` / ${segment.replace(/[-_]/g, ' ')}`; } } catch { finalTitle = 'Enlace web'; } } const newAsset: ClientUrlAsset = { id: `url_${Date.now()}_${randomUUID().slice(0, 8)}`, url: cleanUrl, title: finalTitle, description, category, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), created_by: user?.email || 'sistema', }; const { docId, urls } = await loadUrlsForTenant(tenantId); const updatedUrls = [newAsset, ...urls]; await saveUrlsForTenant(tenantId, docId, updatedUrls); return NextResponse.json({ success: true, url: newAsset, urls: updatedUrls, }); } catch (err: any) { return NextResponse.json( { error: err.message || 'Error al guardar la URL' }, { status: 500 } ); } } export async function PUT(req: NextRequest) { const g = await gate(req); if ('error' in g && g.error) return g.error; const { tenantId } = g as { tenantId: string }; try { const body = await req.json(); const id = body.id; if (!id) { return NextResponse.json({ error: 'ID de URL no proporcionado' }, { status: 400 }); } const { docId, urls } = await loadUrlsForTenant(tenantId); const index = urls.findIndex((u) => u.id === id); if (index === -1) { return NextResponse.json({ error: 'URL no encontrada' }, { status: 404 }); } const current = urls[index]; const cleanUrl = body.url ? normalizeUrl(body.url) : current.url; const updatedItem: ClientUrlAsset = { ...current, url: cleanUrl, title: body.title !== undefined ? body.title.trim() : current.title, description: body.description !== undefined ? body.description.trim() : current.description, category: body.category !== undefined ? body.category.trim() : current.category, updated_at: new Date().toISOString(), }; const updatedUrls = [...urls]; updatedUrls[index] = updatedItem; await saveUrlsForTenant(tenantId, docId, updatedUrls); return NextResponse.json({ success: true, url: updatedItem, urls: updatedUrls, }); } catch (err: any) { return NextResponse.json( { error: err.message || 'Error al actualizar la URL' }, { status: 500 } ); } } export async function DELETE(req: NextRequest) { const g = await gate(req); if ('error' in g && g.error) return g.error; const { tenantId } = g as { tenantId: string }; const id = req.nextUrl.searchParams.get('id'); if (!id) { return NextResponse.json({ error: 'ID requerido' }, { status: 400 }); } const { docId, urls } = await loadUrlsForTenant(tenantId); const updatedUrls = urls.filter((u) => u.id !== id); await saveUrlsForTenant(tenantId, docId, updatedUrls); return NextResponse.json({ success: true, urls: updatedUrls, }); }