fix: make remote password reset reliable

This commit is contained in:
yoma committed 2026-07-15 19:12:48 +08:00
1 parent 0bd3ec920f
commit b5847ffaac
3 files changed
+24 -12

No files matched your search

+13 -11
View File
@@ -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 });
+9
View File
@@ -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();
+2 -1
View File
@@ -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");
});