Release 2.8.0
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
const { renderView } = require('#src/web/view');
|
||||
const { hasPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
|
||||
const { AUDIT_CATEGORY_KEYS } = require('#src/data/audit-log');
|
||||
const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
function requireAuditLogAccess(setAuthMessageCookie) {
|
||||
return function (req, res, next) {
|
||||
if (!req.currentUser) {
|
||||
if (typeof setAuthMessageCookie === 'function') {
|
||||
setAuthMessageCookie(res, 'Please sign in to continue.');
|
||||
}
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (hasPermission(req.currentUser, 'audit-log.read')) {
|
||||
return next();
|
||||
}
|
||||
const error = new Error(PERMISSION_DENIED_MESSAGE);
|
||||
error.statusCode = 403;
|
||||
error.expose = true;
|
||||
return next(error);
|
||||
};
|
||||
}
|
||||
|
||||
function requireAuditExportAccess(setAuthMessageCookie) {
|
||||
return function (req, res, next) {
|
||||
if (!req.currentUser) {
|
||||
if (typeof setAuthMessageCookie === 'function') setAuthMessageCookie(res, 'Please sign in to continue.');
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (hasPermission(req.currentUser, 'audit-log.allow')) return next();
|
||||
const error = new Error(PERMISSION_DENIED_MESSAGE);
|
||||
error.statusCode = 403;
|
||||
error.expose = true;
|
||||
return next(error);
|
||||
};
|
||||
}
|
||||
|
||||
function csvCell(value) {
|
||||
let text = String(value === undefined || value === null ? '' : value);
|
||||
if (/^[=+\-@]/.test(text)) text = "'" + text;
|
||||
return '"' + text.replace(/"/g, '""').replace(/\r?\n/g, ' ') + '"';
|
||||
}
|
||||
|
||||
function mapAuditRow(row, formatDashboardDate) {
|
||||
let details = '';
|
||||
if (row.details_json) {
|
||||
try {
|
||||
details = JSON.stringify(typeof row.details_json === 'string' ? JSON.parse(row.details_json) : row.details_json);
|
||||
} catch (_error) {
|
||||
details = String(row.details_json);
|
||||
}
|
||||
}
|
||||
const occurredAt = new Date(row.occurred_at);
|
||||
return Object.assign({}, row, {
|
||||
occurredAtValue: Number.isNaN(occurredAt.getTime()) ? '' : occurredAt.toISOString(),
|
||||
occurredAtLabel: formatDashboardDate(row.occurred_at),
|
||||
actorLabel: row.actor_name || row.actor_username || 'System',
|
||||
eventLabel: String(row.event_type || '').replace(/[._-]+/g, ' '),
|
||||
targetLabelDisplay: row.target_label || (row.target_type && row.target_id ? row.target_type + ' #' + row.target_id : ''),
|
||||
detailsDisplay: details
|
||||
});
|
||||
}
|
||||
|
||||
function filterAuditRows(rows, req, formatDashboardDate) {
|
||||
const category = String(req.query.category || '').trim().toLowerCase();
|
||||
const eventType = String(req.query.event_type || '').trim().toLowerCase();
|
||||
const search = getSearchQuery(req);
|
||||
const requestedSort = getSortQuery(req);
|
||||
const sort = requestedSort || 'time';
|
||||
const direction = requestedSort ? getSortDirectionQuery(req) : 'desc';
|
||||
const mappedRows = (rows || []).map(function (row) { return mapAuditRow(row, formatDashboardDate); });
|
||||
const filteredRows = sortRows(mappedRows.filter(function (event) {
|
||||
return (!category || event.category === category) && (!eventType || event.event_type === eventType);
|
||||
}).filter(createSearchMatcher(search, ['category', 'event_type', 'actorLabel', 'targetLabelDisplay', 'ip_address', 'user_agent'])), function (event) {
|
||||
if (sort === 'event') return event.eventLabel;
|
||||
if (sort === 'actor') return event.actorLabel;
|
||||
if (sort === 'target') return event.targetLabelDisplay;
|
||||
if (sort === 'category') return event.category;
|
||||
return event.occurred_at;
|
||||
}, direction);
|
||||
return { rows: filteredRows, category: category, eventType: eventType, search: search, sort: sort, direction: direction };
|
||||
}
|
||||
|
||||
async function fetchAuditRows(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id,
|
||||
e.target_label, e.ip_address, e.user_agent, e.details_json,
|
||||
u.name AS actor_name, u.username AS actor_username
|
||||
FROM o_audit_events e
|
||||
LEFT JOIN a_users u ON u.id = e.actor_user_id
|
||||
ORDER BY e.occurred_at DESC, e.id DESC`, []
|
||||
);
|
||||
return rows || [];
|
||||
}
|
||||
|
||||
module.exports = function registerAuditLogRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
const formatDashboardDate = deps.formatDashboardDate;
|
||||
if (!pool || typeof formatDashboardDate !== 'function') {
|
||||
throw new Error('registerAuditLogRoutes requires pool and formatDashboardDate.');
|
||||
}
|
||||
|
||||
app.get('/settings/audit-log', requireAuditLogAccess(deps.setAuthMessageCookie), async function (req, res, next) {
|
||||
try {
|
||||
const category = String(req.query.category || '').trim().toLowerCase();
|
||||
const eventType = String(req.query.event_type || '').trim().toLowerCase();
|
||||
const search = getSearchQuery(req);
|
||||
const sort = getSortQuery(req);
|
||||
const direction = getSortDirectionQuery(req);
|
||||
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
|
||||
const filteredEvents = filtered.rows;
|
||||
const eventTypes = Array.from(new Set(filtered.rows.map(function (event) {
|
||||
return String(event.event_type || '').trim();
|
||||
}).filter(Boolean))).sort();
|
||||
const exportQuery = new URLSearchParams({
|
||||
category: filtered.category,
|
||||
event_type: filtered.eventType,
|
||||
search: filtered.search,
|
||||
sort: filtered.sort,
|
||||
direction: filtered.direction
|
||||
}).toString();
|
||||
const pagination = buildPagination(filteredEvents.length, parsePageNumber(req.query.page), 'page', {
|
||||
search: search,
|
||||
sort: sort,
|
||||
direction: direction,
|
||||
category: category,
|
||||
event_type: eventType
|
||||
}, PAGE_SIZE, 'events', 'Audit event pages');
|
||||
const startIndex = (pagination.currentPage - 1) * PAGE_SIZE;
|
||||
const events = filteredEvents.slice(startIndex, startIndex + PAGE_SIZE);
|
||||
res.send(renderView('settings/audit-log/index', {
|
||||
title: 'Audit log',
|
||||
active: 'audit-log',
|
||||
currentUser: req.currentUser,
|
||||
events: events,
|
||||
categories: AUDIT_CATEGORY_KEYS,
|
||||
eventTypes: eventTypes,
|
||||
selectedCategory: category,
|
||||
selectedEventType: eventType,
|
||||
search: search,
|
||||
sort: sort,
|
||||
direction: direction,
|
||||
pagination: pagination,
|
||||
exportUrl: '/settings/audit-log/export' + (exportQuery ? '?' + exportQuery : '')
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/settings/audit-log/export', requireAuditExportAccess(deps.setAuthMessageCookie), async function (req, res, next) {
|
||||
try {
|
||||
const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate);
|
||||
const lines = [
|
||||
['Occurred At (UTC)', 'Category', 'Event Type', 'Actor', 'Target Type', 'Target ID', 'Target', 'IP Address', 'User Agent', 'Details'].map(csvCell).join(',')
|
||||
];
|
||||
filtered.rows.forEach(function (event) {
|
||||
lines.push([
|
||||
event.occurredAtValue,
|
||||
event.category,
|
||||
event.event_type,
|
||||
event.actorLabel,
|
||||
event.target_type,
|
||||
event.target_id,
|
||||
event.targetLabelDisplay,
|
||||
event.ip_address,
|
||||
event.user_agent,
|
||||
event.detailsDisplay
|
||||
].map(csvCell).join(','));
|
||||
});
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="pulse-signage-audit-log.csv"');
|
||||
res.send('\ufeff' + lines.join('\r\n') + '\r\n');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// Visual-only application settings page.
|
||||
|
||||
const { renderView } = require('../../view');
|
||||
|
||||
module.exports = function renderSettingsPage(settings, suggestedAnnouncementIcons, currentUser, message, mediaSettings, diagnostics) {
|
||||
const iconOptions = Array.isArray(suggestedAnnouncementIcons) ? suggestedAnnouncementIcons : [];
|
||||
return renderView('settings/system/index', {
|
||||
title: 'Settings',
|
||||
active: 'settings',
|
||||
currentUser: currentUser || null,
|
||||
message: message || '',
|
||||
settings: settings || {},
|
||||
suggestedAnnouncementIcons: iconOptions,
|
||||
defaultAnnouncementIconSuggestions: iconOptions.filter(function (option) { return option.isSelected; }),
|
||||
mediaSettings: mediaSettings || {},
|
||||
diagnostics: diagnostics || {},
|
||||
scripts: ['js/icon-picker.js', 'js/settings/settings-page.js']
|
||||
});
|
||||
};
|
||||
@@ -53,17 +53,17 @@ function buildPermissionSections(permissionGroups) {
|
||||
});
|
||||
|
||||
section.groups.sort(function (left, right) {
|
||||
const leftIndex = toSortIndex(left.resourceIndex);
|
||||
const rightIndex = toSortIndex(right.resourceIndex);
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
const leftOrder = toSortIndex(left.resourceOrder);
|
||||
const rightOrder = toSortIndex(right.resourceOrder);
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
|
||||
const leftIndex = toSortIndex(left.resourceIndex);
|
||||
const rightIndex = toSortIndex(right.resourceIndex);
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
return String(left.title || '').localeCompare(String(right.title || ''));
|
||||
});
|
||||
|
||||
@@ -77,17 +77,17 @@ function buildPermissionSections(permissionGroups) {
|
||||
});
|
||||
|
||||
sections.sort(function (left, right) {
|
||||
const leftIndex = toSortIndex(left.sourceIndex);
|
||||
const rightIndex = toSortIndex(right.sourceIndex);
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
|
||||
const leftOrder = toSortIndex(left.sectionOrder);
|
||||
const rightOrder = toSortIndex(right.sectionOrder);
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
|
||||
const leftIndex = toSortIndex(left.sourceIndex);
|
||||
const rightIndex = toSortIndex(right.sourceIndex);
|
||||
if (leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
return String(left.title || '').localeCompare(String(right.title || ''));
|
||||
});
|
||||
|
||||
@@ -101,9 +101,10 @@ function buildRbacAddViewModel(message, currentUser, formValues, permissionGroup
|
||||
isEdit: false,
|
||||
usersPresent: true,
|
||||
formId: 'role-create-form',
|
||||
formAction: '/rbac',
|
||||
footerSaveUrl: '/rbac',
|
||||
footerCancelUrl: '/rbac',
|
||||
formAction: '/settings/roles',
|
||||
footerSaveUrl: '/settings/roles',
|
||||
footerCancelUrl: '/settings/roles',
|
||||
cancelConfirmMessage: "You've made changes. Are you sure you want to leave this page?",
|
||||
footerDeleteUrl: '',
|
||||
footerDeleteDisabled: true,
|
||||
footerDeleteTitle: 'Delete is available after the role has been created.',
|
||||
@@ -116,9 +117,9 @@ function buildRbacAddViewModel(message, currentUser, formValues, permissionGroup
|
||||
permissionSections: buildPermissionSections(permissionGroups),
|
||||
users: users || [],
|
||||
pagination: pagination || null,
|
||||
roleEditBasePath: '/rbac/new',
|
||||
roleEditBasePath: '/settings/roles/new',
|
||||
scripts: ['js/rbac-permissions.js'],
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/rbac" data-async-save-new-redirect="response-url" data-async-save-new-url="/rbac/new"'
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/settings/roles" data-async-save-new-redirect="response-url" data-async-save-new-url="/settings/roles/new"'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,10 +130,11 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
|
||||
isEdit: true,
|
||||
usersPresent: true,
|
||||
formId: 'role-edit-form',
|
||||
formAction: '/rbac/' + Number(role && role.id),
|
||||
footerSaveUrl: '/rbac/' + Number(role && role.id),
|
||||
footerCancelUrl: '/rbac',
|
||||
footerDeleteUrl: '/rbac/' + Number(role && role.id) + '/delete',
|
||||
formAction: '/settings/roles/' + Number(role && role.id),
|
||||
footerSaveUrl: '/settings/roles/' + Number(role && role.id),
|
||||
footerCancelUrl: '/settings/roles',
|
||||
cancelConfirmMessage: "You've made changes. Are you sure you want to leave this page?",
|
||||
footerDeleteUrl: '/settings/roles/' + Number(role && role.id) + '/delete',
|
||||
footerDeleteDisabled: Boolean(role && role.inUse),
|
||||
footerDeleteTitle: role && role.inUse ? 'Delete is disabled while this role is assigned to users.' : '',
|
||||
footerShowDelete: true,
|
||||
@@ -144,9 +146,9 @@ function buildRbacEditViewModel(role, message, currentUser, permissionGroups, us
|
||||
permissionSections: buildPermissionSections(permissionGroups),
|
||||
users: users || [],
|
||||
pagination: pagination || null,
|
||||
roleEditBasePath: '/rbac/' + Number(role && role.id) + '/edit',
|
||||
roleEditBasePath: '/settings/roles/' + Number(role && role.id) + '/edit',
|
||||
scripts: ['js/rbac-permissions.js'],
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/rbac" data-async-save-new-url="/rbac/new"'
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/settings/roles" data-async-save-new-url="/settings/roles/new"'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
// Settings page route registration.
|
||||
|
||||
const { ANNOUNCEMENT_ICON_CATALOG } = require('#src/data/announcement-icons');
|
||||
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS } = require('#src/data/audit-log');
|
||||
const { fetchAppSettings, saveAppSettings } = require('#src/data/app-settings');
|
||||
const { appVersion, detectSchemaVersion } = require('#src/db/migrations');
|
||||
|
||||
const UPLOAD_MIME_TYPE_OPTIONS = [
|
||||
{ value: 'image/png', label: 'PNG', category: 'Images' },
|
||||
{ value: 'image/jpeg', label: 'JPEG', category: 'Images' },
|
||||
{ value: 'image/gif', label: 'GIF', category: 'Images' },
|
||||
{ value: 'image/webp', label: 'WebP', category: 'Images' },
|
||||
{ value: 'image/svg+xml', label: 'SVG', category: 'Images' },
|
||||
{ value: 'video/mp4', label: 'MP4', category: 'Video' },
|
||||
{ value: 'video/webm', label: 'WebM', category: 'Video' },
|
||||
{ value: 'video/ogg', label: 'Ogg', category: 'Video' }
|
||||
];
|
||||
|
||||
module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
const pages = deps.pages;
|
||||
const pool = deps.pool;
|
||||
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
||||
|
||||
if (!pages || !pool || typeof deps.requirePermission !== 'function') {
|
||||
throw new Error('registerSettingsPageRoutes requires pool, pages, and requirePermission.');
|
||||
}
|
||||
|
||||
const requireSettingsRead = deps.requirePermission('system-settings.read');
|
||||
const MAX_SUGGESTED_ICONS = 48;
|
||||
const SECTION_SETTING_KEYS = {
|
||||
defaults: [
|
||||
'player.default_slide_duration_seconds',
|
||||
'player.default_fade_between_slides',
|
||||
'player.skip_unavailable_rtmp',
|
||||
'announcements.default_icon',
|
||||
'announcements.default_duration_value',
|
||||
'announcements.default_duration_unit',
|
||||
'data-sources.rss_default_interval_value',
|
||||
'data-sources.rss_default_interval_unit',
|
||||
'data-sources.api_default_interval_value',
|
||||
'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'
|
||||
],
|
||||
audit: ['audit.enabled', 'audit.categories', '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'],
|
||||
icons: ['announcements.suggested_icons']
|
||||
};
|
||||
|
||||
function buildSettingChanges(previousSettings, nextSettings, keys) {
|
||||
const changes = {};
|
||||
(Array.isArray(keys) ? keys : []).forEach(function (key) {
|
||||
const previousValue = previousSettings && previousSettings[key];
|
||||
const nextValue = nextSettings[key];
|
||||
if (JSON.stringify(previousValue) !== JSON.stringify(nextValue)) {
|
||||
changes[key] = { from: previousValue, to: nextValue };
|
||||
}
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
async function recordSettingsAuditEvent(req, previousSettings, nextSettings, section) {
|
||||
if (typeof recordRequestAuditEvent !== 'function') {
|
||||
return;
|
||||
}
|
||||
const changes = buildSettingChanges(previousSettings, nextSettings, SECTION_SETTING_KEYS[section]);
|
||||
const changedKeys = Object.keys(changes);
|
||||
if (!changedKeys.length) {
|
||||
return;
|
||||
}
|
||||
await recordRequestAuditEvent(pool, req, {
|
||||
category: 'system-settings',
|
||||
eventType: 'system-settings.updated',
|
||||
actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'settings',
|
||||
targetId: section,
|
||||
targetLabel: section,
|
||||
details: { section: section, changes: changes }
|
||||
});
|
||||
}
|
||||
|
||||
function buildSuggestedAnnouncementIcons(settings) {
|
||||
const configuredKeys = settings && Array.isArray(settings['announcements.suggested_icons'])
|
||||
? settings['announcements.suggested_icons']
|
||||
: [];
|
||||
const selectedKeys = new Set(configuredKeys.map(function (key) {
|
||||
return String(key || '').trim().toLowerCase();
|
||||
}));
|
||||
const catalogByKey = new Map(ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
||||
return [option.value, option];
|
||||
}));
|
||||
const selectedOptions = Array.from(new Set(configuredKeys.map(function (key) {
|
||||
return catalogByKey.get(String(key || '').trim().toLowerCase()) || null;
|
||||
}).filter(Boolean)));
|
||||
const remainingOptions = ANNOUNCEMENT_ICON_CATALOG.filter(function (option) {
|
||||
return !selectedKeys.has(option.value);
|
||||
});
|
||||
return selectedOptions.concat(remainingOptions).map(function (option) {
|
||||
return Object.assign({}, option, {
|
||||
isSelected: selectedKeys.has(option.value),
|
||||
isDefault: selectedOptions.length > 0 && selectedOptions[0].value === option.value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function renderSettings(req, res, message) {
|
||||
const settings = await fetchAppSettings(pool);
|
||||
const diagnostics = await buildDiagnostics();
|
||||
res.send(pages.renderSettingsPage(settings, buildSuggestedAnnouncementIcons(settings), req.currentUser, message, {
|
||||
imageMaxMb: Math.round(Number(settings['uploads.image_max_bytes']) / 1024 / 1024),
|
||||
videoMaxMb: Math.round(Number(settings['uploads.video_max_bytes']) / 1024 / 1024),
|
||||
wysiwygImageMaxMb: (Number(settings['uploads.wysiwyg_image_max_bytes']) / 1024 / 1024).toFixed(2).replace(/0+$/, '').replace(/\.$/, '')
|
||||
,allowedMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.map(function (option) {
|
||||
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
||||
}),
|
||||
imageMimeTypes: UPLOAD_MIME_TYPE_OPTIONS.filter(function (option) { return option.category === 'Images'; }).map(function (option) {
|
||||
return Object.assign({}, option, { isSelected: settings['uploads.allowed_mime_types'].indexOf(option.value) !== -1 });
|
||||
}),
|
||||
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']),
|
||||
defaultSlideDurationSeconds: Number(settings['player.default_slide_duration_seconds']) || 10,
|
||||
defaultFadeBetweenSlides: Boolean(settings['player.default_fade_between_slides']),
|
||||
defaultSkipUnavailableRtmp: Boolean(settings['player.skip_unavailable_rtmp']),
|
||||
defaultAnnouncementIcon: String(settings['announcements.default_icon'] || 'megaphone-fill'),
|
||||
defaultAnnouncementDurationValue: Number(settings['announcements.default_duration_value']) || 10,
|
||||
defaultAnnouncementDurationUnit: String(settings['announcements.default_duration_unit'] || 'seconds')
|
||||
,rssDefaultIntervalValue: Number(settings['data-sources.rss_default_interval_value']) || 60
|
||||
,rssDefaultIntervalUnit: String(settings['data-sources.rss_default_interval_unit'] || 'minutes')
|
||||
,apiDefaultIntervalValue: Number(settings['data-sources.api_default_interval_value']) || 60
|
||||
,apiDefaultIntervalUnit: String(settings['data-sources.api_default_interval_unit'] || 'minutes')
|
||||
,auditRetentionDays: Number(settings['audit.retention_days']) || 0
|
||||
,auditEnabled: Boolean(settings['audit.enabled'])
|
||||
,auditIncludeRequestMetadata: Boolean(settings['audit.include_request_metadata'])
|
||||
,auditCategories: AUDIT_CATEGORY_KEYS.map(function (category) {
|
||||
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
||||
})
|
||||
,auditEventCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category !== 'users' && category !== 'roles' && category !== 'system-settings' && category !== 'slides' && category !== 'templates' && category !== 'playlists' && category !== 'screens' && category !== 'announcements' && category !== 'canvas-sizes' && category !== 'api-sources' && category !== 'rss-feeds' && category !== 'timetables'; }).map(function (category) {
|
||||
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
||||
})
|
||||
,auditAdministrationCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'users' || category === 'roles' || category === 'system-settings'; }).map(function (category) {
|
||||
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
||||
})
|
||||
,auditContentCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'slides' || category === 'templates' || category === 'playlists' || category === 'screens' || category === 'announcements' || category === 'canvas-sizes'; }).map(function (category) {
|
||||
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
||||
})
|
||||
,auditDataSourceCategories: AUDIT_CATEGORY_KEYS.filter(function (category) { return category === 'api-sources' || category === 'rss-feeds' || category === 'timetables'; }).map(function (category) {
|
||||
return { key: category, label: AUDIT_CATEGORY_LABELS[category] || category, isSelected: settings['audit.categories'].includes(category) };
|
||||
})
|
||||
}, diagnostics));
|
||||
}
|
||||
|
||||
async function buildDiagnostics() {
|
||||
const diagnostics = {
|
||||
applicationVersion: appVersion,
|
||||
schemaVersion: 'Unknown',
|
||||
databaseStatus: 'Unavailable',
|
||||
activeSessionCount: 0,
|
||||
backgroundTaskCount: 0,
|
||||
criticalTableCounts: []
|
||||
};
|
||||
try {
|
||||
await pool.query('SELECT 1 AS health');
|
||||
diagnostics.databaseStatus = 'Connected';
|
||||
diagnostics.schemaVersion = await detectSchemaVersion(pool);
|
||||
const [sessionRows] = await pool.query('SELECT COUNT(*) AS count FROM a_sessions WHERE expires_at > NOW()');
|
||||
diagnostics.activeSessionCount = Number(sessionRows && sessionRows[0] && sessionRows[0].count) || 0;
|
||||
const [taskRows] = await pool.query('SELECT COUNT(*) AS count FROM o_background_tasks');
|
||||
diagnostics.backgroundTaskCount = Number(taskRows && taskRows[0] && taskRows[0].count) || 0;
|
||||
const criticalTables = [
|
||||
['Users', 'a_users'],
|
||||
['Roles', 'a_roles'],
|
||||
['Screens', 'd_screens'],
|
||||
['Playlists', 'c_playlists'],
|
||||
['Slides', 'c_slides'],
|
||||
['Templates', 'c_templates'],
|
||||
['Announcements', 'd_announcements'],
|
||||
['RSS feeds', 'i_rss_feeds'],
|
||||
['API sources', 'i_api_sources']
|
||||
];
|
||||
for (const [label, tableName] of criticalTables) {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT COUNT(*) AS count FROM ' + tableName);
|
||||
diagnostics.criticalTableCounts.push({ label: label, count: Number(rows && rows[0] && rows[0].count) || 0 });
|
||||
} catch (_error) {
|
||||
diagnostics.criticalTableCounts.push({ label: label, count: 'Unavailable' });
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Keep diagnostics page renderable while the database is unavailable.
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
app.get('/settings/system', requireSettingsRead, async function (req, res, next) {
|
||||
try {
|
||||
await renderSettings(req, res, req.query.message ? String(req.query.message) : '');
|
||||
} 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();
|
||||
if (section !== 'icons' && section !== 'media' && section !== 'security' && section !== 'defaults' && section !== 'audit') {
|
||||
const error = new Error('Unknown settings section.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
const previousSettings = await fetchAppSettings(pool);
|
||||
|
||||
if (section === 'defaults') {
|
||||
const defaultSlideDurationSeconds = Number(req.body && req.body.default_slide_duration_seconds);
|
||||
const defaultAnnouncementDurationValue = Number(req.body && req.body.default_announcement_duration_value);
|
||||
const defaultAnnouncementDurationUnit = String(req.body && req.body.default_announcement_duration_unit || 'seconds').trim().toLowerCase();
|
||||
const rssDefaultIntervalValue = Number(req.body && req.body.rss_default_interval_value);
|
||||
const rssDefaultIntervalUnit = String(req.body && req.body.rss_default_interval_unit || 'minutes').trim().toLowerCase();
|
||||
const apiDefaultIntervalValue = Number(req.body && req.body.api_default_interval_value);
|
||||
const apiDefaultIntervalUnit = String(req.body && req.body.api_default_interval_unit || 'minutes').trim().toLowerCase();
|
||||
const defaultFadeBetweenSlides = req.body && (req.body.default_fade_between_slides === '1' || req.body.default_fade_between_slides === 'true');
|
||||
const defaultSkipUnavailableRtmp = req.body && (req.body.default_skip_unavailable_rtmp === '1' || req.body.default_skip_unavailable_rtmp === 'true');
|
||||
const defaultAnnouncementIcon = String(req.body && req.body.default_announcement_icon || '').trim();
|
||||
if (!Number.isInteger(defaultSlideDurationSeconds) || defaultSlideDurationSeconds < 1 || defaultSlideDurationSeconds > 3600 || !Number.isInteger(defaultAnnouncementDurationValue) || defaultAnnouncementDurationValue < 1 || defaultAnnouncementDurationValue > 3600 || !['seconds', 'minutes'].includes(defaultAnnouncementDurationUnit) || !Number.isInteger(rssDefaultIntervalValue) || rssDefaultIntervalValue < 1 || rssDefaultIntervalValue > 86400 || !['seconds', 'minutes', 'hours'].includes(rssDefaultIntervalUnit) || !Number.isInteger(apiDefaultIntervalValue) || apiDefaultIntervalValue < 1 || apiDefaultIntervalValue > 86400 || !['seconds', 'minutes', 'hours'].includes(apiDefaultIntervalUnit) || !ANNOUNCEMENT_ICON_CATALOG.some(function (option) { return option.value === defaultAnnouncementIcon; })) {
|
||||
const error = new Error('Default values must be valid.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'player.default_slide_duration_seconds': defaultSlideDurationSeconds,
|
||||
'player.default_fade_between_slides': defaultFadeBetweenSlides,
|
||||
'player.skip_unavailable_rtmp': defaultSkipUnavailableRtmp,
|
||||
'announcements.default_icon': defaultAnnouncementIcon,
|
||||
'announcements.default_duration_value': defaultAnnouncementDurationValue,
|
||||
'announcements.default_duration_unit': defaultAnnouncementDurationUnit,
|
||||
'data-sources.rss_default_interval_value': rssDefaultIntervalValue,
|
||||
'data-sources.rss_default_interval_unit': rssDefaultIntervalUnit,
|
||||
'data-sources.api_default_interval_value': apiDefaultIntervalValue,
|
||||
'data-sources.api_default_interval_unit': apiDefaultIntervalUnit
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Defaults saved.') + '#application-defaults');
|
||||
}
|
||||
|
||||
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.');
|
||||
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
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Security settings saved.') + '#security-sessions');
|
||||
}
|
||||
|
||||
if (section === 'audit') {
|
||||
const auditEnabled = req.body && (req.body.audit_enabled === '1' || req.body.audit_enabled === 'true');
|
||||
const includeRequestMetadata = req.body && (req.body.include_request_metadata === '1' || req.body.include_request_metadata === 'true');
|
||||
const selectedCategories = req.body && (req.body['audit_categories[]'] !== undefined ? req.body['audit_categories[]'] : req.body.audit_categories);
|
||||
const auditCategories = Array.from(new Set((Array.isArray(selectedCategories) ? selectedCategories : selectedCategories ? [selectedCategories] : []).map(function (category) {
|
||||
return String(category || '').trim().toLowerCase();
|
||||
}).filter(function (category) {
|
||||
return AUDIT_CATEGORY_KEYS.includes(category);
|
||||
})));
|
||||
const retentionDays = Number(req.body && req.body.audit_retention_days);
|
||||
if (!auditCategories.length || !Number.isInteger(retentionDays) || retentionDays < 0 || retentionDays > 3650) {
|
||||
const error = new Error('Select at least one audit category and use a retention period between 0 and 3650 days.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'audit.enabled': auditEnabled,
|
||||
'audit.categories': auditCategories,
|
||||
'audit.include_request_metadata': includeRequestMetadata,
|
||||
'audit.retention_days': retentionDays
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Audit logging settings saved.') + '#audit-logging');
|
||||
}
|
||||
|
||||
if (section === 'media') {
|
||||
const imageMaxMb = Number(req.body && req.body.image_max_mb);
|
||||
const videoMaxMb = Number(req.body && req.body.video_max_mb);
|
||||
const wysiwygImageMaxMb = Number(req.body && req.body.wysiwyg_image_max_mb);
|
||||
const selectedMimeTypes = req.body && (req.body['allowed_mime_types[]'] !== undefined ? req.body['allowed_mime_types[]'] : req.body.allowed_mime_types);
|
||||
const allowedMimeTypes = Array.from(new Set((Array.isArray(selectedMimeTypes) ? selectedMimeTypes : selectedMimeTypes ? [selectedMimeTypes] : []).map(function (mimeType) {
|
||||
return String(mimeType || '').trim().toLowerCase();
|
||||
}).filter(function (mimeType) {
|
||||
return UPLOAD_MIME_TYPE_OPTIONS.some(function (option) { return option.value === mimeType; });
|
||||
})));
|
||||
const hasImageMimeType = allowedMimeTypes.some(function (mimeType) { return mimeType.indexOf('image/') === 0; });
|
||||
const hasVideoMimeType = allowedMimeTypes.some(function (mimeType) { return mimeType.indexOf('video/') === 0; });
|
||||
if (!Number.isInteger(imageMaxMb) || imageMaxMb < 1 || imageMaxMb > 1024 || !Number.isInteger(videoMaxMb) || videoMaxMb < 1 || videoMaxMb > 4096 || !Number.isFinite(wysiwygImageMaxMb) || wysiwygImageMaxMb < 0.1 || wysiwygImageMaxMb > imageMaxMb || !hasImageMimeType || !hasVideoMimeType) {
|
||||
const error = new Error('Media upload limits must be valid and at least one image and video type must remain enabled.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', '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', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'uploads.image_max_bytes': imageMaxMb * 1024 * 1024,
|
||||
'uploads.video_max_bytes': videoMaxMb * 1024 * 1024,
|
||||
'uploads.wysiwyg_image_max_bytes': Math.round(wysiwygImageMaxMb * 1024 * 1024),
|
||||
'uploads.allowed_mime_types': allowedMimeTypes
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Media settings saved.') + '#media-uploads');
|
||||
}
|
||||
|
||||
const selectedIcons = req.body && (req.body['suggested_icons[]'] !== undefined ? req.body['suggested_icons[]'] : req.body.suggested_icons);
|
||||
const catalogKeys = new Set(ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
||||
return option.value;
|
||||
}));
|
||||
const suggestedIcons = Array.from(new Set((Array.isArray(selectedIcons) ? selectedIcons : selectedIcons ? [selectedIcons] : []).map(function (iconKey) {
|
||||
return String(iconKey || '').trim().toLowerCase();
|
||||
}).filter(function (iconKey) {
|
||||
return catalogKeys.has(iconKey);
|
||||
})));
|
||||
if (suggestedIcons.length > MAX_SUGGESTED_ICONS) {
|
||||
const error = new Error('Choose no more than ' + MAX_SUGGESTED_ICONS + ' suggested icons.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', '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', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'announcements.suggested_icons': suggestedIcons
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
res.redirect('/settings/system?message=' + encodeURIComponent('Settings saved.'));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,6 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const { buildUsersEditViewModel } = require('./form-view-model');
|
||||
|
||||
module.exports = function renderUsersEditPage(user, message, currentUser, roles) {
|
||||
return renderView('settings/users/form', buildUsersEditViewModel(user, message, currentUser, roles));
|
||||
module.exports = function renderUsersEditPage(user, message, currentUser, roles, sessions) {
|
||||
return renderView('settings/users/form', buildUsersEditViewModel(user, message, currentUser, roles, sessions));
|
||||
};
|
||||
@@ -24,9 +24,9 @@ function buildUsersAddViewModel(message, currentUser, roles, formValues, message
|
||||
active: 'users',
|
||||
isEdit: false,
|
||||
formId: 'user-form',
|
||||
formAction: '/users',
|
||||
footerSaveUrl: '/users',
|
||||
footerCancelUrl: '/users',
|
||||
formAction: '/settings/users',
|
||||
footerSaveUrl: '/settings/users',
|
||||
footerCancelUrl: '/settings/users',
|
||||
footerDeleteUrl: '',
|
||||
footerDeleteDisabled: true,
|
||||
footerDeleteTitle: 'Delete is available after the user has been created.',
|
||||
@@ -36,21 +36,21 @@ function buildUsersAddViewModel(message, currentUser, roles, formValues, message
|
||||
currentUser: currentUser || null,
|
||||
roles: mapRolesForForm(roles, []),
|
||||
formValues: formValues || {},
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/users" data-async-save-new-redirect="response-url" data-async-save-new-url="/users/new"'
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/settings/users" data-async-save-new-redirect="response-url" data-async-save-new-url="/settings/users/new"'
|
||||
};
|
||||
}
|
||||
|
||||
function buildUsersEditViewModel(user, message, currentUser, roles) {
|
||||
function buildUsersEditViewModel(user, message, currentUser, roles, sessions) {
|
||||
return {
|
||||
title: 'Edit user',
|
||||
active: 'users',
|
||||
isEdit: true,
|
||||
formId: 'user-profile-form',
|
||||
formAction: '/users/' + user.id + '/username',
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/users"',
|
||||
footerSaveUrl: '/users/' + user.id + '/username',
|
||||
footerCancelUrl: '/users',
|
||||
footerDeleteUrl: '/users/' + user.id + '/delete',
|
||||
formAction: '/settings/users/' + user.id + '/username',
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/settings/users"',
|
||||
footerSaveUrl: '/settings/users/' + user.id + '/username',
|
||||
footerCancelUrl: '/settings/users',
|
||||
footerDeleteUrl: '/settings/users/' + user.id + '/delete',
|
||||
footerDeleteDisabled: Boolean(user && user.inUse),
|
||||
footerDeleteConfirmMessage: 'Delete this user?',
|
||||
footerDeleteTitle: user && user.inUse ? 'Delete is disabled while this user is the only account left.' : '',
|
||||
@@ -59,9 +59,10 @@ function buildUsersEditViewModel(user, message, currentUser, roles) {
|
||||
message: message,
|
||||
currentUser: currentUser || null,
|
||||
user: user,
|
||||
sessions: Array.isArray(sessions) ? sessions : [],
|
||||
inUse: Boolean(user && user.inUse),
|
||||
roles: mapRolesForForm(roles, user && user.roleIds),
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/users" data-async-save-new-url="/users/new"'
|
||||
formAttrs: 'data-async-save data-async-save-close-url="/settings/users" data-async-save-new-url="/settings/users/new"'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user