mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-11 13:50:14 +00:00
2bf34440e6
- Fix RDP display resize for FreeRDP 3.x (patch 004: config.h struct layout) - Add aurora theme (midnight blue with ambient glow gradients) - Add jaguar theme (racing green & gold with subtle gradients) - Add bg_pattern support for CSS gradient backgrounds in themes - Fix Docker config.toml persistence across rebuilds (#38) - Add Docker Compose volume mount documentation - Increase API rate limit to 5/s burst 30 (fix spurious 429s) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
386 lines
21 KiB
HTML
386 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>rustguac - API Tokens</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; --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); }
|
|
nav { margin-bottom: 1.5em; 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); }
|
|
|
|
table { border-collapse: collapse; width: 100%; margin-top: 0.5em; }
|
|
th, td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
|
|
th { color: var(--text-muted); font-size: 0.85em; }
|
|
.btn-small {
|
|
background: none; border: none; color: var(--primary);
|
|
cursor: pointer; font-family: monospace; padding: 0; font-size: 0.9em;
|
|
}
|
|
.btn-small:hover { text-decoration: underline; }
|
|
.btn-action { color: var(--accent); }
|
|
button.btn-primary {
|
|
padding: 0.4em 1.2em; background: var(--primary); color: var(--text-on-primary); border: none;
|
|
font-family: monospace; font-size: 0.9em; border-radius: 3px; cursor: pointer;
|
|
}
|
|
button.btn-primary:hover { background: var(--primary-hover); }
|
|
select {
|
|
background: var(--input); border: 1px solid var(--border); color: var(--text);
|
|
font-family: monospace; font-size: 0.9em; padding: 0.2em 0.4em; border-radius: 3px;
|
|
}
|
|
select:focus { outline: none; border-color: var(--primary); }
|
|
input[type="text"], input[type="date"] {
|
|
background: var(--input); border: 1px solid var(--border); color: var(--text);
|
|
font-family: monospace; font-size: 0.9em; padding: 0.3em 0.5em; border-radius: 3px;
|
|
}
|
|
input[type="text"]:focus, input[type="date"]:focus { outline: none; border-color: var(--primary); }
|
|
.add-form {
|
|
background: var(--surface); padding: 1em 1.5em; border-radius: 6px;
|
|
display: inline-flex; gap: 0.8em; align-items: center; margin-top: 0.8em;
|
|
flex-wrap: wrap;
|
|
}
|
|
.disabled-row { opacity: 0.5; }
|
|
.status-active { color: var(--accent); }
|
|
.status-disabled { color: var(--primary); }
|
|
.status-expired { color: #666; }
|
|
#error { color: var(--primary); margin-top: 0.5em; }
|
|
.token-reveal {
|
|
background: var(--surface); padding: 1em 1.5em; border-radius: 6px;
|
|
margin-top: 1em; border: 1px solid var(--accent);
|
|
}
|
|
.token-reveal code {
|
|
background: var(--input); padding: 0.4em 0.8em; border-radius: 3px;
|
|
font-size: 0.85em; word-break: break-all; display: block; margin: 0.5em 0;
|
|
user-select: all;
|
|
}
|
|
.token-reveal .warning { color: var(--primary); font-size: 0.85em; margin-top: 0.5em; }
|
|
.no-permission {
|
|
background: var(--surface); padding: 1.5em; border-radius: 6px;
|
|
color: var(--text-muted); margin-top: 1em; max-width: 500px;
|
|
}
|
|
</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" id="sessions-link">Sessions</a>
|
|
<a href="/recordings.html">Recordings</a>
|
|
<a href="/docs.html">Docs</a>
|
|
<a href="/tokens.html" class="active">Tokens</a>
|
|
<a href="/admin.html" id="admin-link" style="display:none">Admin</a>
|
|
<span id="user-menu-wrapper"><span id="user-menu-btn" title="Preferences">⚙</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</div></div></span>
|
|
</nav>
|
|
|
|
<div id="loading">Loading...</div>
|
|
<div id="no-permission" class="no-permission" style="display:none">
|
|
Your role does not permit API token management. Contact an administrator if you need API access.
|
|
</div>
|
|
|
|
<div id="create-section" style="display:none">
|
|
<h2>Create Token</h2>
|
|
<div class="add-form" id="create-form">
|
|
<input type="text" id="token-name" placeholder="Token name">
|
|
<select id="token-max-role">
|
|
<option value="">No role cap</option>
|
|
</select>
|
|
<input type="date" id="token-expires" title="Expiry date (optional)">
|
|
<button class="btn-primary" id="create-btn">Create</button>
|
|
</div>
|
|
<div id="token-reveal" class="token-reveal" style="display:none">
|
|
<strong>Token created:</strong>
|
|
<code id="token-plaintext"></code>
|
|
<div style="display:inline-flex;gap:0.8em;align-items:center;">
|
|
<button class="btn-small btn-action" id="copy-token-btn">copy to clipboard</button>
|
|
<button class="btn-small" id="dismiss-token-btn">dismiss</button>
|
|
</div>
|
|
<div class="warning">This token will not be shown again. Copy it now.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tokens-section" style="display:none">
|
|
<h2>Your Tokens</h2>
|
|
<table>
|
|
<thead><tr>
|
|
<th>Name</th><th>Max Role</th><th>Expires</th><th>Created</th><th>Last Used</th><th>Status</th><th></th>
|
|
</tr></thead>
|
|
<tbody id="tokens-body"></tbody>
|
|
</table>
|
|
<div id="no-tokens" style="display:none;color:var(--text-muted);margin-top:0.5em;">No tokens yet.</div>
|
|
</div>
|
|
|
|
<div id="error"></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){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+' - API Tokens';document.querySelector('h1').textContent=d.site_title;}
|
|
initTheme(d.theme);
|
|
});
|
|
|
|
var apiKey = sessionStorage.getItem('rustguac_api_key');
|
|
var roleLevel = { admin: 4, poweruser: 3, operator: 2, viewer: 1 };
|
|
var currentRole = '';
|
|
var canCreate = false;
|
|
|
|
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 errorEl = document.getElementById('error');
|
|
function showError(msg) { errorEl.textContent = msg; setTimeout(function(){ errorEl.textContent = ''; }, 5000); }
|
|
|
|
document.getElementById('logout-item').addEventListener('click', function() {
|
|
sessionStorage.removeItem('rustguac_api_key');
|
|
fetch('/auth/logout', { credentials: 'same-origin' })
|
|
.finally(function() { window.location.href = '/'; });
|
|
});
|
|
|
|
function initPage() {
|
|
if (apiKey) {
|
|
// API key users are admins — redirect to admin page for token management
|
|
document.getElementById('loading').style.display = 'none';
|
|
document.getElementById('admin-link').style.display = '';
|
|
document.getElementById('no-permission').style.display = '';
|
|
document.getElementById('no-permission').textContent = 'API key admins manage user tokens via the Admin page.';
|
|
return;
|
|
}
|
|
|
|
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;
|
|
document.getElementById('loading').style.display = 'none';
|
|
currentRole = data.role;
|
|
var level = roleLevel[data.role] || 0;
|
|
|
|
if (data.role === 'admin') {
|
|
document.getElementById('admin-link').style.display = '';
|
|
}
|
|
|
|
if (level < 2) {
|
|
// viewer — no token access
|
|
document.getElementById('no-permission').style.display = '';
|
|
return;
|
|
}
|
|
|
|
// operator+ can see their tokens
|
|
document.getElementById('tokens-section').style.display = '';
|
|
|
|
if (level >= 3) {
|
|
// poweruser+ can create and revoke tokens
|
|
canCreate = true;
|
|
document.getElementById('create-section').style.display = '';
|
|
populateMaxRoles(data.role);
|
|
}
|
|
|
|
loadTokens();
|
|
})
|
|
.catch(function() {});
|
|
}
|
|
|
|
function populateMaxRoles(userRole) {
|
|
var sel = document.getElementById('token-max-role');
|
|
var roles = ['viewer', 'operator', 'poweruser', 'admin'];
|
|
var userLevel = roleLevel[userRole] || 0;
|
|
for (var i = 0; i < roles.length; i++) {
|
|
if (roleLevel[roles[i]] <= userLevel) {
|
|
var opt = document.createElement('option');
|
|
opt.value = roles[i];
|
|
opt.textContent = roles[i];
|
|
sel.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadTokens() {
|
|
fetch('/api/me/tokens', { headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function(res) {
|
|
if (!res.ok) throw new Error('failed to load tokens');
|
|
return res.json();
|
|
})
|
|
.then(function(tokens) {
|
|
var tbody = document.getElementById('tokens-body');
|
|
tbody.innerHTML = '';
|
|
|
|
if (tokens.length === 0) {
|
|
document.getElementById('no-tokens').style.display = '';
|
|
return;
|
|
}
|
|
document.getElementById('no-tokens').style.display = 'none';
|
|
|
|
tokens.forEach(function(t) {
|
|
var tr = document.createElement('tr');
|
|
var now = new Date().toISOString();
|
|
var expired = t.expires_at && t.expires_at < now;
|
|
if (t.disabled || expired) tr.className = 'disabled-row';
|
|
|
|
tr.appendChild(td(t.name));
|
|
tr.appendChild(td(t.max_role || 'none'));
|
|
tr.appendChild(td(t.expires_at ? t.expires_at.substring(0, 10) : 'never'));
|
|
tr.appendChild(td(t.created_at ? t.created_at.substring(0, 10) : ''));
|
|
tr.appendChild(td(t.last_used_at || 'never'));
|
|
|
|
var statusTd = document.createElement('td');
|
|
if (t.disabled) {
|
|
statusTd.textContent = 'disabled';
|
|
statusTd.className = 'status-disabled';
|
|
} else if (expired) {
|
|
statusTd.textContent = 'expired';
|
|
statusTd.className = 'status-expired';
|
|
} else {
|
|
statusTd.textContent = 'active';
|
|
statusTd.className = 'status-active';
|
|
}
|
|
tr.appendChild(statusTd);
|
|
|
|
var actTd = document.createElement('td');
|
|
if (canCreate) {
|
|
var revokeBtn = document.createElement('button');
|
|
revokeBtn.className = 'btn-small';
|
|
revokeBtn.textContent = 'revoke';
|
|
revokeBtn.addEventListener('click', (function(id, name) {
|
|
return function() {
|
|
if (!confirm('Revoke token "' + name + '"? This cannot be undone.')) return;
|
|
fetch('/api/me/tokens/' + id, {
|
|
method: 'DELETE',
|
|
headers: apiHeaders(),
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(function(res) {
|
|
if (!res.ok) return res.json().then(function(d) { showError(d.error); });
|
|
loadTokens();
|
|
});
|
|
};
|
|
})(t.id, t.name));
|
|
actTd.appendChild(revokeBtn);
|
|
}
|
|
tr.appendChild(actTd);
|
|
|
|
tbody.appendChild(tr);
|
|
});
|
|
})
|
|
.catch(function(e) { showError(e.message); });
|
|
}
|
|
|
|
function td(text) {
|
|
var el = document.createElement('td');
|
|
el.textContent = text || '';
|
|
return el;
|
|
}
|
|
|
|
// Create token
|
|
document.getElementById('create-btn').addEventListener('click', function() {
|
|
var name = document.getElementById('token-name').value.trim();
|
|
if (!name) { showError('Token name is required'); return; }
|
|
|
|
var maxRole = document.getElementById('token-max-role').value || undefined;
|
|
var expiresInput = document.getElementById('token-expires').value;
|
|
var expiresAt = expiresInput ? expiresInput + 'T23:59:59Z' : undefined;
|
|
|
|
var body = { name: name };
|
|
if (maxRole) body.max_role = maxRole;
|
|
if (expiresAt) body.expires_at = expiresAt;
|
|
|
|
var btn = document.getElementById('create-btn');
|
|
btn.disabled = true;
|
|
|
|
fetch('/api/me/tokens', {
|
|
method: 'POST',
|
|
headers: apiHeaders({ 'Content-Type': 'application/json' }),
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify(body)
|
|
})
|
|
.then(function(res) {
|
|
if (!res.ok) return res.json().then(function(d) { showError(d.error); throw new Error(); });
|
|
return res.json();
|
|
})
|
|
.then(function(data) {
|
|
document.getElementById('token-name').value = '';
|
|
document.getElementById('token-expires').value = '';
|
|
document.getElementById('token-max-role').value = '';
|
|
|
|
// Show the plaintext token
|
|
document.getElementById('token-plaintext').textContent = data.token;
|
|
document.getElementById('token-reveal').style.display = '';
|
|
|
|
loadTokens();
|
|
})
|
|
.catch(function() {})
|
|
.finally(function() { btn.disabled = false; });
|
|
});
|
|
|
|
// Copy token
|
|
document.getElementById('copy-token-btn').addEventListener('click', function() {
|
|
var token = document.getElementById('token-plaintext').textContent;
|
|
var copyBtn = document.getElementById('copy-token-btn');
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(token).then(function() {
|
|
copyBtn.textContent = 'copied!';
|
|
setTimeout(function() { copyBtn.textContent = 'copy to clipboard'; }, 2000);
|
|
}).catch(function() { fallbackCopy(token, copyBtn); });
|
|
} else {
|
|
fallbackCopy(token, copyBtn);
|
|
}
|
|
});
|
|
|
|
function fallbackCopy(text, btn) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
btn.textContent = 'copied!';
|
|
setTimeout(function() { btn.textContent = 'copy to clipboard'; }, 2000);
|
|
}
|
|
|
|
// Dismiss token reveal
|
|
document.getElementById('dismiss-token-btn').addEventListener('click', function() {
|
|
document.getElementById('token-reveal').style.display = 'none';
|
|
document.getElementById('token-plaintext').textContent = '';
|
|
});
|
|
|
|
initPage();
|
|
setInterval(function() {
|
|
if (document.getElementById('tokens-section').style.display !== 'none') {
|
|
loadTokens();
|
|
}
|
|
}, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|