Files
keywarden/app/apps/servers/templates/servers/dashboard.html
T
2026-04-28 02:08:11 +01:00

312 lines
16 KiB
HTML

{% extends "base.html" %}
{% block title %}Servers • Keywarden{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">Servers</h1>
<p class="mt-1 text-sm text-gray-500">Request access, review approvals, and open servers you are approved for.</p>
</div>
<span class="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700">
{{ servers|length }} total
</span>
</div>
{% if servers %}
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{% for item in servers %}
<article class="flex h-full flex-col rounded-2xl border border-gray-200 bg-white p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
{% if item.access.granted %}
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-700 text-sm font-semibold text-white">
{{ item.server.initial }}
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900">{{ item.server.display_name }}</h2>
<p class="text-xs text-gray-500">
{{ item.server.hostname|default:item.server.ipv4|default:item.server.ipv6|default:"Unassigned" }}
</p>
</div>
</div>
<div class="relative">
<button
type="button"
data-tooltip-target="server-status-{{ item.server.id }}"
class="{% if item.status.is_active %}inline-flex items-center rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-semibold text-emerald-700{% else %}inline-flex items-center rounded-full bg-rose-50 px-2.5 py-1 text-xs font-semibold text-rose-700{% endif %}"
>
{{ item.status.label }}: {{ item.status.detail }}
</button>
<div
id="server-status-{{ item.server.id }}"
role="tooltip"
class="invisible absolute z-10 inline-block w-64 rounded-lg border border-gray-200 bg-white p-3 text-xs text-gray-700 shadow-sm opacity-0 transition-opacity"
>
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">Status</span>
<span class="font-medium text-gray-900">{{ item.status.label }}: {{ item.status.detail }}</span>
</div>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">Ping</span>
<span class="font-medium text-gray-900">
{% if item.status.ping_ms is not None %}{{ item.status.ping_ms }}ms{% else %}—{% endif %}
</span>
</div>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">Hostname</span>
<span class="font-medium text-gray-900">{{ item.server.hostname|default:"—" }}</span>
</div>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">IPv4</span>
<span class="font-medium text-gray-900">{{ item.server.ipv4|default:"—" }}</span>
</div>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">IPv6</span>
<span class="font-medium text-gray-900">{{ item.server.ipv6|default:"—" }}</span>
</div>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-500">Last heartbeat</span>
<span class="font-medium text-gray-900">
{% if item.status.heartbeat_at %}{{ item.status.heartbeat_at|date:"M j, Y H:i:s" }}{% else %}—{% endif %}
</span>
</div>
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
</div>
<dl class="mt-5 divide-y divide-gray-100 text-sm text-gray-600">
<div class="flex items-center justify-between py-2">
<dt>Access until</dt>
<dd class="font-medium text-gray-900">
{% if item.expires_at %}
{{ item.expires_at|date:"M j, Y H:i" }}
{% else %}
No expiry
{% endif %}
</dd>
</div>
<div class="flex items-center justify-between py-2">
<dt>Scopes</dt>
<dd class="font-medium text-gray-900">
<div class="flex flex-wrap justify-end gap-1">
{% if item.access.can_shell %}<span class="inline-flex rounded-full bg-blue-50 px-2 py-0.5 text-xs font-semibold text-blue-700">Shell</span>{% endif %}
{% if item.access.can_logs %}<span class="inline-flex rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700">Logs</span>{% endif %}
{% if item.access.can_users %}<span class="inline-flex rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-semibold text-indigo-700">Users</span>{% endif %}
</div>
</dd>
</div>
</dl>
<div class="mt-5 border-t border-gray-100 pt-4">
{% if item.access.can_users %}
<a
href="{% url 'servers:detail' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open details
</a>
{% elif item.access.can_logs %}
<a
href="{% url 'servers:audit' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open audit
</a>
{% elif item.access.can_shell %}
<a
href="{% url 'servers:shell' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open shell
</a>
{% endif %}
</div>
{% else %}
<div class="flex items-start justify-between">
<div>
<h2 class="text-lg font-semibold text-gray-900">{{ item.server.hostname|default:"Unassigned" }}</h2>
<p class="mt-1 text-xs text-gray-500">No access granted</p>
</div>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-700">Hostname only</span>
</div>
<div class="mt-5 border-t border-gray-100 pt-4">
{% if item.access.pending %}
<span class="inline-flex items-center rounded-lg bg-amber-100 px-3 py-1.5 text-xs font-semibold text-amber-800">
Request pending since {{ item.access.pending.requested_at|date:"M j, Y H:i" }}
</span>
{% else %}
<button
type="button"
class="inline-flex items-center rounded-lg bg-blue-700 px-3 py-2 text-xs font-semibold text-white hover:bg-blue-800"
data-open-request-modal
data-server-id="{{ item.server.id }}"
data-server-name="{{ item.server.display_name|default:item.server.hostname|default:item.server.ipv4|default:item.server.ipv6|default:'Server'|escape }}"
data-request-url="{% url 'servers:request_access' item.server.id %}"
>
Request access
</button>
{% endif %}
<p class="mt-2 text-xs text-gray-500">Choose scopes, duration, and preferred server username in the request popup.</p>
</div>
{% endif %}
</article>
{% endfor %}
</div>
{% else %}
<div class="rounded-2xl border border-dashed border-gray-200 bg-white p-10 text-center">
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-700">
<svg class="h-6 w-6" aria-hidden="true" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6v6l4 2" />
<circle cx="12" cy="12" r="9" stroke-width="1.5"></circle>
</svg>
</div>
<h2 class="mt-4 text-lg font-semibold text-gray-900">No servers yet</h2>
<p class="mt-2 text-sm text-gray-600">Create or enroll a server to start handling access requests.</p>
</div>
{% endif %}
</div>
<div id="access-request-modal" class="fixed inset-0 z-50 hidden items-center justify-center px-4 py-6" aria-hidden="true">
<div class="absolute inset-0 bg-gray-900/50" data-request-modal-close></div>
<div class="relative z-10 w-full max-w-xl rounded-2xl border border-gray-200 bg-white p-5 shadow-xl sm:p-6">
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="text-lg font-semibold text-gray-900">Request Scoped Access</h2>
<p class="mt-1 text-sm text-gray-500">
Server:
<span id="request-modal-server-name" class="font-semibold text-gray-700">-</span>
</p>
</div>
<button
type="button"
class="inline-flex h-8 w-8 items-center justify-center rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50"
data-request-modal-close
aria-label="Close access request"
>
&times;
</button>
</div>
<form id="access-request-form" method="post" class="mt-5 space-y-5">
{% csrf_token %}
<input type="hidden" name="scoped_request" value="1">
<fieldset class="space-y-2">
<legend class="text-xs font-semibold uppercase tracking-wide text-gray-500">Requested scopes</legend>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="request_shell" value="1" class="h-4 w-4 rounded border-gray-300 text-blue-700" checked>
Shell access
</label>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="request_logs" value="1" class="h-4 w-4 rounded border-gray-300 text-blue-700" checked>
Audit log access
</label>
<label class="flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="request_users" value="1" class="h-4 w-4 rounded border-gray-300 text-blue-700" checked>
Server users/details access
</label>
</fieldset>
<div>
<label for="requested-duration-hours" class="block text-xs font-semibold uppercase tracking-wide text-gray-500">Requested duration</label>
<select id="requested-duration-hours" name="requested_duration_hours" class="mt-1 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700">
<option value="">No preferred expiry</option>
<option value="1">1 hour</option>
<option value="4">4 hours</option>
<option value="8" selected>8 hours</option>
<option value="24">24 hours</option>
<option value="72">3 days</option>
<option value="168">7 days</option>
</select>
</div>
<div>
<label for="requested-server-username" class="block text-xs font-semibold uppercase tracking-wide text-gray-500">Preferred server username</label>
<input
id="requested-server-username"
name="requested_server_username"
type="text"
maxlength="128"
value="{{ request.user.username }}"
placeholder="e.g. ubuntu"
class="mt-1 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700"
>
</div>
<div>
<label for="request-reason" class="block text-xs font-semibold uppercase tracking-wide text-gray-500">Reason (optional)</label>
<textarea
id="request-reason"
name="reason"
rows="3"
placeholder="Why you need this access."
class="mt-1 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700"
></textarea>
</div>
<div class="flex items-center justify-end gap-2 border-t border-gray-100 pt-4">
<button type="button" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" data-request-modal-close>
Cancel
</button>
<button type="submit" class="inline-flex items-center rounded-lg bg-blue-700 px-3 py-2 text-xs font-semibold text-white hover:bg-blue-800">
Submit request
</button>
</div>
</form>
</div>
</div>
<script>
(function () {
var modal = document.getElementById("access-request-modal");
var modalServerName = document.getElementById("request-modal-server-name");
var form = document.getElementById("access-request-form");
var openButtons = document.querySelectorAll("[data-open-request-modal]");
var closeButtons = document.querySelectorAll("[data-request-modal-close]");
function closeModal() {
modal.classList.add("hidden");
modal.classList.remove("flex");
modal.setAttribute("aria-hidden", "true");
}
function openModal(button) {
var requestUrl = button.getAttribute("data-request-url");
var serverName = button.getAttribute("data-server-name") || "Server";
form.setAttribute("action", requestUrl);
modalServerName.textContent = serverName;
modal.classList.remove("hidden");
modal.classList.add("flex");
modal.setAttribute("aria-hidden", "false");
}
openButtons.forEach(function (button) {
button.addEventListener("click", function () {
openModal(button);
});
});
closeButtons.forEach(function (button) {
button.addEventListener("click", closeModal);
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && modal.classList.contains("flex")) {
closeModal();
}
});
form.addEventListener("submit", function (event) {
var checked = form.querySelectorAll("input[name='request_shell']:checked, input[name='request_logs']:checked, input[name='request_users']:checked");
if (!checked.length) {
event.preventDefault();
window.alert("Select at least one scope.");
}
});
})();
</script>
{% endblock %}