Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
via flow.kind=value + defines edges from attendance_root anchor;
qa/seeds/003_attendance_sites.mjs seeds them idempotently)
Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
103 lines
4.6 KiB
JavaScript
103 lines
4.6 KiB
JavaScript
// Versioned, idempotent seed for the three canvas personas.
|
|
//
|
|
// Replaces the round-1 raw `UPDATE auth_service.user SET is_superuser=TRUE` step
|
|
// with a deterministic, auditable script that:
|
|
// 1. POSTs each persona to /api/v1/auth/register (idempotent: 409 is OK).
|
|
// 2. Promotes is_superuser via a controlled SQL UPDATE through the auth-service
|
|
// pod, recorded in this file under git so the change is reviewable.
|
|
// 3. Verifies dev-login succeeds for each persona before exiting.
|
|
//
|
|
// Usage: node qa/seeds/001_personas.mjs
|
|
// Idempotent: safe to re-run; will skip personas that already exist + are active.
|
|
|
|
import { execSync } from "node:child_process";
|
|
|
|
const TENANT_ID = "a0000000-0000-0000-0000-000000000001";
|
|
const BASE = "https://demo.flow-master.ai";
|
|
|
|
const PERSONAS = [
|
|
{ email: "ceo-head@flow-master.ai", username: "ceo_head", first: "Mariana", last: "Cole", title: "Chief Executive" },
|
|
{ email: "hr-head@flow-master.ai", username: "hr_head", first: "Aisha", last: "Khan", title: "HR Director" },
|
|
{ email: "it-head@flow-master.ai", username: "it_head", first: "Rohan", last: "Patel", title: "IT Director" },
|
|
];
|
|
|
|
const PASSWORD = "CanvasDog!ood-2026";
|
|
|
|
async function register(persona) {
|
|
const body = {
|
|
email: persona.email,
|
|
username: persona.username,
|
|
first_name: persona.first,
|
|
last_name: persona.last,
|
|
display_name: `${persona.first} ${persona.last} — ${persona.title}`,
|
|
password: PASSWORD,
|
|
confirm_password: PASSWORD,
|
|
tenant_id: TENANT_ID,
|
|
};
|
|
const r = await fetch(`${BASE}/api/v1/auth/register`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (r.status === 200 || r.status === 201) return { created: true };
|
|
if (r.status === 409) return { existing: true };
|
|
const txt = await r.text();
|
|
if (r.status === 400 && /already exists/i.test(txt)) return { existing: true };
|
|
if (r.status === 422 && /already exists/i.test(txt)) return { existing: true };
|
|
if (r.status === 429) return { rate_limited: true };
|
|
throw new Error(`register ${persona.email} → ${r.status}: ${txt.slice(0, 200)}`);
|
|
}
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
function promoteToSuperuser() {
|
|
const pod = execSync(`ssh build01 'kubectl -n baseline get pod -l app=auth-service -o jsonpath="{.items[?(@.status.phase==\\"Running\\")].metadata.name}"'`, { encoding: "utf8" }).trim();
|
|
if (!pod) throw new Error("no running auth-service pod found");
|
|
const remoteScript = "/tmp/canvas_personas_promote.py";
|
|
const py = [
|
|
"import asyncio, os",
|
|
"from sqlalchemy.ext.asyncio import create_async_engine",
|
|
"from sqlalchemy import text",
|
|
"async def main():",
|
|
" e = create_async_engine(os.environ['DATABASE_URL'])",
|
|
" async with e.connect() as c:",
|
|
" await c.execute(text(\"UPDATE auth_service.\\\"user\\\" SET is_superuser=TRUE, status='active' WHERE email LIKE '%-head@flow-master.ai'\"))",
|
|
" await c.commit()",
|
|
" r = await c.execute(text(\"SELECT email, is_superuser, status FROM auth_service.\\\"user\\\" WHERE email LIKE '%-head@flow-master.ai' ORDER BY email\"))",
|
|
" for row in r: print(row)",
|
|
"asyncio.run(main())",
|
|
].join("\n");
|
|
const b64 = Buffer.from(py).toString("base64");
|
|
execSync(`ssh build01 'kubectl -n baseline exec ${pod} -- sh -c "echo ${b64} | base64 -d > ${remoteScript}"'`, { encoding: "utf8" });
|
|
return execSync(`ssh build01 'kubectl -n baseline exec ${pod} -- python3 ${remoteScript}'`, { encoding: "utf8" });
|
|
}
|
|
|
|
async function verifyDevLogin(persona) {
|
|
const r = await fetch(`${BASE}/api/v1/auth/dev-login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: persona.email }),
|
|
});
|
|
if (!r.ok) throw new Error(`dev-login ${persona.email} → ${r.status}`);
|
|
const body = await r.json();
|
|
if (!body.access_token) throw new Error(`dev-login ${persona.email} no access_token`);
|
|
return body.access_token.slice(0, 24) + "...";
|
|
}
|
|
|
|
const results = [];
|
|
for (const p of PERSONAS) {
|
|
const reg = await register(p);
|
|
results.push({ email: p.email, ...reg });
|
|
const status = reg.existing ? "already exists" : reg.rate_limited ? "rate-limited (skip)" : "created";
|
|
console.log(`register ${p.email}: ${status}`);
|
|
await sleep(1500);
|
|
}
|
|
console.log("\npromoting to superuser via auth_service DB:");
|
|
console.log(promoteToSuperuser());
|
|
console.log("\nverifying dev-login:");
|
|
for (const p of PERSONAS) {
|
|
const token = await verifyDevLogin(p);
|
|
console.log(` ${p.email} → ${token}`);
|
|
}
|
|
console.log("\nseed complete. all 3 personas registered, promoted, and dev-login verified.");
|