diff --git a/scripts/create-user.ts b/scripts/create-user.ts index ecb72a7..2defac2 100644 --- a/scripts/create-user.ts +++ b/scripts/create-user.ts @@ -1,8 +1,12 @@ // 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" --password "..."` +// `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"; @@ -24,19 +28,91 @@ function parseArgs(argv: string[]): Record { 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: args.password, + 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 "..."' + 'Usage: npx tsx scripts/create-user.ts --email foo@company.com --name "Foo Bar" (password prompted interactively)' ); process.exitCode = 1; return;