feat: agency portfolio with per-client integrations hub

Admin home lists tenants, alerts, and connection health; header
selector enables superadmin context; Meta/GA4/GSC/GTM/CRM/email
slots live per tenant (manual status until OAuth).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-22 01:31:47 +02:00
parent 9d1609c88b
commit 791976a3d8
19 changed files with 1503 additions and 20 deletions

View File

@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import {
VIEW_TENANT_COOKIE,
VIEW_TENANT_NAME_COOKIE,
VIEW_TENANT_SLUG_COOKIE,
isAgencyRole,
serviceHeaders,
supabaseUrl,
userRole,
} from '@/lib/agency';
export const dynamic = 'force-dynamic';
/** POST { tenantId } — set superadmin view context cookie. null clears. */
export async function POST(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
if (!isAgencyRole(userRole(user))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await req.json().catch(() => ({}));
const tenantId =
typeof body.tenantId === 'string' && body.tenantId.trim()
? body.tenantId.trim()
: null;
const cookieOpts = {
path: '/',
maxAge: 60 * 60 * 24 * 30,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
};
if (!tenantId) {
const res = NextResponse.json({ ok: true, cleared: true });
res.cookies.set(VIEW_TENANT_COOKIE, '', { ...cookieOpts, maxAge: 0 });
res.cookies.set(VIEW_TENANT_SLUG_COOKIE, '', { ...cookieOpts, maxAge: 0 });
res.cookies.set(VIEW_TENANT_NAME_COOKIE, '', { ...cookieOpts, maxAge: 0 });
return res;
}
const headers = serviceHeaders();
if (!headers) {
return NextResponse.json(
{ error: 'Falta SUPABASE_SERVICE_ROLE_KEY' },
{ status: 503 }
);
}
const r = await fetch(
`${supabaseUrl()}/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}&select=id,name,slug`,
{ headers, cache: 'no-store' }
);
if (!r.ok) {
return NextResponse.json({ error: 'Tenant lookup failed' }, { status: 500 });
}
const rows = await r.json();
const tenant = Array.isArray(rows) ? rows[0] : null;
if (!tenant) {
return NextResponse.json({ error: 'Tenant no encontrado' }, { status: 404 });
}
const res = NextResponse.json({
ok: true,
tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug },
});
res.cookies.set(VIEW_TENANT_COOKIE, tenant.id, cookieOpts);
res.cookies.set(VIEW_TENANT_SLUG_COOKIE, tenant.slug || '', cookieOpts);
res.cookies.set(VIEW_TENANT_NAME_COOKIE, tenant.name || '', cookieOpts);
return res;
}
export async function GET() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// readable from client via document.cookie too; this is for SSR helpers
return NextResponse.json({ ok: true });
}