create-user.js (4354B)
1 const fs = require("fs"); 2 const path = require("path"); 3 const crypto = require("crypto"); 4 const readline = require("readline"); 5 6 const USERS_FILE = process.env.USERS_FILE || path.join(__dirname, "..", "data", "users.json"); 7 8 function normalizeUsername(username) { 9 if (typeof username !== "string") return ""; 10 const cleaned = username.trim().toLowerCase(); 11 if (!cleaned) return ""; 12 if (cleaned.length > 32) return ""; 13 if (!/^[a-z0-9_][a-z0-9_.-]*$/.test(cleaned)) return ""; 14 return cleaned; 15 } 16 17 function hashPassword(password, saltHex) { 18 const salt = Buffer.from(saltHex, "hex"); 19 const derived = crypto.scryptSync(String(password), salt, 64); 20 return derived.toString("hex"); 21 } 22 23 function loadUsers() { 24 try { 25 const raw = fs.readFileSync(USERS_FILE, "utf8"); 26 const data = JSON.parse(raw); 27 const list = Array.isArray(data) ? data : Array.isArray(data?.users) ? data.users : []; 28 return { version: 1, users: list }; 29 } catch (e) { 30 if (e?.code === "ENOENT") return { version: 1, users: [] }; 31 throw e; 32 } 33 } 34 35 function normalizeUsernameList(users) { 36 if (!Array.isArray(users)) return []; 37 return users.filter((u) => u && typeof u === "object"); 38 } 39 40 function saveUsers(data) { 41 fs.mkdirSync(path.dirname(USERS_FILE), { recursive: true }); 42 fs.writeFileSync(USERS_FILE, JSON.stringify({ version: 1, users: data.users }, null, 2) + "\n", "utf8"); 43 } 44 45 async function promptHidden(query) { 46 return await new Promise((resolve) => { 47 // Some Windows shells / double-clicked wrappers can behave oddly with manual stdin listeners. 48 // Use readline's built-in masking approach, and fall back to visible input when not a TTY. 49 if (!process.stdin.isTTY || !process.stdout.isTTY) { 50 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); 51 rl.question(`${query}(input will be visible) `, (value) => { 52 rl.close(); 53 resolve(value); 54 }); 55 return; 56 } 57 58 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); 59 rl.stdoutMuted = true; 60 // eslint-disable-next-line no-underscore-dangle 61 rl._writeToOutput = function _writeToOutput(stringToWrite) { 62 const s = String(stringToWrite || ""); 63 if (!rl.stdoutMuted) return rl.output.write(s); 64 // Preserve newlines (enter) so the prompt advances properly. 65 if (s === "\n" || s === "\r\n" || s === "\r") return rl.output.write(s); 66 return rl.output.write("*"); 67 }; 68 69 try { 70 rl.output.write(query); 71 } catch { 72 // ignore 73 } 74 rl.question("", (value) => { 75 rl.history = rl.history.slice(1); 76 rl.close(); 77 try { 78 process.stdout.write("\n"); 79 } catch { 80 // ignore 81 } 82 resolve(value); 83 }); 84 }); 85 } 86 87 async function main() { 88 const rawUsername = process.argv[2] || ""; 89 const username = normalizeUsername(rawUsername); 90 if (!username) { 91 console.error('Usage: node scripts/create-user.js <username>\nAllowed: a-z 0-9 _ . - (max 32 chars)'); 92 process.exit(1); 93 } 94 95 const password = await promptHidden("Password: "); 96 const confirm = await promptHidden("Confirm: "); 97 if (password !== confirm) { 98 console.error("\nPasswords did not match."); 99 process.exit(1); 100 } 101 if (password.length < 4) { 102 console.error("\nPassword too short (min 4)."); 103 process.exit(1); 104 } 105 106 const data = loadUsers(); 107 data.users = normalizeUsernameList(data.users); 108 const exists = (data.users || []).some((u) => normalizeUsername(u?.username) === username); 109 if (exists) { 110 console.error(`\nUser "${username}" already exists in ${USERS_FILE}`); 111 process.exit(1); 112 } 113 114 const salt = crypto.randomBytes(16).toString("hex"); 115 const hash = hashPassword(password, salt); 116 const isFirst = data.users.length === 0; 117 const role = isFirst ? "owner" : "member"; 118 data.users.push({ 119 username, 120 salt, 121 hash, 122 role, 123 customRoles: [], 124 mutedUntil: 0, 125 suspendedUntil: 0, 126 banned: false, 127 pronouns: "", 128 bioHtml: "", 129 themeSongUrl: "", 130 links: [], 131 starredPostIds: [], 132 hiddenPostIds: [], 133 createdAt: Date.now() 134 }); 135 saveUsers(data); 136 console.log(`\nCreated "${username}" (${role}) in ${USERS_FILE}`); 137 } 138 139 main().catch((err) => { 140 console.error("Failed:", err?.message || err); 141 process.exit(1); 142 });