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
+30 -2
View File
@@ -10,7 +10,7 @@ import {
enableTotp, disableTotp, setBackupCodeHashes, setPodcastName,
getShareToken, setShareToken, clearShareToken, getProjectByShareToken,
adminGetAllUsers, adminDeleteUser, adminGetAllProjects, adminGetProject, adminDeleteProject,
adminSetPassword, destroyAllSessionsForUser,
setUserPassword, destroyAllSessionsForUser,
} from './db.js';
import { SqliteSessionStore } from './sessionStore.js';
import {
@@ -187,6 +187,34 @@ app.patch('/api/auth/profile', requireAuth, (req, res) => {
}
});
// ---------------------------------------------------------------------------
// POST /api/auth/change-password — self-service password change (Account Settings)
// ---------------------------------------------------------------------------
app.post('/api/auth/change-password', requireAuth, async (req, res) => {
try {
const user = getUserById(req.session.userId);
const currentPassword = String(req.body?.currentPassword ?? '');
const newPassword = String(req.body?.newPassword ?? '');
const valid = await verifyPassword(currentPassword, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: 'Current password is incorrect.' });
}
if (!isValidPassword(newPassword)) {
return res.status(400).json({ error: 'New password must be at least 8 characters.' });
}
const passwordHash = await hashPassword(newPassword);
setUserPassword(user.id, passwordHash);
// Sign out every other session (e.g. a stolen one) but keep this one logged in.
destroyAllSessionsForUser(user.id, req.sessionID);
res.json({ ok: true });
} catch (err) {
console.error('POST /api/auth/change-password error:', err);
res.status(500).json({ error: 'Failed to change password.' });
}
});
// ---------------------------------------------------------------------------
// Two-factor auth setup (requires an already-authenticated session)
// ---------------------------------------------------------------------------
@@ -396,7 +424,7 @@ app.post('/api/admin/users/:id/reset-password', requireAuth, requireAdmin, async
try {
const temporaryPassword = generateTemporaryPassword();
const passwordHash = await hashPassword(temporaryPassword);
adminSetPassword(req.params.id, passwordHash);
setUserPassword(req.params.id, passwordHash);
destroyAllSessionsForUser(req.params.id);
res.json({ temporaryPassword });
} catch (err) {