// Standalone CLI entry point for creating an internal auth account by hand: // `npx tsx scripts/create-user.ts --email foo@company.com --name "Foo Bar"` // (you'll be prompted for the password interactively — see readPassword() // below; it is deliberately never accepted via a CLI flag, to avoid leaking // it through shell history / `ps` / process listings). // There is deliberately no self-registration UI or public API route for // this — see src/lib/users.ts / prisma/schema.prisma's User model comment // for why (a handful of trusted IT staff accounts, not company-wide). import readline from "readline"; import { createUser, validateCreateUserInput } from "../src/lib/users"; import { prisma } from "../src/lib/prisma"; function parseArgs(argv: string[]): Record { const args: Record = {}; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (!arg.startsWith("--")) { continue; } const key = arg.slice(2); const value = argv[i + 1]; if (value === undefined || value.startsWith("--")) { throw new Error(`Missing value for --${key}`); } args[key] = value; i += 1; } return args; } /** * Prompts for a password on stdin without echoing the typed characters back * to the terminal (each keystroke is masked with `*`). Only Node built-ins * (`readline` + `process.stdin` raw mode) — no extra dependency. * * Falls back to a plain (unmasked) `readline` question when stdin isn't a * TTY (e.g. input piped in for scripting/testing) since raw mode requires a * real terminal. */ function readPassword(promptText: string): Promise { const stdin = process.stdin; const stdout = process.stdout; if (!stdin.isTTY) { return new Promise((resolve) => { const rl = readline.createInterface({ input: stdin, output: stdout, terminal: false }); rl.question(promptText, (answer) => { rl.close(); resolve(answer); }); }); } return new Promise((resolve, reject) => { stdout.write(promptText); stdin.resume(); stdin.setRawMode(true); stdin.setEncoding("utf8"); let password = ""; const cleanup = () => { stdin.setRawMode(false); stdin.pause(); stdin.removeListener("data", onData); }; const onData = (chunk: string) => { switch (chunk) { case "\n": case "\r": case "\u0004": // Ctrl-D cleanup(); stdout.write("\n"); resolve(password); break; case "\u0003": // Ctrl-C cleanup(); stdout.write("\n"); reject(new Error("Aborted")); break; case "\u007f": // Backspace (most terminals) case "\b": if (password.length > 0) { password = password.slice(0, -1); stdout.write("\b \b"); } break; default: password += chunk; stdout.write("*"); break; } }; stdin.on("data", onData); }); } async function main(): Promise { const args = parseArgs(process.argv.slice(2)); const password = await readPassword("Password: "); const validation = validateCreateUserInput({ email: args.email, name: args.name, password, }); if (!validation.ok) { console.error(`Error: ${validation.error}`); console.error( 'Usage: npx tsx scripts/create-user.ts --email foo@company.com --name "Foo Bar" (password prompted interactively)' ); process.exitCode = 1; return; } const user = await createUser(validation.value); console.log(`Created user ${user.email} (id: ${user.id})`); } main() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });