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:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -805,6 +806,101 @@ func TestAdminStorageJobRoutesRequireAdminAndCSRF(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminStorageDeleteAction(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
adminToken := createAdminSession(t, app)
|
||||
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
|
||||
Provider: services.StorageProviderWebDAV,
|
||||
Name: "DAV",
|
||||
Endpoint: "https://dav.example.test",
|
||||
Username: "warpbox",
|
||||
Password: "secret",
|
||||
RemotePath: "/warpbox",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBackend returned error: %v", err)
|
||||
}
|
||||
|
||||
deleteRequest := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/delete", strings.NewReader("csrf_token=test-csrf"))
|
||||
deleteRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
deleteRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
||||
deleteRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
||||
deleteRequest.SetPathValue("backendID", cfg.ID)
|
||||
deleteResponse := httptest.NewRecorder()
|
||||
app.AdminDeleteStorage(deleteResponse, deleteRequest)
|
||||
if deleteResponse.Code != http.StatusSeeOther {
|
||||
t.Fatalf("AdminDeleteStorage status = %d, body = %s", deleteResponse.Code, deleteResponse.Body.String())
|
||||
}
|
||||
if _, err := app.uploadService.Storage().BackendConfig(cfg.ID); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("BackendConfig after delete = %v, want not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminStorageDeleteResetsDefaultsAndUserOverrides(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
adminToken := createAdminSession(t, app)
|
||||
user, err := app.authService.UserByEmail("admin@example.test")
|
||||
if err != nil {
|
||||
t.Fatalf("UserByEmail returned error: %v", err)
|
||||
}
|
||||
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
|
||||
Provider: services.StorageProviderWebDAV,
|
||||
Name: "DAV",
|
||||
Endpoint: "https://dav.example.test",
|
||||
Username: "warpbox",
|
||||
Password: "secret",
|
||||
RemotePath: "/warpbox",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBackend returned error: %v", err)
|
||||
}
|
||||
settings, err := app.settingsService.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
settings.UserStorageBackend = cfg.ID
|
||||
if err := app.settingsService.UpdateUploadPolicy(settings); err != nil {
|
||||
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
||||
}
|
||||
if err := app.authService.SetUserStorageBackend(user.ID, cfg.ID); err != nil {
|
||||
t.Fatalf("SetUserStorageBackend returned error: %v", err)
|
||||
}
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/delete", strings.NewReader("csrf_token=test-csrf"))
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
||||
request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
||||
request.SetPathValue("backendID", cfg.ID)
|
||||
response := httptest.NewRecorder()
|
||||
app.AdminDeleteStorage(response, request)
|
||||
if response.Code != http.StatusSeeOther {
|
||||
t.Fatalf("AdminDeleteStorage status = %d, body = %s", response.Code, response.Body.String())
|
||||
}
|
||||
location := response.Header().Get("Location")
|
||||
if !strings.Contains(location, "Storage+backend+deleted") || !strings.Contains(location, "cleared+1+user+overrides") {
|
||||
t.Fatalf("delete redirect did not include cascade notice: %s", location)
|
||||
}
|
||||
if _, err := app.uploadService.Storage().BackendConfig(cfg.ID); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("BackendConfig after delete = %v, want not exist", err)
|
||||
}
|
||||
nextSettings, err := app.settingsService.UploadPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPolicy returned error: %v", err)
|
||||
}
|
||||
if nextSettings.UserStorageBackend != services.StorageBackendLocal {
|
||||
t.Fatalf("UserStorageBackend = %q, want local", nextSettings.UserStorageBackend)
|
||||
}
|
||||
nextUser, err := app.authService.UserByID(user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("UserByID returned error: %v", err)
|
||||
}
|
||||
if nextUser.Policy.StorageBackendID != nil {
|
||||
t.Fatalf("user storage override was not cleared: %+v", nextUser.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminStorageSpeedTestStartsBackgroundJob(t *testing.T) {
|
||||
app, cleanup := newTestApp(t)
|
||||
defer cleanup()
|
||||
@@ -888,8 +984,12 @@ func TestAdminLogsAndBansPagesRender(t *testing.T) {
|
||||
t.Fatalf("MkdirAll returned error: %v", err)
|
||||
}
|
||||
logPath := filepath.Join(logDir, "2026-05-31.log")
|
||||
line := `{"date":"2026-05-31","time":"12:34:56","source":"user-upload","severity":"user_activity","code":2001,"log":"upload response sent","ip":"127.0.0.1","box_id":"box123"}` + "\n"
|
||||
if err := os.WriteFile(logPath, []byte(line), 0o644); err != nil {
|
||||
lines := strings.Join([]string{
|
||||
`{"date":"2026-05-31","time":"12:34:56","source":"user-upload","severity":"user_activity","code":2001,"log":"upload response sent","ip":"127.0.0.1","box_id":"box123"}`,
|
||||
`{"date":"2026-05-31","time":"12:35:56","source":"http","severity":"dev","code":200,"log":"http request","remote_addr":"172.30.0.1:48358","box_id":"box456"}`,
|
||||
"",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(logPath, []byte(lines), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
|
||||
@@ -904,6 +1004,9 @@ func TestAdminLogsAndBansPagesRender(t *testing.T) {
|
||||
if !strings.Contains(logsBody, "upload response sent") || !strings.Contains(logsBody, "box123") {
|
||||
t.Fatalf("AdminLogs missing expected log entry: %s", logsBody)
|
||||
}
|
||||
if strings.Contains(logsBody, "172.30.0.1:48358") {
|
||||
t.Fatalf("AdminLogs rendered remote address with port: %s", logsBody)
|
||||
}
|
||||
|
||||
bansRequest := httptest.NewRequest(http.MethodGet, "/admin/bans", nil)
|
||||
bansRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
||||
|
||||
Reference in New Issue
Block a user