feat(storage): support deleting backends and improve admin UI
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m41s

- Implement storage backend deletion, which automatically resets default storage settings and user-specific overrides when a backend is removed.
- Add unit tests covering the delete action and its cleanup side effects.
- Improve admin UI responsiveness, fixing table scrolling, flex wrapping, and text truncation for long storage backend names.
- Update security documentation to clarify trusted proxy configurations and explain how trusted proxies are protected from automatic bans.
This commit is contained in:
2026-06-01 02:24:51 +03:00
parent 4eacb4cde2
commit 73bd14572d
27 changed files with 1124 additions and 128 deletions

View File

@@ -564,6 +564,10 @@ func (a *App) AdminCreateBan(w http.ResponseWriter, r *http.Request) {
if user, ok := a.currentUser(r); ok {
createdBy = user.ID
}
if services.ProtectedBanTarget(r.FormValue("target"), a.cfg.TrustedProxies) {
http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape("Refusing to ban loopback or trusted proxy addresses."), http.StatusSeeOther)
return
}
ban, err := a.banService.CreateManualBan(r.FormValue("target"), r.FormValue("reason"), createdBy, expiresAt.UTC())
if err != nil {
http.Redirect(w, r, "/admin/bans?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
@@ -883,32 +887,45 @@ func (a *App) AdminStartStorageSpeedTest(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/tests?notice="+url.QueryEscape("Storage speed test started in the background."), http.StatusSeeOther)
}
func (a *App) AdminDisableStorage(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
return
}
id := r.PathValue("backendID")
inUse, _ := a.storageBackendInUse(id)
if err := a.uploadService.Storage().DisableBackend(id, inUse); err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
a.logger.Info("storage backend disabled", "source", "admin", "severity", "user_activity", "code", 2324, "ip", uploadClientIP(r), "backend_id", id)
http.Redirect(w, r, "/admin/storage", http.StatusSeeOther)
}
func (a *App) AdminDeleteStorage(w http.ResponseWriter, r *http.Request) {
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
return
}
id := r.PathValue("backendID")
inUse, _ := a.storageBackendInUse(id)
if err := a.uploadService.Storage().DeleteBackend(id, inUse); err != nil {
cfg, err := a.uploadService.Storage().BackendConfig(id)
if err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
if cfg.ID == services.StorageBackendLocal {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape("local storage cannot be deleted"), http.StatusSeeOther)
return
}
deletedBoxes, err := a.uploadService.DeleteBoxesForStorageBackend(id, "storage-delete")
if err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
resetAnonymous, resetUsersDefault, err := a.settingsService.ResetStorageBackend(id)
if err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
clearedUsers, err := a.authService.ClearStorageBackendOverrides(id)
if err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
if err := a.uploadService.Storage().DeleteBackend(id, false); err != nil {
http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
return
}
notice := fmt.Sprintf("Storage backend deleted. Removed %d related boxes and cleared %d user overrides.", deletedBoxes, clearedUsers)
if resetAnonymous || resetUsersDefault {
notice += " Global storage defaults were reset to local."
}
a.logger.Info("storage backend deleted", "source", "admin", "severity", "user_activity", "code", 2325, "ip", uploadClientIP(r), "backend_id", id)
http.Redirect(w, r, "/admin/storage", http.StatusSeeOther)
http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(notice), http.StatusSeeOther)
}
func (a *App) AdminRunStorageCleanup(w http.ResponseWriter, r *http.Request) {
@@ -1548,7 +1565,7 @@ func logEntryFromMap(raw map[string]any) adminLogEntry {
Method: logString(raw, "method"),
Path: logString(raw, "path"),
Status: logAnyString(raw["status"]),
IP: firstLogString(raw, "ip", "client_ip", "remote_addr"),
IP: services.IPOnly(firstLogString(raw, "ip", "client_ip", "remote_addr")),
UserID: logString(raw, "user_id"),
}
entry.Details = logDetails(raw)
@@ -1767,13 +1784,14 @@ func (a *App) storageBackendViews() ([]services.StorageBackendView, error) {
usage, _ = concrete.Usage(context.Background())
}
}
inUse, _ := a.storageBackendInUse(cfg.ID)
inUse, inUseReason, _ := a.storageBackendUseReason(cfg.ID)
speedTests, _ := a.uploadService.Storage().ListSpeedTests(cfg.ID, 25)
views = append(views, services.StorageBackendView{
Config: cfg,
UsageBytes: usage,
UsageLabel: services.FormatMegabytesFromBytes(usage),
InUse: inUse,
InUseReason: inUseReason,
SpeedTests: speedTests,
CanSpeedTest: cfg.LastTestSuccess,
})
@@ -1822,32 +1840,40 @@ func (a *App) adminUserEdit(user services.User, settings services.UploadPolicySe
}
func (a *App) storageBackendInUse(id string) (bool, error) {
inUse, _, err := a.storageBackendUseReason(id)
return inUse, err
}
func (a *App) storageBackendUseReason(id string) (bool, string, error) {
settings, err := a.settingsService.UploadPolicy()
if err != nil {
return false, err
return false, "", err
}
if settings.AnonymousStorageBackend == id || settings.UserStorageBackend == id {
return true, nil
if settings.AnonymousStorageBackend == id {
return true, "selected as the global anonymous storage backend", nil
}
if settings.UserStorageBackend == id {
return true, "selected as the global user storage backend", nil
}
boxes, err := a.uploadService.ListBoxes(0)
if err != nil {
return false, err
return false, "", err
}
for _, box := range boxes {
if a.uploadService.BoxStorageBackendID(box) == id {
return true, nil
return true, "used by existing boxes", nil
}
}
users, err := a.authService.ListUsers()
if err != nil {
return false, err
return false, "", err
}
for _, user := range users {
if user.Policy.StorageBackendID != nil && *user.Policy.StorageBackendID == id {
return true, nil
return true, "assigned to one or more users", nil
}
}
return false, nil
return false, "", nil
}
func floatPtrString(value *float64) string {