Unify Ads/RRSS approval flow and add personalized Meta creative regen.

Agency validates first then client; Meta copy is 5+5+1 with on-image vs feed layers, emojis on feed, and partial v2 by image/hook/support/copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-13 13:17:36 +02:00
parent 4bc9ed7820
commit 46492c3873
26 changed files with 2014 additions and 204 deletions

View File

@@ -0,0 +1,136 @@
/**
* Provision agency admin accounts (role=admin, tenant=4Ventures Central).
* npx tsx scripts/provision_agency_admins.ts
*
* Writes credentials to .ops/agency-admins-credentials.txt (gitignored).
*/
import { config } from 'dotenv';
import { resolve } from 'path';
import { writeFileSync, mkdirSync } from 'fs';
import { createClient } from '@supabase/supabase-js';
import { randomBytes } from 'crypto';
config({ path: resolve(process.cwd(), '.env.local') });
const HUB_TENANT = '0b3c5fc0-6901-4caf-b26a-b6a0201d8541';
const COMPANY = '4Ventures';
const SLUG = '4ventures-hub';
const EMAILS = [
'marcos@4ventures.es',
'belen@4ventures.es',
'rebecca@4ventures.es',
'victor@4ventures.es',
] as const;
function tempPassword(local: string): string {
const tag = local.split('@')[0].replace(/[^a-z]/gi, '').slice(0, 8) || 'User';
const cap = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
return `4V${cap}-${randomBytes(4).toString('hex')}!9`;
}
async function findUser(
admin: ReturnType<typeof createClient>,
email: string
) {
for (let page = 1; page <= 40; page++) {
const { data, error } = await admin.auth.admin.listUsers({
page,
perPage: 200,
});
if (error) throw error;
const hit = data.users.find(
(u) => (u.email || '').toLowerCase() === email.toLowerCase()
);
if (hit) return hit;
if (data.users.length < 200) break;
}
return null;
}
async function main() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim();
const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();
if (!url || !key) {
throw new Error(
'Faltan NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY en .env.local'
);
}
const admin = createClient(url, key, {
auth: { persistSession: false, autoRefreshToken: false },
});
const lines: string[] = [
'# Agency admin access — app.4ventures.es/login',
`# generated ${new Date().toISOString()}`,
`# tenant ${COMPANY} (${HUB_TENANT}) role=admin`,
'',
];
for (const email of EMAILS) {
const password = tempPassword(email);
const app_metadata = {
role: 'admin',
tenant_id: HUB_TENANT,
company: COMPANY,
slug: SLUG,
};
const user_metadata = {
role: 'admin',
tenant_id: HUB_TENANT,
company: COMPANY,
slug: SLUG,
};
const found = await findUser(admin, email);
if (found) {
const { error } = await admin.auth.admin.updateUserById(found.id, {
password,
email_confirm: true,
app_metadata: { ...(found.app_metadata || {}), ...app_metadata },
user_metadata: { ...(found.user_metadata || {}), ...user_metadata },
});
if (error) throw new Error(`${email}: ${error.message}`);
console.log('updated', email, found.id);
lines.push(`email=${email}`);
lines.push(`password=${password}`);
lines.push(`user_id=${found.id}`);
lines.push(`action=updated`);
lines.push('');
} else {
const { data, error } = await admin.auth.admin.createUser({
email,
password,
email_confirm: true,
app_metadata,
user_metadata,
});
if (error) throw new Error(`${email}: ${error.message}`);
console.log('created', email, data.user?.id);
lines.push(`email=${email}`);
lines.push(`password=${password}`);
lines.push(`user_id=${data.user?.id || ''}`);
lines.push(`action=created`);
lines.push('');
}
}
mkdirSync(resolve(process.cwd(), '.ops'), { recursive: true });
const out = resolve(process.cwd(), '.ops/agency-admins-credentials.txt');
writeFileSync(out, lines.join('\n'), { mode: 0o600 });
console.log('---');
console.log('credentials', out);
console.log('login https://app.4ventures.es/login');
for (const email of EMAILS) {
const pass = lines.find(
(l, i) => lines[i - 1] === `email=${email}` && l.startsWith('password=')
);
console.log(email, pass?.replace('password=', '') || '?');
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});