387 lines
18 KiB
HTML
387 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API Keys — Nexus One AI</title>
|
|
<link rel="stylesheet" href="style.css?v=4">
|
|
<style>
|
|
.ak-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:28px; margin-bottom:28px; }
|
|
.ak-card-title { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:4px; }
|
|
.ak-card-sub { font-size:13px; color:var(--lt); margin-bottom:20px; }
|
|
|
|
.ak-btn { padding:9px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; font-family:inherit; transition:.15s; }
|
|
.ak-btn.primary { background:var(--teal); color:var(--ink); }
|
|
.ak-btn.primary:hover { background:#0B7A70; }
|
|
.ak-btn.danger { background:rgba(185,28,28,.08); color:#B91C1C; border:1px solid rgba(239,68,68,.25); }
|
|
.ak-btn.danger:hover { background:#DC2626; color:var(--ink); }
|
|
.ak-btn:disabled { opacity:.45; cursor:not-allowed; }
|
|
|
|
.ak-table-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
|
|
table.ak-table { width:100%; border-collapse:collapse; }
|
|
.ak-table th { background:rgba(255,255,255,.03); padding:10px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--lt); border-bottom:1px solid var(--bdr); }
|
|
.ak-table td { padding:12px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:middle; }
|
|
.ak-table tr:last-child td { border-bottom:none; }
|
|
.ak-table tr:hover td { background:rgba(255,255,255,.03); }
|
|
.ak-empty { text-align:center; color:var(--lt); padding:40px 0; font-size:13px; }
|
|
|
|
.ak-key-code { font-family:monospace; font-size:12px; background:rgba(255,255,255,.03); padding:3px 8px; border-radius:5px; color:var(--med); letter-spacing:.5px; }
|
|
.badge-active { display:inline-block; font-size:10px; font-weight:700; padding:2px 9px; border-radius:10px; background:rgba(34,197,94,.15); color:#15803D; text-transform:uppercase; }
|
|
.badge-revoked { display:inline-block; font-size:10px; font-weight:700; padding:2px 9px; border-radius:10px; background:#F3F4F6; color:#6B7280; text-transform:uppercase; }
|
|
.badge-expired { display:inline-block; font-size:10px; font-weight:700; padding:2px 9px; border-radius:10px; background:#FEE2E2; color:#B91C1C; text-transform:uppercase; }
|
|
|
|
/* Tier gate */
|
|
.tier-gate { background:#FFFBEB; border:1px solid #FDE68A; border-radius:12px; padding:28px; text-align:center; }
|
|
.tier-gate h3 { color:#92400E; margin:0 0 8px; }
|
|
.tier-gate p { color:#B45309; font-size:13px; margin:0; }
|
|
|
|
/* Reveal box */
|
|
.ak-reveal { background:#0F1117; border-radius:10px; padding:16px 20px; font-family:monospace; font-size:13px; color:#A3E635; margin:16px 0; word-break:break-all; letter-spacing:.5px; }
|
|
.ak-reveal-label { font-size:12px; color:#EF4444; font-weight:700; margin-bottom:6px; }
|
|
|
|
/* Modal */
|
|
.ak-modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:1000; display:none; align-items:center; justify-content:center; padding:20px; }
|
|
.ak-modal-bg.open { display:flex; }
|
|
.ak-modal { background:var(--navy2); border-radius:16px; padding:32px; width:100%; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,.25); }
|
|
.ak-modal h3 { margin:0 0 20px; font-size:18px; color:var(--ink); }
|
|
.ak-modal label { display:block; font-size:12px; font-weight:600; color:var(--med); margin-bottom:5px; }
|
|
.ak-modal input, .ak-modal select {
|
|
width:100%; padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
|
|
font-size:13px; font-family:inherit; outline:none; color:var(--ink); box-sizing:border-box; margin-bottom:14px;
|
|
}
|
|
.ak-modal input:focus, .ak-modal select:focus { border-color:var(--teal); }
|
|
.ak-modal-footer { display:flex; gap:10px; justify-content:flex-end; margin-top:4px; }
|
|
.ak-modal-footer button { padding:9px 20px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; font-family:inherit; }
|
|
.ak-modal-footer .save { background:var(--teal); color:var(--ink); }
|
|
.ak-modal-footer .save:hover { background:#0B7A70; }
|
|
.ak-modal-footer .cancel { background:rgba(255,255,255,.03); color:var(--med); border:1px solid var(--bdr); }
|
|
.ak-modal-footer .cancel:hover { border-color:var(--teal); color:var(--teal); }
|
|
|
|
/* Toast */
|
|
#ak-toast { position:fixed; bottom:24px; right:24px; background:#1E3A5F; color:var(--ink); padding:12px 20px; border-radius:10px; font-size:13px; display:none; z-index:9999; box-shadow:0 8px 24px rgba(0,0,0,.2); max-width:340px; }
|
|
#ak-toast.err { background:#7F1D1D; }
|
|
</style>
|
|
</head>
|
|
<body data-role="admin">
|
|
|
|
<header class="topnav">
|
|
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
|
<nav>
|
|
<a href="index.html">Home</a>
|
|
<a href="quickstart.html">Quick Start</a>
|
|
<a href="prompts.html">Prompt Library</a>
|
|
<a href="usecases.html">Use Cases</a>
|
|
<span class="nav-sep"></span>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn">Help ▾</button>
|
|
<div class="nav-drop-menu">
|
|
<span class="nav-drop-cat">LEARN /</span>
|
|
<a href="quickstart.html">Quick Start</a>
|
|
<a href="models.html">Models</a>
|
|
<span class="nav-drop-cat">SUPPORT /</span>
|
|
<a href="troubleshooting.html">Troubleshoot</a>
|
|
<a href="faq.html">FAQ</a>
|
|
<span class="nav-drop-cat">MORE /</span>
|
|
<a href="glossary.html">Glossary</a>
|
|
<a href="whats-new.html">What's New</a>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn active">Admin ▾</button>
|
|
<div class="nav-drop-menu nav-drop-menu-wide">
|
|
<span class="nav-drop-cat">DOCS /</span>
|
|
<a href="security.html">Security & Privacy</a>
|
|
<a href="admin.html">Admin Guide</a>
|
|
<span class="nav-drop-cat">MONITOR /</span>
|
|
<a href="dashboard.html">Dashboard</a>
|
|
<a href="analytics.html">Usage Analytics</a>
|
|
<a href="audit.html">Audit Log</a>
|
|
<a href="feedback.html">Feedback & Ratings</a>
|
|
<span class="nav-drop-cat">MANAGE /</span>
|
|
<a href="users.html">Users</a>
|
|
<a href="teams.html">Teams</a>
|
|
<a href="models-admin.html">Model Manager</a>
|
|
<a href="training.html">Training</a>
|
|
<a href="knowledge.html">Knowledge Base</a>
|
|
<span class="nav-drop-cat">TOOLS /</span>
|
|
<a href="apikeys.html">API Keys</a>
|
|
<a href="benchmark.html">Benchmarking</a>
|
|
<a href="model-compare.html">Model Compare</a>
|
|
<a href="api-playground.html">API Playground</a>
|
|
<a href="guardrails.html">Guardrails</a>
|
|
<a href="rag-quality.html">RAG Quality</a>
|
|
<a href="router.html">Model Router</a>
|
|
<a href="connectors.html">Connectors</a>
|
|
<span class="nav-drop-cat">SYSTEM /</span>
|
|
<a href="console.html">Console</a>
|
|
<a href="settings.html">Settings</a>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn">AI Tools ▾</button>
|
|
<div class="nav-drop-menu">
|
|
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
|
<a href="documents.html">Document Intelligence</a>
|
|
<a href="chat-multi.html">Multimodal Chat</a>
|
|
<a href="prompt-studio.html">Prompt Studio</a>
|
|
<a href="meeting.html">Meeting Assistant</a>
|
|
<span class="nav-drop-cat">AUTOMATION /</span>
|
|
<a href="agents.html">Agent Builder</a>
|
|
<a href="schedules.html">Scheduled Jobs</a>
|
|
<a href="workflows.html">Workflow Automation</a>
|
|
<span class="nav-drop-cat">QUALITY /</span>
|
|
<a href="evals.html">AI Eval Suite</a>
|
|
<a href="chatrooms.html">Chat Rooms</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<a href="notifications.html" style="position:relative">🔔</a>
|
|
<span class="badge" data-brand="tier">Basic Tier</span>
|
|
<div id="nav-org-logo" class="nav-org-logo"></div>
|
|
</header>
|
|
|
|
<div class="page-hero">
|
|
<div class="label">Admin · Integrations</div>
|
|
<h1>API Key Manager</h1>
|
|
<p>Generate API keys for programmatic access to Nexus One AI from scripts, pipelines, and external tools.</p>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<!-- Tier gate — shown only on Entry tier -->
|
|
<div class="tier-gate" id="tier-gate" style="display:none">
|
|
<h3>🔒 Pro Tier & Above</h3>
|
|
<p>API Key management is available on Mid and Advanced tiers. Upgrade to enable programmatic access for pipelines and integrations.</p>
|
|
</div>
|
|
|
|
<!-- Main content — hidden on Entry tier -->
|
|
<div id="ak-main" style="display:none">
|
|
|
|
<!-- Create card -->
|
|
<div class="ak-card">
|
|
<div class="ak-card-title">🔑 Your API Keys</div>
|
|
<div class="ak-card-sub">Keys authenticate requests to <code>/api/*</code> via <code>Authorization: Bearer <key></code>. Each key is shown only once on creation.</div>
|
|
<button class="ak-btn primary" onclick="openModal()">+ Generate New Key</button>
|
|
</div>
|
|
|
|
<!-- Keys table -->
|
|
<div class="ak-card">
|
|
<div class="ak-card-title" id="keys-title">API Keys</div>
|
|
<div id="keys-list"><div class="ak-empty">Loading…</div></div>
|
|
</div>
|
|
|
|
<!-- Usage example -->
|
|
<div class="ak-card">
|
|
<div class="ak-card-title">📖 How to Use</div>
|
|
<div class="ak-card-sub">Pass your API key in the <code>Authorization</code> header on every request.</div>
|
|
<div class="ak-reveal">
|
|
<div class="ak-reveal-label" style="color:#A3E635">cURL example</div>
|
|
curl -H "Authorization: Bearer czk_your_key_here" \
|
|
https://ai.local/api/prompts</div>
|
|
<div style="font-size:13px;color:var(--lt);margin-top:12px">
|
|
Keys can also be verified at <code>/api/apikeys/verify</code> — useful for custom middleware or integrations that need to validate a key before proxying requests to Ollama or Open WebUI.
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- end ak-main -->
|
|
|
|
</div>
|
|
|
|
<!-- Create key modal -->
|
|
<div class="ak-modal-bg" id="ak-modal-bg">
|
|
<div class="ak-modal">
|
|
<h3>Generate API Key</h3>
|
|
<label>Key Name <span style="font-weight:400;color:var(--lt)">(e.g. "CI pipeline", "RAG script")</span></label>
|
|
<input type="text" id="m-name" placeholder="My Integration">
|
|
<label>Expiry</label>
|
|
<select id="m-expiry">
|
|
<option value="">Never</option>
|
|
<option value="30">30 days</option>
|
|
<option value="90">90 days</option>
|
|
<option value="180">180 days</option>
|
|
<option value="365">1 year</option>
|
|
</select>
|
|
<div class="ak-modal-footer">
|
|
<button class="cancel" onclick="closeModal()">Cancel</button>
|
|
<button class="save" onclick="generateKey()">Generate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key reveal modal -->
|
|
<div class="ak-modal-bg" id="ak-reveal-bg">
|
|
<div class="ak-modal">
|
|
<h3>🔑 Your New API Key</h3>
|
|
<div style="font-size:13px;color:var(--lt);margin-bottom:12px">This key will not be shown again. Copy it now and store it securely.</div>
|
|
<div class="ak-reveal">
|
|
<div class="ak-reveal-label">SECRET KEY — COPY NOW</div>
|
|
<span id="reveal-key"></span>
|
|
</div>
|
|
<button class="ak-btn primary" onclick="copyKey()" style="width:100%;margin-bottom:10px" id="copy-key-btn">📋 Copy Key</button>
|
|
<div class="ak-modal-footer">
|
|
<button class="save" onclick="closeReveal()">Done</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ak-toast"></div>
|
|
|
|
<footer>
|
|
<p>Nexus One AI · Powered by Cezen · <span data-brand="tier">Basic Tier</span></p>
|
|
<p>Questions? <a href="mailto:support@cezentech.com">support@cezentech.com</a> · <a href="https://cezentech.com" target="_blank">cezentech.com</a></p>
|
|
</footer>
|
|
|
|
<script>
|
|
const _API = '/api';
|
|
let allKeys = [];
|
|
let newKeyValue = '';
|
|
|
|
// ── Tier gate ─────────────────────────────────────────────────────────────────
|
|
// Runs after auth so we know the tier label
|
|
function checkTier() {
|
|
// Tier gate disabled for testing — all features visible
|
|
document.getElementById("tier-gate").style.display = "none";
|
|
var main = document.getElementById("ak-main");
|
|
if (main) main.style.display = "";
|
|
}
|
|
|
|
// ── Load ──────────────────────────────────────────────────────────────────────
|
|
async function loadKeys() {
|
|
try {
|
|
const res = await fetch(`${_API}/apikeys`);
|
|
if (!res.ok) throw new Error('Failed');
|
|
allKeys = await res.json();
|
|
renderKeys();
|
|
} catch(e) {
|
|
document.getElementById('keys-list').innerHTML = '<div class="ak-empty">Failed to load keys</div>';
|
|
}
|
|
}
|
|
|
|
function renderKeys() {
|
|
const el = document.getElementById('keys-list');
|
|
const isAdmin = window.cezenUser?.role === 'admin';
|
|
document.getElementById('keys-title').textContent = isAdmin ? 'All API Keys' : 'My API Keys';
|
|
if (!allKeys.length) {
|
|
el.innerHTML = '<div class="ak-empty">No API keys yet. Generate one above.</div>'; return;
|
|
}
|
|
const now = new Date().toISOString();
|
|
let html = `<div class="ak-table-wrap"><table class="ak-table">
|
|
<tr>
|
|
<th>Name</th>
|
|
${isAdmin ? '<th>User</th>' : ''}
|
|
<th>Prefix</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Last Used</th>
|
|
<th>Expires</th>
|
|
<th></th>
|
|
</tr>`;
|
|
allKeys.forEach(k => {
|
|
const expired = k.expires_at && now > k.expires_at;
|
|
const statusBadge = !k.is_active ? '<span class="badge-revoked">Revoked</span>'
|
|
: expired ? '<span class="badge-expired">Expired</span>'
|
|
: '<span class="badge-active">Active</span>';
|
|
html += `<tr>
|
|
<td><strong>${escHtml(k.name)}</strong></td>
|
|
${isAdmin ? `<td style="color:var(--lt)">${escHtml(k.username)}</td>` : ''}
|
|
<td><span class="ak-key-code">${escHtml(k.key_prefix)}…</span></td>
|
|
<td>${statusBadge}</td>
|
|
<td style="color:var(--lt)">${fmtDate(k.created_at)}</td>
|
|
<td style="color:var(--lt)">${k.last_used_at ? fmtDate(k.last_used_at) : '—'}</td>
|
|
<td style="color:var(--lt)">${k.expires_at ? fmtDate(k.expires_at) : 'Never'}</td>
|
|
<td>
|
|
${k.is_active ? `<button class="ak-btn danger" style="padding:5px 12px;font-size:11px" onclick="revokeKey(${k.id},'${escHtml(k.name).replace(/'/g,"\\'")}')">Revoke</button>` : ''}
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
html += '</table></div>';
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
// ── Modal ─────────────────────────────────────────────────────────────────────
|
|
function openModal() {
|
|
document.getElementById('m-name').value = '';
|
|
document.getElementById('m-expiry').value = '';
|
|
document.getElementById('ak-modal-bg').classList.add('open');
|
|
setTimeout(() => document.getElementById('m-name').focus(), 50);
|
|
}
|
|
function closeModal() { document.getElementById('ak-modal-bg').classList.remove('open'); }
|
|
function closeReveal() {
|
|
document.getElementById('ak-reveal-bg').classList.remove('open');
|
|
newKeyValue = '';
|
|
}
|
|
|
|
async function generateKey() {
|
|
const name = document.getElementById('m-name').value.trim();
|
|
if (!name) { toast('Key name is required', true); return; }
|
|
const expiry = document.getElementById('m-expiry').value;
|
|
const body = { name, expires_days: expiry ? parseInt(expiry) : null };
|
|
try {
|
|
const res = await fetch(`${_API}/apikeys`, {
|
|
method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
|
|
});
|
|
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
|
|
const data = await res.json();
|
|
closeModal();
|
|
newKeyValue = data.key;
|
|
document.getElementById('reveal-key').textContent = data.key;
|
|
document.getElementById('ak-reveal-bg').classList.add('open');
|
|
allKeys.unshift({
|
|
id: data.id, name: data.name, username: window.cezenUser?.username,
|
|
key_prefix: data.key_prefix, is_active: 1,
|
|
created_at: data.created_at, last_used_at: null, expires_at: data.expires_at
|
|
});
|
|
renderKeys();
|
|
} catch(e) { toast(e.message, true); }
|
|
}
|
|
|
|
function copyKey() {
|
|
navigator.clipboard.writeText(newKeyValue).then(() => {
|
|
const btn = document.getElementById('copy-key-btn');
|
|
btn.textContent = '✓ Copied!';
|
|
setTimeout(() => btn.textContent = '📋 Copy Key', 2000);
|
|
});
|
|
}
|
|
|
|
async function revokeKey(id, name) {
|
|
if (!confirm(`Revoke key "${name}"? Any integrations using it will stop working immediately.`)) return;
|
|
try {
|
|
const res = await fetch(`${_API}/apikeys/${id}`, { method: 'DELETE' });
|
|
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
|
|
allKeys = allKeys.map(k => k.id === id ? {...k, is_active: 0} : k);
|
|
renderKeys();
|
|
toast('Key revoked');
|
|
} catch(e) { toast(e.message, true); }
|
|
}
|
|
|
|
// ── Utils ─────────────────────────────────────────────────────────────────────
|
|
function fmtDate(iso) {
|
|
if (!iso) return '—';
|
|
return new Date(iso).toLocaleDateString('en-IN', {day:'2-digit',month:'short',year:'numeric'});
|
|
}
|
|
function escHtml(s) {
|
|
return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
function toast(msg, err) {
|
|
const el = document.getElementById('ak-toast');
|
|
el.textContent = msg;
|
|
el.className = err ? 'err' : '';
|
|
el.style.display = 'block';
|
|
setTimeout(() => el.style.display = 'none', 3000);
|
|
}
|
|
|
|
document.getElementById('ak-modal-bg').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(); });
|
|
document.getElementById('ak-reveal-bg').addEventListener('click', e => { if (e.target === e.currentTarget) closeReveal(); });
|
|
</script>
|
|
|
|
<script src="auth.js"></script>
|
|
<script>
|
|
document.addEventListener('cezenAuthReady', function() {
|
|
// Wait for branding to apply tier label, then check
|
|
setTimeout(checkTier, 200);
|
|
});
|
|
</script>
|
|
<script src="branding.js"></script>
|
|
</body>
|
|
</html>
|