feat: conectar GA4/GSC/Drive desde la ficha (verificar + sync real)

Al guardar ID o URL en el cliente se valida con OAuth de agencia;
Drive acepta URL o folder id; Orgánico se rellena si hay acceso.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 12:27:15 +02:00
parent 81ba5df37d
commit 99895fe918
3 changed files with 387 additions and 21 deletions

View File

@@ -7,6 +7,11 @@ import {
supabaseUrl,
userRole,
} from '@/lib/agency';
import {
activateGoogleFromFicha,
LIVE_GOOGLE_PROVIDERS,
normalizeDriveFolderId,
} from '@/lib/integration-activate';
export const dynamic = 'force-dynamic';
@@ -93,7 +98,7 @@ export async function GET(req: NextRequest) {
});
}
/** PATCH — update one integration status/account (manual connect for now). */
/** PATCH — update integration; GA4/GSC/Drive verify + sync via agency Google OAuth. */
export async function PATCH(req: NextRequest) {
const gate = await requireAgency();
if ('error' in gate && gate.error) return gate.error;
@@ -124,34 +129,69 @@ export async function PATCH(req: NextRequest) {
return NextResponse.json({ error: 'status inválido' }, { status: 400 });
}
const wantsLiveConnect =
LIVE_GOOGLE_PROVIDERS.has(provider) &&
(status === 'connected' ||
(status === 'pending' && Boolean(externalAccountId)));
if (wantsLiveConnect) {
let rawId = externalAccountId || '';
if (!rawId) {
const cur = await fetch(
`${supabaseUrl()}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&provider=eq.${encodeURIComponent(provider)}&select=external_account_id`,
{ headers, cache: 'no-store' }
);
if (cur.ok) {
const rows = (await cur.json()) as { external_account_id?: string }[];
rawId = rows[0]?.external_account_id || '';
}
}
if (!rawId) {
return NextResponse.json(
{
error: 'Indica ID o URL antes de conectar',
activation: { ok: false, message: 'Falta ID' },
},
{ status: 400 }
);
}
const activation = await activateGoogleFromFicha({
tenantId,
provider: provider as 'ga4' | 'search_console' | 'google_drive',
rawId,
});
const list = await fetch(
`${supabaseUrl()}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&provider=eq.${encodeURIComponent(provider)}&select=*`,
{ headers, cache: 'no-store' }
);
const rows = list.ok ? await list.json() : [];
return NextResponse.json(
{
integration: rows[0] || null,
activation,
error: activation.ok ? undefined : activation.errorMessage || activation.message,
},
{ status: activation.ok ? 200 : 422 }
);
}
const patch: Record<string, unknown> = {
updated_at: new Date().toISOString(),
};
if (status) patch.status = status;
if (externalAccountId !== undefined) {
let id = externalAccountId || null;
// Drive: accept full folder URL → store folder id only
if (provider === 'google_drive' && id) {
const m = id.match(/\/folders\/([a-zA-Z0-9_-]+)/);
if (m) id = m[1];
else if (id.includes('http')) {
const q = id.match(/[?&]id=([a-zA-Z0-9_-]+)/);
if (q) id = q[1];
}
id = normalizeDriveFolderId(id) || id;
}
patch.external_account_id = id;
}
if (label !== undefined) patch.label = label || null;
if (errorMessage !== undefined) patch.error_message = errorMessage || null;
if (enabled !== undefined) patch.enabled = enabled;
// Manual «Conectar» only marks the reference — do not fake a data sync timestamp.
// Real syncs (Ads discover, Orgánico, Superhuman…) set last_sync_at themselves.
if (status === 'connected') {
patch.error_message = null;
}
if (status === 'disconnected') {
// keep external id for re-link unless cleared
}
if (!status && enabled === undefined && externalAccountId === undefined) {
return NextResponse.json(
@@ -173,7 +213,6 @@ export async function PATCH(req: NextRequest) {
});
if (!r.ok) {
// upsert if missing
const ins = await fetch(`${base}/rest/v1/integrations`, {
method: 'POST',
headers: {