358 lines
No EOL
19 KiB
HTML
358 lines
No EOL
19 KiB
HTML
<!doctypehtml>
|
|
<link href=https://cdn.jsdelivr.net/npm/bulma@1.0.3/css/bulma.min.css rel=stylesheet>
|
|
<style>
|
|
body {
|
|
padding: 32px
|
|
}
|
|
|
|
.container-x {
|
|
width: 640px
|
|
}
|
|
|
|
.next {
|
|
text-align: right
|
|
}
|
|
|
|
.spacer {
|
|
height: 16px
|
|
}
|
|
|
|
.main-content {
|
|
padding-left: 32px;
|
|
width: 100%;
|
|
vertical-align: top
|
|
}
|
|
|
|
.menu {
|
|
width: 240px
|
|
}
|
|
</style>
|
|
<aside class="menu is-inline-block">
|
|
<p class="menu-label">Info</p>
|
|
<ul class="menu-list">
|
|
<li><a onclick="infoStats()" id="menuitem-stats">Statistics</a></li>
|
|
</ul>
|
|
<p class="menu-label">Manage</p>
|
|
<ul class="menu-list">
|
|
<li><a onclick="manageAccounts(0)" id="menuitem-accounts">Accounts</a></li>
|
|
<li><a onclick="manageAnnouncements()" id="menuitem-announcements">Announcements</a></li>
|
|
<li><a onclick="manageInstance()" id="menuitem-instance">Instance</a></li>
|
|
<li><a onclick="manageSettings()" id="menuitem-settings">Settings</a></li>
|
|
</ul>
|
|
<p class="menu-label">Diagnostics</p>
|
|
<ul class="menu-list">
|
|
<li><a onclick="diagsLogs()" id="menuitem-logs">Logs</a></li>
|
|
</ul>
|
|
</aside>
|
|
<div id="content" class="container main-content is-inline-block">
|
|
</div>
|
|
<script>
|
|
let obscured_setting_names = ["catbox_userhash","whitelist_ip"];
|
|
function clearActiveLinks() {
|
|
document.querySelectorAll("a").forEach(function (a) { a.className = ""; });
|
|
}
|
|
function setActiveLink(link) {
|
|
document.getElementById("menuitem-" + link).className = "is-active";
|
|
}
|
|
function setContent(html) {
|
|
document.getElementById("content").innerHTML = html;
|
|
}
|
|
async function infoStats() {
|
|
clearActiveLinks();
|
|
const request = new Request("/info/stats");
|
|
const response = await fetch(request);
|
|
const stats = await response.json();
|
|
let html = "<h4 class=\"title is-4\">Statistics</h4><div class=spacer></div><div>Uptime: " + formatTime(stats["uptime"]) + "<br/>Free memory: " + stats["free_memory"].toString() + " bytes<br/></div>";
|
|
setContent(html);
|
|
setActiveLink("stats");
|
|
}
|
|
async function manageAccounts(page) {
|
|
clearActiveLinks();
|
|
const request = new Request("/manage/accounts?skip=" + (page * 10).toString());
|
|
const response = await fetch(request);
|
|
const result = await response.json();
|
|
const accounts = result["accounts"];
|
|
let html = "<h4 class=\"title is-4\">Accounts</h4><div class=spacer></div><div>" + result["total"].toString() + " known account(s)</div><div class=spacer></div>";
|
|
if (accounts.length) {
|
|
html += "<table class=table><thead><tr><th></th><th>id</th><th>acct</th><th>display_name</th><th>type</th><th>delete</th></tr></head><tbody>";
|
|
for (let i = 0; i < accounts.length; i++) {
|
|
html += "<tr><td><img src=\"" + accounts[i]["avatar_static"] + "\" style=\"width:48px;height:48px\"></td><td>" + accounts[i]["id"] + "</td><td>" + accounts[i]["acct"] + "</td><td>" + accounts[i]["display_name"] + "</td><td>" + (accounts[i]["remote_actor"] == undefined ? "local" : "remote") + "</td><td style=\"text-align:center\"><a href=\"javascript:confirmDeleteUser('" + accounts[i]["acct"] + "','" + accounts[i]["id"] + "');\">❌</a></td><tr>";
|
|
}
|
|
html += "</tbody></table>";
|
|
html += "<nav class=\"pagination\" role=\"navigation\" aria-label=\"pagination\">";
|
|
html += "<ul class=\"pagination-list\">";
|
|
for (let i = 0; i < result["total"] / 10; i++) {
|
|
html += "<li><a href=\"javascript:manageAccounts(" + i.toString() + ")\"class=\"pagination-link";
|
|
if (i == result["skip"] / 10) {
|
|
html += " is-current";
|
|
}
|
|
html += "\">" + (i + 1).toString() + "</a></li>";
|
|
}
|
|
html += "</ul></nav>";
|
|
} else {
|
|
html += "No users";
|
|
}
|
|
html += "<br><br><input onclick=manageNewUser() class=button type=button value=\"New User\">";
|
|
setContent(html);
|
|
setActiveLink("accounts");
|
|
}
|
|
async function manageAnnouncements() {
|
|
clearActiveLinks();
|
|
const request = new Request("/manage/announcements");
|
|
const response = await fetch(request);
|
|
const announcements = await response.json();
|
|
let html = "<h4 class=\"title is-4\">Announcements</h4><div class=spacer></div><div>";
|
|
if (announcements.length) {
|
|
html += announcements.length + " announcement(s)</div><div class=spacer></div>";
|
|
html += "<table class=table><thead><tr><th>id</th><th>published_at</th><th>updated_at</th><th>content</th><th>delete</th></tr></head><tbody>";
|
|
for (let i = 0; i < announcements.length; i++) {
|
|
html += "<tr><td>" + announcements[i]["id"] + "</td><td>" + announcements[i]["published_at"] + "</td><td>" + announcements[i]["updated_at"] + "</td><td>" + announcements[i]["content"] + "</td><td style=\"text-align:center\"><a href=\"javascript:confirmDeleteAnnouncement('" + announcements[i]["id"] + "');\">❌</a></td><tr>";
|
|
}
|
|
} else {
|
|
html += "No announcements</div><div class=spacer></div>";
|
|
}
|
|
html += "<br><br><input onclick=createNewAnnouncement() class=button type=button value=\"New Announcement\">";
|
|
setContent(html);
|
|
setActiveLink("announcements");
|
|
}
|
|
async function manageInstance() {
|
|
clearActiveLinks();
|
|
const request = new Request("/manage/instance");
|
|
const response = await fetch(request);
|
|
const instance = await response.json();
|
|
let html = "<h4 class=\"title is-4\">Instance</h4><div class=spacer></div>";
|
|
|
|
html += "<form action=\"javascript:saveInstance()\"><div>";
|
|
|
|
html += "<div class=\"section is-inline-block\" style=\"width:420px;vertical-align:top\">";
|
|
html += "<label class=label>URI</label><div class=control><input id=uri class=input placeholder=my-slon-instance.foo required autocomplete=off value=\"" + instance["uri"] + "\"></div><div class=spacer></div>";
|
|
html += "<label class=label>Title</label><div class=control><input id=title class=input placeholder=\"My Slon Instance\" required autocomplete=off value=\"" + instance["title"] + "\"></div><div class=spacer></div>";
|
|
html += "<label class=label>Description</label><div class=control><input id=description class=input placeholder=\"A fediverse instance running on TempleOS\" required autocomplete=off value=\"" + instance["description"] + "\"></div><div class=spacer></div>";
|
|
html += "<label class=label>Email</label><div class=control><input id=email class=input type=email placeholder=\"alec@checksum.fail\" required autocomplete=off value=\"" + instance["email"] + "\"></div><div class=spacer></div>";
|
|
html += "<label class=label>Enable Registrations</label><div class=control><input id=registrations type=checkbox" + (instance["registrations"] ? " checked" : "") + "></div><div class=spacer></div>";
|
|
html += "</div>";
|
|
|
|
html += "</div>";
|
|
html += "<div class=\"control next\"><input class=\"button is-link\" type=submit value=Save></div>"
|
|
html += "</div></form>";
|
|
|
|
setContent(html);
|
|
setActiveLink("instance");
|
|
}
|
|
async function manageSettings() {
|
|
clearActiveLinks();
|
|
const request = new Request("/manage/settings");
|
|
const response = await fetch(request);
|
|
const settings = await response.json();
|
|
const keys = Object.keys(settings);
|
|
|
|
let html = "<h4 class=\"title is-4\">Settings</h4><div class=spacer></div>";
|
|
html += "<form action=\"javascript:saveSettings()\"><div>";
|
|
html += "<table class=table><thead><tr><th>name</th><th>value</th><th>delete</th></tr></head><tbody>";
|
|
if (keys.length) {
|
|
for (let i = 0; i < keys.length; i++) {
|
|
let inputType = "text";
|
|
if (obscured_setting_names.indexOf(keys[i]) != -1) {
|
|
inputType = "password";
|
|
}
|
|
html += "<tr><td>" + keys[i] + "</td><td><div class=control><input autocomplete=off class=input id=settings_" + keys[i] + " type=\"" + inputType + "\" value=\"" + settings[keys[i]] + "\"></div></td><td style=\"text-align:center\"><a href=\"javascript:deleteSetting('" + keys[i] + "');\">❌</a></td>"
|
|
}
|
|
}
|
|
html += "</tbody></table>";
|
|
html += "<div class=\"control is-inline\"><input autocomplete=off class=\"input is-inline\" id=new_name placeholder=name></div> ";
|
|
html += "<div class=\"control is-inline\"><input autocomplete=off class=\"input is-inline\" id=new_value placeholder=value></div> ";
|
|
html += "<div class=\"control is-inline\"><input class=\"button is-inline\" onclick=\"addSetting()\" type=button value=Add></div> ";
|
|
html += "<div class=\"control next\"><input class=\"button is-link\" type=submit value=Save></div>";
|
|
html += "</div></form>";
|
|
setContent(html);
|
|
setActiveLink("settings");
|
|
}
|
|
function addSetting() {
|
|
let name = document.getElementById("new_name").value;
|
|
let value = document.getElementById("new_value").value;
|
|
if (name.indexOf(" ") != -1) {
|
|
alert("key can not contain spaces: " + name);
|
|
return;
|
|
}
|
|
if (document.getElementById("settings_" + name)) {
|
|
alert("key already exists: " + name);
|
|
return;
|
|
}
|
|
let inputType = "text";
|
|
if (obscured_setting_names.indexOf(name) != -1) {
|
|
inputType = "password";
|
|
}
|
|
let new_setting_html = "<tr><td>" + name + "</td><td><div class=control><input autocomplete=off class=input id=settings_" + name + " type=\"" + inputType + "\" value=\"" + value + "\"></div></td><td style=\"text-align:center\"><a href=\"javascript:deleteSetting('" + name + "');\">❌</a></td>";
|
|
document.getElementsByTagName("tbody")[0].innerHTML += new_setting_html;
|
|
}
|
|
function deleteSetting(name) {
|
|
document.getElementById("settings_" + name).parentElement.parentElement.parentElement.remove();
|
|
}
|
|
async function saveSettings() {
|
|
let data = {};
|
|
let fields = document.querySelectorAll("[id^=settings_]")
|
|
for (var i = 0; i < fields.length; i++) {
|
|
data[fields[i].id.split("settings_")[1]] = fields[i].value;
|
|
}
|
|
const request = new Request("/save/settings", {
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
body: JSON.stringify(data)
|
|
});
|
|
const response = await fetch(request);
|
|
manageSettings();
|
|
}
|
|
async function confirmDeleteAnnouncement(id) {
|
|
if (confirm("Are you sure you want to delete announcement id " + id + " ?")) {
|
|
const request = new Request("/delete/announcement?id=" + id);
|
|
const response = await fetch(request);
|
|
const empty_json = await response.json();
|
|
manageAnnouncements();
|
|
}
|
|
}
|
|
async function confirmDeleteUser(user, id) {
|
|
if (confirm("Are you sure you want to delete '" + user + "' ?")) {
|
|
const request = new Request("/delete/account?id=" + id);
|
|
const response = await fetch(request);
|
|
const empty_json = await response.json();
|
|
manageAccounts(0);
|
|
}
|
|
}
|
|
function createNewAnnouncement() {
|
|
clearActiveLinks();
|
|
let html = "<h4 class=\"title is-4\">New Announcement</h4><div class=spacer></div>";
|
|
|
|
html += "<form action=\"javascript:saveNewAnnouncement()\"><div>";
|
|
|
|
html += "<div class=\"section is-inline-block\" style=\"width:420px;vertical-align:top\">";
|
|
html += "<textarea class=\"textarea\" placeholder=\"content goes here\" id=\"announcement-content\"></textarea>";
|
|
html += "</div>";
|
|
|
|
html += "</div>";
|
|
html += "<div class=\"control next\"><input class=\"button is-link\" type=submit value=Save></div>"
|
|
html += "</div></form>";
|
|
|
|
setContent(html);
|
|
setActiveLink("announcements");
|
|
}
|
|
function manageNewUser() {
|
|
clearActiveLinks();
|
|
let html = "<h4 class=\"title is-4\">New User</h4><div class=spacer></div>";
|
|
|
|
html += "<form action=\"javascript:saveNewUser()\"><div>";
|
|
|
|
html += "<div class=\"section is-inline-block\" style=\"width:420px;vertical-align:top\">";
|
|
html += "<label class=label>Username</label><div class=control><input id=username class=input placeholder=baoh required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Display Name</label><div class=control><input id=display_name class=input placeholder=\"Ikuro Hashizawa\" required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Email</label><div class=control><input id=email class=input type=email placeholder=\"cooldude42069@checksum.fail\" required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Bio</label><div class=control><input id=bio class=input placeholder=\"ima firin mah lazer cannon\" required autocomplete=off></div><div class=spacer></div>";
|
|
html += "</div>";
|
|
html += "<div class=\"section is-inline-block\" style=\"width:420px;vertical-align:top\">";
|
|
html += "<label class=label>Avatar</label><div class=control><input id=avatar class=input placeholder=\"https://full.path.to/my/avatar.png\" required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Header</label><div class=control><input id=header class=input placeholder=\"https://full.path.to/my/header.png\" required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Private Key (must be in DER format)</label><div class=control><input onchange=updateBase64(this) id=privatekey type=file required autocomplete=off></div><div class=spacer></div>";
|
|
html += "<label class=label>Public Key (must be in PEM format)</label><div class=control><input onchange=updateBase64(this) id=publickey type=file required autocomplete=off></div><div class=spacer></div>";
|
|
html += "</div>";
|
|
|
|
html += "</div>";
|
|
html += "<div class=\"control next\"><input class=\"button is-link\" type=submit value=Save></div>"
|
|
html += "</div></form>";
|
|
|
|
setContent(html);
|
|
setActiveLink("accounts");
|
|
}
|
|
async function saveNewAnnouncement() {
|
|
let data = {"content": document.getElementById("announcement-content").value};
|
|
const request = new Request("/new/announcement", {
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
body: JSON.stringify(data)
|
|
});
|
|
const response = await fetch(request);
|
|
const json = await response.json();
|
|
if (!Object.keys(json).length) {
|
|
manageAnnouncements();
|
|
} else {
|
|
alert(JSON.stringify(json));
|
|
}
|
|
}
|
|
async function saveNewUser() {
|
|
let data = {};
|
|
let fields = document.getElementsByTagName("input");
|
|
for (var i = 0; i < fields.length; i++) {
|
|
switch (fields[i].type) {
|
|
case "checkbox":
|
|
data[fields[i].id] = fields[i].checked;
|
|
break;
|
|
case "file":
|
|
data[fields[i].id] = fields[i].base64;
|
|
break;
|
|
case "submit":
|
|
break;
|
|
default:
|
|
data[fields[i].id] = fields[i].value;
|
|
break;
|
|
}
|
|
}
|
|
const request = new Request("/new/account", {
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
body: JSON.stringify(data)
|
|
});
|
|
const response = await fetch(request);
|
|
const json = await response.json();
|
|
if (!Object.keys(json).length) {
|
|
manageAccounts(0);
|
|
} else {
|
|
alert(JSON.stringify(json));
|
|
}
|
|
}
|
|
async function saveInstance() {
|
|
let data = {};
|
|
let fields = document.getElementsByTagName("input");
|
|
for (var i = 0; i < fields.length; i++) {
|
|
switch (fields[i].type) {
|
|
case "checkbox":
|
|
data[fields[i].id] = fields[i].checked;
|
|
break;
|
|
case "submit":
|
|
break;
|
|
default:
|
|
data[fields[i].id] = fields[i].value;
|
|
break;
|
|
}
|
|
}
|
|
const request = new Request("/save/instance", {
|
|
headers: { "Content-Type": "application/json" },
|
|
method: "POST",
|
|
body: JSON.stringify(data)
|
|
});
|
|
const response = await fetch(request);
|
|
}
|
|
function updateBase64(el) {
|
|
let reader = new FileReader();
|
|
reader.readAsDataURL(el.files[0]);
|
|
reader.addEventListener(
|
|
"load",
|
|
() => {
|
|
el.base64 = reader.result.split(";base64,")[1];
|
|
}
|
|
);
|
|
}
|
|
const formatTime = milliseconds => {
|
|
const seconds = Math.floor((milliseconds / 1000) % 60);
|
|
const minutes = Math.floor((milliseconds / 1000 / 60) % 60);
|
|
const hours = Math.floor((milliseconds / 1000 / 60 / 60) % 24);
|
|
return [
|
|
hours.toString().padStart(2, "0"),
|
|
minutes.toString().padStart(2, "0"),
|
|
seconds.toString().padStart(2, "0")
|
|
].join(":");
|
|
}
|
|
addEventListener("DOMContentLoaded", (event) => {
|
|
infoStats();
|
|
})
|
|
document.querySelectorAll("input[type=file]").forEach(function (e) {
|
|
a.className = "";
|
|
});
|
|
</script> |