diff --git a/scripts/reset-password.mjs b/scripts/reset-password.mjs index b7355c5a..b7e47233 100644 --- a/scripts/reset-password.mjs +++ b/scripts/reset-password.mjs @@ -50,6 +50,9 @@ const sqlString = (value) => `'${value.replaceAll("'", "''")}'`; export const buildUserLookupSql = (username) => `SELECT id FROM users WHERE username = ${sqlString(username)} AND is_disabled = 0 LIMIT 1`; +export const buildPasswordHashLookupSql = (username) => + `SELECT password_hash FROM users WHERE username = ${sqlString(username)} AND is_disabled = 0 LIMIT 1`; + export const buildPasswordResetSql = (username, passwordHash) => { const escapedUsername = sqlString(username); const escapedHash = sqlString(passwordHash); @@ -57,13 +60,11 @@ export const buildPasswordResetSql = (username, passwordHash) => { return `UPDATE users SET password_hash = ${escapedHash}, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE username = ${escapedUsername} AND is_disabled = 0; -SELECT changes() AS updated_users; UPDATE sessions SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE user_id IN ( SELECT id FROM users WHERE username = ${escapedUsername} AND is_disabled = 0 ) AND revoked_at IS NULL; -SELECT changes() AS revoked_sessions; `; }; @@ -125,23 +126,24 @@ const main = () => { const sqlPath = join(temporaryDirectory, "reset-password.sql"); try { - writeFileSync(sqlPath, buildPasswordResetSql(options.username, createPasswordHash(password)), { + const passwordHash = createPasswordHash(password); + writeFileSync(sqlPath, buildPasswordResetSql(options.username, passwordHash), { mode: 0o600, }); - const resetOutput = runWrangler([ + runWrangler(["d1", "execute", "DB", targetFlag, "--file", sqlPath, "--yes"]); + + const verificationOutput = runWrangler([ "d1", "execute", "DB", targetFlag, - "--file", - sqlPath, - "--yes", + "--command", + buildPasswordHashLookupSql(options.username), "--json", ]); - const resetRows = parseD1Rows(resetOutput); - const updateResult = resetRows.find((row) => "updated_users" in row); - if (Number(updateResult?.updated_users) !== 1) { - throw new Error(`Password reset did not update exactly one user: ${options.username}`); + const [updatedUser] = parseD1Rows(verificationOutput); + if (updatedUser?.password_hash !== passwordHash) { + throw new Error(`Password reset verification failed: ${options.username}`); } } finally { rmSync(temporaryDirectory, { recursive: true, force: true }); diff --git a/scripts/run-wrangler.mjs b/scripts/run-wrangler.mjs index 8a654b5d..88c29e06 100644 --- a/scripts/run-wrangler.mjs +++ b/scripts/run-wrangler.mjs @@ -12,6 +12,8 @@ if (wranglerArgs.length === 0) { process.exit(1); } +const requestedInstance = process.env.EDGE_EVER_INSTANCE; + const loadLocalEnv = () => { const envPath = resolve(".env.local"); if (!existsSync(envPath)) { @@ -47,6 +49,13 @@ const loadLocalEnv = () => { loadLocalEnv(); +// An instance selected for this command must take precedence over the default +// stored in .env.local. Bun loads .env.local before this script starts, so +// capture the effective process value before the explicit reload above. +if (requestedInstance !== undefined) { + process.env.EDGE_EVER_INSTANCE = requestedInstance; +} + const baseConfigPath = resolve(process.env.WRANGLER_CONFIG ?? "wrangler.toml"); const instance = process.env.EDGE_EVER_INSTANCE?.trim(); const instanceKey = instance?.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase(); diff --git a/tests/reset-password.test.ts b/tests/reset-password.test.ts index 98874d32..bf0e74e1 100644 --- a/tests/reset-password.test.ts +++ b/tests/reset-password.test.ts @@ -6,6 +6,7 @@ import { createPasswordHash, } from "../scripts/password-hash.mjs"; import { + buildPasswordHashLookupSql, buildPasswordResetSql, buildUserLookupSql, parseD1Rows, @@ -40,13 +41,13 @@ describe("password reset script", () => { test("escapes usernames and revokes existing sessions", () => { expect(buildUserLookupSql("demo'user")).toContain("username = 'demo''user'"); + expect(buildPasswordHashLookupSql("demo'user")).toContain("username = 'demo''user'"); const sql = buildPasswordResetSql("demo'user", "pbkdf2$hash"); expect(sql).toContain("password_hash = 'pbkdf2$hash'"); expect(sql).toContain("username = 'demo''user'"); expect(sql).toContain("UPDATE sessions"); expect(sql).toContain("revoked_at IS NULL"); - expect(sql).toContain("SELECT changes() AS updated_users"); expect(sql).not.toContain("BEGIN TRANSACTION"); });