feat(admin): allow editing boxes and deleting individual files
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m44s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m44s
Introduce new admin capabilities to manage uploaded boxes and files: - Add routes and handlers for editing boxes and deleting individual files. - Implement `RemoveFileFromBox` in `UploadService` to delete a file's stored objects and remove it from the box (deleting the box if empty). - Implement `AdminUpdateBox` in `UploadService` to update expiry, download limits, and clear password protection. - Remove the unused `AdminFiles` handler. - Add `.claude` to `.gitignore`.
This commit is contained in:
131
backend/templates/pages/admin_box_edit.html
Normal file
131
backend/templates/pages/admin_box_edit.html
Normal file
@@ -0,0 +1,131 @@
|
||||
{{define "admin_box_edit.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="app-shell admin-shell" aria-labelledby="admin-box-edit-title">
|
||||
<aside class="app-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a class="sidebar-link" href="/admin">{{template "icon-dashboard" .}}<span>Overview</span></a>
|
||||
<a class="sidebar-link is-active" href="/admin/files">{{template "icon-folder" .}}<span>Files</span></a>
|
||||
<a class="sidebar-link" href="/admin/users">{{template "icon-user-circle" .}}<span>Users</span></a>
|
||||
<a class="sidebar-link" href="/admin/settings">{{template "icon-settings" .}}<span>Settings</span></a>
|
||||
<a class="sidebar-link" href="/admin/storage">{{template "icon-database" .}}<span>Storage</span></a>
|
||||
<a class="sidebar-link" href="/admin/logs">{{template "icon-database" .}}<span>Logs</span></a>
|
||||
<a class="sidebar-link" href="/admin/bans">{{template "icon-settings" .}}<span>Bans</span></a>
|
||||
</nav>
|
||||
<hr class="sidebar-sep">
|
||||
<nav class="sidebar-nav">
|
||||
<a class="sidebar-link" href="/app">{{template "icon-home-simple" .}}<span>My Files</span></a>
|
||||
</nav>
|
||||
<hr class="sidebar-sep">
|
||||
<form class="sidebar-logout" action="/admin/logout" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button class="button button-outline" type="submit">{{template "icon-log-out" .}}<span>Sign out</span></button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div class="app-main">
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<p class="kicker">Operator console · <a href="/admin/files">Files</a></p>
|
||||
<h1 id="admin-box-edit-title">{{.Data.PageTitle}}</h1>
|
||||
<p class="muted-copy">Box <code>{{.Data.Box.ID}}</code> · {{.Data.Box.Owner}}</p>
|
||||
</div>
|
||||
<a class="button button-outline" href="/admin/boxes/{{.Data.Box.ID}}/view">Open box</a>
|
||||
</div>
|
||||
|
||||
{{if .Data.Notice}}<p class="form-success">{{.Data.Notice}}</p>{{end}}
|
||||
{{if .Data.Error}}<p class="form-error">{{.Data.Error}}</p>{{end}}
|
||||
|
||||
<div class="card admin-table-card">
|
||||
<div class="card-content">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h2>Box settings</h2>
|
||||
<p>Change expiration, download limit, and protection.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="manage-details">
|
||||
<div><dt>Created</dt><dd>{{.Data.Box.CreatedAt}}</dd></div>
|
||||
<div><dt>Files</dt><dd>{{.Data.Box.FileCount}}</dd></div>
|
||||
<div><dt>Total size</dt><dd>{{.Data.Box.TotalSize}}</dd></div>
|
||||
<div><dt>Downloads</dt><dd>{{.Data.Box.DownloadCount}}{{if .Data.Box.MaxDownloads}} / {{.Data.Box.MaxDownloads}}{{end}}</dd></div>
|
||||
<div><dt>Expires</dt><dd>{{.Data.Box.ExpiresLabel}}</dd></div>
|
||||
<div><dt>Storage backend</dt><dd>{{.Data.Box.BackendID}}</dd></div>
|
||||
<div><dt>Protected</dt><dd>{{if .Data.Box.Protected}}Yes{{else}}No{{end}}</dd></div>
|
||||
</dl>
|
||||
|
||||
<form class="settings-form settings-form-narrow" action="/admin/boxes/{{.Data.Box.ID}}/edit" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<label>
|
||||
<span>Expires at (UTC)</span>
|
||||
<input type="datetime-local" name="expires_at" value="{{.Data.Box.ExpiresInput}}">
|
||||
</label>
|
||||
<label class="checkbox-field">
|
||||
<input type="checkbox" name="never_expires" {{if .Data.Box.NeverExpires}}checked{{end}}>
|
||||
<span>Never expires (overrides the date above)</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Max downloads (0 = unlimited)</span>
|
||||
<input type="number" min="0" name="max_downloads" value="{{.Data.Box.MaxDownloads}}">
|
||||
</label>
|
||||
{{if .Data.Box.Protected}}
|
||||
<label class="checkbox-field">
|
||||
<input type="checkbox" name="remove_password">
|
||||
<span>Remove password protection</span>
|
||||
</label>
|
||||
{{end}}
|
||||
<button class="button button-primary" type="submit">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card admin-table-card">
|
||||
<div class="card-content">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h2>Files</h2>
|
||||
<p>Remove individual files from this box. Removing the last file deletes the box.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-list">
|
||||
{{range .Data.Files}}
|
||||
<article class="download-item">
|
||||
{{if .HasPreview}}<a class="thumb-link" href="{{.DownloadURL}}?inline=1" target="_blank" rel="noopener noreferrer"><img src="{{.ThumbnailURL}}" alt="" loading="lazy"></a>{{end}}
|
||||
<a class="file-main" href="{{.DownloadURL}}?inline=1" target="_blank" rel="noopener noreferrer">
|
||||
<strong class="file-name" title="{{.Name}}">{{.Name}}</strong>
|
||||
<small>{{.Size}} · {{.ContentType}}</small>
|
||||
</a>
|
||||
<div class="file-actions">
|
||||
<a class="button button-outline button-sm" href="{{.DownloadURL}}" download="{{.Name}}">Download</a>
|
||||
<form action="/admin/boxes/{{$.Data.Box.ID}}/files/{{.ID}}/delete" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button class="button button-danger button-sm" type="submit">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{{else}}
|
||||
<p class="muted-copy">This box has no files.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card admin-table-card">
|
||||
<div class="card-content">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h2>Danger zone</h2>
|
||||
<p>Permanently delete this box and all of its files.</p>
|
||||
</div>
|
||||
<form action="/admin/boxes/{{.Data.Box.ID}}/delete" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button class="button button-danger" type="submit">Delete box</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
107
backend/templates/pages/admin_files.html
Normal file
107
backend/templates/pages/admin_files.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{{define "admin_files.html"}}{{template "base" .}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<section class="app-shell admin-shell" aria-labelledby="admin-files-title">
|
||||
<aside class="app-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<a class="sidebar-link" href="/admin">{{template "icon-dashboard" .}}<span>Overview</span></a>
|
||||
<a class="sidebar-link is-active" href="/admin/files">{{template "icon-folder" .}}<span>Files</span></a>
|
||||
<a class="sidebar-link" href="/admin/users">{{template "icon-user-circle" .}}<span>Users</span></a>
|
||||
<a class="sidebar-link" href="/admin/settings">{{template "icon-settings" .}}<span>Settings</span></a>
|
||||
<a class="sidebar-link" href="/admin/storage">{{template "icon-database" .}}<span>Storage</span></a>
|
||||
<a class="sidebar-link" href="/admin/logs">{{template "icon-database" .}}<span>Logs</span></a>
|
||||
<a class="sidebar-link" href="/admin/bans">{{template "icon-settings" .}}<span>Bans</span></a>
|
||||
</nav>
|
||||
<hr class="sidebar-sep">
|
||||
<nav class="sidebar-nav">
|
||||
<a class="sidebar-link" href="/app">{{template "icon-home-simple" .}}<span>My Files</span></a>
|
||||
</nav>
|
||||
<hr class="sidebar-sep">
|
||||
<form class="sidebar-logout" action="/admin/logout" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button class="button button-outline" type="submit">{{template "icon-log-out" .}}<span>Sign out</span></button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div class="app-main">
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<p class="kicker">Operator console</p>
|
||||
<h1 id="admin-files-title">{{.Data.PageTitle}}</h1>
|
||||
<p class="muted-copy">{{.Data.Total}} box{{if ne .Data.Total 1}}es{{end}} total.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card admin-table-card">
|
||||
<div class="card-content">
|
||||
<div class="table-header">
|
||||
<div>
|
||||
<h2>All uploads</h2>
|
||||
<p>Search, sort, and manage every box.</p>
|
||||
</div>
|
||||
<form class="inline-controls" method="get" action="/admin/files">
|
||||
<input type="hidden" name="sort" value="{{.Data.Sort}}">
|
||||
<input type="hidden" name="dir" value="{{.Data.Dir}}">
|
||||
<label>
|
||||
<span class="sr-only">Search</span>
|
||||
<input type="search" name="q" value="{{.Data.Query}}" placeholder="Search box id or owner">
|
||||
</label>
|
||||
<button class="button button-primary button-sm" type="submit">Search</button>
|
||||
{{if .Data.Query}}<a class="button button-outline button-sm" href="/admin/files">Clear</a>{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{range .Data.Columns}}
|
||||
<th><a class="sort-link {{if .Sorted}}is-sorted{{end}}" href="{{.Href}}">{{.Label}}{{if .Sorted}}<span class="sort-arrow" aria-hidden="true">{{if .Ascending}}▲{{else}}▼{{end}}</span>{{end}}</a></th>
|
||||
{{end}}
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Data.Boxes}}
|
||||
<tr>
|
||||
<td><a href="/admin/boxes/{{.ID}}/edit"><code>{{.ID}}</code></a></td>
|
||||
<td>{{.Owner}}</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-primary button-sm" href="/admin/boxes/{{.ID}}/edit">Edit</a>
|
||||
<a class="button button-outline button-sm" href="/admin/boxes/{{.ID}}/view">View</a>
|
||||
<form action="/admin/boxes/{{.ID}}/delete" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button class="button button-danger button-sm" type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="9">No boxes match.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if gt .Data.TotalPages 1}}
|
||||
<nav class="pagination" aria-label="Pagination">
|
||||
{{if .Data.HasPrev}}<a class="button button-outline button-sm" href="{{.Data.PrevHref}}">← Prev</a>{{end}}
|
||||
{{range .Data.PageLinks}}<a class="button button-sm {{if .Active}}is-active{{else}}button-outline{{end}}" href="{{.Href}}">{{.Page}}</a>{{end}}
|
||||
{{if .Data.HasNext}}<a class="button button-outline button-sm" href="{{.Data.NextHref}}">Next →</a>{{end}}
|
||||
</nav>
|
||||
{{end}}
|
||||
<p class="pagination-summary">Showing {{.Data.RangeFrom}}–{{.Data.RangeTo}} of {{.Data.Total}} · Page {{.Data.Page}} of {{.Data.TotalPages}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user