feat: auto-discover Meta and Google Ads accounts for tenants
Agency panel lists accessible ad accounts and matches them to clients by name, same pattern as Drive folders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
76
src/app/api/admin/ads/discover/route.ts
Normal file
76
src/app/api/admin/ads/discover/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import {
|
||||
isAgencyRole,
|
||||
serviceHeaders,
|
||||
userRole,
|
||||
} from '@/lib/agency';
|
||||
import { discoverAndMatch, applyProviderMatches } from '@/lib/ads-discovery';
|
||||
import type { AccountMatch } from '@/lib/match';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 120;
|
||||
|
||||
async function requireAgency() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user || !isAgencyRole(userRole(user))) {
|
||||
return { error: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) };
|
||||
}
|
||||
if (!serviceHeaders()) {
|
||||
return {
|
||||
error: NextResponse.json({ error: 'no service role' }, { status: 503 }),
|
||||
};
|
||||
}
|
||||
return { user };
|
||||
}
|
||||
|
||||
/** POST — discover Meta/Google accounts and auto-match to tenants.
|
||||
* Body: { apply?: boolean, providers?: ['meta_ads'|'google_ads'],
|
||||
* assign?: { provider, tenantId, accountId, accountName } }
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const gate = await requireAgency();
|
||||
if ('error' in gate && gate.error) return gate.error;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
|
||||
if (body.assign?.tenantId && body.assign?.accountId && body.assign?.provider) {
|
||||
const provider = body.assign.provider as 'meta_ads' | 'google_ads';
|
||||
if (provider !== 'meta_ads' && provider !== 'google_ads') {
|
||||
return NextResponse.json({ error: 'provider inválido' }, { status: 400 });
|
||||
}
|
||||
const fake: AccountMatch[] = [
|
||||
{
|
||||
tenantId: body.assign.tenantId,
|
||||
tenantName: '',
|
||||
tenantSlug: '',
|
||||
accountId: String(body.assign.accountId),
|
||||
accountName: String(body.assign.accountName || body.assign.accountId),
|
||||
score: 100,
|
||||
status: 'matched',
|
||||
},
|
||||
];
|
||||
const { linked } = await applyProviderMatches(
|
||||
provider,
|
||||
fake,
|
||||
provider === 'meta_ads' ? 'Meta Ads' : 'Google Ads'
|
||||
);
|
||||
return NextResponse.json({ linked, manual: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discoverAndMatch({
|
||||
apply: body.apply !== false,
|
||||
providers: body.providers,
|
||||
});
|
||||
return NextResponse.json(result);
|
||||
} catch (e) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : 'discover failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/app/api/admin/ads/status/route.ts
Normal file
25
src/app/api/admin/ads/status/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { isAgencyRole, userRole } from '@/lib/agency';
|
||||
import {
|
||||
googleAdsConfigured,
|
||||
metaConfigured,
|
||||
} from '@/lib/ads-discovery';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** GET — whether Meta / Google Ads discovery credentials are ready. */
|
||||
export async function GET() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user || !isAgencyRole(userRole(user))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
meta: { configured: metaConfigured() },
|
||||
googleAds: { configured: googleAdsConfigured() },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user