SPB Git
3.0 KB · 49 lines typescript
1/**2 * npm run setup:owner — interactively sets (or resets) the single owner account.3 * Prompts for a password without echo, stores only its Argon2id hash. Never logs the password.4 */5import postgres from "postgres";6import { hash } from "@node-rs/argon2";7import readline from "node:readline";89const url = process.env.DATABASE_URL;10if (!url) { console.error("DATABASE_URL is required"); process.exit(1); }11const sql = postgres(url, { max: 1, onnotice: () => {} });1213function ask(q: string, hidden = false): Promise<string> {14  return new Promise(resolve => {15    const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });16    if (hidden) {17      const anyRl = rl as unknown as { _writeToOutput: (s: string) => void; output: NodeJS.WritableStream };18      const orig = anyRl._writeToOutput;19      anyRl._writeToOutput = (s: string) => { if (s.includes(q)) orig.call(rl, q); else if (s === "\r\n" || s === "\n") orig.call(rl, s); };20    }21    rl.question(q, a => { rl.close(); if (hidden) process.stdout.write("\n"); resolve(a); });22  });23}2425async function main() {26  // Non-interactive mode for automation: OWNER_USERNAME + OWNER_PASSWORD env (never commit them).27  if (process.env.OWNER_PASSWORD) {28    if (process.env.OWNER_PASSWORD.length < 12) { console.error("OWNER_PASSWORD too short"); process.exit(1); }29    const h = await hash(process.env.OWNER_PASSWORD, { memoryCost: 65536, timeCost: 3, parallelism: 2, algorithm: 2 });30    await sql`INSERT INTO owner (id, username, password_hash) VALUES (1, ${process.env.OWNER_USERNAME ?? "simon"}, ${h}) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, password_hash = EXCLUDED.password_hash, updated_at = now()`;31    await sql`UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL`;32    console.log("Owner saved (non-interactive). All sessions revoked."); await sql.end(); return;33  }34  const existing = await sql`SELECT username FROM owner WHERE id = 1`;35  if (existing.length) console.log(`Owner already configured (${existing[0].username}). Continuing will RESET the password and revoke all sessions.`);36  const username = (await ask(`Username [${existing[0]?.username ?? "simon"}]: `)).trim() || existing[0]?.username || "simon";37  const p1 = await ask("New password (min 12 chars): ", true);38  if (p1.length < 12) { console.error("Password too short."); process.exit(1); }39  const p2 = await ask("Confirm password: ", true);40  if (p1 !== p2) { console.error("Passwords do not match."); process.exit(1); }41  const h = await hash(p1, { memoryCost: 65536, timeCost: 3, parallelism: 2, algorithm: 2 });42  await sql`INSERT INTO owner (id, username, password_hash) VALUES (1, ${username}, ${h})43            ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username, password_hash = EXCLUDED.password_hash, updated_at = now()`;44  await sql`UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL`;45  console.log(`Owner "${username}" saved (Argon2id). All sessions revoked.`);46  await sql.end();47}48main().catch(e => { console.error(e); process.exit(1); });49
Files · e271e75196f58c092c7f0be7ddb52ff27d11b54f
Menu

Theme