312 lines
16 KiB
HTML
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"
|
|
>
|
|
×
|
|
</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 %}
|