Add admin interface

This commit is contained in:
2026-09-13 00:27:29 +02:00
parent 505e4f472f
commit ca34f1506d
12 changed files with 576 additions and 13 deletions
+8
View File
@@ -28,6 +28,14 @@
}
wireCopy(document);
// Destructive buttons ask first. The attribute carries the question, so the
// markup decides what is worth confirming and this stays generic.
Array.prototype.forEach.call(document.querySelectorAll('[data-confirm]'), function (button) {
button.addEventListener('click', function (e) {
if (!window.confirm(button.dataset.confirm)) e.preventDefault();
});
});
var form = document.getElementById('upload');
if (!form) return;
+25
View File
@@ -187,3 +187,28 @@ button.link {
.copyrow { flex-wrap: wrap; }
.copyrow input { flex-basis: 100%; }
}
/* Administration --------------------------------------------------------- */
header nav { float: right; font-size: .875rem; }
dl.stats { grid-template-columns: auto 1fr; }
dl.stats em { font-style: normal; color: var(--muted); }
/* Tables scroll on their own rather than making the page scroll sideways. */
.tablewrap { overflow-x: auto; margin: 0 -.25rem; }
table.admin { border-collapse: collapse; width: 100%; font-size: .8125rem; }
table.admin th, table.admin td { padding: .4rem .5rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line); }
table.admin th { color: var(--muted); font-weight: 500; white-space: nowrap; }
table.admin th a { color: inherit; text-decoration: none; }
table.admin th a:hover { color: var(--accent); text-decoration: underline; }
table.admin tr:last-child td { border-bottom: 0; }
table.admin .num { text-align: right; white-space: nowrap; }
table.admin .small { font-size: .75rem; color: var(--muted); white-space: nowrap; }
table.admin .wrap { overflow-wrap: anywhere; min-width: 8rem; }
table.admin form { margin: 0; }
button.small { padding: .2rem .5rem; font-size: .75rem; }
.cli code, .hint code { background: var(--bg); padding: .1rem .3rem; border-radius: 4px; }
+81
View File
@@ -0,0 +1,81 @@
{{define "content"}}
<section class="card">
<h1>Administration</h1>
<dl class="stats">
<dt>Files</dt><dd>{{.Count}}{{if .Anonymous}} <em>({{.Anonymous}} anonymous)</em>{{end}}</dd>
<dt>Stored</dt><dd>{{.Total}}{{if .Quota}} of {{.Quota}} ({{.QuotaPct}}%){{end}}</dd>
{{if .FreeDisk}}<dt>Free disk</dt><dd>{{.FreeDisk}}</dd>{{end}}
</dl>
</section>
<section class="card">
<h2>Files</h2>
{{if not .Objects}}
<p class="hint">Nothing stored right now.</p>
{{else}}
<div class="tablewrap">
<table class="admin">
<thead>
<tr>
<th><a href="?sort=name">Name</a></th>
<th><a href="?sort=name">Filename</a></th>
<th class="num"><a href="?sort=size">Size</a></th>
<th><a href="?sort=owner">Owner</a></th>
<th><a href="?sort=created">Uploaded</a></th>
<th><a href="?sort=expires">Expires</a></th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Objects}}
<tr>
<td class="mono"><a href="{{$.Base}}i/{{.ID}}">{{.ID}}</a>{{if .Vanity}} <em title="custom name">★</em>{{end}}</td>
<td class="wrap">{{.Filename}}</td>
<td class="num">{{.Size}}</td>
<td>{{if .Owner}}{{.Owner}}{{else}}<em>anonymous</em>{{end}}</td>
<td class="mono small">{{.Created}}</td>
<td class="mono small">{{.Expires}}</td>
<td>
<form method="post" action="{{$.Base}}api/d/{{.ID}}/delete">
<input type="hidden" name="from" value="admin">
<button type="submit" class="danger small"
data-confirm="Delete {{.Filename}}?">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</section>
<section class="card">
<h2>Tokens</h2>
<p class="hint">
Tokens are minted and revoked with <code>send token</code> on the server.
They are deliberately not manageable from here: nothing that hands out
credentials should be reachable over the network.
</p>
{{if .Tokens}}
<div class="tablewrap">
<table class="admin">
<thead>
<tr><th>Name</th><th>Max size</th><th>Max lifetime</th><th>Vanity</th><th>Admin</th></tr>
</thead>
<tbody>
{{range .Tokens}}
<tr>
<td>{{.Name}}</td>
<td>{{.MaxSize}}</td>
<td>{{.MaxExpiry}}</td>
<td>{{if .Vanity}}yes{{else}}no{{end}}</td>
<td>{{if .Admin}}yes{{else}}no{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</section>
{{end}}
+1
View File
@@ -9,6 +9,7 @@
<body data-base="{{.Base}}">
<header>
<a class="brand" href="{{.Base}}">Uncensored&nbsp;Send</a>
{{if .Admin}}<nav><a href="{{.Base}}admin">Administration</a></nav>{{end}}
</header>
<main>
{{template "content" .}}