feat: add admin console, cleanup, and thumbnail workers
- Implement a token-authenticated admin console at `/admin` with overview metrics and file management. - Add a background worker to periodically clean up expired boxes based on `WARPBOX_CLEANUP_EVERY`. - Add a background worker to generate image and video thumbnails based on `WARPBOX_THUMBNAIL_EVERY`. - Update file storage paths to use `@each@` and `@thumb@` prefixes to separate original files from thumbnails. - Add severity fields to startup logs and update configuration templates.
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
<meta property="og:description" content="{{.Description}}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{.BaseURL}}">
|
||||
{{if .ImageURL}}<meta property="og:image" content="{{.ImageURL}}">{{end}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
{{if .ImageURL}}<meta name="twitter:image" content="{{.ImageURL}}">{{end}}
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<script defer src="/static/js/app.js"></script>
|
||||
</head>
|
||||
95
backend/templates/pages/admin.html
Normal file
95
backend/templates/pages/admin.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{{define "admin.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="admin-view" aria-labelledby="admin-title">
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<p class="kicker">Operator console</p>
|
||||
<h1 id="admin-title">Admin overview</h1>
|
||||
</div>
|
||||
<form action="/admin/logout" method="post">
|
||||
<button class="button button-outline" type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<span>Total boxes</span>
|
||||
<strong>{{.Data.Stats.TotalBoxes}}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Total files</span>
|
||||
<strong>{{.Data.Stats.TotalFiles}}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Storage used</span>
|
||||
<strong>{{.Data.Stats.TotalSizeLabel}}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Uploads 24h</span>
|
||||
<strong>{{.Data.Stats.UploadsLast24H}}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Protected</span>
|
||||
<strong>{{.Data.Stats.ProtectedBoxes}}</strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Expired</span>
|
||||
<strong>{{.Data.Stats.ExpiredBoxes}}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="card admin-table-card">
|
||||
<div class="card-content">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h2>Recent uploads</h2>
|
||||
<p>View or remove anonymous boxes.</p>
|
||||
</div>
|
||||
<a class="button button-outline" href="/admin/files">View all</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Box</th>
|
||||
<th>Files</th>
|
||||
<th>Size</th>
|
||||
<th>Downloads</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Data.Boxes}}
|
||||
<tr>
|
||||
<td><code>{{.ID}}</code></td>
|
||||
<td>{{.FileCount}}</td>
|
||||
<td>{{.TotalSizeLabel}}</td>
|
||||
<td>{{.DownloadCount}}{{if .MaxDownloads}} / {{.MaxDownloads}}{{end}}</td>
|
||||
<td>{{.CreatedAt}}</td>
|
||||
<td>{{.ExpiresAt}}</td>
|
||||
<td>
|
||||
{{if .Expired}}<span class="badge">expired</span>{{else}}<span class="badge">active</span>{{end}}
|
||||
{{if .Protected}}<span class="badge">protected</span>{{end}}
|
||||
</td>
|
||||
<td class="table-actions">
|
||||
<a class="button button-outline" href="/d/{{.ID}}">View</a>
|
||||
<form action="/admin/boxes/{{.ID}}/delete" method="post">
|
||||
<button class="button button-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="8">No uploads yet.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
23
backend/templates/pages/admin_login.html
Normal file
23
backend/templates/pages/admin_login.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{{define "admin_login.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="download-view" aria-labelledby="admin-login-title">
|
||||
<form class="card download-card" action="/admin/login" method="post">
|
||||
<div class="card-content">
|
||||
<div class="file-emblem" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M12 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" /><path d="M19 11V8A7 7 0 0 0 5 8v3" /><path d="M5 11h14v10H5z" /></svg>
|
||||
</div>
|
||||
<h1 id="admin-login-title">Admin login</h1>
|
||||
<p class="download-subtitle">Use the token from <code>WARPBOX_ADMIN_TOKEN</code>.</p>
|
||||
{{if .Data.Error}}<p class="form-error">{{.Data.Error}}</p>{{end}}
|
||||
<div class="unlock-form">
|
||||
<label>
|
||||
<span>Admin token</span>
|
||||
<input type="password" name="token" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Sign in</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,41 +0,0 @@
|
||||
{{define "download.gohtml"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="download-view" aria-labelledby="download-title">
|
||||
<div class="card download-card">
|
||||
<div class="card-content">
|
||||
<div class="file-emblem" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /></svg>
|
||||
</div>
|
||||
<h1 id="download-title">Download files</h1>
|
||||
<p class="download-subtitle">Bucket id: {{.Data.Box.ID}}</p>
|
||||
|
||||
{{if .Data.Files}}
|
||||
<div class="badge-row">
|
||||
<span class="badge">Expires {{.Data.ExpiresLabel}}</span>
|
||||
{{if .Data.MaxDownloads}}<span class="badge">{{.Data.DownloadCount}} / {{.Data.MaxDownloads}} downloads</span>{{end}}
|
||||
</div>
|
||||
|
||||
<a class="button button-primary button-wide" href="{{.Data.ZipURL}}">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||
Download zip
|
||||
</a>
|
||||
|
||||
<div class="download-list">
|
||||
{{range .Data.Files}}
|
||||
<a class="download-item" href="{{.URL}}">
|
||||
<span>
|
||||
<strong>{{.Name}}</strong>
|
||||
<small>{{.Size}} · {{.ContentType}}</small>
|
||||
</span>
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="download-subtitle">{{.Data.ExpiresLabel}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
70
backend/templates/pages/download.html
Normal file
70
backend/templates/pages/download.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{{define "download.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="download-view download-view-wide" aria-labelledby="download-title">
|
||||
<div class="card download-card">
|
||||
<div class="card-content">
|
||||
<div class="file-emblem" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /></svg>
|
||||
</div>
|
||||
<h1 id="download-title">{{if .Data.Locked}}Protected box{{else}}Download files{{end}}</h1>
|
||||
<p class="download-subtitle">Bucket id: {{.Data.Box.ID}}</p>
|
||||
|
||||
{{if .Data.Locked}}
|
||||
<form class="unlock-form" action="/d/{{.Data.Box.ID}}/unlock" method="post">
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Unlock box</button>
|
||||
</form>
|
||||
{{if .Data.Obfuscated}}
|
||||
<p class="download-subtitle">File names, counts, and previews are hidden until the password is entered.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Data.Files}}
|
||||
<div class="badge-row">
|
||||
<span class="badge">Expires {{.Data.ExpiresLabel}}</span>
|
||||
{{if .Data.MaxDownloads}}<span class="badge">{{.Data.DownloadCount}} / {{.Data.MaxDownloads}} downloads</span>{{end}}
|
||||
</div>
|
||||
|
||||
{{if not .Data.Locked}}
|
||||
<a class="button button-primary button-wide" href="{{.Data.ZipURL}}">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||
Download zip
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<div class="view-toolbar" aria-label="File view options">
|
||||
<button class="button button-outline is-active" type="button" data-view-button="list">List</button>
|
||||
<button class="button button-outline" type="button" data-view-button="thumbs">Thumbnails</button>
|
||||
<button class="button button-outline" type="button" data-preview-images>Preview images only</button>
|
||||
</div>
|
||||
|
||||
<div class="download-list file-browser is-list" data-file-browser>
|
||||
{{range .Data.Files}}
|
||||
<article class="download-item file-card" data-kind="{{.PreviewKind}}">
|
||||
<a class="thumb-link" href="{{.URL}}" aria-label="Preview {{.Name}}">
|
||||
<img src="{{.ThumbnailURL}}" alt="" loading="lazy">
|
||||
</a>
|
||||
<a class="file-main" href="{{.URL}}">
|
||||
<strong>{{.Name}}</strong>
|
||||
<small>{{.Size}} · {{.ContentType}}</small>
|
||||
</a>
|
||||
{{if not $.Data.Locked}}
|
||||
<a class="button button-outline" href="{{.DownloadURL}}">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||
Download
|
||||
</a>
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if not .Data.Locked}}
|
||||
<p class="download-subtitle">{{.Data.ExpiresLabel}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{define "home.gohtml"}}{{template "base" .}}{{end}}
|
||||
{{define "home.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="upload-view" aria-labelledby="upload-title">
|
||||
@@ -39,7 +39,11 @@
|
||||
</label>
|
||||
<label>
|
||||
<span>Password</span>
|
||||
<input type="password" name="password" autocomplete="new-password" placeholder="Coming soon" disabled>
|
||||
<input type="password" name="password" autocomplete="new-password" placeholder="Optional">
|
||||
</label>
|
||||
<label class="checkbox-field">
|
||||
<input type="checkbox" name="obfuscate_metadata">
|
||||
<span>Hide file names/count until unlocked</span>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
@@ -49,8 +53,9 @@
|
||||
<span>Uploading</span>
|
||||
<span id="upload-status">Preparing...</span>
|
||||
</div>
|
||||
<div class="progress"><span></span></div>
|
||||
<div class="progress"><span id="total-progress-bar"></span></div>
|
||||
</div>
|
||||
<div class="result-list upload-queue" id="upload-queue" hidden></div>
|
||||
|
||||
<div class="form-footer">
|
||||
<p id="file-summary">Choose one or more files to begin.</p>
|
||||
@@ -70,7 +75,7 @@
|
||||
<p id="result-meta"></p>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button class="button button-outline" type="button" id="copy-all">Copy all</button>
|
||||
<button class="button button-outline" type="button" id="copy-url">Copy URL</button>
|
||||
<a class="button button-primary" id="open-box" href="/">Open box</a>
|
||||
</div>
|
||||
</div>
|
||||
36
backend/templates/pages/preview.html
Normal file
36
backend/templates/pages/preview.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{{define "preview.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="download-view" aria-labelledby="preview-title">
|
||||
<div class="card download-card">
|
||||
<div class="card-content">
|
||||
{{if .Data.Locked}}
|
||||
<div class="file-emblem" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M12 17a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" /><path d="M19 11V8A7 7 0 0 0 5 8v3" /><path d="M5 11h14v10H5z" /></svg>
|
||||
</div>
|
||||
<h1 id="preview-title">Protected file</h1>
|
||||
<p class="download-subtitle">Unlock the box before viewing this file.</p>
|
||||
<a class="button button-primary button-wide" href="/d/{{.Data.Box.ID}}">Unlock box</a>
|
||||
{{else}}
|
||||
<div class="preview-stage">
|
||||
{{if eq .Data.File.PreviewKind "image"}}
|
||||
<img src="{{.Data.DownloadURL}}?inline=1" alt="{{.Data.File.Name}}">
|
||||
{{else if eq .Data.File.PreviewKind "video"}}
|
||||
<video src="{{.Data.DownloadURL}}?inline=1" poster="{{.Data.File.ThumbnailURL}}" controls preload="metadata"></video>
|
||||
{{else if eq .Data.File.PreviewKind "audio"}}
|
||||
<audio src="{{.Data.DownloadURL}}?inline=1" controls preload="metadata"></audio>
|
||||
{{else}}
|
||||
<img src="{{.Data.File.ThumbnailURL}}" alt="">
|
||||
{{end}}
|
||||
</div>
|
||||
<h1 id="preview-title">{{.Data.File.Name}}</h1>
|
||||
<p class="download-subtitle">{{.Data.File.Size}} · {{.Data.File.ContentType}}</p>
|
||||
<a class="button button-primary button-wide" href="{{.Data.DownloadURL}}">
|
||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||
Download file
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user