From 772ead28a26f31cd392dec43d74a15fc1cd7676f Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 21 Apr 2026 19:08:49 -0500 Subject: [PATCH] Fix reset-admin.js: honor recovery token in requireAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/reset-admin.js signed a JWT with a synthetic id ("recovery-XXX") and instructed the operator to paste it into localStorage. But the requireAuth middleware always SELECTs the user row by id, so every authed API call under the recovery token returned 401 "User not found" and the recovery flow was effectively dead. Fix: - reset-admin.js now sets a `recovery: true` claim on the JWT. - requireAuth / optionalAuth short-circuit the DB lookup when decoded.recovery === true and synthesize a req.user record in memory (role: admin, plan_id: enterprise). The synthetic user is never persisted, so FK-constrained writes that expect a real user (creating devices, etc.) will still fail — which is fine, recovery is only meant to let the operator reset a password or create a fresh admin via the Settings UI. Security: a recovery token still requires the jwtSecret to sign, so only someone with filesystem access to the server can mint one. Token TTL remains 1h. Co-Authored-By: Claude Opus 4.7 --- scripts/reset-admin.js | 2 +- server/middleware/auth.js | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/reset-admin.js b/scripts/reset-admin.js index e0f267d..6343ba3 100644 --- a/scripts/reset-admin.js +++ b/scripts/reset-admin.js @@ -13,7 +13,7 @@ const crypto = require('crypto'); const nonce = crypto.randomBytes(8).toString('hex'); const token = jwt.sign( - { id: 'recovery-' + nonce, email: 'admin@localhost', role: 'admin' }, + { id: 'recovery-' + nonce, email: 'admin@localhost', role: 'admin', recovery: true }, config.jwtSecret, { expiresIn: '1h' } ); diff --git a/server/middleware/auth.js b/server/middleware/auth.js index afae556..a5404c2 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -14,6 +14,20 @@ function verifyToken(token) { return jwt.verify(token, config.jwtSecret, { algorithms: ['HS256'] }); } +// Synthetic user record for recovery tokens (scripts/reset-admin.js). Not +// persisted; only exists for the lifetime of the request. +function recoveryUser(decoded) { + return { + id: decoded.id, + email: decoded.email || 'admin@localhost', + name: 'Recovery Admin', + role: decoded.role || 'admin', + auth_provider: 'recovery', + avatar_url: null, + plan_id: 'enterprise' + }; +} + // Express middleware - requires valid JWT function requireAuth(req, res, next) { const authHeader = req.headers.authorization; @@ -24,6 +38,10 @@ function requireAuth(req, res, next) { try { const token = authHeader.split(' ')[1]; const decoded = verifyToken(token); + if (decoded.recovery) { + req.user = recoveryUser(decoded); + return next(); + } const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id); if (!user) return res.status(401).json({ error: 'User not found' }); req.user = user; @@ -40,7 +58,9 @@ function optionalAuth(req, res, next) { try { const token = authHeader.split(' ')[1]; const decoded = verifyToken(token); - req.user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id); + req.user = decoded.recovery + ? recoveryUser(decoded) + : db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id); } catch (err) { // Token invalid, continue without user }