feat(activos): seccion para guardar y gestionar URLs con descripcion del cliente
This commit is contained in:
287
src/app/api/assets/urls/route.ts
Normal file
287
src/app/api/assets/urls/route.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user