2026-05-18 22:56:51 -04:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-05-21 22:39:20 -04:00
< title > API Tokens — Dragonflight< / title >
2026-05-21 23:14:09 -04:00
< link rel = "stylesheet" href = "/dist/app.css" >
2026-05-18 22:56:51 -04:00
< style >
.token-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: var(--sp-4);
display: flex;
align-items: center;
gap: var(--sp-4);
}
.token-card-icon {
width: 36px;
height: 36px;
border-radius: var(--r-md);
background: var(--accent-subtle);
border: 1px solid var(--accent-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
flex-shrink: 0;
}
.token-card-body { flex: 1; min-width: 0; }
.token-card-name { font-weight: 500; font-size: var(--text-sm); }
.token-card-meta { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
.token-prefix {
font-family: 'SF Mono', 'Consolas', monospace;
font-size: var(--text-xs);
color: var(--text-secondary);
background: var(--bg-raised);
padding: 1px 6px;
border-radius: var(--r-sm);
border: 1px solid var(--border);
}
.tokens-list {
display: flex;
flex-direction: column;
gap: var(--sp-2);
max-width: 680px;
}
.new-token-banner {
background: var(--status-green-bg);
border: 1px solid oklch(68% 0.18 148 / 0.30);
border-radius: var(--r-lg);
padding: var(--sp-4) var(--sp-5);
margin-bottom: var(--sp-5);
max-width: 680px;
}
.new-token-banner-title {
font-size: var(--text-sm);
font-weight: 500;
color: var(--status-green);
margin-bottom: var(--sp-2);
display: flex;
align-items: center;
gap: var(--sp-2);
}
.new-token-banner-warning {
font-size: var(--text-xs);
color: var(--text-secondary);
margin-top: var(--sp-3);
}
.copy-btn {
margin-top: var(--sp-3);
}
< / style >
< / head >
< body >
2026-05-21 23:14:09 -04:00
< div class = "wd-shell" style = "display:flex;min-height:100vh;" >
2026-05-18 22:56:51 -04:00
<!-- Sidebar -->
2026-05-21 23:14:09 -04:00
< nav class = "wd-sidebar" aria-label = "Main navigation" >
< div class = "wd-sidebar-header" >
< img src = "img/dragon-logo.png?v=1" alt = "Dragonflight" style = "width:18px;height:18px;" >
< span class = "wd-sidebar-brand" > Dragonflight< / span >
2026-05-18 22:56:51 -04:00
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-sidebar-nav" >
< a href = "home.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M2 7l6-5 6 5v7a1 1 0 0 1-1 1h-3v-5H6v5H3a1 1 0 0 1-1-1z" / > < / svg >
Home
< / a >
2026-05-21 23:14:09 -04:00
< a href = "index.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < rect x = "1" y = "1" width = "6" height = "6" rx = "1" / > < rect x = "9" y = "1" width = "6" height = "6" rx = "1" / > < rect x = "1" y = "9" width = "6" height = "6" rx = "1" / > < rect x = "9" y = "9" width = "6" height = "6" rx = "1" / > < / svg >
Library
< / a >
2026-05-21 23:14:09 -04:00
< a href = "projects.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M1 4a1 1 0 0 1 1-1h4l2 2h5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z" / > < / svg >
Projects
< / a >
2026-05-21 23:14:09 -04:00
< a href = "upload.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M8 11V3M5 6l3-3 3 3" / > < path d = "M2 13h12" / > < / svg >
Ingest
< / a >
2026-05-21 23:14:09 -04:00
< a href = "recorders.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < rect x = "1" y = "4" width = "10" height = "8" rx = "1" / > < path d = "M11 7l4-2v6l-4-2" / > < / svg >
Recorders
< / a >
2026-05-21 23:14:09 -04:00
< a href = "capture.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "8" cy = "8" r = "3" / > < circle cx = "8" cy = "8" r = "6.5" / > < / svg >
Capture
< / a >
2026-05-21 23:14:09 -04:00
< a href = "jobs.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M2 4h12M2 8h8M2 12h5" / > < / svg >
Jobs
< / a >
2026-05-21 23:14:09 -04:00
< a href = "edit.html" class = "wd-nav-item" target = "_blank" rel = "noopener" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3" / > < / svg >
Editor
< / a >
2026-05-21 23:14:09 -04:00
< div class = "wd-sidebar-section" > Admin< / div >
< a href = "users.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "6" cy = "5" r = "2.5" / > < path d = "M1 13c0-2.8 2.2-5 5-5s5 2.2 5 5" / > < circle cx = "12" cy = "5" r = "2" / > < path d = "M15 12c0-1.9-1.3-3.5-3-4" / > < / svg >
Users
< / a >
2026-05-21 23:14:09 -04:00
< a href = "tokens.html" class = "wd-nav-item" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "6" cy = "10" r = "3.5" / > < path d = "M8.7 7.3L13 3M11.5 3.5l1.5 1.5M13.5 2.5l1 1" / > < / svg >
Tokens
< / a >
2026-05-21 23:14:09 -04:00
< / div >
< div class = "wd-sidebar-footer" >
< div class = "wd-sidebar-user" >
< div class = "wd-sidebar-user-avatar" id = "userAvatar" > ?< / div >
< div class = "wd-sidebar-user-info" >
< div class = "wd-sidebar-user-name" id = "userName" > —< / div >
< div class = "wd-sidebar-user-role" id = "userRole" > < / div >
2026-05-18 22:56:51 -04:00
< / div >
2026-05-21 23:14:09 -04:00
< button class = "wd-btn wd-btn--ghost wd-btn--sm wd-btn--icon wd-sidebar-user-logout" id = "logoutBtn" title = "Sign out" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" width = "14" height = "14" > < path d = "M10 8H3M6 5l-3 3 3 3" / > < path d = "M7 3h5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H7" / > < / svg >
< / button >
< / div >
< / div >
< / nav >
<!-- Main -->
2026-05-21 23:14:09 -04:00
< div style = "flex:1;display:flex;flex-direction:column;" >
< header class = "wd-topbar" >
< div class = "wd-topbar-left" >
2026-05-18 22:56:51 -04:00
< span class = "page-title" > API Tokens< / span >
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-topbar-right" >
< button class = "wd-btn wd-btn--primary wd-btn--sm" onclick = "openNewTokenPanel()" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M8 2v12M2 8h12" / > < / svg >
New token
< / button >
< / div >
< / header >
2026-05-21 23:14:09 -04:00
< div style = "flex:1;overflow:auto;padding:24px;" >
2026-05-18 22:56:51 -04:00
< div style = "max-width:680px;margin-bottom:var(--sp-5);" >
< p class = "text-sm text-secondary" style = "line-height:1.7;" >
API tokens let scripts and integrations authenticate as you without using your password.
Tokens are shown once at creation — store them securely.
Use < code style = "font-family:monospace;font-size:11px;background:var(--bg-surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border)" > Authorization: Bearer < token> < / code > in your requests.
< / p >
< / div >
<!-- New token reveal (shown after creation) -->
< div id = "newTokenBanner" style = "display:none;" class = "new-token-banner" >
< div class = "new-token-banner-title" >
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" width = "14" height = "14" > < circle cx = "8" cy = "8" r = "6.5" / > < path d = "M5 8l2 2 4-4" / > < / svg >
Token created
< / div >
< div class = "token-reveal" id = "newTokenValue" > < / div >
< div class = "new-token-banner-warning" >
⚠ This is the only time this token will be shown. Copy it now.
< / div >
2026-05-21 23:14:09 -04:00
< button class = "wd-btn wd-btn--secondary wd-btn--sm copy-btn" onclick = "copyToken()" > Copy to clipboard< / button >
2026-05-18 22:56:51 -04:00
< / div >
<!-- Token list -->
< div class = "tokens-list" id = "tokensList" >
< div style = "color:var(--text-tertiary);font-size:var(--text-sm)" > Loading…< / div >
< / div >
2026-05-21 23:14:09 -04:00
< div id = "tokensEmpty" class = "wd-empty" style = "display:none;padding:var(--sp-12) 0;" >
< div class = "wd-empty-icon" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 40 40" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "15" cy = "25" r = "8" / > < path d = "M21 19l10-10M28 10l3 3M30 8l2 2" / > < / svg >
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-empty-title" > No tokens yet< / div >
< div class = "wd-empty-body" > Create a token to authenticate API requests without a password.< / div >
< div class = "wd-empty-actions" >
< button class = "wd-btn wd-btn--primary wd-btn--sm" onclick = "openNewTokenPanel()" > New token< / button >
2026-05-18 22:56:51 -04:00
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- New token slide panel -->
2026-05-21 23:14:09 -04:00
< div class = "wd-slide-overlay" id = "tokenOverlay" onclick = "closeNewTokenPanel()" > < / div >
< div class = "wd-slide-panel" id = "tokenPanel" >
< div class = "wd-slide-panel-header" >
< span class = "wd-slide-panel-title" > New API token< / span >
< button class = "wd-btn wd-btn--ghost wd-btn--sm" onclick = "closeNewTokenPanel()" style = "padding:0;width:28px;height:28px;" >
2026-05-18 22:56:51 -04:00
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M3 3l10 10M13 3L3 13" / > < / svg >
< / button >
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-slide-panel-body" >
< div class = "wd-form-group" >
< label class = "wd-label" for = "tokenName" > Token name< / label >
2026-05-18 22:56:51 -04:00
< input type = "text" id = "tokenName" placeholder = "e.g. Premiere Plugin, CI/CD Script" >
2026-05-21 23:14:09 -04:00
< div class = "wd-hint" > A label to help you remember what this token is for.< / div >
2026-05-18 22:56:51 -04:00
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-form-group" >
< label class = "wd-label" for = "tokenExpiry" > Expiry< / label >
< select id = "tokenExpiry" class = "wd-select" >
2026-05-18 22:56:51 -04:00
< option value = "" > No expiry< / option >
< option value = "30" > 30 days< / option >
< option value = "90" > 90 days< / option >
< option value = "365" > 1 year< / option >
< / select >
< / div >
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-slide-panel-footer" >
< button class = "wd-btn wd-btn--ghost" onclick = "closeNewTokenPanel()" > Cancel< / button >
< button class = "wd-btn wd-btn--primary" id = "createTokenBtn" onclick = "createNewToken()" > Create token< / button >
2026-05-18 22:56:51 -04:00
< / div >
< / div >
2026-05-21 23:14:09 -04:00
< div class = "wd-toast-container" id = "toastContainer" aria-live = "polite" > < / div >
2026-05-18 22:56:51 -04:00
2026-05-19 00:27:31 -04:00
< script src = "js/api.js?v=6" > < / script >
2026-05-18 22:56:51 -04:00
< script >
let latestToken = null;
document.addEventListener('DOMContentLoaded', loadTokens);
async function loadTokens() {
const r = await getTokens();
const list = document.getElementById('tokensList');
const empty = document.getElementById('tokensEmpty');
if (!r.success) {
list.innerHTML = `< div class = "text-sm text-tertiary" > Could not load tokens.< / div > `;
return;
}
const tokens = r.data;
if (!tokens.length) {
list.style.display = 'none';
empty.style.display = 'flex';
return;
}
list.style.display = 'flex';
empty.style.display = 'none';
list.innerHTML = tokens.map(t => {
const created = t.created_at ? new Date(t.created_at).toLocaleDateString() : '—';
const lastUsed = t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : 'Never';
const expires = t.expires_at ? new Date(t.expires_at).toLocaleDateString() : 'Never';
const isExpired = t.expires_at & & new Date(t.expires_at) < new Date ( ) ;
return `
< div class = "token-card" >
< div class = "token-card-icon" >
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" width = "16" height = "16" > < circle cx = "6" cy = "10" r = "3.5" / > < path d = "M8.7 7.3L13 3M11.5 3.5l1.5 1.5M13.5 2.5l1 1" / > < / svg >
< / div >
< div class = "token-card-body" >
< div class = "token-card-name" > ${esc(t.name)}< / div >
< div class = "token-card-meta" >
< span class = "token-prefix" > ${esc(t.token_prefix)}…< / span >
· Created ${created}
· Last used: ${lastUsed}
· Expires: ${isExpired ? '< span style = "color:var(--status-red)" > Expired< / span > ' : expires}
< / div >
< / div >
2026-05-21 23:14:09 -04:00
< button class = "wd-btn wd-btn--danger wd-btn--sm" onclick = "confirmRevoke('${t.id}',${esc(JSON.stringify(t.name))})" > Revoke< / button >
2026-05-18 22:56:51 -04:00
< / div > `;
}).join('');
}
function openNewTokenPanel() {
document.getElementById('tokenName').value = '';
document.getElementById('tokenExpiry').value = '';
2026-05-21 23:14:09 -04:00
document.getElementById('tokenPanel').classList.add('is-open');
document.getElementById('tokenOverlay').classList.add('is-open');
2026-05-18 22:56:51 -04:00
}
function closeNewTokenPanel() {
2026-05-21 23:14:09 -04:00
document.getElementById('tokenPanel').classList.remove('is-open');
document.getElementById('tokenOverlay').classList.remove('is-open');
2026-05-18 22:56:51 -04:00
}
async function createNewToken() {
const name = document.getElementById('tokenName').value.trim();
if (!name) { toast('Token name required', '', 'warning'); return; }
const expires_in_days = document.getElementById('tokenExpiry').value || null;
const btn = document.getElementById('createTokenBtn');
btn.disabled = true;
const r = await createToken({ name, expires_in_days: expires_in_days ? parseInt(expires_in_days) : null });
btn.disabled = false;
if (r.success) {
closeNewTokenPanel();
latestToken = r.data.token;
document.getElementById('newTokenValue').textContent = latestToken;
document.getElementById('newTokenBanner').style.display = 'block';
document.getElementById('newTokenBanner').scrollIntoView({ behavior: 'smooth' });
toast('Token created', name, 'success');
loadTokens();
} else {
toast('Failed to create token', r.error, 'error');
}
}
async function confirmRevoke(id, name) {
if (!confirm(`Revoke token "${name}"? Any scripts using it will stop working.`)) return;
const r = await revokeToken(id);
if (r.success) { toast('Token revoked', name, 'success'); loadTokens(); }
else toast('Failed to revoke token', r.error, 'error');
}
function copyToken() {
if (!latestToken) return;
navigator.clipboard.writeText(latestToken).then(() => {
toast('Copied to clipboard', '', 'success');
}).catch(() => {
toast('Copy failed — select and copy manually', '', 'warning');
});
}
function toast(title, msg, type = 'info') {
const icons = {
success: `< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "8" cy = "8" r = "6.5" / > < path d = "M5 8l2 2 4-4" / > < / svg > `,
error: `< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "8" cy = "8" r = "6.5" / > < path d = "M8 5v4M8 11v.5" / > < / svg > `,
warning: `< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < path d = "M8 2L1 14h14L8 2z" / > < path d = "M8 7v3M8 12v.5" / > < / svg > `,
info: `< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" > < circle cx = "8" cy = "8" r = "6.5" / > < path d = "M8 7v5M8 5v.5" / > < / svg > `,
};
const el = document.createElement('div');
2026-05-21 23:14:09 -04:00
el.className = `wd-toast wd-toast--${type}`;
2026-05-18 22:56:51 -04:00
el.innerHTML = `< div class = "toast-icon" > ${icons[type]||icons.info}< / div > < div class = "toast-body" > < div class = "toast-title" > ${esc(title)}< / div > ${msg?`< div class = "toast-msg" > ${esc(msg)}< / div > `:''}< / div > `;
document.getElementById('toastContainer').appendChild(el);
setTimeout(() => el.remove(), 4000);
}
function esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'& ').replace(/< /g,'< ').replace(/>/g,'> ').replace(/"/g,'" ');
}
< / script >
< script src = "js/auth-guard.js" > < / script >
< / body >
2026-05-19 00:27:31 -04:00
< / html >