feat(permissions): add server attestation permission and update related UI

- Introduced a new permission for server attestation, allowing users to run and view server performance attestations.
- Updated localization files in English and Polish to include descriptions for the new permission.
- Modified authentication middleware and various UI components to integrate the server attestation feature, ensuring proper access control and visibility in the sidebar.
- Enhanced the server attestation page to handle rendering and error management effectively.
This commit is contained in:
UNITRONIX
2026-06-06 00:45:30 +02:00
parent 66f594e53f
commit bcd32eefe5
8 changed files with 57 additions and 31 deletions
+5 -4
View File
@@ -24,8 +24,9 @@ const (
PermUserDelete = "user.delete"
// Server configuration
PermServerConfig = "server.config" // read/write server_config
PermServerKeys = "server.keys" // manage API keys
PermServerConfig = "server.config" // read/write server_config
PermServerKeys = "server.keys" // manage API keys
PermServerAttestation = "server.attestation" // run/view server performance attestation
// Organization permissions
PermOrgCreate = "org.create"
@@ -61,7 +62,7 @@ var AllPermissions = []string{
PermDeviceView, PermDeviceConnect, PermDeviceEdit, PermDeviceDelete,
PermDeviceBan, PermDeviceChangeID,
PermUserView, PermUserCreate, PermUserEdit, PermUserDelete,
PermServerConfig, PermServerKeys,
PermServerConfig, PermServerKeys, PermServerAttestation,
PermOrgCreate, PermOrgEdit, PermOrgDelete, PermOrgManageUsers, PermOrgManageDevices,
PermAuditView, PermMetricsView, PermBlocklistEdit,
PermCDAPView, PermCDAPCommand, PermCDAPTerminal, PermCDAPFiles,
@@ -88,7 +89,7 @@ var DefaultRolePermissions = map[string]map[string]bool{
// Server Admin: infrastructure + monitoring, read-only user visibility.
// Cannot create/edit/delete users, cannot manage orgs.
RoleServerAdmin: buildPermMap([]string{
PermServerConfig, PermServerKeys,
PermServerConfig, PermServerKeys, PermServerAttestation,
PermBlocklistEdit,
PermUserView, // read-only
PermDeviceView, // read-only
+3 -1
View File
@@ -2130,6 +2130,7 @@
"perm_user_delete": "Delete users",
"perm_server_config": "Server configuration",
"perm_server_keys": "Manage API keys",
"perm_server_attestation": "Server performance attestation",
"perm_org_create": "Create organizations",
"perm_org_edit": "Edit organizations",
"perm_org_delete": "Delete organizations",
@@ -3546,6 +3547,7 @@
"starting": "Starting…",
"complete": "Benchmark complete",
"signal_peers": "Signal peers",
"relay_sessions": "Relay sessions"
"relay_sessions": "Relay sessions",
"page_error": "Failed to load the Server Attestation page"
}
}
+3 -1
View File
@@ -2131,6 +2131,7 @@
"perm_user_delete": "Usuwanie użytkowników",
"perm_server_config": "Konfiguracja serwera",
"perm_server_keys": "Zarządzanie kluczami API",
"perm_server_attestation": "Atest wydajności serwera",
"perm_org_create": "Tworzenie organizacji",
"perm_org_edit": "Edycja organizacji",
"perm_org_delete": "Usuwanie organizacji",
@@ -3547,6 +3548,7 @@
"starting": "Uruchamianie…",
"complete": "Benchmark zakończony",
"signal_peers": "Peery signal",
"relay_sessions": "Sesje relay"
"relay_sessions": "Sesje relay",
"page_error": "Nie udało się załadować strony atestu serwera"
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ const DEFAULT_ROLE_PERMISSIONS = {
// Server Admin: infrastructure + read-only users
server_admin: new Set([
'server.config', 'server.keys',
'server.config', 'server.keys', 'server.attestation',
'blocklist.edit',
'user.view',
'device.view',
+1 -1
View File
@@ -39,7 +39,7 @@
{
id: 'server',
icon: 'dns',
permissions: ['server.config', 'server.keys']
permissions: ['server.config', 'server.keys', 'server.attestation']
},
{
id: 'org',
+18 -10
View File
@@ -9,7 +9,7 @@ const router = express.Router();
const { requireAuth, requirePermission } = require('../middleware/auth');
const attestation = require('../services/serverAttestation');
const REQUIRED_PERMISSION = 'server.config';
const REQUIRED_PERMISSION = 'server.attestation';
const auth = [requireAuth, requirePermission(REQUIRED_PERMISSION)];
let runPromise = null;
@@ -17,15 +17,23 @@ let runPromise = null;
// ─── Page ─────────────────────────────────────────────────────────────────────
router.get('/server-attestation', ...auth, async (req, res) => {
const lastResult = await attestation.getLastResult();
res.render('server-attestation', {
title: req.t('server_attestation.title'),
pageStyles: ['server-attestation'],
pageScripts: ['server-attestation'],
currentPage: 'server-attestation',
breadcrumb: [{ label: req.t('server_attestation.title') }],
lastResult
});
try {
const lastResult = await attestation.getLastResult();
res.render('server-attestation', {
title: req.t('server_attestation.title'),
pageStyles: ['server-attestation'],
pageScripts: ['server-attestation'],
currentPage: 'server-attestation',
breadcrumb: [{ label: req.t('server_attestation.title') }],
lastResult
});
} catch (err) {
console.error('[ServerAttestation] page render failed:', err);
res.status(500).render('errors/500', {
title: 'Error',
message: req.t('server_attestation.page_error') || 'Failed to load Server Attestation page'
});
}
});
// ─── Authenticated API ────────────────────────────────────────────────────────
+3 -1
View File
@@ -48,6 +48,8 @@
data-category="server-mgmt" title="<%= _('nav.server_management') || 'Server Management' %>">
<span class="material-icons">dns</span>
</button>
<% } %>
<% if (hasPermission('server.attestation')) { %>
<button class="sidebar-rail-btn <%= currentPage === 'server-attestation' ? 'active' : '' %>"
data-category="server-attest" title="<%= _('nav.server_attestation') || 'Server Attestation' %>">
<span class="material-icons">verified</span>
@@ -280,7 +282,7 @@
<% } %>
<!-- Server Attestation -->
<% if (hasPermission('server.config')) { %>
<% if (hasPermission('server.attestation')) { %>
<div class="sidebar-flyout-panel" data-panel="server-attest">
<a href="/server-attestation" class="sidebar-link <%= currentPage === 'server-attestation' ? 'active' : '' %>">
<span class="material-icons">verified</span>
+23 -12
View File
@@ -1,3 +1,19 @@
<%
var saTier = (typeof lastResult !== 'undefined' && lastResult && lastResult.tier)
? String(lastResult.tier).toLowerCase() : '';
var saTierLabel = saTier
? (_('server_attestation.tier_' + saTier) || saTier.toUpperCase())
: (_('server_attestation.tier_none') || 'UNRATED');
var saTierClass = saTier ? ('bd-attest-tier-' + saTier) : 'bd-attest-tier-none';
var saMaxConn = (typeof lastResult !== 'undefined' && lastResult) ? lastResult.maxConnections : null;
var saHasResult = typeof lastResult !== 'undefined' && lastResult;
var saHeroBadge = ''
+ '<div class="bd-attest-badge bd-attest-badge--large ' + saTierClass + '" title="' + (_('server_attestation.badge_tooltip') || 'BetterDesk Server Attestation') + '">'
+ '<div class="bd-attest-brand">Better<span class="bd-attest-i">i</span>Desk</div>'
+ '<div class="bd-attest-tier">' + saTierLabel + '</div>'
+ (saMaxConn ? ('<div class="bd-attest-conn">' + saMaxConn + ' ' + (_('server_attestation.connections_short') || 'conn.') + '</div>') : '')
+ '</div>';
%>
<%- include('layouts/main', {
title: _('server_attestation.title'),
pageStyles: ['server-attestation'],
@@ -27,14 +43,9 @@
<div class="sa-layout">
<div class="sa-hero">
<%- include('partials/server-attestation-badge', {
tier: (typeof lastResult !== 'undefined' && lastResult && lastResult.tier) ? lastResult.tier : null,
size: 'large',
showConnections: true,
maxConnections: (typeof lastResult !== 'undefined' && lastResult) ? lastResult.maxConnections : null
}) %>
${saHeroBadge}
<div class="sa-hero-meta" id="sa-hero-meta">
${typeof lastResult !== 'undefined' && lastResult && lastResult.testedAt
${saHasResult && lastResult.testedAt
? '<span>' + _('server_attestation.last_test') + ': ' + new Date(lastResult.testedAt).toLocaleString() + '</span>'
: '<span class="sa-muted">' + _('server_attestation.no_test_yet') + '</span>'}
</div>
@@ -49,7 +60,7 @@
<span class="material-icons">stop</span>
${_('server_attestation.abort')}
</button>
<button type="button" class="btn btn-ghost" id="sa-download-btn" ${!(typeof lastResult !== 'undefined' && lastResult) ? 'disabled' : ''}>
<button type="button" class="btn btn-ghost" id="sa-download-btn" ${!saHasResult ? 'disabled' : ''}>
<span class="material-icons">download</span>
${_('server_attestation.download_report')}
</button>
@@ -66,7 +77,7 @@
<span class="material-icons">hub</span>
<h3>${_('server_attestation.max_connections')}</h3>
</div>
<div class="sa-stat-value" id="sa-max-conn">${(typeof lastResult !== 'undefined' && lastResult && lastResult.maxConnections) ? lastResult.maxConnections : '—'}</div>
<div class="sa-stat-value" id="sa-max-conn">${saHasResult && lastResult.maxConnections ? lastResult.maxConnections : '—'}</div>
<div class="sa-stat-detail" id="sa-conn-detail"></div>
</div>
@@ -77,9 +88,9 @@
</div>
<table class="sa-threshold-table">
<tbody>
<tr><td>CPU</td><td id="sa-th-cpu">${(typeof lastResult !== 'undefined' && lastResult && lastResult.finalMetrics) ? lastResult.finalMetrics.cpu + '%' : '—'}</td></tr>
<tr><td>RAM</td><td id="sa-th-mem">${(typeof lastResult !== 'undefined' && lastResult && lastResult.finalMetrics) ? lastResult.finalMetrics.mem + '%' : '—'}</td></tr>
<tr><td>${_('server_attestation.disk')}</td><td id="sa-th-disk">${(typeof lastResult !== 'undefined' && lastResult && lastResult.finalMetrics) ? lastResult.finalMetrics.disk + '%' : '—'}</td></tr>
<tr><td>CPU</td><td id="sa-th-cpu">${saHasResult && lastResult.finalMetrics ? lastResult.finalMetrics.cpu + '%' : '—'}</td></tr>
<tr><td>RAM</td><td id="sa-th-mem">${saHasResult && lastResult.finalMetrics ? lastResult.finalMetrics.mem + '%' : '—'}</td></tr>
<tr><td>${_('server_attestation.disk')}</td><td id="sa-th-disk">${saHasResult && lastResult.finalMetrics ? lastResult.finalMetrics.disk + '%' : '—'}</td></tr>
</tbody>
</table>
</div>