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.
This commit is contained in:
+79
-3
@@ -1,8 +1,12 @@
|
||||
// Standalone CLI entry point for creating an internal auth account by hand:
|
||||
// `npx tsx scripts/create-user.ts --email [email protected] --name "Foo Bar" --password "..."`
|
||||
// `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";
|
||||
|
||||
@@ -24,19 +28,91 @@ function parseArgs(argv: string[]): Record<string, string> {
|
||||
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: args.password,
|
||||
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 "..."'
|
||||
'Usage: npx tsx scripts/create-user.ts --email [email protected] --name "Foo Bar" (password prompted interactively)'
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user