From 255003499649cb9bf88f932c1a055b214c54d3d1 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 3 Jun 2026 20:31:27 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20v0.36.0=20patch=20set=20=E2=80=94=20404?= =?UTF-8?q?=20page,=20OIDC=20encryption,=20session=20rotate,=20user=20vali?= =?UTF-8?q?dation,=20calendar=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HISTORY.md | 2 ++ services/authService.js | 15 +++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index f3e62da..8f1cce1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,8 @@ - **Bump** — `0.35.1` → `0.36.0` +- **`rotateSessionId` uses `db.transaction()` instead of raw SQL** — `rotateSessionId()` in `authService.js` managed its DELETE + INSERT pair with explicit `db.prepare('BEGIN').run()` / `COMMIT` / `ROLLBACK` calls. This is fragile: if the rollback itself throws (e.g. connection in a bad state), the transaction is left open. Replaced with better-sqlite3's `db.transaction()` wrapper, which commits automatically on success and rolls back automatically on any thrown error with no manual try/catch required. + ### 🔒 Security - **OIDC client secret encrypted at rest** — The OIDC client secret was stored as plaintext in the `settings` table alongside all other application settings. It is now encrypted using the same AES-256-GCM + HKDF pipeline already in use for SMTP passwords and SimpleFIN tokens. A new `getOidcClientSecret()` helper in `oidcService.js` decrypts on read (with a plaintext fallback for legacy values), and the write path calls `encryptSecret()` before `setSetting`. DB migration `v0.79` encrypts any existing plaintext value on first startup — no manual action required. Env-var-sourced secrets (`OIDC_CLIENT_SECRET`) are unaffected and bypass the DB path entirely. diff --git a/services/authService.js b/services/authService.js index e281c4e..95633c8 100644 --- a/services/authService.js +++ b/services/authService.js @@ -127,19 +127,14 @@ function rotateSessionId(oldSessionId, userId) { const expiresAt = new Date(Date.now() + SESSION_DAYS * 86400000) .toISOString().slice(0, 19).replace('T', ' '); - // Delete old session and create new one in a transaction - db.prepare('BEGIN').run(); - try { + // Delete old session and create new one atomically + db.transaction(() => { db.prepare('DELETE FROM sessions WHERE id = ?').run(oldSessionId); db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)') .run(newSessionId, userId, expiresAt); - db.prepare('COMMIT').run(); - - return newSessionId; - } catch (err) { - db.prepare('ROLLBACK').run(); - throw err; - } + })(); + + return newSessionId; } function getSessionUser(sessionId) {