mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 01:26:06 +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).
389 lines
22 KiB
HTML
389 lines
22 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-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); }
|
|
|
|
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="/reports.html" id="reports-link" style="display:none">Reports</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"><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="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){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+' - 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('reports-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 = '';
|
|
document.getElementById('reports-link').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>
|