g1t/services/identity/scripts/create-user.mjs
| 1 | // Creates a user (or resets their password) in the remote D1 database and |
| 2 | // writes the generated password to the gitignored .credentials directory. |
| 3 | // |
| 4 | // node scripts/create-user.mjs <username> [email] |
| 5 | import { execSync } from "node:child_process"; |
| 6 | import { pbkdf2Sync, randomBytes } from "node:crypto"; |
| 7 | import { mkdirSync, rmSync, writeFileSync } from "node:fs"; |
| 8 | import { tmpdir } from "node:os"; |
| 9 | import { join } from "node:path"; |
| 10 | |
| 11 | const [username, email] = process.argv.slice(2); |
| 12 | if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(username ?? "")) { |
| 13 | console.error("usage: node scripts/create-user.mjs <username> [email]"); |
| 14 | process.exit(1); |
| 15 | } |
| 16 | if (email && !/^[^\s'@]+@[^\s'@]+$/.test(email)) { |
| 17 | console.error("invalid email"); |
| 18 | process.exit(1); |
| 19 | } |
| 20 | |
| 21 | // Must match hashPassword in src/crypto.ts. |
| 22 | const iterations = 100_000; |
| 23 | const password = randomBytes(18).toString("base64url"); |
| 24 | const salt = randomBytes(16); |
| 25 | const hash = pbkdf2Sync(password, salt, iterations, 32, "sha256"); |
| 26 | const stored = `pbkdf2$${iterations}$${salt.toString("base64")}$${hash.toString("base64")}`; |
| 27 | |
| 28 | const sqlFile = join(tmpdir(), `g1t-create-user-${process.pid}.sql`); |
| 29 | writeFileSync( |
| 30 | sqlFile, |
| 31 | `INSERT INTO users (id, username, email, password_hash) |
| 32 | VALUES ('usr_' || lower(hex(randomblob(13))), '${username}', ${email ? `'${email}'` : "NULL"}, '${stored}') |
| 33 | ON CONFLICT (username) DO UPDATE SET password_hash = excluded.password_hash;`, |
| 34 | ); |
| 35 | try { |
| 36 | execSync(`npx wrangler d1 execute g1t --remote --yes --file "${sqlFile}"`, { |
| 37 | cwd: join(import.meta.dirname, ".."), |
| 38 | stdio: ["ignore", "ignore", "inherit"], |
| 39 | }); |
| 40 | } finally { |
| 41 | rmSync(sqlFile, { force: true }); |
| 42 | } |
| 43 | |
| 44 | const directory = join(import.meta.dirname, "../../../.credentials"); |
| 45 | mkdirSync(directory, { recursive: true }); |
| 46 | const file = join(directory, `${username}.txt`); |
| 47 | writeFileSync(file, `username: ${username}\npassword: ${password}\n`); |
| 48 | console.log(`Password for ${username} written to ${file}`); |