Skip to content

Commit bf5d666

Browse files
committed
feat: full embedded dashboard + auth fix + dev mode key logging
Dashboard: - Overview: key stats, active connections, uptime, recent audit - Keys: list with state filter tabs, create, activate, revoke, detail view - Connections: active KMIP client connections - Audit: full audit log with operation/status/client detail - Shared dark theme (same as PKI server) - Auth via ?key= URL parameter when API key is set - Dashboard auth-protected when API key configured Fixes: - Dev mode logs full API key (needed for usage, it's auto-generated) - Dashboard auth conditional on API key being set
1 parent da73d19 commit bf5d666

3 files changed

Lines changed: 357 additions & 4 deletions

File tree

internal/api/api.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,15 @@ func (a *API) Serve(addr string) error {
152152
mux.HandleFunc("GET /v1/status", a.auth(a.handleStatus))
153153
mux.HandleFunc("GET /v1/audit", a.auth(a.handleAuditLog))
154154
mux.HandleFunc("GET /v1/inventory", a.auth(a.handleInventory))
155-
mux.HandleFunc("GET /metrics", a.handleMetrics)
156-
mux.Handle("/ui/", http.StripPrefix("/ui", dashboard.Handler()))
155+
mux.HandleFunc("GET /metrics", a.handleMetrics) // public for Prometheus scraping
156+
// Dashboard auth-protected when API key is set
157+
if a.apiKey != "" {
158+
mux.HandleFunc("/ui/", a.auth(func(w http.ResponseWriter, r *http.Request) {
159+
http.StripPrefix("/ui", dashboard.Handler()).ServeHTTP(w, r)
160+
}))
161+
} else {
162+
mux.Handle("/ui/", http.StripPrefix("/ui", dashboard.Handler()))
163+
}
157164

158165
handler := a.rateLimitMiddleware(a.limitBodyMiddleware(a.corsMiddleware(mux)))
159166

Lines changed: 255 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,257 @@
11
<!DOCTYPE html>
2-
<html><head><title>Cyphera Open KMIP Server</title></head>
3-
<body><h1>Open KMIP Server</h1><p>Dashboard coming soon.</p></body>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Cyphera Open KMIP Server</title>
7+
<link rel="stylesheet" href="theme.css">
8+
</head>
9+
<body>
10+
<div class="shell">
11+
<div class="sidebar">
12+
<div class="logo">
13+
<h1>Open KMIP</h1>
14+
<span>Cyphera Labs</span>
15+
</div>
16+
<nav>
17+
<a href="#" data-page="overview" class="active">Overview</a>
18+
<a href="#" data-page="keys">Keys</a>
19+
<a href="#" data-page="connections">Connections</a>
20+
<a href="#" data-page="audit">Audit Log</a>
21+
</nav>
22+
</div>
23+
<div class="main">
24+
25+
<!-- Overview -->
26+
<div id="page-overview" class="page active">
27+
<h2>Overview</h2>
28+
<div class="stats" id="stats"></div>
29+
<div class="section">
30+
<h3 style="margin-bottom:12px">Recent Activity</h3>
31+
<div id="recent-audit"></div>
32+
</div>
33+
</div>
34+
35+
<!-- Keys -->
36+
<div id="page-keys" class="page">
37+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
38+
<h2>Managed Keys</h2>
39+
<button class="btn btn-primary btn-sm" onclick="showCreateKey()">Create Key</button>
40+
</div>
41+
<div class="filter-tabs">
42+
<button class="btn btn-sm" style="background:var(--surface2);color:var(--text)" onclick="loadKeys()">All</button>
43+
<button class="btn btn-sm" style="background:rgba(99,102,241,0.15);color:var(--accent2)" onclick="loadKeys('pre-active')">Pre-Active</button>
44+
<button class="btn btn-sm" style="background:rgba(34,197,94,0.15);color:var(--green)" onclick="loadKeys('active')">Active</button>
45+
<button class="btn btn-sm" style="background:rgba(239,68,68,0.15);color:var(--red)" onclick="loadKeys('compromised')">Revoked</button>
46+
</div>
47+
<table>
48+
<thead><tr><th>Name</th><th>UID</th><th>Algorithm</th><th>Length</th><th>State</th><th>Created</th><th></th></tr></thead>
49+
<tbody id="key-table"></tbody>
50+
</table>
51+
</div>
52+
53+
<!-- Key Detail -->
54+
<div id="page-key-detail" class="page">
55+
<h2>Key Detail</h2>
56+
<div id="key-detail-content"></div>
57+
</div>
58+
59+
<!-- Create Key -->
60+
<div id="page-create-key" class="page">
61+
<h2>Create Key</h2>
62+
<div class="detail">
63+
<div class="detail-grid" style="max-width:400px">
64+
<div class="label">Name</div><div><input id="ck-name" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:4px;width:100%" placeholder="my-aes-key"></div>
65+
<div class="label">Algorithm</div><div><select id="ck-algo" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:4px"><option value="AES">AES</option><option value="CHACHA20">ChaCha20</option></select></div>
66+
<div class="label">Length</div><div><select id="ck-length" style="background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:4px"><option value="128">128</option><option value="256" selected>256</option></select></div>
67+
</div>
68+
<div style="margin-top:16px">
69+
<button class="btn btn-primary" onclick="createKey()">Create</button>
70+
<button class="btn btn-sm" style="background:var(--surface2);color:var(--text);margin-left:8px" onclick="showPage('keys')">Cancel</button>
71+
</div>
72+
</div>
73+
</div>
74+
75+
<!-- Connections -->
76+
<div id="page-connections" class="page">
77+
<h2>Active Connections</h2>
78+
<table>
79+
<thead><tr><th>Client CN</th><th>Remote Address</th><th>Connected</th><th>Operations</th><th>Last Op</th></tr></thead>
80+
<tbody id="conn-table"></tbody>
81+
</table>
82+
</div>
83+
84+
<!-- Audit -->
85+
<div id="page-audit" class="page">
86+
<h2>Audit Log</h2>
87+
<div id="audit-list"></div>
88+
</div>
89+
</div>
90+
</div>
91+
92+
<script>
93+
const API_KEY = new URLSearchParams(window.location.search).get('key') || '';
94+
const headers = API_KEY ? {'Authorization': `Bearer ${API_KEY}`} : {};
95+
96+
async function api(path) {
97+
const r = await fetch(path, {headers});
98+
return r.json();
99+
}
100+
async function apiPost(path, body) {
101+
const r = await fetch(path, {method:'POST', headers:{...headers,'Content-Type':'application/json'}, body:JSON.stringify(body)});
102+
return r.json();
103+
}
104+
105+
const algoName = {3:'AES',4:'RSA',6:'ECDSA',0x1E:'ChaCha20'};
106+
107+
// Nav
108+
document.querySelectorAll('[data-page]').forEach(a => {
109+
a.addEventListener('click', e => { e.preventDefault(); showPage(a.dataset.page); });
110+
});
111+
function showPage(name) {
112+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
113+
document.querySelectorAll('[data-page]').forEach(a => a.classList.remove('active'));
114+
document.getElementById('page-'+name).classList.add('active');
115+
document.querySelector('[data-page="'+name+'"]')?.classList.add('active');
116+
if(name==='overview') loadOverview();
117+
if(name==='keys') loadKeys();
118+
if(name==='connections') loadConnections();
119+
if(name==='audit') loadAudit();
120+
}
121+
122+
// Overview
123+
async function loadOverview() {
124+
const s = await api('/v1/status');
125+
const k = s.keys || {};
126+
document.getElementById('stats').innerHTML = `
127+
<div class="stat"><div class="label">Total Keys</div><div class="value">${k.total||0}</div></div>
128+
<div class="stat"><div class="label">Active</div><div class="value green">${k.active||0}</div></div>
129+
<div class="stat"><div class="label">Pre-Active</div><div class="value">${k.pre_active||0}</div></div>
130+
<div class="stat"><div class="label">Revoked</div><div class="value red">${k.compromised||0}</div></div>
131+
<div class="stat"><div class="label">Connections</div><div class="value">${s.connections||0}</div></div>
132+
<div class="stat"><div class="label">Uptime</div><div class="value">${Math.floor((s.uptime_seconds||0)/60)}m</div></div>
133+
`;
134+
const audit = await api('/v1/audit?limit=10');
135+
const entries = audit.entries || [];
136+
document.getElementById('recent-audit').innerHTML = entries.map(e => `
137+
<div class="audit-entry">
138+
<span class="audit-action">${e.operation}</span>
139+
<span class="mono" style="margin-left:8px">${e.object_uid||''}</span>
140+
<span class="badge badge-${e.status==='success'?'active':'revoked'}" style="margin-left:8px">${e.status}</span>
141+
<div class="audit-details">${e.source}${e.client_id||'anonymous'}${e.remote_addr||''}</div>
142+
</div>
143+
`).join('') || '<div class="loading">No events yet</div>';
144+
}
145+
146+
// Keys
147+
async function loadKeys(stateFilter) {
148+
const data = await api('/v1/keys');
149+
let keys = data.keys || [];
150+
if(stateFilter) keys = keys.filter(k => k.state === stateFilter);
151+
document.getElementById('key-table').innerHTML = keys.map(k => `
152+
<tr>
153+
<td><a href="#" onclick="showKeyDetail('${k.uid}');return false">${k.name||'(unnamed)'}</a></td>
154+
<td class="mono truncate">${k.uid}</td>
155+
<td>${algoName[k.algorithm]||k.algorithm}</td>
156+
<td>${k.length}</td>
157+
<td><span class="badge badge-${k.state}">${k.state}</span></td>
158+
<td class="mono">${(k.created_at||'').split('T')[0]}</td>
159+
<td>
160+
${k.state==='pre-active'?`<button class="btn btn-primary btn-sm" onclick="activateKey('${k.uid}')">Activate</button>`:''}
161+
${k.state==='active'?`<button class="btn btn-danger btn-sm" onclick="revokeKey('${k.uid}')">Revoke</button>`:''}
162+
</td>
163+
</tr>
164+
`).join('') || '<tr><td colspan="7" class="loading">No keys</td></tr>';
165+
}
166+
167+
async function showKeyDetail(uid) {
168+
const k = await api('/v1/keys/'+uid);
169+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
170+
document.getElementById('page-key-detail').classList.add('active');
171+
document.getElementById('key-detail-content').innerHTML = `
172+
<div class="detail">
173+
<h3>${k.name||'(unnamed)'}</h3>
174+
<div class="detail-grid">
175+
<div class="label">UID</div><div class="val mono">${k.uid}</div>
176+
<div class="label">Algorithm</div><div class="val">${algoName[k.algorithm]||k.algorithm}</div>
177+
<div class="label">Length</div><div class="val">${k.length} bits</div>
178+
<div class="label">State</div><div class="val"><span class="badge badge-${k.state}">${k.state}</span></div>
179+
<div class="label">Object Type</div><div class="val">${k.object_type===2?'Symmetric Key':k.object_type===3?'Public Key':k.object_type===4?'Private Key':'Type '+k.object_type}</div>
180+
<div class="label">Usage Mask</div><div class="val mono">0x${(k.usage_mask||0).toString(16).padStart(8,'0')}</div>
181+
<div class="label">Created</div><div class="val mono">${k.created_at||''}</div>
182+
</div>
183+
<div style="margin-top:16px;display:flex;gap:8px">
184+
${k.state==='pre-active'?'<button class="btn btn-primary btn-sm" onclick="activateKey(\''+k.uid+'\')">Activate</button>':''}
185+
${k.state==='active'?'<button class="btn btn-danger btn-sm" onclick="revokeKey(\''+k.uid+'\')">Revoke</button>':''}
186+
<button class="btn btn-sm" style="background:var(--surface2);color:var(--text)" onclick="showPage('keys')">Back</button>
187+
</div>
188+
</div>
189+
`;
190+
}
191+
192+
function showCreateKey() {
193+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
194+
document.getElementById('page-create-key').classList.add('active');
195+
}
196+
197+
async function createKey() {
198+
const name = document.getElementById('ck-name').value;
199+
const algo = document.getElementById('ck-algo').value;
200+
const length = parseInt(document.getElementById('ck-length').value);
201+
if(!name) { alert('Name required'); return; }
202+
const result = await apiPost('/v1/keys', {name, algorithm:algo, length});
203+
if(result.uid) {
204+
showKeyDetail(result.uid);
205+
} else {
206+
alert(result.error || 'Failed');
207+
}
208+
}
209+
210+
async function activateKey(uid) {
211+
await apiPost('/v1/keys/'+uid+'/activate', {});
212+
loadKeys();
213+
}
214+
215+
async function revokeKey(uid) {
216+
if(!confirm('Revoke key '+uid+'?')) return;
217+
await apiPost('/v1/keys/'+uid+'/revoke', {reason:1});
218+
loadKeys();
219+
}
220+
221+
// Connections
222+
async function loadConnections() {
223+
const data = await api('/v1/connections');
224+
const conns = data.connections || [];
225+
document.getElementById('conn-table').innerHTML = conns.map(c => `
226+
<tr>
227+
<td>${c.client_cn||'unknown'}</td>
228+
<td class="mono">${c.remote_addr}</td>
229+
<td class="mono">${(c.connected_at||'').replace('T',' ').split('.')[0]}</td>
230+
<td>${c.operations}</td>
231+
<td>${c.last_op||''}</td>
232+
</tr>
233+
`).join('') || '<tr><td colspan="5" class="loading">No active connections</td></tr>';
234+
}
235+
236+
// Audit
237+
async function loadAudit() {
238+
const data = await api('/v1/audit?limit=100');
239+
const entries = data.entries || [];
240+
document.getElementById('audit-list').innerHTML = entries.map(e => `
241+
<div class="audit-entry">
242+
<div>
243+
<span class="audit-action">${e.operation}</span>
244+
<span class="mono" style="margin-left:8px">${e.object_uid||''}</span>
245+
<span class="badge badge-${e.status==='success'?'active':'revoked'}" style="margin-left:8px;font-size:10px">${e.status}</span>
246+
<span class="audit-time" style="float:right">${(e.timestamp||'').replace('T',' ').split('.')[0]}</span>
247+
</div>
248+
<div class="audit-details">${e.source}${e.client_id||'anonymous'}${e.message||''}</div>
249+
</div>
250+
`).join('') || '<div class="loading">No audit events</div>';
251+
}
252+
253+
// Init
254+
loadOverview();
255+
</script>
256+
</body>
4257
</html>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
:root {
2+
--bg: #0f1117;
3+
--surface: #1a1d27;
4+
--surface2: #242836;
5+
--border: #2e3344;
6+
--text: #e4e5e9;
7+
--text2: #8b8fa3;
8+
--accent: #6366f1;
9+
--accent2: #818cf8;
10+
--green: #22c55e;
11+
--yellow: #eab308;
12+
--red: #ef4444;
13+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
14+
--mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
15+
}
16+
17+
* { margin: 0; padding: 0; box-sizing: border-box; }
18+
body { background: var(--bg); color: var(--text); font-family: var(--font); font-size: 14px; line-height: 1.5; }
19+
a { color: var(--accent2); text-decoration: none; }
20+
a:hover { text-decoration: underline; }
21+
22+
.shell { display: flex; min-height: 100vh; }
23+
24+
/* Sidebar */
25+
.sidebar { width: 220px; background: var(--surface); border-right: 1px solid var(--border); padding: 20px 0; flex-shrink: 0; }
26+
.sidebar .logo { padding: 0 20px 20px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
27+
.sidebar .logo h1 { font-size: 15px; font-weight: 600; }
28+
.sidebar .logo span { font-size: 11px; color: var(--text2); }
29+
.sidebar nav a { display: block; padding: 8px 20px; color: var(--text2); font-size: 13px; transition: all 0.15s; }
30+
.sidebar nav a:hover, .sidebar nav a.active { color: var(--text); background: var(--surface2); text-decoration: none; }
31+
.sidebar nav a.active { border-left: 2px solid var(--accent); }
32+
33+
/* Main */
34+
.main { flex: 1; padding: 24px 32px; overflow-x: auto; }
35+
.main h2 { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
36+
37+
/* Stats cards */
38+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 24px; }
39+
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
40+
.stat .label { font-size: 12px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; }
41+
.stat .value { font-size: 28px; font-weight: 700; margin-top: 4px; font-family: var(--mono); }
42+
.stat .value.green { color: var(--green); }
43+
.stat .value.yellow { color: var(--yellow); }
44+
.stat .value.red { color: var(--red); }
45+
46+
/* Table */
47+
table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
48+
thead { background: var(--surface2); }
49+
th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text2); text-align: left; padding: 10px 14px; font-weight: 600; }
50+
td { padding: 10px 14px; border-top: 1px solid var(--border); font-size: 13px; }
51+
tr:hover td { background: var(--surface2); }
52+
53+
/* Badges */
54+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
55+
.badge-active { background: rgba(34,197,94,0.15); color: var(--green); }
56+
.badge-revoked, .badge-compromised { background: rgba(239,68,68,0.15); color: var(--red); }
57+
.badge-expired, .badge-deactivated { background: rgba(234,179,8,0.15); color: var(--yellow); }
58+
.badge-pre-active { background: rgba(99,102,241,0.15); color: var(--accent2); }
59+
.badge-root, .badge-intermediate { background: rgba(99,102,241,0.1); color: var(--accent); }
60+
61+
/* Utilities */
62+
.mono { font-family: var(--mono); font-size: 12px; color: var(--text2); }
63+
.truncate { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64+
65+
/* Buttons */
66+
.btn { display: inline-block; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 500; border: none; cursor: pointer; transition: all 0.15s; }
67+
.btn-primary { background: var(--accent); color: white; }
68+
.btn-primary:hover { background: var(--accent2); }
69+
.btn-danger { background: var(--red); color: white; }
70+
.btn-sm { padding: 3px 10px; font-size: 12px; }
71+
72+
/* Detail panel */
73+
.detail { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px; margin-bottom: 16px; }
74+
.detail h3 { font-size: 16px; margin-bottom: 12px; }
75+
.detail-grid { display: grid; grid-template-columns: 140px 1fr; gap: 8px; }
76+
.detail-grid .label { color: var(--text2); font-size: 12px; }
77+
.detail-grid .val { font-size: 13px; word-break: break-all; }
78+
79+
/* Filter tabs */
80+
.filter-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
81+
82+
/* Audit */
83+
.audit-entry { padding: 10px 0; border-bottom: 1px solid var(--border); }
84+
.audit-entry:last-child { border-bottom: none; }
85+
.audit-action { font-weight: 600; }
86+
.audit-time { font-size: 12px; color: var(--text2); font-family: var(--mono); }
87+
.audit-details { font-size: 12px; color: var(--text2); margin-top: 2px; }
88+
89+
/* Pages */
90+
.page { display: none; }
91+
.page.active { display: block; }
92+
.loading { text-align: center; padding: 40px; color: var(--text2); }
93+
.section { margin-bottom: 24px; }

0 commit comments

Comments
 (0)