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:
4Ventures
2026-10-07 15:57:21 +02:00
parent 83de3fb3aa
commit e3fe157fa5
10 changed files with 347 additions and 58 deletions

View File

@@ -34,26 +34,31 @@ export function isAgencyRole(role: string | undefined | null): boolean {
return role === 'admin' || role === 'team';
}
/**
* Authorization role. Privileged roles (admin/team) ONLY from app_metadata
* so clients cannot self-elevate via user_metadata.updateUser.
*/
export function userRole(user: {
app_metadata?: Record<string, unknown>;
user_metadata?: Record<string, unknown>;
} | null): string {
return (
(user?.app_metadata?.role as string) ||
(user?.user_metadata?.role as string) ||
'client'
);
const app = (user?.app_metadata?.role as string | undefined)?.trim();
if (app === 'admin' || app === 'team' || app === 'client') return app;
// Legacy clients may only have user_metadata.role=client — never admin/team.
const um = (user?.user_metadata?.role as string | undefined)?.trim();
if (um === 'client') return 'client';
return 'client';
}
export function homeTenantId(user: {
app_metadata?: Record<string, unknown>;
user_metadata?: Record<string, unknown>;
} | null): string | null {
return (
(user?.app_metadata?.tenant_id as string) ||
(user?.user_metadata?.tenant_id as string) ||
null
);
const app = (user?.app_metadata?.tenant_id as string | undefined)?.trim();
if (app) return app;
// Legacy client JWT may still carry tenant only in user_metadata.
const um = (user?.user_metadata?.tenant_id as string | undefined)?.trim();
return um || null;
}
export function serviceHeaders():