Release v2.11.1
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Admin account route registration and profile helpers.
|
||||
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { renderAccountEmailTemplate } = require('#src/data/account-email-templates');
|
||||
|
||||
module.exports = function registerAccountRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -18,6 +19,8 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
const hashSessionToken = deps.hashSessionToken;
|
||||
const sessionCookieName = deps.sessionCookieName;
|
||||
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
||||
const sendAccountEmail = deps.sendAccountEmail;
|
||||
const createOneTimeToken = deps.createOneTimeToken;
|
||||
|
||||
async function getPasswordRequirements() {
|
||||
const settings = await fetchAppSettings(pool);
|
||||
@@ -43,6 +46,29 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
};
|
||||
}
|
||||
|
||||
function getRequestOrigin(req) {
|
||||
const forwardedProto = String(req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim();
|
||||
const forwardedHost = String(req.headers['x-forwarded-host'] || req.headers.host || '').split(',')[0].trim();
|
||||
return forwardedHost ? forwardedProto + '://' + forwardedHost : '';
|
||||
}
|
||||
|
||||
app.post('/account/email', async function (req, res, next) {
|
||||
try {
|
||||
const email = String(req.body.email || '').trim().toLowerCase();
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return res.status(400).send('Email address is invalid.');
|
||||
const settings = await fetchAppSettings(pool);
|
||||
if (!settings['email.smtp_enabled'] || typeof sendAccountEmail !== 'function') return res.status(400).send('Email delivery is not configured.');
|
||||
const token = createOneTimeToken();
|
||||
await pool.query('UPDATE a_users SET pending_email = ?, pending_email_token_hash = ?, pending_email_expires_at = DATE_ADD(NOW(), INTERVAL 30 MINUTE) WHERE id = ?', [email, hashSessionToken(token), req.currentUser.id]);
|
||||
const url = getRequestOrigin(req) + '/verify-email?token=' + encodeURIComponent(token);
|
||||
await sendAccountEmail(settings, Object.assign({ to: email }, renderAccountEmailTemplate(settings['email.verification_subject'], settings['email.verification_body'], { url: url, username: req.currentUser.username, display_name: req.currentUser.name, email: email, action_alignment: settings['email.verification_button_alignment'], action_label: settings['email.verification_button_text'] })));
|
||||
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'users', eventType: 'user.email_change_requested', actorUserId: req.currentUser.id, targetType: 'user', targetId: req.currentUser.id, targetLabel: email });
|
||||
res.redirect('/account?message=' + encodeURIComponent('Check your new email address for a verification link.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/account', function (req, res) {
|
||||
fetchAppSettings(pool).then(function (settings) {
|
||||
const passwordRequirements = buildPasswordRequirements(settings);
|
||||
@@ -74,6 +100,52 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/account/profile', async function (req, res, next) {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
const username = common.validateMaxLength(req.body.username || '', 64, 'Username');
|
||||
const email = String(req.body.email || '').trim().toLowerCase();
|
||||
const currentPassword = String(req.body.current_password || '');
|
||||
if (!name) return res.status(400).send('Name is required.');
|
||||
if (!username) return res.status(400).send('Username is required.');
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return res.status(400).send('Email address is invalid.');
|
||||
if (!currentPassword) return res.status(400).send('Current password is required.');
|
||||
|
||||
const [rows] = await pool.query('SELECT id, username, name, email, 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.');
|
||||
if (username !== user.username && await common.fetchDuplicateName(pool, 'a_users', username, user.id, 'username')) return res.status(400).send('That username already exists.');
|
||||
if (name !== user.name && await common.fetchDuplicateName(pool, 'a_users', name, user.id)) return res.status(400).send('That name already exists.');
|
||||
|
||||
const confirmedEmail = String(user.email || '').trim().toLowerCase();
|
||||
const pendingEmail = String(user.pending_email || '').trim().toLowerCase();
|
||||
const emailChanged = email !== confirmedEmail && email !== pendingEmail;
|
||||
const pendingEmailCleared = Boolean(pendingEmail && email === confirmedEmail);
|
||||
const settings = emailChanged ? await fetchAppSettings(pool) : null;
|
||||
if (emailChanged && email && (!settings['email.smtp_enabled'] || typeof sendAccountEmail !== 'function')) return res.status(400).send('Email delivery is not configured.');
|
||||
|
||||
const actorId = getAuditUserId(req);
|
||||
await pool.query('UPDATE a_users SET username = ?, name = ?, modified_by = ? WHERE id = ?', [username, name, actorId, user.id]);
|
||||
if (emailChanged || pendingEmailCleared) {
|
||||
if (!email) {
|
||||
await pool.query('UPDATE a_users SET email = NULL, email_verified_at = NULL, pending_email = NULL, pending_email_token_hash = NULL, pending_email_expires_at = NULL WHERE id = ?', [user.id]);
|
||||
} else if (pendingEmailCleared) {
|
||||
await pool.query('UPDATE a_users SET pending_email = NULL, pending_email_token_hash = NULL, pending_email_expires_at = NULL WHERE id = ?', [user.id]);
|
||||
} else {
|
||||
const token = createOneTimeToken();
|
||||
await pool.query('UPDATE a_users SET pending_email = ?, pending_email_token_hash = ?, pending_email_expires_at = DATE_ADD(NOW(), INTERVAL 30 MINUTE) WHERE id = ?', [email, hashSessionToken(token), user.id]);
|
||||
const url = getRequestOrigin(req) + '/verify-email?token=' + encodeURIComponent(token);
|
||||
await sendAccountEmail(settings, Object.assign({ to: email }, renderAccountEmailTemplate(settings['email.verification_subject'], settings['email.verification_body'], { url: url, username: req.currentUser.username, display_name: req.currentUser.name, email: email, action_alignment: settings['email.verification_button_alignment'], action_label: settings['email.verification_button_text'] })));
|
||||
}
|
||||
}
|
||||
if (emailChanged && typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'users', eventType: email ? 'user.email_change_requested' : 'user.email_cleared', actorUserId: user.id, targetType: 'user', targetId: user.id, targetLabel: email || user.email, details: { previousEmail: confirmedEmail || null, email: email || null, verificationRequired: Boolean(email) } });
|
||||
res.redirect('/account?message=' + encodeURIComponent(emailChanged && email ? 'Account details updated. Check your new email address for a verification link.' : 'Account details updated.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/account/sessions/:id/revoke', async function (req, res, next) {
|
||||
try {
|
||||
const sessionId = Number(req.params.id);
|
||||
@@ -161,6 +233,26 @@ module.exports = function registerAccountRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/account/username', async function (req, res, next) {
|
||||
try {
|
||||
const username = common.validateMaxLength(req.body.username || '', 255, 'Username');
|
||||
const currentPassword = String(req.body.current_password || '');
|
||||
if (!username) return res.status(400).send('Username is required.');
|
||||
if (!currentPassword) return res.status(400).send('Current password is required.');
|
||||
const [rows] = await pool.query('SELECT id, 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 [existingRows] = await pool.query('SELECT id FROM a_users WHERE username = ? AND id <> ? LIMIT 1', [username, user.id]);
|
||||
if (existingRows.length) return res.status(400).send('That username already exists.');
|
||||
await pool.query('UPDATE a_users SET username = ?, modified_by = ? WHERE id = ?', [username, getAuditUserId(req), user.id]);
|
||||
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'users', eventType: 'user.username_updated', actorUserId: user.id, targetType: 'user', targetId: user.id, targetLabel: username, details: { previousUsername: user.username, username: username } });
|
||||
res.redirect('/account?message=' + encodeURIComponent('Username updated.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/account/password', async function (req, res, next) {
|
||||
try {
|
||||
const currentPassword = String(req.body.current_password || '');
|
||||
|
||||
Reference in New Issue
Block a user