231 lines
20 KiB
HTML
231 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>NetDesk — Admin Console</title>
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%233d7eff'/%3E%3Cpath d='M8 20l8-12 8 12-8-4z' fill='white'/%3E%3Crect x='12' y='18' width='8' height='7' rx='2' fill='white'/%3E%3C/svg%3E">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="site.css?v=2">
|
|
<style>
|
|
body{min-height:100vh}
|
|
.gate{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;text-align:center;padding:24px}
|
|
.gate h1{font-size:26px;letter-spacing:-.6px}
|
|
.gate p{color:var(--muted);max-width:420px}
|
|
.admin-shell{display:grid;grid-template-columns:248px 1fr;min-height:100vh}
|
|
.admin-side{position:sticky;top:0;height:100vh;border-right:1px solid var(--hair);background:color-mix(in srgb,var(--bg2) 70%,transparent);backdrop-filter:blur(12px);padding:20px 14px;display:flex;flex-direction:column}
|
|
.as-brand{display:flex;align-items:center;gap:10px;font-weight:800;font-size:18px;letter-spacing:-.4px;padding:4px 8px 18px}
|
|
.as-brand .nz-logo{width:30px;height:30px}
|
|
.as-nav{display:flex;flex-direction:column;gap:3px}
|
|
.as-nav a{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:9px;color:var(--muted);font-weight:600;font-size:14.5px;cursor:pointer;transition:.14s}
|
|
.as-nav a:hover{background:var(--hair);color:var(--fg)}
|
|
.as-nav a.on{background:var(--accent-dim);color:var(--fg)}
|
|
.as-nav a.on svg{color:var(--accent2)}
|
|
.as-nav svg{width:18px;height:18px;flex-shrink:0;opacity:.85}
|
|
.as-foot{margin-top:auto;padding-top:14px;border-top:1px solid var(--hair);display:flex;flex-direction:column;gap:3px}
|
|
.as-foot a{color:var(--faint);font-size:13.5px;padding:8px 12px;border-radius:8px}
|
|
.as-foot a:hover{color:var(--fg);background:var(--hair)}
|
|
.admin-main{padding:30px clamp(18px,4vw,44px);max-width:1200px}
|
|
.pg-head{margin-bottom:22px}
|
|
.pg-head h1{font-size:26px;letter-spacing:-.7px}
|
|
.pg-head p{color:var(--muted);font-size:14.5px;margin-top:4px}
|
|
.ov-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:16px}
|
|
.ov-card{border:1px solid transparent;border-radius:15px;padding:20px 22px;background:linear-gradient(var(--card),var(--card)) padding-box,linear-gradient(180deg,var(--hair-strong),rgba(255,255,255,.015)) border-box}
|
|
.ov-card b{display:block;font-size:30px;font-weight:850;letter-spacing:-1px;background:var(--accent-grad);-webkit-background-clip:text;background-clip:text;color:transparent;line-height:1}
|
|
.ov-card span{display:block;margin-top:7px;color:var(--muted);font-size:13px;font-weight:500}
|
|
.a-card{border:1px solid var(--hair);border-radius:15px;background:var(--card);overflow:hidden;box-shadow:var(--shadow-sm)}
|
|
.a-card-h{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid var(--hair);flex-wrap:wrap}
|
|
.a-card-h h2{font-size:16px;letter-spacing:-.3px}
|
|
table.adt{width:100%;border-collapse:collapse;font-size:14px}
|
|
table.adt th{text-align:left;color:var(--faint);font-size:11.5px;text-transform:uppercase;letter-spacing:.6px;font-weight:700;padding:11px 16px;border-bottom:1px solid var(--hair)}
|
|
table.adt td{padding:11px 16px;border-bottom:1px solid var(--hair-strong);vertical-align:middle}
|
|
table.adt tr:last-child td{border-bottom:none}
|
|
table.adt tr:hover td{background:color-mix(in srgb,var(--accent) 4%,transparent)}
|
|
.pill{display:inline-block;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px;text-transform:uppercase;letter-spacing:.4px}
|
|
.pill.on{background:rgba(52,211,153,.16);color:#6ee7b7}.pill.off{background:var(--hair);color:var(--faint)}
|
|
.pill.admin{background:rgba(77,139,255,.18);color:#9cc0ff}.pill.member{background:var(--hair);color:var(--muted)}
|
|
.in{background:var(--bg2);border:1px solid var(--border);border-radius:8px;color:var(--fg);padding:7px 10px;font-size:13px;font-family:inherit;outline:none}
|
|
.in:focus{border-color:var(--accent);box-shadow:var(--ring)}
|
|
.in-sm{width:70px}
|
|
.b{background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--fg);padding:7px 13px;font-size:13px;font-weight:600;cursor:pointer;transition:.14s}
|
|
.b:hover{border-color:var(--accent);transform:translateY(-1px)}
|
|
.b-cta{background:var(--accent-grad);border-color:transparent;color:#fff}
|
|
.b-danger:hover{border-color:#ef4444;color:#fca5a5}
|
|
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
|
|
.dot.on{background:#34d399;box-shadow:0 0 8px #34d399}.dot.off{background:var(--faint)}
|
|
.tag{display:inline-block;font-size:11px;background:var(--hair);color:var(--muted);padding:2px 8px;border-radius:6px;margin:2px 3px 2px 0}
|
|
.pol-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 24px;padding:20px}
|
|
.pol-field{display:flex;flex-direction:column;gap:5px}
|
|
.pol-field label{font-size:12.5px;font-weight:600;color:var(--muted)}
|
|
.pol-check{display:flex;align-items:center;gap:9px;font-size:14px;padding:6px 0}
|
|
.muted{color:var(--muted)}.faint{color:var(--faint)}.sp{margin-top:22px}
|
|
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--hair);border-radius:10px;padding:11px 18px;font-size:14px;box-shadow:var(--shadow-lg);z-index:100;opacity:0;transition:.2s;pointer-events:none}
|
|
.toast.show{opacity:1}
|
|
/* teams */
|
|
.teams-wrap{display:grid;grid-template-columns:320px 1fr;gap:18px;align-items:start}
|
|
.t-list{display:flex;flex-direction:column}
|
|
.t-item{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 20px;border-bottom:1px solid var(--hair);cursor:pointer;transition:.12s}
|
|
.t-item:hover{background:var(--hair)}
|
|
.t-item.on{background:linear-gradient(90deg,rgba(77,139,255,.14),transparent);box-shadow:inset 2px 0 0 var(--accent)}
|
|
.t-item b{font-size:14.5px}
|
|
.t-item .t-sub{font-size:12px;color:var(--faint);margin-top:2px}
|
|
.td-body{display:grid;grid-template-columns:1fr 1fr;gap:26px;padding:20px}
|
|
.td-col h3{font-size:13px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin-bottom:12px}
|
|
.t-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px solid var(--hair);font-size:13.5px}
|
|
.t-row .t-meta{color:var(--faint);font-size:12px}
|
|
/* access rules */
|
|
.ac-days{display:flex;gap:6px;flex-wrap:wrap}
|
|
.ac-days label{display:inline-flex;align-items:center;gap:5px;font-size:13px;background:var(--hair);padding:5px 10px;border-radius:7px;cursor:pointer}
|
|
.ac-pill{display:inline-block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:2px 8px;border-radius:999px}
|
|
.ac-allow{background:rgba(52,211,153,.16);color:#6ee7b7}.ac-deny{background:rgba(239,68,68,.16);color:#fca5a5}
|
|
.ac-cond{font-size:12px;color:var(--muted);line-height:1.5}
|
|
.ac-res-ok{color:#6ee7b7;font-weight:600}.ac-res-no{color:#fca5a5;font-weight:600}
|
|
@media (max-width:820px){.admin-shell{grid-template-columns:1fr}.admin-side{position:static;height:auto;flex-direction:row;flex-wrap:wrap;overflow:auto}.as-foot{flex-direction:row;margin:0}.pol-grid{grid-template-columns:1fr}.teams-wrap{grid-template-columns:1fr}.td-body{grid-template-columns:1fr}}
|
|
</style>
|
|
</head>
|
|
<body data-page="admin">
|
|
<div id="gate" class="gate"><div class="muted">Checking access…</div></div>
|
|
|
|
<div id="shell" class="admin-shell" hidden>
|
|
<aside class="admin-side">
|
|
<div class="as-brand"><span class="nz-logo"><svg width="18" height="18" viewBox="0 0 32 32"><path d="M8 20l8-12 8 12-8-4z" fill="#fff"/><rect x="12" y="18" width="8" height="7" rx="2" fill="#fff"/></svg></span>Admin</div>
|
|
<nav class="as-nav" id="nav">
|
|
<a data-p="overview" class="on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg> Overview</a>
|
|
<a data-p="users"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg> Users & roles</a>
|
|
<a data-p="devices"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg> Devices</a>
|
|
<a data-p="policy"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> MDM policy</a>
|
|
<a data-p="keys"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.6 7.6a5 5 0 1 0-7 7 5 5 0 0 0 7-7zm0 0L15 7m0 0l3 3L22 6l-3-3"/></svg> API keys</a>
|
|
<a data-p="relays"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49M7.76 16.24a6 6 0 0 1 0-8.49M19.07 4.93a10 10 0 0 1 0 14.14M4.93 19.07a10 10 0 0 1 0-14.14"/></svg> Relays</a>
|
|
<a data-p="teams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg> Teams</a>
|
|
<a data-p="access"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> Access rules</a>
|
|
<a data-p="recordings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3.5" fill="currentColor" stroke="none"/></svg> Recordings</a>
|
|
</nav>
|
|
<div class="as-foot">
|
|
<a href="/portal">Operator console</a>
|
|
<a href="/app">Open viewer</a>
|
|
<a href="/">Website</a>
|
|
</div>
|
|
</aside>
|
|
<main class="admin-main">
|
|
<section id="p-overview">
|
|
<div class="pg-head"><h1>Overview</h1><p>Your NetDesk organization at a glance.</p></div>
|
|
<div class="ov-grid" id="ovGrid"></div>
|
|
</section>
|
|
<section id="p-users" hidden>
|
|
<div class="pg-head"><h1>Users & roles</h1><p>Every account on this server — promote admins, set per-account seat limits.</p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Accounts</h2><span class="faint" id="uCount"></span></div>
|
|
<table class="adt"><thead><tr><th>Email</th><th>Role</th><th>Seats</th><th>Devices</th><th>2FA</th><th>Verified</th></tr></thead><tbody id="uBody"></tbody></table>
|
|
</div>
|
|
</section>
|
|
<section id="p-devices" hidden>
|
|
<div class="pg-head"><h1>Devices</h1><p>Every registered computer across all accounts — tag, group, hide or remove.</p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Computers</h2><span class="faint" id="dCount"></span></div>
|
|
<table class="adt"><thead><tr><th>Computer</th><th>Owner</th><th>Status</th><th>Group</th><th>Tags</th><th></th></tr></thead><tbody id="dBody"></tbody></table>
|
|
</div>
|
|
</section>
|
|
<section id="p-policy" hidden>
|
|
<div class="pg-head"><h1>MDM policy</h1><p>Push a managed, locked configuration to an account's hosts (applied at registration).</p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Account policy</h2>
|
|
<div style="display:flex;gap:8px;align-items:center"><select id="polAcct" class="in"></select><button class="b" id="polLoad">Load</button></div></div>
|
|
<div class="pol-grid" id="polForm"></div>
|
|
<div style="padding:0 20px 20px;display:flex;gap:10px"><button class="b b-cta" id="polSave">Save & push policy</button><button class="b b-danger" id="polClear">Clear policy</button></div>
|
|
</div>
|
|
</section>
|
|
<section id="p-keys" hidden>
|
|
<div class="pg-head"><h1>API keys</h1><p>Programmatic access (X-API-Key) to the /api/v1 REST surface + webhooks.</p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Your keys</h2>
|
|
<div style="display:flex;gap:8px"><input id="kName" class="in" placeholder="Key name"><button class="b b-cta" id="kNew">Create key</button></div></div>
|
|
<table class="adt"><thead><tr><th>Name</th><th>Key</th><th>Created</th><th>Last used</th><th></th></tr></thead><tbody id="kBody"></tbody></table>
|
|
</div>
|
|
<div id="kNewBox" class="muted sp" hidden></div>
|
|
|
|
<div class="a-card sp">
|
|
<div class="a-card-h"><h2>SCIM 2.0 provisioning</h2>
|
|
<div style="display:flex;gap:8px"><input id="scName" class="in" placeholder="Token label (e.g. Okta)"><button class="b b-cta" id="scNew">Mint token</button></div></div>
|
|
<div style="padding:14px 20px 0" class="muted" id="scBase"></div>
|
|
<table class="adt"><thead><tr><th>Label</th><th>Token</th><th>Created</th><th>Last used</th><th></th></tr></thead><tbody id="scBody"></tbody></table>
|
|
<div id="scNewBox" class="muted" style="padding:0 20px 18px" hidden></div>
|
|
</div>
|
|
</section>
|
|
<section id="p-relays" hidden>
|
|
<div class="pg-head"><h1>Relays</h1><p>Configured relay nodes for nearest-relay geo selection (set with --relays).</p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Relay nodes</h2></div>
|
|
<table class="adt"><thead><tr><th>ID</th><th>Region</th><th>URL</th><th>Tier</th><th>Lat, Lon</th></tr></thead><tbody id="rBody"></tbody></table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="p-teams" hidden>
|
|
<div class="pg-head"><h1>Teams</h1><p>Shared address books — a team of operators shares one device list. The team owner (or any account admin) manages its members and shared devices.</p></div>
|
|
<div class="teams-wrap">
|
|
<div class="a-card t-list-card">
|
|
<div class="a-card-h"><h2>Teams</h2><span class="faint" id="tCount"></span></div>
|
|
<div style="display:flex;gap:8px;padding:0 20px 14px"><input id="tName" class="in" placeholder="New team name"><button class="b b-cta" id="tNew">Create</button></div>
|
|
<div id="tList" class="t-list"></div>
|
|
</div>
|
|
<div class="a-card teams-detail" id="tDetail" hidden>
|
|
<div class="a-card-h"><h2 id="tdName">—</h2><span style="white-space:nowrap"><button class="b" id="tdRename">Rename</button> <button class="b b-danger" id="tdDelete">Delete</button></span></div>
|
|
<div class="td-body">
|
|
<div class="td-col">
|
|
<h3>Members</h3>
|
|
<div style="display:flex;gap:8px;margin-bottom:10px"><input id="tmEmail" class="in" placeholder="account email" style="flex:1"><select id="tmRole" class="in in-sm" style="width:auto"><option value="member">member</option><option value="owner">owner</option></select><button class="b" id="tmAdd">Add</button></div>
|
|
<div id="tmList"></div>
|
|
</div>
|
|
<div class="td-col">
|
|
<h3>Shared devices</h3>
|
|
<div style="display:flex;gap:8px;margin-bottom:10px"><select id="tdDev" class="in" style="flex:1"></select><button class="b" id="tdShare">Share</button></div>
|
|
<div id="tdDevList"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="p-access" hidden>
|
|
<div class="pg-head"><h1>Conditional access</h1><p>First-match-wins allow/deny rules, evaluated the moment a viewer connects. Scope by user, device, group, IP range, weekday and time-of-day. When nothing matches, the connection is allowed.</p></div>
|
|
<div class="a-card">
|
|
<div class="a-card-h"><h2>Rules for</h2><div style="display:flex;gap:8px;align-items:center"><select id="acAcct" class="in"></select><button class="b" id="acLoad">Load</button></div></div>
|
|
<table class="adt"><thead><tr><th>#</th><th>Action</th><th>Applies to</th><th>Conditions</th><th>Reason</th><th>On</th><th></th></tr></thead><tbody id="acBody"></tbody></table>
|
|
</div>
|
|
<div class="a-card sp">
|
|
<div class="a-card-h"><h2 id="acFormTitle">Add a rule</h2><button class="b" id="acReset" hidden>Cancel edit</button></div>
|
|
<div class="pol-grid" id="acForm">
|
|
<div class="pol-field"><label>Priority (lower is evaluated first)</label><input id="acPrio" class="in" type="number" value="100"></div>
|
|
<div class="pol-field"><label>Action</label><select id="acAction" class="in"><option value="deny">deny</option><option value="allow">allow</option></select></div>
|
|
<div class="pol-field"><label>Applies to</label><select id="acSubType" class="in"><option value="any">any connection</option><option value="user">a user (email)</option><option value="device">a device (id)</option><option value="group">a device group</option></select></div>
|
|
<div class="pol-field"><label>Subject value</label><input id="acSubject" class="in" placeholder="email / device id / group name"></div>
|
|
<div class="pol-field"><label>From IP range (CIDR, optional)</label><input id="acCidr" class="in" placeholder="e.g. 203.0.113.0/24"></div>
|
|
<div class="pol-field"><label>Deny reason (shown to the viewer)</label><input id="acReason" class="in" placeholder="Access denied by policy"></div>
|
|
<div class="pol-field" style="grid-column:1/-1"><label>Weekdays (leave all off = any day)</label><div id="acDays" class="ac-days"></div></div>
|
|
<div class="pol-field"><label>Time from (HH:MM, optional)</label><input id="acTimeStart" class="in" placeholder="09:00"></div>
|
|
<div class="pol-field"><label>Time to (HH:MM, optional)</label><input id="acTimeEnd" class="in" placeholder="18:00"></div>
|
|
<div class="pol-field"><label>UTC offset for the times, minutes</label><input id="acTz" class="in" type="number" value="0" title="e.g. 60 = CET, -300 = US Eastern"></div>
|
|
</div>
|
|
<div style="padding:0 20px 20px;display:flex;gap:10px"><button class="b b-cta" id="acSave">Save rule</button></div>
|
|
</div>
|
|
<div class="a-card sp">
|
|
<div class="a-card-h"><h2>Test a connection</h2><span class="faint">dry-run the rules above</span></div>
|
|
<div class="pol-grid">
|
|
<div class="pol-field"><label>Viewer email</label><input id="tViewer" class="in" placeholder="user@example.com"></div>
|
|
<div class="pol-field"><label>Device id</label><input id="tHost" class="in" placeholder="123456789"></div>
|
|
<div class="pol-field"><label>Device group</label><input id="tGroup" class="in"></div>
|
|
<div class="pol-field"><label>From IP</label><input id="tIp" class="in" placeholder="203.0.113.5"></div>
|
|
</div>
|
|
<div style="padding:0 20px 20px;display:flex;gap:12px;align-items:center"><button class="b b-cta" id="tRun">Evaluate</button><span id="tResult"></span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="p-recordings" hidden>
|
|
<div class="pg-head"><h1>Session recordings</h1><p>Centrally-stored recordings uploaded by hosts governed by a <b>record_required</b> MDM policy (set it per account in MDM policy). <span id="recRetention" class="faint"></span></p></div>
|
|
<div class="a-card"><div class="a-card-h"><h2>Recordings</h2><span class="faint" id="recCount"></span></div>
|
|
<table class="adt"><thead><tr><th>#</th><th>Host</th><th>Owner</th><th>Viewer</th><th>Size</th><th>Length</th><th>Recorded</th><th></th></tr></thead><tbody id="recBody"></tbody></table>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
<div class="toast" id="toast"></div>
|
|
<script src="admin.js?v=6"></script>
|
|
</body>
|
|
</html>
|