mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 17:47:13 +00:00
fb0f965bb2
Logout link moved inside the float-right user-menu-wrapper so both Logout and Settings appear on the far right of the nav bar. Fixed addressbook.html Settings font size to match other pages (1.3em).
832 lines
42 KiB
HTML
832 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>rustguac - Sessions</title>
|
|
<style>
|
|
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --type-vdi-bg: #0e2a2a; --type-vdi-fg: #2dd4bf; --hop-bg: #1b4332; --hop-fg: #52b788; --bg-pattern: none; }
|
|
body { font-family: monospace; background-color: var(--bg); background-image: var(--bg-pattern); background-attachment: fixed; min-height: 100vh; color: var(--text); padding: 2em; font-size: 18px; }
|
|
h1 { color: var(--primary); }
|
|
h2 { color: var(--primary); font-size: 1em; margin-top: 2em; }
|
|
a { color: var(--accent); }
|
|
code { background: var(--surface); padding: 0.2em 0.5em; border-radius: 3px; }
|
|
nav { margin-top: 0.8em; margin-bottom: 1.5em; padding-bottom: 0.8em; border-bottom: 1px solid var(--border); font-size: 0.95em; }
|
|
nav a { margin-right: 1.5em; text-decoration: none; }
|
|
nav a:hover { text-decoration: underline; }
|
|
nav .active { color: var(--primary); font-weight: bold; }
|
|
#user-menu-wrapper { position: relative; float: right; }
|
|
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
|
|
#user-menu-btn:hover { color: var(--text); }
|
|
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
|
|
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
|
|
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
|
|
.um-item:hover { background:var(--input); }
|
|
.um-item.active { color:var(--accent); }
|
|
.um-theme-info { display:flex; flex-direction:column; }
|
|
.um-theme-name { font-weight:bold; }
|
|
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
|
|
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
|
|
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
|
|
.um-logout { color:var(--text-muted); }
|
|
.um-logout:hover { color:var(--primary); background:var(--input); }
|
|
|
|
form {
|
|
background: var(--surface);
|
|
padding: 1.5em;
|
|
border-radius: 6px;
|
|
max-width: 400px;
|
|
margin: 1.5em 0;
|
|
}
|
|
label { display: block; margin-top: 0.8em; color: var(--text-muted); font-size: 0.9em; }
|
|
input {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.4em;
|
|
margin-top: 0.2em;
|
|
background: var(--input);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-family: monospace;
|
|
font-size: 1em;
|
|
border-radius: 3px;
|
|
box-sizing: border-box;
|
|
}
|
|
input:focus { outline: none; border-color: var(--primary); }
|
|
button {
|
|
margin-top: 1.2em;
|
|
padding: 0.5em 1.5em;
|
|
background: var(--primary);
|
|
color: var(--text-on-primary);
|
|
border: none;
|
|
font-family: monospace;
|
|
font-size: 1em;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
button:hover { background: var(--primary-hover); }
|
|
button:disabled { background: var(--btn-disabled); cursor: default; }
|
|
#error { color: var(--primary); margin-top: 0.8em; white-space: pre-wrap; }
|
|
#sessions { margin-top: 2em; }
|
|
#sessions table { border-collapse: collapse; width: 100%; }
|
|
#sessions th, #sessions td {
|
|
text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border);
|
|
}
|
|
#sessions th { color: var(--text-muted); font-size: 0.85em; }
|
|
#sessions a { text-decoration: none; font-size: 0.9em; }
|
|
#sessions a:hover { text-decoration: underline; }
|
|
.status-pending { color: var(--status-pending); }
|
|
.status-active { color: var(--status-active); }
|
|
.status-completed { color: var(--status-completed); }
|
|
.status-error { color: var(--status-error); }
|
|
.status-expired { color: var(--status-expired); }
|
|
.btn-small {
|
|
background: none; border: none; color: var(--primary);
|
|
cursor: pointer; font-family: monospace; padding: 0; margin: 0; font-size: 0.9em;
|
|
}
|
|
.btn-small:hover { text-decoration: underline; }
|
|
.btn-share { color: var(--accent); }
|
|
|
|
/* Hop cards */
|
|
.hop-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
margin-bottom: 0.6em;
|
|
}
|
|
.hop-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5em;
|
|
padding: 0.4em 0.6em;
|
|
cursor: pointer;
|
|
font-size: 0.9em;
|
|
}
|
|
.hop-card-header:hover { background: var(--input); }
|
|
.hop-card-header .hop-num {
|
|
color: var(--accent);
|
|
font-weight: bold;
|
|
min-width: 2em;
|
|
}
|
|
.hop-card-header .hop-hostname {
|
|
flex: 1;
|
|
color: var(--text);
|
|
}
|
|
.hop-card-header .hop-hostname input {
|
|
display: inline;
|
|
width: 100%;
|
|
padding: 0.2em 0.4em;
|
|
background: var(--input);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-family: monospace;
|
|
font-size: 1em;
|
|
border-radius: 3px;
|
|
box-sizing: border-box;
|
|
}
|
|
.hop-card-header .hop-remove {
|
|
color: var(--primary);
|
|
cursor: pointer;
|
|
font-size: 0.85em;
|
|
background: none;
|
|
border: none;
|
|
padding: 0.2em 0.4em;
|
|
font-family: monospace;
|
|
}
|
|
.hop-card-header .hop-remove:hover { text-decoration: underline; }
|
|
.hop-card-body {
|
|
padding: 0.4em 0.6em 0.6em 2.6em;
|
|
display: none;
|
|
}
|
|
.hop-card-body.expanded { display: block; }
|
|
.hop-card-body label { margin-top: 0.5em; }
|
|
.hop-card-body input, .hop-card-body textarea {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.3em;
|
|
margin-top: 0.2em;
|
|
background: var(--input);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-family: monospace;
|
|
font-size: 0.95em;
|
|
border-radius: 3px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Flow diagram */
|
|
.flow-diagram {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 0.8em;
|
|
font-size: 0.85em;
|
|
}
|
|
.flow-node {
|
|
display: inline-block;
|
|
padding: 0.2em 0.6em;
|
|
border-radius: 3px;
|
|
white-space: nowrap;
|
|
}
|
|
.flow-node-you { background: var(--input); color: var(--text-muted); }
|
|
.flow-node-hop { background: var(--hop-bg); color: var(--hop-fg); }
|
|
.flow-node-target { background: var(--input); color: var(--accent); font-weight: bold; }
|
|
.flow-arrow { color: var(--text-muted); margin: 0 0.3em; }
|
|
|
|
.btn-add-hop {
|
|
background: var(--input);
|
|
color: var(--accent);
|
|
border: 1px solid var(--border);
|
|
font-size: 0.85em;
|
|
padding: 0.3em 0.8em;
|
|
margin-top: 0;
|
|
}
|
|
.btn-add-hop:hover { background: var(--surface); }
|
|
|
|
.share-url {
|
|
background: var(--input);
|
|
padding: 0.3em 0.6em;
|
|
border-radius: 3px;
|
|
font-size: 0.85em;
|
|
word-break: break-all;
|
|
display: none;
|
|
margin-top: 0.3em;
|
|
}
|
|
</style>
|
|
<script>(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k]);if(o.bg_pattern&&o.bg_pattern!=='none'){var s=document.createElement('style');s.id='bg-pattern-style';s.textContent='body{background-image:'+o.bg_pattern+';background-attachment:fixed}';document.head.appendChild(s)}}catch(e){}}})();</script>
|
|
</head>
|
|
<body>
|
|
<img id="site-logo" src="/logo.svg" style="max-height:40px;vertical-align:middle;margin-right:0.5em" alt=""><h1 style="display:inline;vertical-align:middle">rustguac</h1>
|
|
<nav>
|
|
<a href="/addressbook.html">Address Book</a>
|
|
<a href="/sessions.html" class="active" id="sessions-link">Sessions</a>
|
|
<a href="/recordings.html">Recordings</a>
|
|
<a href="/reports.html" id="reports-link" style="display:none">Reports</a>
|
|
<a href="/docs.html">Docs</a>
|
|
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
|
|
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
|
|
<span id="user-menu-wrapper"><a href="#" id="logout-item" style="color:var(--text-muted);text-decoration:none;margin-right:1em">Logout</a><span id="user-menu-btn" title="Preferences">⚙ Settings</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div></div></span>
|
|
</nav>
|
|
|
|
<div id="adhoc-notice" style="display:none;background:var(--surface);padding:1em 1.5em;border-radius:6px;max-width:400px;margin:1.5em 0;color:var(--text-muted);">
|
|
Use the <a href="/addressbook.html">Address Book</a> to connect to saved entries.
|
|
</div>
|
|
|
|
<form id="session-form" style="display:none">
|
|
<div style="display:flex;align-items:center;gap:0.5em;cursor:pointer" id="new-session-toggle">
|
|
<span id="new-session-arrow" style="color:var(--accent)">▶</span>
|
|
<strong style="color:var(--accent)">New Ad-Hoc Session</strong>
|
|
</div>
|
|
<div id="new-session-fields" style="display:none;margin-top:0.8em">
|
|
<label>Type
|
|
<select id="session-type" style="display:block;width:100%;padding:0.4em;margin-top:0.2em;background:var(--input);border:1px solid var(--border);color:var(--text);font-family:monospace;font-size:1em;border-radius:3px;">
|
|
<option value="ssh" selected>SSH</option>
|
|
<option value="rdp">RDP</option>
|
|
<option value="vnc">VNC</option>
|
|
<option value="web">Web</option>
|
|
<option value="vdi">VDI (Docker)</option>
|
|
</select>
|
|
</label>
|
|
<div id="ssh-fields">
|
|
<label>Host
|
|
<input type="text" id="hostname" placeholder="192.168.1.1">
|
|
</label>
|
|
<label>Port
|
|
<input type="number" id="port" value="22">
|
|
</label>
|
|
<label>Username (optional — SSH will prompt if empty)
|
|
<input type="text" id="username">
|
|
</label>
|
|
<label>Password (optional — SSH will prompt if empty)
|
|
<input type="password" id="password">
|
|
</label>
|
|
<label style="margin-top:0.8em">
|
|
<input type="checkbox" id="generate-keypair" style="display:inline;width:auto;margin-right:0.4em"> Generate ephemeral SSH keypair
|
|
</label>
|
|
<div id="private-key-field">
|
|
<label>Private key (optional — paste OpenSSH PEM)
|
|
<textarea id="private-key" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" style="display:block;width:100%;padding:0.4em;margin-top:0.2em;background:var(--input);border:1px solid var(--border);color:var(--text);font-family:monospace;font-size:0.85em;border-radius:3px;box-sizing:border-box;resize:vertical;"></textarea>
|
|
</label>
|
|
<div style="color:var(--primary);font-size:0.8em;margin-top:0.3em;">Warning: Do not paste important production keys here. Use ephemeral or test keys only.</div>
|
|
</div>
|
|
</div>
|
|
<div id="rdp-fields" style="display:none">
|
|
<label>Host
|
|
<input type="text" id="rdp-hostname" placeholder="192.168.1.1">
|
|
</label>
|
|
<label>Port
|
|
<input type="number" id="rdp-port" value="3389">
|
|
</label>
|
|
<label>Username
|
|
<input type="text" id="rdp-username">
|
|
</label>
|
|
<label>Password
|
|
<input type="password" id="rdp-password">
|
|
</label>
|
|
<label>Domain (optional)
|
|
<input type="text" id="rdp-domain">
|
|
</label>
|
|
<label>Security
|
|
<select id="rdp-security" style="display:block;width:100%;padding:0.4em;margin-top:0.2em;background:var(--input);border:1px solid var(--border);color:var(--text);font-family:monospace;font-size:1em;border-radius:3px;">
|
|
<option value="" selected>Default</option>
|
|
<option value="tls">TLS</option>
|
|
<option value="nla">NLA</option>
|
|
<option value="rdp">RDP</option>
|
|
</select>
|
|
</label>
|
|
<label style="margin-top:0.8em">
|
|
<input type="checkbox" id="rdp-ignore-cert" style="display:inline;width:auto;margin-right:0.4em"> Ignore certificate errors
|
|
</label>
|
|
</div>
|
|
<div id="vnc-fields" style="display:none">
|
|
<label>Host
|
|
<input type="text" id="vnc-hostname" placeholder="192.168.1.1">
|
|
</label>
|
|
<label>Port
|
|
<input type="number" id="vnc-port" value="5900">
|
|
</label>
|
|
<label>Password (optional)
|
|
<input type="password" id="vnc-password">
|
|
</label>
|
|
</div>
|
|
<div id="web-fields" style="display:none">
|
|
<label>URL
|
|
<input type="text" id="url" placeholder="https://example.com">
|
|
</label>
|
|
</div>
|
|
<div id="vdi-fields" style="display:none">
|
|
<label>Container Image
|
|
<input type="text" id="vdi-image" placeholder="e.g. myregistry/xrdp-desktop:latest">
|
|
</label>
|
|
<div style="color:var(--text-muted);font-size:0.8em;margin-top:0.3em">Docker image with xrdp on port 3389. Container is ephemeral.</div>
|
|
</div>
|
|
<div id="jump-section" style="display:none;margin-top:1em;padding-top:0.8em;border-top:1px solid var(--border)">
|
|
<label style="cursor:pointer;color:var(--accent);font-size:0.95em" id="jump-toggle">
|
|
<span id="jump-arrow">▶</span> SSH Tunnel / Jump Hosts
|
|
</label>
|
|
<div id="jump-fields" style="display:none">
|
|
<div style="color:#555;font-size:0.8em;margin-bottom:0.5em">Route through one or more SSH bastion hosts (multi-hop).</div>
|
|
<div id="flow-diagram" class="flow-diagram"></div>
|
|
<div id="hops-list"></div>
|
|
<button type="button" class="btn-add-hop" id="add-hop-btn">+ Add Jump Host</button>
|
|
</div>
|
|
</div>
|
|
<label>Banner message (shown to user before session starts)
|
|
<input type="text" id="banner" placeholder="e.g. Please enter your credentials when prompted">
|
|
</label>
|
|
<button type="submit" id="connect-btn">Connect</button>
|
|
<div id="error"></div>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="sessions">
|
|
<strong>Sessions</strong>
|
|
<table>
|
|
<thead><tr>
|
|
<th>ID</th><th>Type</th><th>Host</th><th>User</th><th>Owner</th><th>Status</th><th>Viewers</th><th></th><th></th><th></th>
|
|
</tr></thead>
|
|
<tbody id="session-list"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);var s=document.getElementById('bg-pattern-style');if(!s){s=document.createElement('style');s.id='bg-pattern-style';document.head.appendChild(s)}s.textContent=colors.bg_pattern&&colors.bg_pattern!=='none'?'body{background-image:'+colors.bg_pattern+';background-attachment:fixed}':'';localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
|
|
var _themePresets={},_adminPreset='dark';
|
|
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',aurora:'Midnight blue with ambient glow',jaguar:'Racing green & gold'};
|
|
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){if(l.src!==t.logo_url&&!l.src.endsWith(t.logo_url))l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
|
|
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
|
|
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
|
|
if(d.site_title){document.title=d.site_title+' - Sessions';document.querySelector('h1').textContent=d.site_title;}
|
|
initTheme(d.theme);
|
|
});
|
|
var apiKey = sessionStorage.getItem('rustguac_api_key');
|
|
|
|
// Check auth and role
|
|
var roleLevel = { admin: 4, poweruser: 3, operator: 2, viewer: 1 };
|
|
function checkRole() {
|
|
fetch('/api/me', { headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function(res) {
|
|
if (res.status === 401 || res.status === 403) { window.location.href = '/'; return; }
|
|
if (!res.ok) return;
|
|
return res.json();
|
|
})
|
|
.then(function(data) {
|
|
if (!data) return;
|
|
var level = roleLevel[data.role] || 0;
|
|
if (level < 3) {
|
|
window.location.href = '/addressbook.html';
|
|
return;
|
|
}
|
|
if (data.role === 'admin') {
|
|
document.getElementById('admin-link').style.display = '';
|
|
}
|
|
if (level >= 2) {
|
|
document.getElementById('tokens-link').style.display = '';
|
|
}
|
|
if (level >= 3) {
|
|
// poweruser+ can create ad-hoc sessions
|
|
document.getElementById('session-form').style.display = '';
|
|
document.getElementById('reports-link').style.display = '';
|
|
}
|
|
})
|
|
.catch(function() {});
|
|
}
|
|
if (apiKey) {
|
|
// API key users are always admin
|
|
document.getElementById('session-form').style.display = '';
|
|
document.getElementById('admin-link').style.display = '';
|
|
document.getElementById('tokens-link').style.display = '';
|
|
document.getElementById('reports-link').style.display = '';
|
|
} else {
|
|
checkRole();
|
|
}
|
|
|
|
document.getElementById('logout-item').addEventListener('click', function() {
|
|
sessionStorage.removeItem('rustguac_api_key');
|
|
// Also clear server session cookie via logout endpoint
|
|
fetch('/auth/logout', { credentials: 'same-origin' })
|
|
.finally(function() { window.location.href = '/'; });
|
|
});
|
|
|
|
function apiHeaders(extra) {
|
|
var h = {};
|
|
if (apiKey) h['Authorization'] = 'Bearer ' + apiKey;
|
|
if (extra) { for (var k in extra) h[k] = extra[k]; }
|
|
return h;
|
|
}
|
|
|
|
var form = document.getElementById('session-form');
|
|
var errorEl = document.getElementById('error');
|
|
var btn = document.getElementById('connect-btn');
|
|
var sessionType = document.getElementById('session-type');
|
|
var sshFields = document.getElementById('ssh-fields');
|
|
var rdpFields = document.getElementById('rdp-fields');
|
|
var vncFields = document.getElementById('vnc-fields');
|
|
var webFields = document.getElementById('web-fields');
|
|
var jumpSection = document.getElementById('jump-section');
|
|
|
|
var generateKeypair = document.getElementById('generate-keypair');
|
|
var privateKeyField = document.getElementById('private-key-field');
|
|
var passwordField = document.getElementById('password');
|
|
|
|
generateKeypair.addEventListener('change', function() {
|
|
if (generateKeypair.checked) {
|
|
privateKeyField.style.display = 'none';
|
|
passwordField.disabled = true;
|
|
passwordField.value = '';
|
|
} else {
|
|
privateKeyField.style.display = '';
|
|
passwordField.disabled = false;
|
|
}
|
|
});
|
|
|
|
var vdiFields = document.getElementById('vdi-fields');
|
|
|
|
sessionType.addEventListener('change', function() {
|
|
sshFields.style.display = 'none';
|
|
rdpFields.style.display = 'none';
|
|
vncFields.style.display = 'none';
|
|
webFields.style.display = 'none';
|
|
vdiFields.style.display = 'none';
|
|
var v = sessionType.value;
|
|
if (v === 'ssh') sshFields.style.display = '';
|
|
else if (v === 'rdp') rdpFields.style.display = '';
|
|
else if (v === 'vnc') vncFields.style.display = '';
|
|
else if (v === 'web') webFields.style.display = '';
|
|
else if (v === 'vdi') vdiFields.style.display = '';
|
|
// Hide jump hosts for VDI (local containers)
|
|
jumpSection.style.display = v === 'vdi' ? 'none' : '';
|
|
});
|
|
|
|
// Collapsible "New Ad-Hoc Session" section
|
|
document.getElementById('new-session-toggle').addEventListener('click', function() {
|
|
var fields = document.getElementById('new-session-fields');
|
|
var arrow = document.getElementById('new-session-arrow');
|
|
if (fields.style.display === 'none') {
|
|
fields.style.display = '';
|
|
arrow.innerHTML = '▼';
|
|
} else {
|
|
fields.style.display = 'none';
|
|
arrow.innerHTML = '▶';
|
|
}
|
|
});
|
|
|
|
// Show jump section on initial load (default is SSH)
|
|
jumpSection.style.display = '';
|
|
|
|
document.getElementById('jump-toggle').addEventListener('click', function() {
|
|
var fields = document.getElementById('jump-fields');
|
|
var arrow = document.getElementById('jump-arrow');
|
|
if (fields.style.display === 'none') {
|
|
fields.style.display = '';
|
|
arrow.innerHTML = '▼';
|
|
} else {
|
|
fields.style.display = 'none';
|
|
arrow.innerHTML = '▶';
|
|
}
|
|
});
|
|
|
|
// ── Escape helpers ──
|
|
function escapeHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
function escapeAttr(s) {
|
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
// ── Multi-hop management ──
|
|
var hopList = []; // Array of {hostname, port, username, password, private_key, expanded}
|
|
|
|
function renderHops() {
|
|
var container = document.getElementById('hops-list');
|
|
container.innerHTML = '';
|
|
hopList.forEach(function(hop, i) {
|
|
var card = document.createElement('div');
|
|
card.className = 'hop-card';
|
|
|
|
// Header
|
|
var header = document.createElement('div');
|
|
header.className = 'hop-card-header';
|
|
|
|
var num = document.createElement('span');
|
|
num.className = 'hop-num';
|
|
num.textContent = '#' + (i + 1);
|
|
header.appendChild(num);
|
|
|
|
var hostWrap = document.createElement('span');
|
|
hostWrap.className = 'hop-hostname';
|
|
var hostInput = document.createElement('input');
|
|
hostInput.type = 'text';
|
|
hostInput.placeholder = 'bastion.example.com';
|
|
hostInput.value = hop.hostname || '';
|
|
hostInput.setAttribute('data-hop', i);
|
|
hostInput.setAttribute('data-field', 'hostname');
|
|
hostInput.addEventListener('input', onHopFieldChange);
|
|
hostInput.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
hostWrap.appendChild(hostInput);
|
|
header.appendChild(hostWrap);
|
|
|
|
var toggle = document.createElement('button');
|
|
toggle.type = 'button';
|
|
toggle.className = 'hop-remove';
|
|
toggle.textContent = hop.expanded ? 'collapse' : 'expand';
|
|
toggle.setAttribute('data-hop', i);
|
|
toggle.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
hopList[i].expanded = !hopList[i].expanded;
|
|
renderHops();
|
|
});
|
|
header.appendChild(toggle);
|
|
|
|
var removeBtn = document.createElement('button');
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'hop-remove';
|
|
removeBtn.textContent = 'remove';
|
|
removeBtn.setAttribute('data-hop', i);
|
|
removeBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
hopList.splice(i, 1);
|
|
renderHops();
|
|
updateFlowDiagram();
|
|
});
|
|
header.appendChild(removeBtn);
|
|
|
|
card.appendChild(header);
|
|
|
|
// Body (collapsible)
|
|
var body = document.createElement('div');
|
|
body.className = 'hop-card-body' + (hop.expanded ? ' expanded' : '');
|
|
|
|
body.innerHTML =
|
|
'<label>SSH Port<input type="number" data-hop="' + i + '" data-field="port" value="' + (hop.port || 22) + '"></label>' +
|
|
'<label>Username<input type="text" data-hop="' + i + '" data-field="username" value="' + escapeAttr(hop.username || '') + '"></label>' +
|
|
'<label>Password<input type="password" data-hop="' + i + '" data-field="password" value="' + escapeAttr(hop.password || '') + '"></label>' +
|
|
'<label>Private key (PEM)<textarea data-hop="' + i + '" data-field="private_key" rows="2" style="resize:vertical;font-size:0.85em" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----">' + escapeHtml(hop.private_key || '') + '</textarea></label>';
|
|
|
|
var inputs = body.querySelectorAll('input, textarea');
|
|
for (var j = 0; j < inputs.length; j++) {
|
|
inputs[j].addEventListener('input', onHopFieldChange);
|
|
}
|
|
|
|
card.appendChild(body);
|
|
container.appendChild(card);
|
|
});
|
|
updateFlowDiagram();
|
|
}
|
|
|
|
function onHopFieldChange(e) {
|
|
var idx = parseInt(e.target.getAttribute('data-hop'));
|
|
var field = e.target.getAttribute('data-field');
|
|
if (field === 'port') {
|
|
hopList[idx][field] = parseInt(e.target.value) || 22;
|
|
} else {
|
|
hopList[idx][field] = e.target.value;
|
|
}
|
|
if (field === 'hostname') updateFlowDiagram();
|
|
}
|
|
|
|
function updateFlowDiagram() {
|
|
var diag = document.getElementById('flow-diagram');
|
|
if (!diag) return;
|
|
var html = '<span class="flow-node flow-node-you">You</span>';
|
|
hopList.forEach(function(hop) {
|
|
var label = hop.hostname || '???';
|
|
html += '<span class="flow-arrow">→</span><span class="flow-node flow-node-hop">' + escapeHtml(label) + '</span>';
|
|
});
|
|
// Add target
|
|
var type = sessionType.value;
|
|
var targetHost = '';
|
|
var targetPort = '';
|
|
var typeLabel = type.toUpperCase();
|
|
if (type === 'ssh') {
|
|
targetHost = document.getElementById('hostname').value || '???';
|
|
targetPort = document.getElementById('port').value || '22';
|
|
} else if (type === 'rdp') {
|
|
targetHost = document.getElementById('rdp-hostname').value || '???';
|
|
targetPort = document.getElementById('rdp-port').value || '3389';
|
|
} else if (type === 'vnc') {
|
|
targetHost = document.getElementById('vnc-hostname').value || '???';
|
|
targetPort = document.getElementById('vnc-port').value || '5900';
|
|
} else if (type === 'web') {
|
|
// Parse host:port from URL
|
|
try {
|
|
var u = new URL(document.getElementById('url').value);
|
|
targetHost = u.hostname || '???';
|
|
targetPort = u.port || (u.protocol === 'https:' ? '443' : '80');
|
|
} catch(e) {
|
|
targetHost = '???';
|
|
targetPort = '80';
|
|
}
|
|
}
|
|
if (targetHost) {
|
|
html += '<span class="flow-arrow">→</span><span class="flow-node flow-node-target">' + escapeHtml(targetHost) + ':' + escapeHtml(targetPort) + ' ' + typeLabel + '</span>';
|
|
}
|
|
// Show URL rewrite warning for web sessions with hops
|
|
var warn = '';
|
|
if (type === 'web' && hopList.length > 0) {
|
|
warn = '<div style="color:var(--primary);font-size:0.8em;margin-top:0.4em">Note: The URL will be rewritten to 127.0.0.1:{tunnel_port} inside the headless browser. TLS certificate errors are expected if the target uses HTTPS.</div>';
|
|
}
|
|
diag.innerHTML = hopList.length > 0 ? html + warn : '';
|
|
}
|
|
|
|
document.getElementById('add-hop-btn').addEventListener('click', function() {
|
|
hopList.push({ hostname: '', port: 22, username: '', password: '', private_key: '', expanded: true });
|
|
renderHops();
|
|
});
|
|
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
errorEl.textContent = '';
|
|
btn.disabled = true;
|
|
btn.textContent = 'Connecting...';
|
|
|
|
var body = {};
|
|
if (sessionType.value === 'vdi') {
|
|
body.session_type = 'vdi';
|
|
body.container_image = document.getElementById('vdi-image').value;
|
|
} else if (sessionType.value === 'web') {
|
|
body.session_type = 'web';
|
|
body.url = document.getElementById('url').value;
|
|
} else if (sessionType.value === 'vnc') {
|
|
body.session_type = 'vnc';
|
|
body.hostname = document.getElementById('vnc-hostname').value;
|
|
body.port = parseInt(document.getElementById('vnc-port').value) || 5900;
|
|
var pw = document.getElementById('vnc-password').value;
|
|
if (pw) body.password = pw;
|
|
} else if (sessionType.value === 'rdp') {
|
|
body.session_type = 'rdp';
|
|
body.hostname = document.getElementById('rdp-hostname').value;
|
|
body.port = parseInt(document.getElementById('rdp-port').value) || 3389;
|
|
var un = document.getElementById('rdp-username').value;
|
|
if (un) body.username = un;
|
|
var pw = document.getElementById('rdp-password').value;
|
|
if (pw) body.password = pw;
|
|
var dom = document.getElementById('rdp-domain').value;
|
|
if (dom) body.domain = dom;
|
|
var sec = document.getElementById('rdp-security').value;
|
|
if (sec) body.security = sec;
|
|
if (document.getElementById('rdp-ignore-cert').checked) body.ignore_cert = true;
|
|
} else {
|
|
body.session_type = 'ssh';
|
|
body.hostname = document.getElementById('hostname').value;
|
|
body.port = parseInt(document.getElementById('port').value) || 22;
|
|
var un = document.getElementById('username').value;
|
|
if (un) body.username = un;
|
|
if (document.getElementById('generate-keypair').checked) {
|
|
body.generate_keypair = true;
|
|
} else {
|
|
var pw = document.getElementById('password').value;
|
|
if (pw) body.password = pw;
|
|
var pk = document.getElementById('private-key').value.trim();
|
|
if (pk) body.private_key = pk;
|
|
}
|
|
}
|
|
// Jump hosts (multi-hop, all session types)
|
|
var hops = hopList.filter(function(h) { return h.hostname && h.hostname.trim(); })
|
|
.map(function(h) {
|
|
var hop = { hostname: h.hostname.trim(), port: h.port || 22, username: h.username || '' };
|
|
if (h.password) hop.password = h.password;
|
|
if (h.private_key) hop.private_key = h.private_key;
|
|
return hop;
|
|
});
|
|
if (hops.length > 0) body.jump_hosts = hops;
|
|
var banner = document.getElementById('banner').value;
|
|
if (banner) body.banner = banner;
|
|
// Send browser dimensions so RDP sessions start at correct resolution
|
|
var bw = window.innerWidth, bh = window.innerHeight;
|
|
if (bw > 0 && bh > 0) {
|
|
body.width = bw;
|
|
body.height = bh;
|
|
body.dpi = Math.round((window.devicePixelRatio || 1) * 96);
|
|
}
|
|
fetch('/api/sessions', {
|
|
method: 'POST',
|
|
headers: apiHeaders({ 'Content-Type': 'application/json' }),
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(body),
|
|
})
|
|
.then(function(res) {
|
|
if (!res.ok) return res.text().then(function(t) { throw new Error(t); });
|
|
return res.json();
|
|
})
|
|
.then(function(data) {
|
|
window.open(data.client_url, '_blank');
|
|
hopList = [];
|
|
renderHops();
|
|
refreshSessions();
|
|
})
|
|
.catch(function(err) {
|
|
errorEl.textContent = err.message;
|
|
})
|
|
.finally(function() {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Connect';
|
|
});
|
|
});
|
|
|
|
var expandedShares = {};
|
|
|
|
function h(tag, text, attrs) {
|
|
var el = document.createElement(tag);
|
|
if (text != null) el.textContent = text;
|
|
if (attrs) for (var k in attrs) el.setAttribute(k, attrs[k]);
|
|
return el;
|
|
}
|
|
|
|
function refreshSessions() {
|
|
fetch('/api/sessions', { headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function(res) {
|
|
if (!res.ok) throw new Error('auth');
|
|
return res.json();
|
|
})
|
|
.then(function(sessions) {
|
|
var tbody = document.getElementById('session-list');
|
|
tbody.innerHTML = '';
|
|
sessions.forEach(function(s) {
|
|
var tr = document.createElement('tr');
|
|
var shortId = s.session_id.substring(0, 8);
|
|
var shareFullUrl = window.location.origin + s.share_url;
|
|
var rowId = 'share-' + s.session_id;
|
|
var hostCol = s.session_type === 'web' ? (s.url || '') : s.hostname;
|
|
|
|
var tdId = document.createElement('td');
|
|
var idLink = h('a', shortId, { href: s.client_url, target: '_blank' });
|
|
tdId.appendChild(idLink);
|
|
tr.appendChild(tdId);
|
|
|
|
tr.appendChild(h('td', s.session_type));
|
|
tr.appendChild(h('td', hostCol));
|
|
tr.appendChild(h('td', s.username));
|
|
tr.appendChild(h('td', s.created_by || ''));
|
|
tr.appendChild(h('td', s.status, { 'class': 'status-' + s.status }));
|
|
tr.appendChild(h('td', String(s.active_connections)));
|
|
|
|
var isActive = s.status === 'active';
|
|
var tdOpen = document.createElement('td');
|
|
if (isActive) tdOpen.appendChild(h('a', 'open', { href: s.client_url, target: '_blank' }));
|
|
tr.appendChild(tdOpen);
|
|
|
|
var tdShare = document.createElement('td');
|
|
if (isActive) tdShare.appendChild(h('button', 'share', { 'class': 'btn-small btn-share', 'data-url': shareFullUrl, 'data-row': rowId }));
|
|
tr.appendChild(tdShare);
|
|
|
|
var tdDel = document.createElement('td');
|
|
tdDel.appendChild(h('button', 'delete', { 'class': 'btn-small', 'data-delete': s.session_id }));
|
|
tr.appendChild(tdDel);
|
|
|
|
tbody.appendChild(tr);
|
|
|
|
var shareTr = document.createElement('tr');
|
|
shareTr.id = rowId;
|
|
shareTr.style.display = expandedShares[rowId] ? '' : 'none';
|
|
var shareTd = document.createElement('td');
|
|
shareTd.colSpan = 10;
|
|
var shareDiv = h('div', null, { 'class': 'share-url', style: 'display:block' });
|
|
shareDiv.appendChild(h('span', shareFullUrl));
|
|
shareDiv.appendChild(document.createTextNode(' '));
|
|
shareDiv.appendChild(h('button', 'copy', { 'class': 'btn-small btn-copy', 'data-copy-url': shareFullUrl }));
|
|
shareTd.appendChild(shareDiv);
|
|
shareTr.appendChild(shareTd);
|
|
tbody.appendChild(shareTr);
|
|
});
|
|
});
|
|
}
|
|
|
|
document.getElementById('session-list').addEventListener('click', function(e) {
|
|
if (e.target.getAttribute('data-delete')) {
|
|
var id = e.target.getAttribute('data-delete');
|
|
fetch('/api/sessions/' + id, { method: 'DELETE', headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function() { refreshSessions(); });
|
|
}
|
|
if (e.target.getAttribute('data-copy-url')) {
|
|
var url = e.target.getAttribute('data-copy-url');
|
|
var copyBtn = e.target;
|
|
function onCopied() {
|
|
copyBtn.textContent = 'copied!';
|
|
setTimeout(function() { copyBtn.textContent = 'copy'; }, 2000);
|
|
}
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(url).then(onCopied).catch(function() {
|
|
// Fallback for insecure contexts
|
|
var ta = document.createElement('textarea');
|
|
ta.value = url;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
onCopied();
|
|
});
|
|
} else {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = url;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
onCopied();
|
|
}
|
|
return;
|
|
}
|
|
if (e.target.classList.contains('btn-share')) {
|
|
var rowId = e.target.getAttribute('data-row');
|
|
var row = document.getElementById(rowId);
|
|
if (row) {
|
|
var show = row.style.display === 'none';
|
|
row.style.display = show ? '' : 'none';
|
|
expandedShares[rowId] = show;
|
|
}
|
|
}
|
|
});
|
|
|
|
refreshSessions();
|
|
setInterval(refreshSessions, 5000);
|
|
</script>
|
|
</body>
|
|
</html>
|