// Admin account route registration and profile helpers. const { fetchAppSettings } = require('#src/data/app-settings'); module.exports = function registerAccountRoutes(app, deps) { const pool = deps.pool; const common = deps.common; const pages = deps.pages; const formatDashboardDate = deps.formatDashboardDate; const getAuditUserId = deps.getAuditUserId; const verifyPassword = deps.verifyPassword; const hashPassword = deps.hashPassword; const validatePasswordStrength = deps.validatePasswordStrength; const createUserSession = deps.createUserSession; const setSessionCookie = deps.setSessionCookie; const getSessionMaxAgeMs = deps.getSessionMaxAgeMs; const parseCookies = deps.parseCookies; const hashSessionToken = deps.hashSessionToken; const sessionCookieName = deps.sessionCookieName; const recordRequestAuditEvent = deps.recordRequestAuditEvent; async function getPasswordRequirements() { const settings = await fetchAppSettings(pool); return buildPasswordRequirements(settings); } function buildPasswordRequirements(settings) { return { minimumLength: settings['security.password_min_length'], minimumCategories: settings['security.password_min_categories'], requireLowercase: settings['security.password_require_lowercase'], requireUppercase: settings['security.password_require_uppercase'], requireNumber: settings['security.password_require_number'], requireSymbol: settings['security.password_require_symbol'] }; } function getSessionMetadata(req) { const forwardedAddress = String(req && req.headers && req.headers['x-forwarded-for'] || '').split(',')[0].trim(); return { ipAddress: forwardedAddress || String(req && req.ip || req && req.socket && req.socket.remoteAddress || 'unknown').trim(), userAgent: req && req.headers && req.headers['user-agent'] }; } app.get('/account', function (req, res) { fetchAppSettings(pool).then(function (settings) { const passwordRequirements = buildPasswordRequirements(settings); if (!settings['security.allow_user_session_revocation']) { return res.send(pages.renderAccountPage(req.currentUser, req.query.message ? String(req.query.message) : '', '/dashboard', false, [], passwordRequirements)); } const cookies = parseCookies(req.headers.cookie || ''); const tokenHash = cookies[sessionCookieName] ? hashSessionToken(cookies[sessionCookieName]) : ''; return pool.query( 'SELECT id, session_hash, ip_address, user_agent, created_at, last_used_at, expires_at FROM a_sessions WHERE user_id = ? AND expires_at > NOW() ORDER BY last_used_at DESC', [req.currentUser.id] ).then(function (result) { const rows = result[0] || []; const sessions = rows.map(function (session) { return { id: Number(session.id), isCurrent: Boolean(tokenHash && session.session_hash === tokenHash), ipAddress: String(session.ip_address || 'Unknown'), userAgent: String(session.user_agent || 'Unknown browser'), createdAtLabel: formatDashboardDate(session.created_at), lastUsedAtLabel: formatDashboardDate(session.last_used_at), expiresAtLabel: formatDashboardDate(session.expires_at) }; }); res.send(pages.renderAccountPage(req.currentUser, req.query.message ? String(req.query.message) : '', '/dashboard', true, sessions, passwordRequirements)); }); }).catch(function (error) { res.status(500).send(error.message || 'Unable to load account settings.'); }); }); app.post('/account/sessions/:id/revoke', async function (req, res, next) { try { const sessionId = Number(req.params.id); if (!Number.isInteger(sessionId) || sessionId <= 0) { return res.status(400).send('Invalid session.'); } const settings = await fetchAppSettings(pool); if (!settings['security.allow_user_session_revocation']) { return res.redirect('/account?message=' + encodeURIComponent('Signing out other sessions is disabled by an administrator.')); } const cookies = parseCookies(req.headers.cookie || ''); const token = cookies[sessionCookieName]; if (!token) { return res.redirect('/account?message=' + encodeURIComponent('Current session could not be identified.')); } const [result] = await pool.query( 'DELETE FROM a_sessions WHERE id = ? AND user_id = ? AND session_hash <> ?', [sessionId, req.currentUser.id, hashSessionToken(token)] ); if (result && result.affectedRows && typeof recordRequestAuditEvent === 'function') { await recordRequestAuditEvent(pool, req, { category: 'sessions', eventType: 'session.revoked', actorUserId: req.currentUser.id, targetType: 'user', targetId: req.currentUser.id, targetLabel: req.currentUser.username, details: { scope: 'single' } }); } res.redirect('/account?message=' + encodeURIComponent(result && result.affectedRows ? 'Session signed out.' : 'The current session cannot be signed out.')); } catch (error) { next(error); } }); app.post('/account/sessions/revoke', async function (req, res, next) { try { const settings = await fetchAppSettings(pool); if (!settings['security.allow_user_session_revocation']) { return res.redirect('/account?message=' + encodeURIComponent('Signing out other sessions is disabled by an administrator.')); } const cookies = parseCookies(req.headers.cookie || ''); const token = cookies[sessionCookieName]; if (token) { await pool.query('DELETE FROM a_sessions WHERE user_id = ? AND session_hash <> ?', [req.currentUser.id, hashSessionToken(token)]); } if (typeof recordRequestAuditEvent === 'function') { await recordRequestAuditEvent(pool, req, { category: 'sessions', eventType: 'session.revoked', actorUserId: req.currentUser.id, targetType: 'user', targetId: req.currentUser.id, targetLabel: req.currentUser.username, details: { scope: 'other_sessions' } }); } res.redirect('/account?message=' + encodeURIComponent('Other active sessions were signed out.')); } catch (error) { next(error); } }); app.post('/account/name', async function (req, res, next) { try { const name = String(req.body.name || '').trim(); if (!name) { return res.status(400).send('Name is required.'); } if (await common.fetchDuplicateName(pool, 'a_users', name, req.currentUser.id)) { return res.redirect('/account?message=' + encodeURIComponent('That name already exists.')); } const actorId = getAuditUserId(req); const [result] = await pool.query('UPDATE a_users SET name = ?, modified_by = ? WHERE id = ?', [name, actorId, req.currentUser.id]); if (!result.affectedRows) { return res.status(404).send('User not found.'); } res.redirect('/dashboard?message=' + encodeURIComponent('Name updated.')); } catch (error) { next(error); } }); app.post('/account/password', async function (req, res, next) { try { const currentPassword = String(req.body.current_password || ''); const newPassword = String(req.body.new_password || ''); const confirmPassword = String(req.body.confirm_password || ''); const [rows] = await pool.query('SELECT id, name, username, password_hash, password_salt, password_iterations FROM a_users WHERE id = ? LIMIT 1', [req.currentUser.id]); const user = rows[0] || null; if (!user) { return res.status(404).send('User not found.'); } if (!verifyPassword(currentPassword, user)) { return res.status(400).send('Current password is incorrect.'); } const passwordStrengthMessage = validatePasswordStrength(newPassword, await getPasswordRequirements()); if (passwordStrengthMessage) { return res.status(400).send(passwordStrengthMessage); } if (newPassword !== confirmPassword) { return res.status(400).send('New passwords do not match.'); } const passwordRecord = hashPassword(newPassword); await pool.query( 'UPDATE a_users SET password_hash = ?, password_salt = ?, password_iterations = ?, must_change_password = 0, modified_by = ? WHERE id = ?', [passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, getAuditUserId(req), user.id] ); await pool.query('DELETE FROM a_sessions WHERE user_id = ?', [user.id]); if (typeof recordRequestAuditEvent === 'function') { await recordRequestAuditEvent(pool, req, { category: 'security', eventType: 'password.changed', actorUserId: user.id, targetType: 'user', targetId: user.id, targetLabel: user.username, details: { source: 'account' } }); } const sessionMaxAgeMs = typeof getSessionMaxAgeMs === 'function' ? await getSessionMaxAgeMs() : undefined; const token = await createUserSession(pool, user.id, sessionMaxAgeMs, getSessionMetadata(req)); setSessionCookie(res, token, sessionMaxAgeMs); res.redirect('/account?message=' + encodeURIComponent('Password updated.')); } catch (error) { next(error); } }); };