Files
ubi-dan-itam/scripts/create-user.ts
toanhoang 651ff2d626 fix: read password from stdin prompt instead of CLI arg in create-user script
Avoids leaking the plaintext password via shell history/`ps`/process
listings. --email/--name stay as CLI flags (not sensitive); password is now
read via a masked interactive prompt using only Node built-ins (readline +
raw-mode stdin), no new dependency.
2026-08-04 06:42:35 -04:00

133 lines
3.7 KiB
TypeScript

// Standalone CLI entry point for creating an internal auth account by hand:
// `npx tsx scripts/create-user.ts --email [email protected] --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<string, string> {
const args: Record<string, string> = {};
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<string> {
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<void> {
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 [email protected] --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();
});