Harden per-client isolation for calendars, Meta, creatives, and KPIs.
Validate view-as tenants, scope service-role mutations by tenant_id, add documents RLS, refuse portfolio-wide data dumps, and block Meta publish when a Page/IG is linked to another client. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -205,6 +205,46 @@ async function loadIntegrations(tenantId: string): Promise<{
|
||||
return { pageId, igUserId };
|
||||
}
|
||||
|
||||
/** Block publish if FB Page / IG user id is also stored on another tenant. */
|
||||
async function assertMetaAccountsExclusive(
|
||||
tenantId: string,
|
||||
pageId: string | null,
|
||||
igUserId: string | null
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const ids = [pageId, igUserId].filter(
|
||||
(x): x is string => Boolean(x && x.trim())
|
||||
);
|
||||
if (!ids.length) return { ok: true };
|
||||
const headers = serviceHeaders();
|
||||
const base = supabaseUrl();
|
||||
if (!headers || !base) return { ok: true };
|
||||
const or = ids
|
||||
.map((id) => `external_account_id.eq.${encodeURIComponent(id)}`)
|
||||
.join(',');
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/integrations?or=(${or})&provider=in.(facebook,instagram)&status=eq.connected&select=tenant_id,provider,external_account_id`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return { ok: true };
|
||||
const rows = (await r.json()) as {
|
||||
tenant_id: string;
|
||||
provider: string;
|
||||
external_account_id?: string | null;
|
||||
}[];
|
||||
const foreign = rows.filter((row) => row.tenant_id !== tenantId);
|
||||
if (!foreign.length) return { ok: true };
|
||||
const detail = foreign
|
||||
.map(
|
||||
(f) =>
|
||||
`${f.provider}:${f.external_account_id || '?'}→tenant ${f.tenant_id.slice(0, 8)}`
|
||||
)
|
||||
.join(', ');
|
||||
return {
|
||||
ok: false,
|
||||
error: `Cuenta Meta también vinculada a otro cliente (${detail}). Corrige Integraciones antes de publicar.`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Find Facebook Page that owns an IG business account. */
|
||||
async function findPageForIg(
|
||||
token: string,
|
||||
@@ -249,6 +289,16 @@ export async function resolveTenantMetaTargets(
|
||||
};
|
||||
}
|
||||
|
||||
// Refuse if the same Page/IG id is linked to another tenant (cross-client leak).
|
||||
const exclusive = await assertMetaAccountsExclusive(
|
||||
tenantId,
|
||||
integPage,
|
||||
igUserId
|
||||
);
|
||||
if (!exclusive.ok) {
|
||||
return { targets: null, error: exclusive.error };
|
||||
}
|
||||
|
||||
let pageId = integPage;
|
||||
let pageToken: string | null = null;
|
||||
let working: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user