Files
buildfor_life_ops/scripts/create-user.ts
T
grabowski 0225b204a2 chore(tooling): switch to fnm + pnpm, add DEPLOYMENT.md
Pin Node 24 via .node-version/.nvmrc and pnpm 9.15.0 via
package.json#packageManager. Regenerate lockfile as pnpm-lock.yaml.
Rewrite README setup + scripts table around pnpm, and add a
production deployment guide covering systemd, nginx, upgrades,
rollback, and backups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:25:15 +07:00

105 lines
3.5 KiB
TypeScript

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: pnpm run create-user -- --email <e> --password <p> --name <n> [--company <c>] [--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);
});