Add self-service "change my password" option in Account Settings

New section in Settings: current password + new password + confirm,
verified against the existing hash before accepting. On success it
signs out every other session for that account (in case one was
compromised) but keeps the current session logged in, so changing
your password doesn't immediately kick you back to the login screen.

Renamed the underlying db.js function (adminSetPassword ->
setUserPassword) since it's now shared by both this and the existing
admin-assisted reset.

Verified live: old password rejected after change, new password
works, current session stayed logged in throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-06 14:42:28 -04:00
parent 9e57f8088a
commit 19d3a6b822
4 changed files with 118 additions and 5 deletions
+6 -3
View File
@@ -298,8 +298,8 @@ export function adminDeleteProject(id) {
db.prepare('DELETE FROM projects WHERE id = ?').run(id);
}
/** Overwrites a user's password hash directly — used for admin-assisted password resets. */
export function adminSetPassword(userId, passwordHash) {
/** Overwrites a user's password hash directly — used for both self-service and admin-assisted resets. */
export function setUserPassword(userId, passwordHash) {
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, userId);
}
@@ -337,10 +337,13 @@ export function pruneExpiredSessions() {
* Logs a user out everywhere by deleting every session that belongs to them.
* Sessions don't have an indexed user_id column (they're just an opaque JSON
* blob to express-session), so this scans and parses — fine at this app's scale.
* Pass exceptSid to keep one session alive (e.g. the one completing a self-service
* password change, so the user isn't immediately logged out of their own action).
*/
export function destroyAllSessionsForUser(userId) {
export function destroyAllSessionsForUser(userId, exceptSid = null) {
const rows = db.prepare('SELECT sid, sess FROM sessions').all();
const staleSids = rows
.filter((row) => row.sid !== exceptSid)
.filter((row) => {
try {
return JSON.parse(row.sess)?.userId === userId;