feat: al conectar Google, auto-emparejar Drive/GA4/GSC/Ads en fichas
Bootstrap unificado post-OAuth + botón Emparejar todo. Meta discover también intenta FB/IG pages con el token system user (API 4V2030). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import {
|
||||
applyMatches,
|
||||
exchangeCode,
|
||||
fetchGoogleEmail,
|
||||
listDriveFolders,
|
||||
matchFoldersToTenants,
|
||||
saveAgencyOAuth,
|
||||
} from '@/lib/google-drive';
|
||||
import { bootstrapAgencyGoogle } from '@/lib/agency-google-bootstrap';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 120;
|
||||
|
||||
function appUrl(): string {
|
||||
return (
|
||||
@@ -19,7 +18,7 @@ function appUrl(): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** GET — OAuth callback: save tokens + auto-match folders → tenants. */
|
||||
/** GET — OAuth callback: save tokens + auto-emparejar Drive/GA4/GSC/Ads → fichas. */
|
||||
export async function GET(req: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
@@ -56,35 +55,27 @@ export async function GET(req: NextRequest) {
|
||||
email,
|
||||
});
|
||||
|
||||
// Auto-match folders (non-fatal: tokens already saved)
|
||||
let linked = 0;
|
||||
let ambiguous = 0;
|
||||
let bootstrapMsg = '';
|
||||
try {
|
||||
const headers = serviceHeaders();
|
||||
if (headers) {
|
||||
const tRes = await fetch(
|
||||
`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
if (tRes.ok) {
|
||||
const tenants = (await tRes.json()) as {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}[];
|
||||
const folders = await listDriveFolders();
|
||||
const matches = await matchFoldersToTenants(tenants, folders);
|
||||
const applied = await applyMatches(matches);
|
||||
linked = applied.linked;
|
||||
ambiguous = matches.filter((m) => m.status === 'ambiguous').length;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// match can fail without undoing OAuth
|
||||
const boot = await bootstrapAgencyGoogle({
|
||||
includeGoogleAds: true,
|
||||
syncOrganic: true,
|
||||
organicDays: 28,
|
||||
});
|
||||
linked = boot.drive?.linked || 0;
|
||||
ambiguous = boot.drive?.ambiguous || 0;
|
||||
bootstrapMsg = boot.message;
|
||||
} catch (e) {
|
||||
bootstrapMsg = e instanceof Error ? e.message : 'bootstrap failed';
|
||||
}
|
||||
|
||||
const res = NextResponse.redirect(
|
||||
`${appUrl()}/dashboard/agencia?drive=ok&linked=${linked}&ambiguous=${ambiguous}`
|
||||
`${appUrl()}/dashboard/agencia?drive=ok&linked=${linked}&ambiguous=${ambiguous}` +
|
||||
(bootstrapMsg
|
||||
? `&boot=${encodeURIComponent(bootstrapMsg.slice(0, 160))}`
|
||||
: '')
|
||||
);
|
||||
res.cookies.set('4v_drive_oauth_state', '', { maxAge: 0, path: '/' });
|
||||
return res;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
userRole,
|
||||
} from '@/lib/agency';
|
||||
import { getAgencyOAuthStatus } from '@/lib/google-drive';
|
||||
import { bootstrapAgencyGoogle } from '@/lib/agency-google-bootstrap';
|
||||
import {
|
||||
applyOrganicMatches,
|
||||
listGa4Properties,
|
||||
@@ -90,7 +91,8 @@ export async function GET() {
|
||||
|
||||
/**
|
||||
* POST
|
||||
* - { action: 'match', apply?: boolean } → match + optional write integrations
|
||||
* - { action: 'bootstrap' } → Drive + GA4 + GSC + Google Ads → fichas + sync
|
||||
* - { action: 'match', apply? } → solo GA4/GSC
|
||||
* - { action: 'sync', tenantId?, days? } → sync organic_daily
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
@@ -101,17 +103,38 @@ export async function POST(req: NextRequest) {
|
||||
};
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const action = body.action === 'sync' ? 'sync' : 'match';
|
||||
const action =
|
||||
body.action === 'sync'
|
||||
? 'sync'
|
||||
: body.action === 'bootstrap'
|
||||
? 'bootstrap'
|
||||
: 'match';
|
||||
|
||||
if (action === 'bootstrap') {
|
||||
try {
|
||||
const result = await bootstrapAgencyGoogle({
|
||||
includeGoogleAds: body.includeGoogleAds !== false,
|
||||
syncOrganic: body.syncOrganic !== false,
|
||||
organicDays: Number(body.days) || 28,
|
||||
});
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 422 });
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : 'bootstrap error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'match') {
|
||||
try {
|
||||
const [ga4, gsc, tRes] = await Promise.all([
|
||||
listGa4Properties(),
|
||||
listGscSites(),
|
||||
fetch(`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}),
|
||||
fetch(
|
||||
`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
]);
|
||||
if (!tRes.ok) {
|
||||
return NextResponse.json(
|
||||
@@ -155,7 +178,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// sync — rolling window; default 28 on first fill / manual force
|
||||
const days = Math.min(90, Math.max(3, Number(body.days) || 28));
|
||||
const tenantFilter =
|
||||
typeof body.tenantId === 'string' ? body.tenantId.trim() : '';
|
||||
|
||||
Reference in New Issue
Block a user