feat(storage): support deleting backends and improve admin UI
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m41s
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user