Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions public/css/aurora.css
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,11 @@ input[type=date]:focus, select:focus {
.tabs.edit-route-tabs{border-bottom:0; gap:3px; background:var(--surface-3); border:1px solid var(--line); border-radius:10px; padding:3px}
.tabs.edit-route-tabs .tab{padding:7px 12px; font-size:12.5px; border-radius:7px; border:0; margin:0}
.tabs.edit-route-tabs .tab.active{background:var(--surface); color:var(--text); border:0}
.edit-route-panel{display:none}
.edit-route-panel.active{display:block; animation:fade .2s ease}
/* The shared route-edit JS toggles panels via inline style.display (showing the
active one with display:'' → falls back to the stylesheet). So the base must
NOT be display:none, or no panel ever shows. Inactive panels carry inline
display:none from the template; the fade runs each time one is revealed. */
.edit-route-panel{animation:fade .2s ease}

.toggle{width:42px;height:24px;border-radius:999px;background:var(--surface-3);border:1px solid var(--line-2);position:relative;cursor:pointer;transition:.2s; flex:0 0 auto}
.toggle::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--muted);transition:.2s}
Expand Down
45 changes: 43 additions & 2 deletions templates/aurora/layout.njk
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@
csrfToken: '{{ csrfToken }}',
language: '{{ language }}',
baseUrl: '{{ baseUrl }}',
features: { gateway_lan_discovery_multi_subnet: {{ ('true' if license.features.gateway_lan_discovery_multi_subnet else 'false') | safe }} },
features: {
gateway_lan_discovery_multi_subnet: {{ ('true' if license.features.gateway_lan_discovery_multi_subnet else 'false') | safe }},
gateway_tcp_routing: {{ ('true' if license.features.gateway_tcp_routing else 'false') | safe }},
gateway_scan_egress: {{ ('true' if license.features.gateway_scan_egress else 'false') | safe }},
l4_routes: {{ license.features.l4_routes | default(0) }},
http_routes: {{ license.features.http_routes | default(0) }},
browser_sessions: {{ ('true' if license.hasFeature('browser_sessions') else 'false') | safe }}
},
t: {
'peers.no_peers': {{ t('peers.no_peers') | dump | safe }},
'peers.online': {{ t('peers.online') | dump | safe }},
Expand Down Expand Up @@ -152,6 +159,7 @@
'gateway_pools.select_none': {{ t('gateway_pools.select_none') | dump | safe }},
'rdp.host_hint.default': {{ t('rdp.host_hint.default') | dump | safe }},
'rdp.host_hint.gateway': {{ t('rdp.host_hint.gateway') | dump | safe }},
'rdp.badge.browser': {{ t('rdp.badge.browser') | dump | safe }},
'routes.domain': {{ t('routes.domain') | dump | safe }},
'routes.l4_sni_label': {{ t('routes.l4_sni_label') | dump | safe }},
'routes.l4_sni_required_hint': {{ t('routes.l4_sni_required_hint') | dump | safe }},
Expand Down Expand Up @@ -339,7 +347,40 @@
'autoupdate.setup_note': {{ t('autoupdate.setup_note') | dump | safe }},
'autoupdate.ago_seconds': {{ t('autoupdate.ago_seconds') | dump | safe }},
'autoupdate.ago_minutes': {{ t('autoupdate.ago_minutes') | dump | safe }},
'routes.suggested.unavailable': {{ t('routes.suggested.unavailable') | dump | safe }}
'routes.suggested.unavailable': {{ t('routes.suggested.unavailable') | dump | safe }},
'common.close': {{ t('common.close') | dump | safe }},
'common.loading': {{ t('common.loading') | dump | safe }},
'printer_preset.title': {{ t('printer_preset.title') | dump | safe }},
'printer_preset.gateway': {{ t('printer_preset.gateway') | dump | safe }},
'printer_preset.printer_ip': {{ t('printer_preset.printer_ip') | dump | safe }},
'printer_preset.name': {{ t('printer_preset.name') | dump | safe }},
'printer_preset.adopt': {{ t('printer_preset.adopt') | dump | safe }},
'printer_preset.adopt_unsupported': {{ t('printer_preset.adopt_unsupported') | dump | safe }},
'printer_preset.adopt_not_enabled': {{ t('printer_preset.adopt_not_enabled') | dump | safe }},
'printer_preset.adopt_scanning': {{ t('printer_preset.adopt_scanning') | dump | safe }},
'printer_preset.adopt_none': {{ t('printer_preset.adopt_none') | dump | safe }},
'printer_preset.adopt_failed': {{ t('printer_preset.adopt_failed') | dump | safe }},
'printer_preset.adopt_active_hint': {{ t('printer_preset.adopt_active_hint') | dump | safe }},
'printer_preset.adopt_enable': {{ t('printer_preset.adopt_enable') | dump | safe }},
'printer_preset.adopt_enable_failed': {{ t('printer_preset.adopt_enable_failed') | dump | safe }},
'printer_preset.port_9100': {{ t('printer_preset.port_9100') | dump | safe }},
'printer_preset.port_631': {{ t('printer_preset.port_631') | dump | safe }},
'printer_preset.ews': {{ t('printer_preset.ews') | dump | safe }},
'printer_preset.ews_domain': {{ t('printer_preset.ews_domain') | dump | safe }},
'printer_preset.scan': {{ t('printer_preset.scan') | dump | safe }},
'printer_preset.vip': {{ t('printer_preset.vip') | dump | safe }},
'printer_preset.target_existing': {{ t('printer_preset.target_existing') | dump | safe }},
'printer_preset.target_new': {{ t('printer_preset.target_new') | dump | safe }},
'printer_preset.nas_ip': {{ t('printer_preset.nas_ip') | dump | safe }},
'printer_preset.nas_gateway': {{ t('printer_preset.nas_gateway') | dump | safe }},
'printer_preset.create': {{ t('printer_preset.create') | dump | safe }},
'printer_preset.scan_locked': {{ t('printer_preset.scan_locked') | dump | safe }},
'printer_preset.ews_locked': {{ t('printer_preset.ews_locked') | dump | safe }},
'printer_preset.save_failed': {{ t('printer_preset.save_failed') | dump | safe }},
'printer_preset.step_print': {{ t('printer_preset.step_print') | dump | safe }},
'printer_preset.step_scan': {{ t('printer_preset.step_scan') | dump | safe }},
'printer_preset.step_review': {{ t('printer_preset.step_review') | dump | safe }},
'printer_preset.step_device': {{ t('printer_preset.step_device') | dump | safe }}
}
};
</script>
Expand Down
2 changes: 2 additions & 0 deletions templates/aurora/pages/logs.njk
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<div style="font-size:13px;color:var(--text-3);padding:20px 0;text-align:center">{{ t('common.loading') }}</div>
</div>
</div>
<div id="activity-pagination" style="margin-top:12px"></div>
</div>

{# ─── Access Log Panel ─── #}
Expand All @@ -67,6 +68,7 @@
<div style="font-size:13px;color:var(--text-3);padding:20px 0;text-align:center">{{ t('common.loading') }}</div>
</div>
</div>
<div id="access-pagination" style="margin-top:12px"></div>
</div>

{# ─── Connection History Panel ─── #}
Expand Down
272 changes: 272 additions & 0 deletions templates/aurora/pages/rdp-session.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="{{ language }}" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#0a0e14">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="{{ appName }}">
{# No-FOUC theme-mode pre-paint (matches aurora/layout.njk; nonce for CSP) #}
<script nonce="{{ cspNonce }}">(function(){try{var s=localStorage.getItem('gc-theme-mode');var t=s||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<title>{{ t('rdp.session.title') }} — {{ appName }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/pro.css?v={{ appVersion }}">
<link rel="stylesheet" href="/css/aurora.css?v={{ appVersion }}">
</head>
<body class="rdp-session-body">
<div class="rdp-session-page">

{# ─── Remote display surface ──────────────────────────────────────────── #}
<div id="rdp-session-display" class="rdp-session-display">

{# Reconnecting overlay (shown while player auto-retries) #}
<div id="rdp-reconnect-overlay" class="rdp-reconnect-veil" style="display:none">
<div class="rdp-reconnect-card">
<div class="rdp-big-spin"></div>
<div class="rdp-rc-title">{{ t('rdp.session.reconnecting') }}</div>
<div class="rdp-rc-sub" id="rdp-reconnect-sub"></div>
</div>
</div>

{# Error / limit overlay (shown on terminal error state) #}
<div id="rdp-error-overlay" class="rdp-error-veil" style="display:none">
<div class="rdp-error-modal">
<div class="rdp-em-bar"></div>
<div class="rdp-em-body">
<div class="rdp-em-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01"/>
</svg>
</div>
<h2>{{ t('rdp.session.status.error') }}</h2>
<span class="rdp-em-cause" id="rdp-error-cause"></span>
<p class="rdp-em-text" id="rdp-error-text"></p>
</div>
<div class="rdp-em-actions">
<button class="btn btn-ghost" id="rdp-back-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
{{ t('rdp.wizard.back') }}
</button>
<button class="btn btn-primary" id="rdp-retry-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
{{ t('common.reload') }}
</button>
</div>
</div>
</div>

</div>{# /rdp-session-display #}

{# ─── Slim toolbar ────────────────────────────────────────────────────── #}
<div class="rdp-session-toolbar" id="rdp-toolbar">

{# Status pill #}
<span class="rdp-pill rdp-pill--connecting" id="rdp-status-pill">
<span id="rdp-pill-indicator" class="rdp-dot"></span>
<span id="rdp-status-text">{{ t('rdp.session.status.connecting') }}</span>
</span>

<span class="rdp-spacer"></span>

{# Fullscreen #}
<button class="rdp-tbtn" id="rdp-btn-fullscreen" title="{{ t('rdp.session.fullscreen') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3M16 21h3a2 2 0 0 0 2-2v-3"/>
</svg>
{{ t('rdp.session.fullscreen') }}
</button>

{# Scale toggle (fit ↔ native) #}
<button class="rdp-tbtn" id="rdp-btn-scale" aria-pressed="true" title="{{ t('rdp.session.scale_fit') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
<span class="rdp-tbtn-label">{{ t('rdp.session.scale_fit') }}</span>
</button>

{# Ctrl+Alt+Del — RDP protocol only #}
{% if route.protocol == 'rdp' %}
<button class="rdp-tbtn" id="rdp-btn-cad" title="{{ t('rdp.session.ctrl_alt_del') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M7 12h10M6 16h12"/>
</svg>
{{ t('rdp.session.ctrl_alt_del') }}
</button>
{% endif %}

{# Paste from clipboard #}
<button class="rdp-tbtn" id="rdp-btn-paste" title="{{ t('rdp.session.paste') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
{{ t('rdp.session.paste') }}
</button>
{# Inline clipboard-unavailable indicator — shown by JS when clipboard permission is denied #}
<span id="rdp-clipboard-warn" class="rdp-clip-warn" style="display:none"></span>

{# Disconnect #}
<button class="rdp-tbtn rdp-tbtn--danger" id="rdp-btn-disconnect" title="{{ t('rdp.session.disconnect') }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0M12 2v10"/>
</svg>
{{ t('rdp.session.disconnect') }}
</button>

</div>{# /rdp-session-toolbar #}

</div>{# /rdp-session-page #}

{# Script load order: guacamole → player-logic → player → JSON islands → init #}
<script src="/vendor/guacamole-common-js/guacamole-common.min.js?v={{ appVersion }}"></script>
<script src="/js/rdp-player-logic.js?v={{ appVersion }}"></script>
<script src="/js/rdp-player.js?v={{ appVersion }}"></script>

{# JSON island 1: init config — no user free-text, only numeric id + enum protocol + CSRF + guac object #}
<script type="application/json" id="rdp-session-cfg" nonce="{{ cspNonce }}">{{ { routeId: route.id, protocol: route.protocol, csrf: csrfToken, guac: guac, resolutionMode: route.resolution_mode, resWidth: route.resolution_width, resHeight: route.resolution_height } | dump | safe }}</script>

{# JSON island 2: i18n strings needed for JS state updates #}
<script type="application/json" id="rdp-session-i18n" nonce="{{ cspNonce }}">{{ {
connecting: t('rdp.session.status.connecting'),
connected: t('rdp.session.status.connected'),
reconnecting: t('rdp.session.status.reconnecting'),
disconnected: t('rdp.session.status.disconnected'),
error: t('rdp.session.status.error'),
limit_reached: t('rdp.session.limit_reached'),
expired: t('rdp.session.expired'),
not_authorized: t('rdp.session.not_authorized'),
scale_fit: t('rdp.session.scale_fit'),
scale_native: t('rdp.session.scale_native'),
clipboard_unavailable: t('rdp.session.clipboard_unavailable')
} | dump | safe }}</script>

{# Init script — reads from JSON islands only; no server values interpolated into JS strings #}
<script nonce="{{ cspNonce }}">
(function () {
'use strict';
var cfg = JSON.parse(document.getElementById('rdp-session-cfg').textContent);
var i18n = JSON.parse(document.getElementById('rdp-session-i18n').textContent);

/* Expose guac config for player retry-window logic */
window.GC = window.GC || {};
window.GC.guac = cfg.guac;

var statusPill = document.getElementById('rdp-status-pill');
var pillIndicator = document.getElementById('rdp-pill-indicator');
var statusText = document.getElementById('rdp-status-text');
var reconnectOvl = document.getElementById('rdp-reconnect-overlay');
var errorOvl = document.getElementById('rdp-error-overlay');
var errorCauseEl = document.getElementById('rdp-error-cause');
var errorTextEl = document.getElementById('rdp-error-text');
var scaleBtn = document.getElementById('rdp-btn-scale');
var scaleFit = true;

var PILL_CLS = {
connecting: 'rdp-pill--connecting',
connected: 'rdp-pill--connected',
reconnecting: 'rdp-pill--reconnect',
disconnected: 'rdp-pill--disconnect',
error: 'rdp-pill--error'
};

function setPillState(s) {
statusPill.className = 'rdp-pill ' + (PILL_CLS[s] || PILL_CLS.disconnected);
statusText.textContent = i18n[s] || s;
pillIndicator.className = s === 'reconnecting' ? 'rdp-spin' : 'rdp-dot';
}

var player = GCRdpPlayer.create({
container: document.getElementById('rdp-session-display'),
wsBase: (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host,
display: { mode: cfg.resolutionMode, fixedWidth: cfg.resWidth, fixedHeight: cfg.resHeight },
onState: function (s, detail) {
setPillState(s);
/* Clipboard unavailable — show persistent inline toolbar indicator (no blocking alert) */
if (detail && detail.clipboardUnavailable) {
var clipWarn = document.getElementById('rdp-clipboard-warn');
if (clipWarn) { clipWarn.textContent = i18n.clipboard_unavailable; clipWarn.style.display = ''; }
return;
}
reconnectOvl.style.display = s === 'reconnecting' ? 'flex' : 'none';
if (s === 'error') {
errorOvl.style.display = 'flex';
var dStatus = detail && detail.status;
var dReason = detail && detail.reason;
var causeKey = (dStatus === 429 || dReason === 'limit-reached') ? 'limit_reached'
: dStatus === 403 ? 'not_authorized'
: '';
errorCauseEl.textContent = (causeKey && i18n[causeKey]) || i18n.error;
errorTextEl.textContent = (detail && detail.message) || (causeKey && i18n[causeKey]) || i18n.error;
} else {
errorOvl.style.display = 'none';
}
},
mint: function () {
return fetch('/api/v1/rdp/' + cfg.routeId + '/browser-session', {
method: 'POST',
headers: { 'X-CSRF-Token': cfg.csrf },
credentials: 'same-origin'
}).then(function (r) {
return r.json().then(function (b) {
if (!r.ok || !b.ok) {
var e = new Error(b.error || 'mint failed');
e.status = r.status;
throw e;
}
return { token: b.token, wsPath: b.wsPath, ttl: b.ttl, protocol: cfg.protocol };
});
});
}
});

player.connect();
window.addEventListener('beforeunload', function () { player.disconnect(); });

/* ── Toolbar wiring ────────────────────────────────────────── */

/* Fullscreen */
document.getElementById('rdp-btn-fullscreen').addEventListener('click', function () {
player.requestFullscreen();
});

/* Scale fit ↔ native toggle */
scaleBtn.addEventListener('click', function () {
scaleFit = !scaleFit;
player.setScale(scaleFit ? 'fit' : 'native');
scaleBtn.setAttribute('aria-pressed', scaleFit ? 'true' : 'false');
scaleBtn.querySelector('.rdp-tbtn-label').textContent = scaleFit ? i18n.scale_fit : i18n.scale_native;
});

/* Ctrl+Alt+Del (RDP only — element may be absent for ssh/vnc/telnet) */
var cadBtn = document.getElementById('rdp-btn-cad');
if (cadBtn) {
cadBtn.addEventListener('click', function () { player.sendCtrlAltDel(); });
}

/* Paste (player handles clipboard read + CSP/permissions internally) */
document.getElementById('rdp-btn-paste').addEventListener('click', function () {
player.paste();
});

/* Disconnect */
document.getElementById('rdp-btn-disconnect').addEventListener('click', function () {
player.disconnect();
});

/* Back to RDP list */
var backBtn = document.getElementById('rdp-back-btn');
if (backBtn) backBtn.addEventListener('click', function () { location.href = '/rdp'; });

/* Retry after error */
document.getElementById('rdp-retry-btn').addEventListener('click', function () {
errorOvl.style.display = 'none';
player.connect();
});
})();
</script>
</body>
</html>
Loading
Loading