ASGI via Daphne for websockets, WSGI via Gunicorn. Implemented xterm.js for shell proxy to target servers.
This commit is contained in:
43
app/apps/servers/templates/servers/_header.html
Normal file
43
app/apps/servers/templates/servers/_header.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-600 text-white text-xl font-semibold">
|
||||
{{ server.initial }}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">{{ server.display_name }}</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'servers:dashboard' %}" class="text-sm font-semibold text-purple-700 hover:text-purple-800">Back to servers</a>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-wrap gap-2 border-b border-gray-200 pb-2 text-sm font-semibold text-gray-500">
|
||||
<a
|
||||
href="{% url 'servers:detail' server.id %}"
|
||||
class="{% if active_tab == 'details' %}rounded-full bg-purple-50 px-3 py-1 text-purple-700{% else %}rounded-full bg-gray-100 px-3 py-1 text-gray-500 hover:text-gray-700{% endif %}"
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
<a
|
||||
href="{% url 'servers:audit' server.id %}"
|
||||
class="{% if active_tab == 'audit' %}rounded-full bg-purple-50 px-3 py-1 text-purple-700{% else %}rounded-full bg-gray-100 px-3 py-1 text-gray-500 hover:text-gray-700{% endif %}"
|
||||
>
|
||||
Audit
|
||||
</a>
|
||||
{% if can_shell %}
|
||||
<a
|
||||
href="{% url 'servers:shell' server.id %}"
|
||||
class="{% if active_tab == 'shell' %}rounded-full bg-purple-50 px-3 py-1 text-purple-700{% else %}rounded-full bg-gray-100 px-3 py-1 text-gray-500 hover:text-gray-700{% endif %}"
|
||||
>
|
||||
Shell
|
||||
</a>
|
||||
{% endif %}
|
||||
<a
|
||||
href="{% url 'servers:settings' server.id %}"
|
||||
class="{% if active_tab == 'settings' %}rounded-full bg-purple-50 px-3 py-1 text-purple-700{% else %}rounded-full bg-gray-100 px-3 py-1 text-gray-500 hover:text-gray-700{% endif %}"
|
||||
>
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
29
app/apps/servers/templates/servers/audit.html
Normal file
29
app/apps/servers/templates/servers/audit.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Audit • {{ server.display_name }} • Keywarden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
{% include "servers/_header.html" %}
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Audit logs</h2>
|
||||
<span class="text-xs font-semibold text-gray-500">Placeholder</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-sm text-gray-600">
|
||||
Logs will appear here once collection is enabled for this server.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Metrics</h2>
|
||||
<span class="text-xs font-semibold text-gray-500">Placeholder</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-sm text-gray-600">
|
||||
Metrics will appear here once collection is enabled for this server.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,27 +4,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-600 text-white text-xl font-semibold">
|
||||
{{ server.initial }}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">{{ server.display_name }}</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'servers:dashboard' %}" class="text-sm font-semibold text-purple-700 hover:text-purple-800">Back to servers</a>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-wrap gap-2 border-b border-gray-200 pb-2 text-sm font-semibold text-gray-500">
|
||||
<span class="rounded-full bg-purple-50 px-3 py-1 text-purple-700">Details</span>
|
||||
<span class="rounded-full bg-gray-100 px-3 py-1 text-gray-500">Audit</span>
|
||||
<span class="rounded-full bg-gray-100 px-3 py-1 text-gray-500">Shell</span>
|
||||
<span class="rounded-full bg-gray-100 px-3 py-1 text-gray-500">Settings</span>
|
||||
</nav>
|
||||
{% include "servers/_header.html" %}
|
||||
|
||||
<section class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
@@ -58,6 +38,18 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Account status</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if account_present is None %}
|
||||
Unknown
|
||||
{% elif account_present %}
|
||||
Present
|
||||
{% else %}
|
||||
Not on server
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Certificate</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
@@ -121,16 +113,6 @@
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Logs</h2>
|
||||
<span class="text-xs font-semibold text-gray-500">Placeholder</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-sm text-gray-600">
|
||||
Logs will appear here once collection is enabled for this server.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
19
app/apps/servers/templates/servers/settings.html
Normal file
19
app/apps/servers/templates/servers/settings.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings • {{ server.display_name }} • Keywarden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
{% include "servers/_header.html" %}
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Settings</h2>
|
||||
<span class="text-xs font-semibold text-gray-500">Placeholder</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-dashed border-gray-200 bg-gray-50 p-6 text-center text-sm text-gray-600">
|
||||
Settings will appear here as server options are added.
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
271
app/apps/servers/templates/servers/shell.html
Normal file
271
app/apps/servers/templates/servers/shell.html
Normal file
@@ -0,0 +1,271 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Shell • {{ server.display_name }} • Keywarden{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.4.0/css/xterm.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
{% if not is_popout %}
|
||||
{% include "servers/_header.html" %}
|
||||
{% else %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900">Shell • {{ server.display_name }}</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="text-sm font-semibold text-gray-500 hover:text-gray-700" onclick="window.close()">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Shell access</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Connect with your private key and the signed certificate for this server.
|
||||
</p>
|
||||
</div>
|
||||
{% if not is_popout %}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-gray-50"
|
||||
id="shell-popout"
|
||||
data-popout-url="{% url 'servers:shell' server.id %}?popout=1"
|
||||
>
|
||||
Pop out terminal
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-4 text-sm text-gray-600">
|
||||
<dl class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Account name</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if system_username %}
|
||||
{{ system_username }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Host</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if shell_target %}
|
||||
{{ shell_target }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{% if shell_command %}
|
||||
<div class="rounded-xl border border-dashed border-gray-200 bg-gray-50 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-xs font-semibold text-gray-500">SSH command</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs font-semibold text-purple-700 hover:text-purple-800"
|
||||
data-copy-target="shell-command"
|
||||
>
|
||||
Copy command
|
||||
</button>
|
||||
</div>
|
||||
<code class="mt-2 block break-all text-xs text-gray-800" id="shell-command">{{ shell_command }}</code>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{% if certificate_key_id %}
|
||||
<div class="inline-flex overflow-hidden rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-l-md bg-purple-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-purple-700"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-r-md border border-l-0 border-gray-200 bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-gray-200"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate.sha256"
|
||||
>
|
||||
Hash
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700 js-regenerate-cert"
|
||||
data-key-id="{{ certificate_key_id }}"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
{% endif %}
|
||||
<span class="text-xs text-gray-500">Use the command above for local SSH.</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-600">Upload a key to enable downloads and a local SSH command.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Browser terminal</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Launch a proxied terminal session to the target host in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-gray-500">Beta</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-gray-200 bg-gray-900 p-2">
|
||||
<div id="shell-terminal" class="h-96"></div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500">
|
||||
Sessions are proxied through Keywarden and end when this page closes.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.4.0/lib/xterm.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length === 2) {
|
||||
return parts.pop().split(";").shift();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function handleDownload(event) {
|
||||
var button = event.currentTarget;
|
||||
var url = button.getAttribute("data-download-url");
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function handleRegenerate(event) {
|
||||
var button = event.currentTarget;
|
||||
var keyId = button.getAttribute("data-key-id");
|
||||
if (!keyId) {
|
||||
return;
|
||||
}
|
||||
if (!window.confirm("Regenerate the certificate for this key?")) {
|
||||
return;
|
||||
}
|
||||
var csrf = getCookie("csrftoken");
|
||||
fetch("/api/v1/keys/" + keyId + "/certificate", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": csrf,
|
||||
},
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Certificate regeneration failed.");
|
||||
}
|
||||
window.alert("Certificate regenerated.");
|
||||
})
|
||||
.catch(function (err) {
|
||||
window.alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function handleCopy(event) {
|
||||
var targetId = event.currentTarget.getAttribute("data-copy-target");
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
var node = document.getElementById(targetId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
var text = node.textContent || "";
|
||||
if (!navigator.clipboard || !text) {
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
window.alert("Command copied.");
|
||||
});
|
||||
}
|
||||
|
||||
var popout = document.getElementById("shell-popout");
|
||||
if (popout) {
|
||||
popout.addEventListener("click", function () {
|
||||
var url = popout.getAttribute("data-popout-url");
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
window.open(url, "_blank", "width=900,height=700");
|
||||
});
|
||||
}
|
||||
|
||||
var downloadButtons = document.querySelectorAll("[data-download-url]");
|
||||
for (var i = 0; i < downloadButtons.length; i += 1) {
|
||||
downloadButtons[i].addEventListener("click", handleDownload);
|
||||
}
|
||||
var buttons = document.querySelectorAll(".js-regenerate-cert");
|
||||
for (var j = 0; j < buttons.length; j += 1) {
|
||||
buttons[j].addEventListener("click", handleRegenerate);
|
||||
}
|
||||
var copyButtons = document.querySelectorAll("[data-copy-target]");
|
||||
for (var k = 0; k < copyButtons.length; k += 1) {
|
||||
copyButtons[k].addEventListener("click", handleCopy);
|
||||
}
|
||||
|
||||
var termContainer = document.getElementById("shell-terminal");
|
||||
if (termContainer && window.Terminal) {
|
||||
var term = new window.Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
fontSize: 13,
|
||||
theme: {
|
||||
background: "#0b0f12",
|
||||
foreground: "#d1d5db",
|
||||
cursor: "#a855f7"
|
||||
}
|
||||
});
|
||||
term.open(termContainer);
|
||||
term.write("Connecting...\r\n");
|
||||
|
||||
var protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
var socketUrl = protocol + "://" + window.location.host + "/ws/servers/{{ server.id }}/shell/";
|
||||
var socket = new WebSocket(socketUrl);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
if (typeof event.data === "string") {
|
||||
term.write(event.data);
|
||||
return;
|
||||
}
|
||||
var data = new Uint8Array(event.data);
|
||||
var text = new TextDecoder("utf-8").decode(data);
|
||||
term.write(text);
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
term.write("\r\nSession closed.\r\n");
|
||||
};
|
||||
|
||||
term.onData(function (data) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user