|
1 | 1 | <!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> |
4 | 257 | </html> |
0 commit comments