380 lines
17 KiB
HTML
380 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>User Management — Nexus One AI</title>
|
|
<link rel="stylesheet" href="style.css?v=4">
|
|
<style>
|
|
.um-toolbar { display:flex; align-items:center; gap:12px; margin-bottom:24px; flex-wrap:wrap; }
|
|
.um-btn { padding:9px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; border:none; transition:all .15s; font-family:inherit; }
|
|
.um-btn.primary { background:var(--teal); color:var(--ink); }
|
|
.um-btn.primary:hover { background:#0B7A70; }
|
|
.um-btn.danger { background:rgba(185,28,28,.08); color:#B91C1C; border:1px solid rgba(239,68,68,.25); }
|
|
.um-btn.danger:hover { background:#DC2626; color:var(--ink); }
|
|
.um-btn.ghost { background:rgba(255,255,255,.03); color:var(--med); border:1px solid var(--bdr); }
|
|
.um-btn.ghost:hover { border-color:var(--teal); color:var(--teal); }
|
|
.um-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
|
|
.um-table-wrap { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; overflow:hidden; }
|
|
table.um-table { width:100%; border-collapse:collapse; }
|
|
.um-table th { background:rgba(255,255,255,.03); padding:11px 16px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:var(--lt); border-bottom:1px solid var(--bdr); }
|
|
.um-table td { padding:13px 16px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:middle; }
|
|
.um-table tr:last-child td { border-bottom:none; }
|
|
.um-table tr:hover td { background:rgba(255,255,255,.03); }
|
|
|
|
.role-pill { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:10px; text-transform:uppercase; letter-spacing:.4px; }
|
|
.role-pill.admin { background:var(--teal); color:var(--ink); }
|
|
.role-pill.user { background:#3B82F6; color:var(--ink); }
|
|
|
|
.status-pill { display:inline-flex; align-items:center; gap:5px; font-size:12px; font-weight:600; }
|
|
.status-pill.active { color:#15803D; }
|
|
.status-pill.inactive { color:#B91C1C; }
|
|
.status-dot { width:7px; height:7px; border-radius:50%; }
|
|
.status-dot.active { background:#22C55E; }
|
|
.status-dot.inactive { background:#EF4444; }
|
|
|
|
.um-actions { display:flex; gap:6px; }
|
|
|
|
/* Modal */
|
|
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:1000; align-items:center; justify-content:center; }
|
|
.modal-overlay.open { display:flex; }
|
|
.modal { background:var(--navy2); border-radius:16px; padding:32px; width:100%; max-width:440px; box-shadow:0 24px 60px rgba(0,0,0,.3); }
|
|
.modal-title { font-size:18px; font-weight:700; margin-bottom:6px; }
|
|
.modal-sub { font-size:13px; color:var(--lt); margin-bottom:24px; }
|
|
.modal-field { margin-bottom:16px; }
|
|
.modal-label { display:block; font-size:13px; font-weight:600; color:var(--med); margin-bottom:5px; }
|
|
.modal-input, .modal-select { width:100%; padding:10px 12px; border:1.5px solid var(--bdr); border-radius:8px; font-size:14px; font-family:inherit; outline:none; color:var(--ink); }
|
|
.modal-input:focus, .modal-select:focus { border-color:var(--teal); }
|
|
.modal-actions { display:flex; gap:10px; margin-top:20px; justify-content:flex-end; }
|
|
.modal-error { background:rgba(185,28,28,.08); border:1px solid rgba(239,68,68,.25); color:#B91C1C; padding:9px 12px; border-radius:7px; font-size:12px; margin-bottom:14px; display:none; }
|
|
.modal-error.show { display:block; }
|
|
|
|
/* Sessions section */
|
|
.sessions-table td { font-family:monospace; font-size:12px; }
|
|
.sessions-table .sname { font-family:inherit; font-size:13px; font-weight:600; }
|
|
.revoke-btn { font-size:11px; padding:4px 10px; background:rgba(185,28,28,.08); color:#B91C1C; border:1px solid rgba(239,68,68,.25); border-radius:6px; cursor:pointer; font-family:inherit; }
|
|
.revoke-btn:hover { background:#DC2626; color:var(--ink); }
|
|
</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" class="active">Users</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 · User Management</div>
|
|
<h1>Users & Sessions</h1>
|
|
<p>Create and manage user accounts, assign roles, and view or revoke active sessions.</p>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<div class="section-title">User Accounts</div>
|
|
<div class="um-toolbar">
|
|
<button class="um-btn primary" onclick="openCreate()">+ New User</button>
|
|
<button class="um-btn ghost" onclick="loadAll()">↺ Refresh</button>
|
|
</div>
|
|
|
|
<div class="um-table-wrap">
|
|
<table class="um-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>Last Login</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="users-tbody">
|
|
<tr><td colspan="6" style="color:var(--lt);text-align:center;padding:24px">Loading…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section-title" style="margin-top:40px">Active Sessions</div>
|
|
<div class="um-table-wrap">
|
|
<table class="um-table sessions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Role</th>
|
|
<th>IP Address</th>
|
|
<th>Signed In</th>
|
|
<th>Expires</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="sessions-tbody">
|
|
<tr><td colspan="6" style="color:var(--lt);text-align:center;padding:24px">Loading…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- CREATE USER MODAL -->
|
|
<div class="modal-overlay" id="modal-create">
|
|
<div class="modal">
|
|
<div class="modal-title">New User</div>
|
|
<div class="modal-sub">Create a new portal account. The user will be prompted to change their password on first login.</div>
|
|
<div class="modal-error" id="create-err"></div>
|
|
<div class="modal-field">
|
|
<label class="modal-label">Username</label>
|
|
<input class="modal-input" id="new-username" type="text" placeholder="e.g. jsmith" autocomplete="off">
|
|
</div>
|
|
<div class="modal-field">
|
|
<label class="modal-label">Email (optional)</label>
|
|
<input class="modal-input" id="new-email" type="email" placeholder="jsmith@agency.gov">
|
|
</div>
|
|
<div class="modal-field">
|
|
<label class="modal-label">Temporary Password</label>
|
|
<input class="modal-input" id="new-password" type="text" placeholder="Temporary password" value="Cezen@2024!">
|
|
</div>
|
|
<div class="modal-field">
|
|
<label class="modal-label">Role</label>
|
|
<select class="modal-select" id="new-role">
|
|
<option value="user">User — can use the AI portal</option>
|
|
<option value="admin">Admin — full access including this panel</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="um-btn ghost" onclick="closeModals()">Cancel</button>
|
|
<button class="um-btn primary" onclick="createUser()">Create User</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RESET PASSWORD MODAL -->
|
|
<div class="modal-overlay" id="modal-reset">
|
|
<div class="modal">
|
|
<div class="modal-title">Reset Password</div>
|
|
<div class="modal-sub">Set a new temporary password for <strong id="reset-username-lbl"></strong>. They will be prompted to change it on next login.</div>
|
|
<div class="modal-error" id="reset-err"></div>
|
|
<div class="modal-field">
|
|
<label class="modal-label">New Password</label>
|
|
<input class="modal-input" id="reset-password" type="text" value="Cezen@2024!">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="um-btn ghost" onclick="closeModals()">Cancel</button>
|
|
<button class="um-btn primary" onclick="doReset()">Reset Password</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<p>Nexus One AI · Powered by Cezen · Basic Tier</p>
|
|
</footer>
|
|
|
|
<script>
|
|
const _API = '/api';
|
|
let resetTargetId = null;
|
|
let meId = null;
|
|
|
|
function fmt(iso) {
|
|
if (!iso) return '—';
|
|
// Backend may return +00:00 or no tz — normalise to Z
|
|
const s = iso.replace(/\+00:00$/, 'Z').replace(/\.\d+Z$/, 'Z');
|
|
return new Date(s).toLocaleString([], {dateStyle:'short', timeStyle:'short'});
|
|
}
|
|
|
|
async function loadUsers() {
|
|
const data = await fetch(`${_API}/users`, { credentials:'include' }).then(r => r.json()).catch(() => []);
|
|
const rows = Array.isArray(data) ? data : (Array.isArray(data.users) ? data.users : []);
|
|
const tb = document.getElementById('users-tbody');
|
|
if (!rows.length) {
|
|
tb.innerHTML = '<tr><td colspan="6" style="color:var(--lt);text-align:center;padding:24px">No users available.</td></tr>';
|
|
return;
|
|
}
|
|
tb.innerHTML = rows.map(u => `
|
|
<tr>
|
|
<td><strong>${u.username}</strong></td>
|
|
<td>${u.email || '—'}</td>
|
|
<td><span class="role-pill ${u.role}">${u.role}</span></td>
|
|
<td><span class="status-pill ${u.is_active ? 'active' : 'inactive'}">
|
|
<span class="status-dot ${u.is_active ? 'active' : 'inactive'}"></span>
|
|
${u.is_active ? 'Active' : 'Disabled'}
|
|
</span></td>
|
|
<td>${fmt(u.last_login)}</td>
|
|
<td>
|
|
<div class="um-actions">
|
|
<button class="um-btn ghost" onclick="openReset(${u.id},'${u.username}')">Reset PW</button>
|
|
<button class="um-btn ghost" onclick="toggleUser(${u.id}, ${u.is_active})">${u.is_active ? 'Disable' : 'Enable'}</button>
|
|
${u.id !== meId ? `<button class="um-btn danger" onclick="deleteUser(${u.id},'${u.username}')">Delete</button>` : ''}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function loadSessions() {
|
|
const data = await fetch(`${_API}/users/sessions`, { credentials:'include' }).then(r => r.json()).catch(() => ({}));
|
|
const rows = data.sessions || [];
|
|
const tb = document.getElementById('sessions-tbody');
|
|
if (!rows.length) {
|
|
tb.innerHTML = '<tr><td colspan="6" style="color:var(--lt);text-align:center;padding:20px">No active sessions</td></tr>';
|
|
return;
|
|
}
|
|
tb.innerHTML = rows.map(s => {
|
|
const lastSeen = s.last_login ? fmt(s.last_login) : '—';
|
|
// Estimate expiry: last_login + 8h
|
|
let expiry = '—';
|
|
if (s.last_login) {
|
|
const exp = new Date(s.last_login.replace(/\+00:00$/, 'Z').replace(/\.\d+Z$/, 'Z'));
|
|
exp.setHours(exp.getHours() + 8);
|
|
expiry = exp.toLocaleString();
|
|
}
|
|
return `<tr>
|
|
<td class="sname">${s.username}</td>
|
|
<td><span class="role-pill ${s.role}">${s.role}</span></td>
|
|
<td>—</td>
|
|
<td>${lastSeen}</td>
|
|
<td>${expiry}</td>
|
|
<td><span style="font-size:11px;color:var(--lt)">n/a</span></td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function loadAll() {
|
|
await Promise.all([loadUsers(), loadSessions()]);
|
|
}
|
|
|
|
async function createUser() {
|
|
const errEl = document.getElementById('create-err');
|
|
errEl.classList.remove('show');
|
|
const body = {
|
|
username: document.getElementById('new-username').value.trim(),
|
|
email: document.getElementById('new-email').value.trim() || null,
|
|
password: document.getElementById('new-password').value,
|
|
role: document.getElementById('new-role').value,
|
|
};
|
|
if (!body.username || !body.password) { errEl.textContent='Username and password are required.'; errEl.classList.add('show'); return; }
|
|
const res = await fetch(`${_API}/users`, { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
|
|
if (!res.ok) { const d = await res.json().catch(()=>({})); errEl.textContent = Array.isArray(d.detail) ? d.detail.map(e=>e.msg||String(e)).join(', ') : (d.detail||'Failed to create user.'); errEl.classList.add('show'); return; }
|
|
closeModals();
|
|
loadAll();
|
|
}
|
|
|
|
async function toggleUser(id, currentlyActive) {
|
|
await fetch(`${_API}/users/${id}`, { method:'PUT', credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({is_active: currentlyActive ? 0 : 1}) });
|
|
loadAll();
|
|
}
|
|
|
|
async function deleteUser(id, username) {
|
|
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
|
await fetch(`${_API}/users/${id}`, { method:'DELETE', credentials:'include' });
|
|
loadAll();
|
|
}
|
|
|
|
function openReset(id, username) {
|
|
resetTargetId = id;
|
|
document.getElementById('reset-username-lbl').textContent = username;
|
|
document.getElementById('modal-reset').classList.add('open');
|
|
}
|
|
|
|
async function doReset() {
|
|
const errEl = document.getElementById('reset-err');
|
|
const pw = document.getElementById('reset-password').value;
|
|
if (!pw) { errEl.textContent='Enter a new password.'; errEl.classList.add('show'); return; }
|
|
const res = await fetch(`${_API}/users/${resetTargetId}/reset-password`, { method:'POST', credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({new_password:pw}) });
|
|
if (!res.ok) { const d = await res.json().catch(()=>({})); errEl.textContent = Array.isArray(d.detail) ? d.detail.map(e=>e.msg||String(e)).join(', ') : (d.detail||'Failed.'); errEl.classList.add('show'); return; }
|
|
closeModals();
|
|
}
|
|
|
|
async function revokeSession(id) {
|
|
await fetch(`${_API}/users/sessions/${id}`, { method:'DELETE', credentials:'include' });
|
|
loadSessions();
|
|
}
|
|
|
|
function openCreate() { document.getElementById('modal-create').classList.add('open'); }
|
|
function closeModals() { document.querySelectorAll('.modal-overlay').forEach(m=>m.classList.remove('open')); }
|
|
|
|
// Close on backdrop click
|
|
document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', e => { if(e.target===o) closeModals(); }));
|
|
|
|
// Init
|
|
(async () => {
|
|
const me = await fetch(`${_API}/auth/me`, {credentials:'include'}).then(r=>r.json()).catch(()=>({}));
|
|
meId = me.id;
|
|
loadAll();
|
|
})();
|
|
</script>
|
|
<script src="auth.js"></script>
|
|
<script src="branding.js"></script>
|
|
</body>
|
|
</html>
|