Release v2.20.1: migrate authentication to Better Auth
This commit is contained in:
1 parent
e9a53f577c
commit
fba4f99ff7
92 files changed
+5085
-1943
No files matched your search
@@ -3,6 +3,7 @@
|
||||
const { ANNOUNCEMENT_ICON_CATALOG } = require('#src/data/announcement-icons');
|
||||
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, SCREEN_CONTROL_COMMAND_KEYS, SCREEN_CONTROL_COMMAND_LABELS } = require('#src/data/audit-log');
|
||||
const { fetchAppSettings, saveAppSettings } = require('#src/data/app-settings');
|
||||
const { normalizeOtpEmailBody } = require('#src/data/account-email-templates');
|
||||
const { appVersion, detectSchemaVersion } = require('#src/db/migrations');
|
||||
|
||||
const UPLOAD_MIME_TYPE_OPTIONS = [
|
||||
@@ -42,24 +43,17 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
'data-sources.api_default_interval_unit'
|
||||
],
|
||||
security: [
|
||||
'security.session_lifetime_days',
|
||||
'security.allow_user_session_revocation',
|
||||
'security.max_active_sessions',
|
||||
'security.login_max_attempts',
|
||||
'security.login_lockout_minutes',
|
||||
'security.login_rate_limit_scope',
|
||||
'security.password_min_length',
|
||||
'security.password_min_categories',
|
||||
'security.password_require_lowercase',
|
||||
'security.password_require_uppercase',
|
||||
'security.password_require_number',
|
||||
'security.password_require_symbol',
|
||||
'security.require_password_change_for_new_users',
|
||||
'security.require_password_change_after_admin_reset',
|
||||
'security.allow_admin_email_verification_bypass'
|
||||
],
|
||||
email: ['email.smtp_enabled', 'email.smtp_host', 'email.smtp_port', 'email.smtp_security', 'email.smtp_username', 'email.from_address', 'email.from_name'],
|
||||
'email-templates': ['email.verification_subject', 'email.verification_body', 'email.verification_button_alignment', 'email.verification_button_text', 'email.reset_subject', 'email.reset_body', 'email.reset_button_alignment', 'email.reset_button_text', 'email.invitation_subject', 'email.invitation_body', 'email.invitation_button_alignment', 'email.invitation_button_text'],
|
||||
email: ['email.smtp_enabled', 'email.smtp_host', 'email.smtp_port', 'email.smtp_security', 'email.smtp_username', 'email.from_address', 'email.from_name', 'email.otp_expiry_minutes'],
|
||||
'email-templates': ['email.verification_subject', 'email.verification_body', 'email.reset_subject', 'email.reset_body'],
|
||||
audit: ['audit.enabled', 'audit.categories', 'audit.screen_control_commands', 'audit.include_request_metadata', 'audit.retention_days'],
|
||||
media: ['uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types'],
|
||||
weather: ['weather.open_meteo_api_key', 'weather.pirate_weather_api_key'],
|
||||
@@ -152,20 +146,13 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
videoMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.filter(function (option) { return option.category === 'Video'; }).map(function (option) {
|
||||
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
||||
}),
|
||||
sessionLifetimeDays: Number(settings['security.session_lifetime_days']) || 14,
|
||||
allowUserSessionRevocation: Boolean(settings['security.allow_user_session_revocation']),
|
||||
maxActiveSessions: Number(settings['security.max_active_sessions']) || 0,
|
||||
loginMaxAttempts: Number(settings['security.login_max_attempts']) || 5,
|
||||
loginLockoutMinutes: Number(settings['security.login_lockout_minutes']) || 15,
|
||||
loginRateLimitScope: String(settings['security.login_rate_limit_scope'] || 'both'),
|
||||
passwordMinLength: Number(settings['security.password_min_length']) || 10,
|
||||
passwordMinCategories: Number(settings['security.password_min_categories']) || 3,
|
||||
passwordRequireLowercase: Boolean(settings['security.password_require_lowercase']),
|
||||
passwordRequireUppercase: Boolean(settings['security.password_require_uppercase']),
|
||||
passwordRequireNumber: Boolean(settings['security.password_require_number']),
|
||||
passwordRequireSymbol: Boolean(settings['security.password_require_symbol']),
|
||||
requirePasswordChangeForNewUsers: Boolean(settings['security.require_password_change_for_new_users']),
|
||||
requirePasswordChangeAfterAdminReset: Boolean(settings['security.require_password_change_after_admin_reset']),
|
||||
allowAdminEmailVerificationBypass: Boolean(settings['security.allow_admin_email_verification_bypass']),
|
||||
smtpEnabled: Boolean(settings['email.smtp_enabled']),
|
||||
smtpHost: String(settings['email.smtp_host'] || ''),
|
||||
@@ -175,21 +162,11 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
smtpConfigured: Boolean(settings['email.smtp_password']),
|
||||
emailFromAddress: String(settings['email.from_address'] || ''),
|
||||
emailFromName: String(settings['email.from_name'] || ''),
|
||||
verificationExpiryMinutes: Number(settings['email.verification_expiry_minutes']) || 30,
|
||||
resetExpiryMinutes: Number(settings['email.reset_expiry_minutes']) || 30,
|
||||
invitationExpiryHours: Number(settings['email.invitation_expiry_hours']) || 24,
|
||||
otpExpiryMinutes: Number(settings['email.otp_expiry_minutes']) || 30,
|
||||
verificationEmailSubject: String(settings['email.verification_subject'] || ''),
|
||||
verificationEmailBody: String(settings['email.verification_body'] || ''),
|
||||
verificationButtonAlignment: String(settings['email.verification_button_alignment'] || 'center'),
|
||||
verificationButtonText: String(settings['email.verification_button_text'] || 'Verify email address'),
|
||||
verificationEmailBody: normalizeOtpEmailBody(settings['email.verification_body']),
|
||||
resetEmailSubject: String(settings['email.reset_subject'] || ''),
|
||||
resetEmailBody: String(settings['email.reset_body'] || ''),
|
||||
resetButtonAlignment: String(settings['email.reset_button_alignment'] || 'center'),
|
||||
resetButtonText: String(settings['email.reset_button_text'] || 'Reset password'),
|
||||
invitationEmailSubject: String(settings['email.invitation_subject'] || ''),
|
||||
invitationEmailBody: String(settings['email.invitation_body'] || ''),
|
||||
invitationButtonAlignment: String(settings['email.invitation_button_alignment'] || 'center'),
|
||||
invitationButtonText: String(settings['email.invitation_button_text'] || 'Accept invitation'),
|
||||
resetEmailBody: normalizeOtpEmailBody(settings['email.reset_body']),
|
||||
defaultSlideDurationSeconds: Number(settings['player.default_slide_duration_seconds']) || 10,
|
||||
defaultFadeBetweenSlides: Boolean(settings['player.default_fade_between_slides']),
|
||||
defaultSkipUnavailableRtmp: Boolean(settings['player.skip_unavailable_rtmp']),
|
||||
@@ -279,6 +256,25 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/settings/system/login-lockouts/reset', deps.requirePermission('login-lockouts.allow'), async function (req, res, next) {
|
||||
try {
|
||||
await pool.query('DELETE FROM a_login_attempts WHERE rate_key LIKE ? OR rate_key LIKE ?', ['%|/sign-in/%', '%|/two-factor/verify-%']);
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(pool, req, {
|
||||
category: 'system-settings',
|
||||
eventType: 'system-settings.login_lockouts_reset',
|
||||
actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'settings',
|
||||
targetId: 'security',
|
||||
targetLabel: 'All login lockouts'
|
||||
});
|
||||
}
|
||||
res.redirect('/settings/system?message=' + encodeURIComponent('Sign-in and two-factor request rate limits reset for all clients.') + '#security-sessions');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/settings/system', deps.requirePermission('system-settings.update'), async function (req, res, next) {
|
||||
try {
|
||||
const section = String(req.body && req.body.settings_section || '').trim().toLowerCase();
|
||||
@@ -314,16 +310,11 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
|
||||
if (section === 'email-templates') {
|
||||
const verificationSubject = String(req.body && req.body.verification_subject || '').trim();
|
||||
const verificationBody = String(req.body && req.body.verification_body || '').trim();
|
||||
const verificationBody = normalizeOtpEmailBody(req.body && req.body.verification_body).trim();
|
||||
const resetSubject = String(req.body && req.body.reset_subject || '').trim();
|
||||
const resetBody = String(req.body && req.body.reset_body || '').trim();
|
||||
const verificationButtonText = String(req.body && req.body.verification_button_text || '').trim();
|
||||
const resetButtonText = String(req.body && req.body.reset_button_text || '').trim();
|
||||
const invitationSubject = String(req.body && req.body.invitation_subject || '').trim();
|
||||
const invitationBody = String(req.body && req.body.invitation_body || '').trim();
|
||||
const invitationButtonText = String(req.body && req.body.invitation_button_text || '').trim();
|
||||
if (!verificationSubject || !verificationBody.includes('[[url]]') || !resetSubject || !resetBody.includes('[[url]]') || !invitationSubject || !invitationBody.includes('[[url]]') || !verificationButtonText || !resetButtonText || !invitationButtonText) {
|
||||
const error = new Error('Email templates require a subject, message, and the correct link placeholder.');
|
||||
const resetBody = normalizeOtpEmailBody(req.body && req.body.reset_body).trim();
|
||||
if (!verificationSubject || !verificationBody.includes('[[otp]]') || !resetSubject || !resetBody.includes('[[otp]]')) {
|
||||
const error = new Error('Email templates require a subject, message, and the correct code or link placeholder.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
@@ -331,16 +322,8 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'email.verification_subject': verificationSubject,
|
||||
'email.verification_body': verificationBody,
|
||||
'email.verification_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.verification_button_alignment || '').trim()) ? String(req.body.verification_button_alignment).trim() : 'center',
|
||||
'email.verification_button_text': verificationButtonText,
|
||||
'email.reset_subject': resetSubject,
|
||||
'email.reset_body': resetBody,
|
||||
'email.reset_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.reset_button_alignment || '').trim()) ? String(req.body.reset_button_alignment).trim() : 'center',
|
||||
'email.reset_button_text': resetButtonText
|
||||
,'email.invitation_subject': invitationSubject
|
||||
,'email.invitation_body': invitationBody
|
||||
,'email.invitation_button_alignment': ['left', 'center', 'right'].includes(String(req.body && req.body.invitation_button_alignment || '').trim()) ? String(req.body.invitation_button_alignment).trim() : 'center'
|
||||
,'email.invitation_button_text': invitationButtonText
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Account email templates saved.') + '#email-templates');
|
||||
@@ -380,43 +363,29 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
}
|
||||
|
||||
if (section === 'security') {
|
||||
const sessionLifetimeDays = Number(req.body && req.body.session_lifetime_days);
|
||||
const allowUserSessionRevocation = req.body && (req.body.allow_user_session_revocation === '1' || req.body.allow_user_session_revocation === 'true');
|
||||
const maxActiveSessions = Number(req.body && req.body.max_active_sessions);
|
||||
const loginMaxAttempts = Number(req.body && req.body.login_max_attempts);
|
||||
const loginLockoutMinutes = Number(req.body && req.body.login_lockout_minutes);
|
||||
const loginRateLimitScope = String(req.body && req.body.login_rate_limit_scope || 'both').trim().toLowerCase();
|
||||
const passwordMinLength = Number(req.body && req.body.password_min_length);
|
||||
const passwordMinCategories = Number(req.body && req.body.password_min_categories);
|
||||
const passwordRequireLowercase = req.body && (req.body.password_require_lowercase === '1' || req.body.password_require_lowercase === 'true');
|
||||
const passwordRequireUppercase = req.body && (req.body.password_require_uppercase === '1' || req.body.password_require_uppercase === 'true');
|
||||
const passwordRequireNumber = req.body && (req.body.password_require_number === '1' || req.body.password_require_number === 'true');
|
||||
const passwordRequireSymbol = req.body && (req.body.password_require_symbol === '1' || req.body.password_require_symbol === 'true');
|
||||
const requirePasswordChangeForNewUsers = req.body && (req.body.require_password_change_for_new_users === '1' || req.body.require_password_change_for_new_users === 'true');
|
||||
const requirePasswordChangeAfterAdminReset = req.body && (req.body.require_password_change_after_admin_reset === '1' || req.body.require_password_change_after_admin_reset === 'true');
|
||||
const requiredCategoryCount = [passwordRequireLowercase, passwordRequireUppercase, passwordRequireNumber, passwordRequireSymbol].filter(Boolean).length;
|
||||
if (!Number.isInteger(sessionLifetimeDays) || sessionLifetimeDays < 1 || sessionLifetimeDays > 365 || !Number.isInteger(maxActiveSessions) || maxActiveSessions < 0 || maxActiveSessions > 100 || !Number.isInteger(loginMaxAttempts) || loginMaxAttempts < 1 || loginMaxAttempts > 100 || !Number.isInteger(loginLockoutMinutes) || loginLockoutMinutes < 1 || loginLockoutMinutes > 1440 || !['both', 'username', 'ip'].includes(loginRateLimitScope) || !Number.isInteger(passwordMinLength) || passwordMinLength < 8 || passwordMinLength > 128 || !Number.isInteger(passwordMinCategories) || passwordMinCategories < 1 || passwordMinCategories > 4 || requiredCategoryCount > passwordMinCategories) {
|
||||
const error = new Error('Session and password settings must use valid values; required password categories cannot exceed the minimum category count.');
|
||||
if (!Number.isInteger(passwordMinLength) || passwordMinLength < 8 || passwordMinLength > 128 || !Number.isInteger(passwordMinCategories) || passwordMinCategories < 1 || passwordMinCategories > 4 || requiredCategoryCount > passwordMinCategories) {
|
||||
const error = new Error('Password settings must use valid values; required password categories cannot exceed the minimum category count.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'security.session_lifetime_days': sessionLifetimeDays,
|
||||
'security.allow_user_session_revocation': allowUserSessionRevocation,
|
||||
'security.max_active_sessions': maxActiveSessions,
|
||||
'security.login_max_attempts': loginMaxAttempts,
|
||||
'security.login_lockout_minutes': loginLockoutMinutes,
|
||||
'security.login_rate_limit_scope': loginRateLimitScope,
|
||||
'security.password_min_length': passwordMinLength,
|
||||
'security.password_min_categories': passwordMinCategories,
|
||||
'security.password_require_lowercase': passwordRequireLowercase,
|
||||
'security.password_require_uppercase': passwordRequireUppercase,
|
||||
'security.password_require_number': passwordRequireNumber,
|
||||
'security.password_require_symbol': passwordRequireSymbol,
|
||||
'security.require_password_change_for_new_users': requirePasswordChangeForNewUsers,
|
||||
'security.require_password_change_after_admin_reset': requirePasswordChangeAfterAdminReset
|
||||
,'security.allow_admin_email_verification_bypass': req.body && (req.body.allow_admin_email_verification_bypass === '1' || req.body.allow_admin_email_verification_bypass === 'true')
|
||||
'security.allow_admin_email_verification_bypass': req.body && (req.body.allow_admin_email_verification_bypass === '1' || req.body.allow_admin_email_verification_bypass === 'true')
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Security settings saved.') + '#security-sessions');
|
||||
@@ -431,10 +400,8 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
const smtpPassword = String(req.body && req.body.smtp_password || '');
|
||||
const fromAddress = String(req.body && req.body.from_address || '').trim();
|
||||
const fromName = String(req.body && req.body.from_name || '').trim();
|
||||
const verificationExpiryMinutes = Number(req.body && req.body.verification_expiry_minutes);
|
||||
const resetExpiryMinutes = Number(req.body && req.body.reset_expiry_minutes);
|
||||
const invitationExpiryHours = Number(req.body && req.body.invitation_expiry_hours);
|
||||
if (!Number.isInteger(verificationExpiryMinutes) || verificationExpiryMinutes < 1 || verificationExpiryMinutes > 10080 || !Number.isInteger(resetExpiryMinutes) || resetExpiryMinutes < 1 || resetExpiryMinutes > 10080 || !Number.isInteger(invitationExpiryHours) || invitationExpiryHours < 1 || invitationExpiryHours > 720 || (smtpEnabled && (!smtpHost || !Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535 || !fromAddress))) {
|
||||
const otpExpiryMinutes = Number(req.body && req.body.otp_expiry_minutes);
|
||||
if (!Number.isInteger(otpExpiryMinutes) || otpExpiryMinutes < 1 || otpExpiryMinutes > 10080 || (smtpEnabled && (!smtpHost || !Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535 || !fromAddress))) {
|
||||
const error = new Error('Enabled email delivery requires an SMTP host, valid port, and from address.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
@@ -449,9 +416,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
'email.smtp_password': smtpPassword || previousSettings['email.smtp_password'],
|
||||
'email.from_address': fromAddress,
|
||||
'email.from_name': fromName,
|
||||
'email.verification_expiry_minutes': verificationExpiryMinutes,
|
||||
'email.reset_expiry_minutes': resetExpiryMinutes,
|
||||
'email.invitation_expiry_hours': invitationExpiryHours
|
||||
'email.otp_expiry_minutes': otpExpiryMinutes
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Email settings saved.') + '#email-delivery');
|
||||
|
||||
Reference in new issue
Block a user