diff --git a/public/components/settings.html b/public/components/settings.html index c5eec2d..6cb7343 100644 --- a/public/components/settings.html +++ b/public/components/settings.html @@ -85,8 +85,8 @@

Trade-off: Immediate transcription vs. privacy. For maximum privacy, use Browser Whisper (offline batch mode) or Server transcription with HIPAA-eligible provider.

- -
+ +

Change Password

@@ -104,8 +104,8 @@
- -
+ +

Two-Factor Authentication

Status: Loading...

diff --git a/public/js/auth.js b/public/js/auth.js index 6b9be3c..b16805c 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -725,6 +725,19 @@ document.addEventListener('DOMContentLoaded', function() { .then(function(r) { return r.json(); }) .then(function(data) { if (!data || !data.user) return; + // Hide password-change + 2FA sections entirely for SSO-only users. + // Their auth is owned by the IdP — changing a random-blob "password" + // or adding TOTP here has no effect on how they sign in. + var pwSection = document.getElementById('change-password-section'); + var twofaSection = document.getElementById('2fa-section'); + if (data.user.canLocalAuth === false) { + if (pwSection) pwSection.style.display = 'none'; + if (twofaSection) twofaSection.style.display = 'none'; + return; // Nothing more to load for SSO-only users + } else { + if (pwSection) pwSection.style.display = ''; + if (twofaSection) twofaSection.style.display = ''; + } var status = document.getElementById('2fa-status'); var setupBtn = document.getElementById('btn-setup-2fa'); var disableBtn = document.getElementById('btn-disable-2fa'); diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 7400814..c6287c4 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -3,9 +3,11 @@ const db = require('../db/database'); const { hashToken } = require('../utils/sessions'); const { isMobileClient } = require('../utils/platform'); -// Sliding idle timeout — web only. Mobile stays persistent. +// Sliding idle timeout — web only. Mobile stays persistent (no idle check, +// 365-day JWT in Keychain/Keystore — user only logs out on manual action, +// password change, admin revoke, or app uninstall). const WEB_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24h since last request -const ACTIVITY_THROTTLE_MS = 5 * 60 * 1000; // don't UPDATE more than every 5 min +const ACTIVITY_THROTTLE_MS = 10 * 60 * 1000; // throttle DB writes to ≤ 1 per 10 min / user const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // cookie hard expiry — session table drives actual session if (!process.env.JWT_SECRET) { @@ -65,6 +67,12 @@ async function authMiddleware(req, res, next) { // Sliding idle timeout — web only. Mobile stays persistent. if (!mobile && idleMs > WEB_IDLE_TIMEOUT_MS) { + var idleMinutes = Math.round(idleMs / 60000); + console.warn('[Auth] session expired (idle ' + idleMinutes + ' min) user=' + user.id + ' ip=' + req.ip); + db.run( + 'INSERT INTO audit_log (user_id, action, category, details, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?)', + [user.id, 'session_idle_timeout', 'auth', 'Idle ' + idleMinutes + ' minutes', req.ip, (req.headers['user-agent'] || '').substring(0, 255)] + ).catch(function(){}); db.run('DELETE FROM user_sessions WHERE id = ?', [session.id]).catch(function(){}); res.clearCookie('ped_auth', { path: '/' }); return res.status(401).json({ error: 'Session expired due to inactivity', idleTimeout: true }); diff --git a/src/routes/auth.js b/src/routes/auth.js index c71c858..6217029 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -596,9 +596,17 @@ router.post('/change-password', authMiddleware, async (req, res) => { router.get('/me', authMiddleware, async (req, res) => { try { var user = await db.get( - 'SELECT id, email, name, role, totp_enabled, email_verified, nextcloud_url, nextcloud_user, nextcloud_folder, created_at FROM users WHERE id = ?', + 'SELECT id, email, name, role, totp_enabled, email_verified, nextcloud_url, nextcloud_user, nextcloud_folder, oidc_sub, password, created_at FROM users WHERE id = ?', [req.user.id] ); + // canLocalAuth: true if the user has a real password hash (can log in + // locally, so password change / 2FA are meaningful). SSO-auto-created + // users have a random hex blob in `password` which can't be verified + // — hide those UIs for them. + var canLocalAuth = !!(user && user.password && (/^\$2[aby]\$/.test(user.password) || user.password.indexOf('$argon2') === 0)); + // Don't leak the password hash in the response. + if (user) delete user.password; + if (user) user.canLocalAuth = canLocalAuth; res.json({ user: user }); } catch (err) { console.error('[Auth] Me error:', err.message); res.status(500).json({ error: 'Failed to load user' }); } });