0225b204a2
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>
105 lines
3.5 KiB
TypeScript
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);
|
|
});
|