Files
warpbox-dev/backend/templates/pages/admin_storage.html
Daniel Legt 1513030c2a
Some checks failed
Build and Publish Docker Image / deploy (push) Has been cancelled
feat(admin): implement provider-specific storage configuration pages
Refactor the admin storage backend creation and editing flows to use
provider-specific pages (e.g., `/admin/storage/new/sftp`) instead of a
single generic form. This ensures only relevant fields are rendered for
each storage provider (such as SFTP, S3, or WebDAV).

Additionally:
- Prevent mutation of the storage provider type during backend edits.
- Add comprehensive unit tests for provider-specific rendering, edit
  validation, and CSRF/admin route protection.
2026-05-31 19:52:46 +03:00

140 lines
8.4 KiB
HTML

{{define "admin_storage.html"}}{{template "base" .}}{{end}}
{{define "content"}}
<section class="app-shell admin-shell" aria-labelledby="admin-storage-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" 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 is-active" href="/admin/storage">{{template "icon-database" .}}<span>Storage</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-storage-title">{{.Data.PageTitle}}</h1>
<p class="muted-copy">Local storage is always active. Remote backends are proxied through Warpbox.</p>
</div>
<a class="button button-primary" href="/admin/storage/new">{{template "icon-plus-circle" .}}<span>Add storage</span></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="storage-ops-grid">
<form class="storage-op-card" action="/admin/storage/jobs/cleanup" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<strong>Run cleanup now</strong>
<span>Remove expired boxes and boxes that reached their download limit.</span>
<button class="button button-outline button-sm" type="submit">Run cleanup</button>
</form>
<form class="storage-op-card" action="/admin/storage/jobs/thumbnails" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<strong>Generate thumbnails now</strong>
<span>Scan active boxes and create missing image or video thumbnails.</span>
<button class="button button-outline button-sm" type="submit">Generate</button>
</form>
<form class="storage-op-card" action="/admin/storage/jobs/verify" method="post">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<strong>Verify storage now</strong>
<span>Test every enabled backend and update its last-test status.</span>
<button class="button button-outline button-sm" type="submit">Verify all</button>
</form>
</div>
<div class="storage-stack">
{{range .Data.Storage}}
<div class="storage-card {{if eq .Config.ID "local"}}is-local{{end}}" data-storage-id="{{.Config.ID}}">
<div class="storage-card-header">
<div class="storage-card-identity">
<div class="storage-card-icon">
{{if eq .Config.Type "local"}}{{template "icon-hard-drive" $}}
{{else if eq .Config.Type "sftp"}}{{template "icon-database" $}}
{{else if eq .Config.Type "smb"}}{{template "icon-folder" $}}
{{else if eq .Config.Type "webdav"}}{{template "icon-cloud-sync" $}}
{{else}}{{template "icon-cloud-upload" $}}{{end}}
</div>
<div>
<strong class="storage-card-name">{{.Config.Name}}</strong>
<div class="storage-card-meta">
<span class="badge">{{if eq .Config.Provider "contabo"}}Contabo{{else if eq .Config.Type "sftp"}}SFTP{{else if eq .Config.Type "smb"}}Samba{{else if eq .Config.Type "webdav"}}WebDAV{{else if eq .Config.Type "s3"}}S3{{else if eq .Config.Type "local"}}Local files{{else}}{{.Config.Type}}{{end}}</span>
{{if eq .Config.ID "local"}}<span class="badge">Required</span>
{{else if .Config.Enabled}}<span class="badge badge-active">Enabled</span>
{{else}}<span class="badge badge-disabled">Disabled</span>{{end}}
{{if .UsageLabel}}<span class="storage-card-usage">{{.UsageLabel}}</span>{{end}}
</div>
</div>
</div>
<div class="storage-card-actions">
{{if .CanSpeedTest}}
<a class="button button-outline button-sm" href="/admin/storage/{{.Config.ID}}/tests">Testing</a>
{{else}}
<form action="/admin/storage/{{.Config.ID}}/test" method="post">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button class="button button-outline button-sm" type="submit">Test</button>
</form>
{{end}}
{{if ne .Config.ID "local"}}
<a class="button button-outline button-sm" href="/admin/storage/{{.Config.ID}}/edit">Edit</a>
{{if .Config.Enabled}}
<form action="/admin/storage/{{.Config.ID}}/disable" method="post">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button class="button button-outline button-sm" type="submit" {{if .InUse}}disabled title="Backend is in use"{{end}}>Disable</button>
</form>
{{end}}
<form action="/admin/storage/{{.Config.ID}}/delete" method="post">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button class="button button-danger button-sm" type="submit" {{if .InUse}}disabled title="Backend is in use"{{end}}>Delete</button>
</form>
{{end}}
</div>
</div>
<div class="storage-card-summary">
{{if eq .Config.Type "local"}}
<div class="storage-detail"><span>Path</span><code>{{.Config.LocalPath}}</code></div>
{{else if or (eq .Config.Type "s3") (eq .Config.Provider "contabo")}}
{{if .Config.Endpoint}}<div class="storage-detail"><span>Endpoint</span><span>{{.Config.Endpoint}}</span></div>{{end}}
{{if .Config.Bucket}}<div class="storage-detail"><span>Bucket</span><span>{{.Config.Bucket}}</span></div>{{end}}
{{if .Config.Region}}<div class="storage-detail"><span>Region</span><span>{{.Config.Region}}</span></div>{{end}}
{{if .Config.AccessKey}}<div class="storage-detail"><span>Access key</span><span>{{.Config.AccessKey}}</span></div>{{end}}
{{else if eq .Config.Type "sftp"}}
{{if .Config.Host}}<div class="storage-detail"><span>Host</span><span>{{.Config.Host}}{{if .Config.Port}}:{{.Config.Port}}{{end}}</span></div>{{end}}
{{if .Config.Username}}<div class="storage-detail"><span>Username</span><span>{{.Config.Username}}</span></div>{{end}}
{{if .Config.RemotePath}}<div class="storage-detail"><span>Remote path</span><span>{{.Config.RemotePath}}</span></div>{{end}}
{{else if eq .Config.Type "smb"}}
{{if .Config.Host}}<div class="storage-detail"><span>Host</span><span>{{if .Config.Domain}}{{.Config.Domain}}\{{end}}{{.Config.Username}}@{{.Config.Host}}/{{.Config.Share}}</span></div>{{end}}
{{if .Config.RemotePath}}<div class="storage-detail"><span>Remote path</span><span>{{.Config.RemotePath}}</span></div>{{end}}
{{else if eq .Config.Type "webdav"}}
{{if .Config.Endpoint}}<div class="storage-detail"><span>URL</span><span>{{.Config.Endpoint}}</span></div>{{end}}
{{if .Config.Username}}<div class="storage-detail"><span>Username</span><span>{{.Config.Username}}</span></div>{{end}}
{{if .Config.RemotePath}}<div class="storage-detail"><span>Remote path</span><span>{{.Config.RemotePath}}</span></div>{{end}}
{{end}}
{{if not (.Config.LastTestedAt.IsZero)}}
<div class="storage-detail storage-detail-test {{if .Config.LastTestSuccess}}is-ok{{else}}is-err{{end}}">
<span>Last test</span>
<span>{{.Config.LastTestedAt.Format "Jan 2, 15:04"}} · {{if .Config.LastTestSuccess}}Passed{{else}}{{if .Config.LastTestError}}{{.Config.LastTestError}}{{else}}Failed{{end}}{{end}}</span>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
</section>
{{end}}