Release 2.8.0
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// Admin account route registration and profile helpers.
|
||||
|
||||
const { fetchAppSettings } = require('../../../data/app-settings');
|
||||
|
||||
module.exports = function registerAccountRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
const common = deps.common;
|
||||
@@ -11,9 +13,129 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
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) {
|
||||
res.send(pages.renderAccountPage(req.currentUser, req.query.message ? String(req.query.message) : '', '/dashboard'));
|
||||
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) {
|
||||
@@ -53,7 +175,7 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
if (!verifyPassword(currentPassword, user)) {
|
||||
return res.status(400).send('Current password is incorrect.');
|
||||
}
|
||||
const passwordStrengthMessage = validatePasswordStrength(newPassword);
|
||||
const passwordStrengthMessage = validatePasswordStrength(newPassword, await getPasswordRequirements());
|
||||
if (passwordStrengthMessage) {
|
||||
return res.status(400).send(passwordStrengthMessage);
|
||||
}
|
||||
@@ -63,13 +185,25 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
|
||||
const passwordRecord = hashPassword(newPassword);
|
||||
await pool.query(
|
||||
'UPDATE a_users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?',
|
||||
'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 token = await createUserSession(pool, user.id);
|
||||
setSessionCookie(res, token);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user