fix: make remote password reset reliable
This commit is contained in:
1 parent
0bd3ec920f
commit
b5847ffaac
3 files changed
+24
-12
No files matched your search
+13
-11
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user