feat(activos): seccion para guardar y gestionar URLs con descripcion del cliente

This commit is contained in:
Marcos Fargas
2026-10-15 02:27:47 +02:00
parent 9a4ed8b66c
commit 4f8ea26938
2 changed files with 820 additions and 13 deletions

View File

@@ -0,0 +1,287 @@
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,
});
}