Cada tenant tiene una ficha con plataformas, invitaciones por email, logo de marca y toggles de módulos visibles en el portal cliente. Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { createClient } from '@/lib/supabase/server';
|
|
import {
|
|
isAgencyRole,
|
|
serviceHeaders,
|
|
supabaseUrl,
|
|
userRole,
|
|
} from '@/lib/agency';
|
|
import type { TenantSettings } from '@/lib/tenant-modules';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
async function requireAgency() {
|
|
const supabase = await createClient();
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
if (!user) {
|
|
return { error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
|
|
}
|
|
if (!isAgencyRole(userRole(user))) {
|
|
return { error: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) };
|
|
}
|
|
const headers = serviceHeaders();
|
|
if (!headers) {
|
|
return {
|
|
error: NextResponse.json({ error: 'no service role' }, { status: 503 }),
|
|
};
|
|
}
|
|
return { headers };
|
|
}
|
|
|
|
/** GET ?tenantId= — full ficha payload */
|
|
export async function GET(req: NextRequest) {
|
|
const gate = await requireAgency();
|
|
if ('error' in gate && gate.error) return gate.error;
|
|
const { headers } = gate as {
|
|
headers: { apikey: string; Authorization: string };
|
|
};
|
|
|
|
const tenantId = req.nextUrl.searchParams.get('tenantId')?.trim() || '';
|
|
if (!tenantId) {
|
|
return NextResponse.json({ error: 'tenantId requerido' }, { status: 400 });
|
|
}
|
|
|
|
const base = supabaseUrl();
|
|
const [tRes, iRes] = await Promise.all([
|
|
fetch(
|
|
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}&select=id,name,slug,logo_url,settings,agency_followed,agency_notes,created_at`,
|
|
{ headers, cache: 'no-store' }
|
|
),
|
|
fetch(
|
|
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&select=provider,status,enabled,external_account_id,label,last_sync_at,error_message,meta`,
|
|
{ headers, cache: 'no-store' }
|
|
),
|
|
]);
|
|
|
|
if (!tRes.ok) {
|
|
const msg = await tRes.text();
|
|
return NextResponse.json(
|
|
{
|
|
error: msg.includes('logo_url') || msg.includes('settings')
|
|
? 'Aplicar sql/tenant_ficha.sql'
|
|
: msg.slice(0, 300),
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const tenants = await tRes.json();
|
|
const tenant = tenants[0];
|
|
if (!tenant) {
|
|
return NextResponse.json({ error: 'Cliente no encontrado' }, { status: 404 });
|
|
}
|
|
|
|
let integrations: unknown[] = [];
|
|
let integWarning: string | null = null;
|
|
if (iRes.ok) {
|
|
integrations = await iRes.json();
|
|
} else {
|
|
integWarning = (await iRes.text()).slice(0, 200);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
tenant: {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
slug: tenant.slug,
|
|
logoUrl: tenant.logo_url || null,
|
|
settings: (tenant.settings || {}) as TenantSettings,
|
|
followed: tenant.agency_followed !== false,
|
|
notes: tenant.agency_notes || '',
|
|
createdAt: tenant.created_at,
|
|
},
|
|
integrations,
|
|
warning: integWarning,
|
|
});
|
|
}
|
|
|
|
/** PATCH — follow/notes + ficha fields (name, logoUrl, settings). */
|
|
export async function PATCH(req: NextRequest) {
|
|
const gate = await requireAgency();
|
|
if ('error' in gate && gate.error) return gate.error;
|
|
const { headers } = gate as {
|
|
headers: { apikey: string; Authorization: string };
|
|
};
|
|
|
|
const body = await req.json().catch(() => ({}));
|
|
const tenantId =
|
|
typeof body.tenantId === 'string' ? body.tenantId.trim() : '';
|
|
if (!tenantId) {
|
|
return NextResponse.json({ error: 'tenantId requerido' }, { status: 400 });
|
|
}
|
|
|
|
const patch: Record<string, unknown> = {};
|
|
if (typeof body.followed === 'boolean') {
|
|
patch.agency_followed = body.followed;
|
|
}
|
|
if (typeof body.notes === 'string') {
|
|
patch.agency_notes = body.notes.trim() || null;
|
|
}
|
|
if (typeof body.name === 'string' && body.name.trim()) {
|
|
patch.name = body.name.trim();
|
|
}
|
|
if (typeof body.logoUrl === 'string') {
|
|
patch.logo_url = body.logoUrl.trim() || null;
|
|
}
|
|
if (body.logoUrl === null) {
|
|
patch.logo_url = null;
|
|
}
|
|
if (body.settings && typeof body.settings === 'object') {
|
|
patch.settings = body.settings as TenantSettings;
|
|
}
|
|
|
|
if (!Object.keys(patch).length) {
|
|
return NextResponse.json(
|
|
{ error: 'Nada que actualizar' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const r = await fetch(
|
|
`${supabaseUrl()}/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}`,
|
|
{
|
|
method: 'PATCH',
|
|
headers: {
|
|
...headers,
|
|
'Content-Type': 'application/json',
|
|
Prefer: 'return=representation',
|
|
},
|
|
body: JSON.stringify(patch),
|
|
}
|
|
);
|
|
if (!r.ok) {
|
|
const msg = await r.text();
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
msg.includes('logo_url') || msg.includes('settings')
|
|
? 'Aplicar sql/tenant_ficha.sql en Supabase'
|
|
: msg.includes('agency_followed')
|
|
? 'Aplicar sql/agency_control.sql'
|
|
: msg.slice(0, 300),
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
const rows = await r.json();
|
|
return NextResponse.json({ tenant: rows[0] || rows });
|
|
}
|