feat(server): splits user and client into separate key access
Build & Deploy / build (push) Successful in 1m47s
Build & Deploy / build (push) Successful in 1m47s
This commit is contained in:
@@ -3,6 +3,7 @@ const state = {
|
||||
detail: null,
|
||||
groupDetail: null,
|
||||
newKey: null,
|
||||
newClientKey: null,
|
||||
loading: false,
|
||||
usageData: null
|
||||
};
|
||||
@@ -85,6 +86,16 @@ content.addEventListener("click", async (event) => {
|
||||
await refresh();
|
||||
}
|
||||
|
||||
if (action === "delete-client-key") {
|
||||
if (!confirm("Delete this client key?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api(`/client-keys/${encodeURIComponent(keyId)}`, { method: "DELETE" });
|
||||
setNotice("Client key deleted.");
|
||||
await refresh();
|
||||
}
|
||||
|
||||
if (action === "copy-key") {
|
||||
await navigator.clipboard.writeText(button.dataset.key);
|
||||
setNotice("API key copied.");
|
||||
@@ -174,6 +185,15 @@ content.addEventListener("submit", async (event) => {
|
||||
await refresh();
|
||||
}
|
||||
|
||||
if (form.dataset.form === "client-key") {
|
||||
state.newClientKey = await api("/client-keys", {
|
||||
method: "POST",
|
||||
body: { name: data.name }
|
||||
});
|
||||
setNotice("Client key created.");
|
||||
await refresh();
|
||||
}
|
||||
|
||||
if (form.dataset.form === "create-group") {
|
||||
const result = await api("/groups", {
|
||||
method: "POST",
|
||||
@@ -359,6 +379,11 @@ async function renderRoute(showLoading = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === "client-keys") {
|
||||
renderClientKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === "groups") {
|
||||
renderGroups();
|
||||
return;
|
||||
@@ -382,6 +407,8 @@ function updateShell() {
|
||||
<div>${escapeHtml(summary.user?.name || "Signed in")}</div>
|
||||
<div>${summary.clients.length} clients</div>
|
||||
<div>${summary.models.length} models</div>
|
||||
<div>${(summary.clientKeys || []).length} client keys</div>
|
||||
<div>${(summary.apiKeys || []).length} API keys</div>
|
||||
<div>${(summary.groups || []).length} groups</div>
|
||||
<div>${formatDate(summary.generatedAtUtc)}</div>
|
||||
`);
|
||||
@@ -714,6 +741,39 @@ function renderApiKeys() {
|
||||
`);
|
||||
}
|
||||
|
||||
function renderClientKeys() {
|
||||
const keys = state.summary?.clientKeys || [];
|
||||
pageTitle.textContent = "Client Keys";
|
||||
pageSubtitle.textContent = "Keys accepted by GPU clients to establish tunnel connections.";
|
||||
|
||||
patchContent(`
|
||||
${state.newClientKey ? newKeyPanel(state.newClientKey) : ""}
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Create client key</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-row" data-form="client-key">
|
||||
<div class="field">
|
||||
<label for="clientKeyName">Name</label>
|
||||
<input class="input" id="clientKeyName" name="name" placeholder="e.g. gpu_workstation_1" required>
|
||||
</div>
|
||||
<button class="button" type="submit">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Client keys</h2>
|
||||
<span class="badge">${keys.length} total</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
${keys.length ? clientKeysTable(keys) : emptyState("No client keys have been created.")}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function newKeyPanel(key) {
|
||||
return `
|
||||
<div class="new-key">
|
||||
@@ -756,6 +816,36 @@ function apiKeysTable(keys) {
|
||||
`;
|
||||
}
|
||||
|
||||
function clientKeysTable(keys) {
|
||||
const rows = keys.map((key) => `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cell-main">${escapeHtml(key.name)}</div>
|
||||
<div class="cell-sub">${escapeHtml(key.keyPrefix)}...</div>
|
||||
</td>
|
||||
<td>${formatDate(key.createdAtUtc)}</td>
|
||||
<td>${key.lastUsedUtc ? formatDate(key.lastUsedUtc) : "Never"}</td>
|
||||
<td>
|
||||
<button class="button danger" data-action="delete-client-key" data-key-id="${escapeAttr(key.id)}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
return `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created</th>
|
||||
<th>Last used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
const groups = state.summary?.groups || [];
|
||||
pageTitle.textContent = "Groups";
|
||||
|
||||
Reference in New Issue
Block a user