import 'dotenv/config'; import { and, eq } from 'drizzle-orm'; import { pool } from '../src/lib/server/db/client'; import { db } from '../src/lib/server/db/client'; import { users, companies, companyUsers } from '../src/lib/server/db/schema/tenancy'; import { hashPassword } from '../src/lib/server/auth/password'; import { normalizeEmail } from '../src/lib/utils/email'; function slugify(s: string): string { return s .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); } function stripSurroundingQuotes(s: string | undefined): string | undefined { if (!s || s.length < 2) return s; const first = s[0]; const last = s[s.length - 1]; if ((first === "'" && last === "'") || (first === '"' && last === '"')) { return s.slice(1, -1); } return s; } function readArg(flag: string, fallback?: string): string | undefined { const i = process.argv.indexOf(flag); return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback); } async function main() { const email = readArg('--email'); const password = readArg('--password'); const name = readArg('--name'); const companyName = readArg('--company'); const role = (readArg('--role', 'admin') ?? 'admin') as 'admin' | 'manager' | 'user' | 'viewer'; if (!email || !password || !name) { console.error('Usage: npm run create-user -- --email --password

--name [--company ] [--role admin|manager|user|viewer]'); process.exit(1); } const normalized = normalizeEmail(email); const hash = await hashPassword(password); const [existing] = await db.select().from(users).where(eq(users.emailNormalized, normalized)).limit(1); let userId: string; if (existing) { console.log(`User ${normalized} already exists; updating password.`); await db.update(users).set({ passwordHash: hash, displayName: name }).where(eq(users.id, existing.id)); userId = existing.id; } else { const [created] = await db .insert(users) .values({ email, emailNormalized: normalized, displayName: name, passwordHash: hash }) .returning({ id: users.id }); userId = created.id; console.log(`Created user ${normalized} (id ${userId}).`); } if (companyName) { const slug = slugify(companyName); // Look up by slug so a previously-corrupted name (e.g. "'B4L'" with literal // quotes) doesn't cause a duplicate-slug insert on re-run. let [company] = await db.select().from(companies).where(eq(companies.slug, slug)).limit(1); if (!company) { const [created] = await db .insert(companies) .values({ name: companyName, slug }) .returning(); company = created; console.log(`Created company ${companyName} (id ${company.id}).`); } else if (company.name !== companyName) { await db.update(companies).set({ name: companyName }).where(eq(companies.id, company.id)); console.log( `Renamed company ${JSON.stringify(company.name)} → ${JSON.stringify(companyName)} (id ${company.id}).` ); company = { ...company, name: companyName }; } const [link] = await db .select() .from(companyUsers) .where(and(eq(companyUsers.userId, userId), eq(companyUsers.companyId, company.id))) .limit(1); if (!link) { await db .insert(companyUsers) .values({ companyId: company.id, userId, role }) .onConflictDoNothing(); console.log(`Linked user to company ${company.name} as ${role}.`); } else if (link.role !== role) { await db.update(companyUsers).set({ role }).where(eq(companyUsers.id, link.id)); console.log(`Updated role in ${company.name} to ${role}.`); } } await pool.end(); } main().catch((err) => { console.error(err); process.exit(1); });