package handlers import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/url" "path" "strconv" "time" "warpbox.dev/backend/libs/jobs" "warpbox.dev/backend/libs/services" "warpbox.dev/backend/libs/web" ) const adminCookieName = "warpbox_admin" type adminPageData struct { Stats services.AdminStats Boxes []adminBoxView Users []adminUserView Settings services.UploadPolicySettings Storage []services.StorageBackendView UserEdit adminUserEditView StorageForm adminStorageFormView StorageTest adminStorageTestView StorageTypes []adminStorageProviderView Section string PageTitle string LastInviteURL string Notice string Error string } type adminStorageFormView struct { Mode string Provider string ProviderLabel string Action string BackHref string Config services.StorageBackendConfig } type adminStorageTestView struct { Config services.StorageBackendConfig UsageLabel string Tests []services.StorageSpeedTest CanRun bool } type adminStorageSpeedTestJSON struct { ID string `json:"id"` Mode string `json:"mode"` ModeLabel string `json:"modeLabel"` Status string `json:"status"` Stage string `json:"stage"` Progress int `json:"progress"` CustomLabel string `json:"customLabel,omitempty"` StartedLabel string `json:"startedLabel"` FinishedLabel string `json:"finishedLabel"` Files int `json:"files"` SizeLabel string `json:"sizeLabel"` WriteSpeed string `json:"writeSpeed"` ReadSpeed string `json:"readSpeed"` Error string `json:"error,omitempty"` } type adminStorageProviderView struct { Provider string Label string Description string Icon string } type adminBoxView struct { ID string Owner string CreatedAt string ExpiresAt string FileCount int TotalSizeLabel string DownloadCount int MaxDownloads int Protected bool Expired bool } type adminUserView struct { ID string Username string Email string Role string Status string StorageUsed string StorageQuota string DailyUsed string StorageBackend string CreatedAt string } type adminUserEditView struct { ID string Username string Email string Role string Status string StorageUsed string DailyUsed string EffectiveStorage string EffectiveDaily string EffectiveMaxDays int EffectiveDailyBoxes int EffectiveActiveBoxes int EffectiveBackend string MaxUploadMB string DailyUploadMB string StorageQuotaMB string MaxDays string DailyBoxes string ActiveBoxes string ShortWindowRequests string StorageBackendID string } func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) { if a.isAdmin(r) { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } a.renderAdminLogin(w, r, http.StatusOK, "") } func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) { if !a.rateLimiter.Allow("admin-login:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) { a.renderAdminLogin(w, r, http.StatusTooManyRequests, "Too many admin login attempts.") return } if err := r.ParseForm(); err != nil { a.renderAdminLogin(w, r, http.StatusBadRequest, "Unable to read login form.") return } if a.cfg.AdminToken == "" || r.FormValue("token") != a.cfg.AdminToken { a.logger.Warn("admin login failed", "source", "admin", "severity", "warn", "code", 4301) a.renderAdminLogin(w, r, http.StatusUnauthorized, "Invalid admin token.") return } http.SetCookie(w, &http.Cookie{ Name: adminCookieName, Value: adminCookieValue(a.cfg.AdminToken), Path: "/admin", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: time.Now().Add(12 * time.Hour), }) a.logger.Info("admin login", "source", "admin", "severity", "user_activity", "code", 2301) http.Redirect(w, r, "/admin", http.StatusSeeOther) } func (a *App) AdminLogout(w http.ResponseWriter, r *http.Request) { if !a.validateCSRF(w, r) { return } a.clearUserSessionCookie(w) http.SetCookie(w, &http.Cookie{ Name: adminCookieName, Value: "", Path: "/admin", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1, }) http.Redirect(w, r, "/admin/login", http.StatusSeeOther) } func (a *App) AdminDashboard(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } stats, err := a.uploadService.AdminStats() if err != nil { http.Error(w, "unable to load admin stats", http.StatusInternalServerError) return } boxes, err := a.adminBoxes(8) if err != nil { http.Error(w, "unable to load recent boxes", http.StatusInternalServerError) return } a.renderPage(w, r, http.StatusOK, "admin.html", web.PageData{ Title: "Admin overview", Description: "Warpbox admin overview.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Stats: stats, Boxes: boxes, Section: "overview", PageTitle: "Admin overview", }, }) } func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } stats, err := a.uploadService.AdminStats() if err != nil { http.Error(w, "unable to load admin stats", http.StatusInternalServerError) return } boxes, err := a.adminBoxes(100) if err != nil { http.Error(w, "unable to load boxes", http.StatusInternalServerError) return } a.renderPage(w, r, http.StatusOK, "admin.html", web.PageData{ Title: "Admin files", Description: "Manage Warpbox uploads.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Stats: stats, Boxes: boxes, Section: "files", PageTitle: "Admin files", }, }) } func (a *App) AdminUsers(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } stats, err := a.uploadService.AdminStats() if err != nil { http.Error(w, "unable to load admin stats", http.StatusInternalServerError) return } users, err := a.authService.ListUsers() if err != nil { http.Error(w, "unable to load users", http.StatusInternalServerError) return } rows := make([]adminUserView, 0, len(users)) settings, err := a.settingsService.UploadPolicy() if err != nil { http.Error(w, "unable to load settings", http.StatusInternalServerError) return } for _, user := range users { storageUsed, _ := a.uploadService.UserActiveStorageUsed(user.ID) usage, _ := a.settingsService.UsageForUser(user.ID, time.Now().UTC()) policy := a.settingsService.EffectivePolicyForUser(settings, user) quota := "unlimited" if policy.StorageQuotaSet { quota = formatMB(policy.StorageQuotaMB) } rows = append(rows, adminUserView{ ID: user.ID, Username: user.Username, Email: user.Email, Role: user.Role, Status: user.Status, StorageUsed: services.FormatMegabytesFromBytes(storageUsed), StorageQuota: quota, DailyUsed: services.FormatMegabytesFromBytes(usage.UploadedBytes), StorageBackend: policy.StorageBackendID, CreatedAt: user.CreatedAt.Format("Jan 2 15:04"), }) } a.renderPage(w, r, http.StatusOK, "admin_users.html", web.PageData{ Title: "Admin users", Description: "Manage Warpbox users and invites.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Stats: stats, Users: rows, Section: "users", PageTitle: "Users", LastInviteURL: r.URL.Query().Get("invite"), }, }) } func (a *App) AdminEditUser(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } user, err := a.authService.UserByID(r.PathValue("userID")) if err != nil { http.NotFound(w, r) return } settings, err := a.settingsService.UploadPolicy() if err != nil { http.Error(w, "unable to load settings", http.StatusInternalServerError) return } storage, err := a.storageBackendViews() if err != nil { http.Error(w, "unable to load storage", http.StatusInternalServerError) return } edit, err := a.adminUserEdit(user, settings) if err != nil { http.Error(w, "unable to load user policy", http.StatusInternalServerError) return } a.renderPage(w, r, http.StatusOK, "admin_user_edit.html", web.PageData{ Title: "Edit user", Description: "Edit a Warpbox user.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ UserEdit: edit, Storage: storage, Section: "users", PageTitle: "Edit user", LastInviteURL: r.URL.Query().Get("invite"), Error: r.URL.Query().Get("error"), }, }) } func (a *App) AdminSettings(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } settings, err := a.settingsService.UploadPolicy() if err != nil { http.Error(w, "unable to load settings", http.StatusInternalServerError) return } storage, err := a.storageBackendViews() if err != nil { http.Error(w, "unable to load storage", http.StatusInternalServerError) return } a.renderPage(w, r, http.StatusOK, "admin_settings.html", web.PageData{ Title: "Admin settings", Description: "Manage Warpbox upload policy.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Settings: settings, Storage: storage, Section: "settings", PageTitle: "Settings", }, }) } func (a *App) AdminSettingsPost(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/settings", http.StatusSeeOther) return } settings, err := a.settingsService.UploadPolicy() if err != nil { http.Error(w, "unable to load settings", http.StatusInternalServerError) return } settings.AnonymousUploadsEnabled = r.FormValue("anonymous_uploads_enabled") == "on" if value := parsePositiveInt(r.FormValue("usage_retention_days")); value > 0 { settings.UsageRetentionDays = value } if value := parsePositiveFloat(r.FormValue("local_storage_max_gb")); value > 0 { settings.LocalStorageMaxGB = value } if value := parsePositiveInt(r.FormValue("anonymous_max_days")); value > 0 { settings.AnonymousMaxDays = value } if value := parsePositiveInt(r.FormValue("user_max_days")); value > 0 { settings.UserMaxDays = value } if value := parsePositiveInt(r.FormValue("anonymous_daily_boxes")); value > 0 { settings.AnonymousDailyBoxes = value } if value := parsePositiveInt(r.FormValue("user_daily_boxes")); value > 0 { settings.UserDailyBoxes = value } if value := parsePositiveInt(r.FormValue("anonymous_active_boxes")); value > 0 { settings.AnonymousActiveBoxes = value } if value := parsePositiveInt(r.FormValue("user_active_boxes")); value > 0 { settings.UserActiveBoxes = value } if value := parsePositiveInt(r.FormValue("short_window_requests")); value > 0 { settings.ShortWindowRequests = value } if value := parsePositiveInt(r.FormValue("short_window_seconds")); value > 0 { settings.ShortWindowSeconds = value } if value := r.FormValue("anonymous_storage_backend"); value != "" { settings.AnonymousStorageBackend = value } if value := r.FormValue("user_storage_backend"); value != "" { settings.UserStorageBackend = value } if settings.AnonymousMaxUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_max_upload_mb")); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if settings.AnonymousDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("anonymous_daily_upload_mb")); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if settings.UserDailyUploadMB, err = services.ParseMegabytesLimitValue(r.FormValue("user_daily_upload_mb")); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if settings.DefaultUserStorageMB, err = services.ParseMegabytesValue(r.FormValue("default_user_storage_mb")); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if settings.UsageRetentionDays <= 0 { http.Error(w, "usage retention days must be positive", http.StatusBadRequest) return } if _, err := a.uploadService.Storage().BackendConfig(settings.AnonymousStorageBackend); err != nil { http.Error(w, "anonymous storage backend not found", http.StatusBadRequest) return } if _, err := a.uploadService.Storage().BackendConfig(settings.UserStorageBackend); err != nil { http.Error(w, "user storage backend not found", http.StatusBadRequest) return } if err := a.settingsService.UpdateUploadPolicy(settings); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } http.Redirect(w, r, "/admin/settings", http.StatusSeeOther) } func (a *App) AdminStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } settings, err := a.settingsService.UploadPolicy() if err != nil { http.Error(w, "unable to load settings", http.StatusInternalServerError) return } views, err := a.storageBackendViews() if err != nil { http.Error(w, "unable to load storage", http.StatusInternalServerError) return } a.renderPage(w, r, http.StatusOK, "admin_storage.html", web.PageData{ Title: "Admin storage", Description: "Manage Warpbox storage backends.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Settings: settings, Storage: views, Section: "storage", PageTitle: "Storage", Notice: r.URL.Query().Get("notice"), Error: r.URL.Query().Get("error"), }, }) } func (a *App) AdminNewStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } a.renderPage(w, r, http.StatusOK, "admin_storage_new.html", web.PageData{ Title: "Add storage", Description: "Choose a Warpbox storage provider.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Section: "storage", PageTitle: "Add storage", StorageTypes: adminStorageProviderOptions(), Error: r.URL.Query().Get("error"), }, }) } func (a *App) AdminNewStorageProvider(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } rawProvider := adminStorageProviderFromRequest(r) if !validAdminStorageProvider(rawProvider) { http.NotFound(w, r) return } provider := normalizeAdminStorageProvider(rawProvider) a.renderStorageForm(w, r, http.StatusOK, adminStorageFormView{ Mode: "create", Provider: provider, ProviderLabel: adminStorageProviderLabel(provider), Action: "/admin/storage/new/" + provider, BackHref: "/admin/storage/new", Config: services.StorageBackendConfig{ Provider: provider, Type: adminStorageTypeForProvider(provider), UseSSL: true, }, }, r.URL.Query().Get("error")) } func (a *App) AdminEditStorageForm(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID")) if err != nil || cfg.ID == services.StorageBackendLocal { http.NotFound(w, r) return } provider := normalizeAdminStorageProvider(cfg.Provider) a.renderStorageForm(w, r, http.StatusOK, adminStorageFormView{ Mode: "edit", Provider: provider, ProviderLabel: adminStorageProviderLabel(provider), Action: "/admin/storage/" + cfg.ID + "/edit", BackHref: "/admin/storage", Config: cfg, }, r.URL.Query().Get("error")) } func (a *App) AdminStorageTests(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID")) if err != nil { http.NotFound(w, r) return } tests, err := a.uploadService.Storage().ListSpeedTests(cfg.ID, 100) if err != nil { http.Error(w, "unable to load storage tests", http.StatusInternalServerError) return } var usage int64 if cfg.Enabled { if backend, err := a.uploadService.Storage().Backend(cfg.ID); err == nil { usage, _ = backend.Usage(context.Background()) } } a.renderPage(w, r, http.StatusOK, "admin_storage_tests.html", web.PageData{ Title: cfg.Name + " tests", Description: "Storage speed-test history.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Section: "storage", PageTitle: cfg.Name + " tests", StorageTest: adminStorageTestView{ Config: cfg, UsageLabel: services.FormatMegabytesFromBytes(usage), Tests: tests, CanRun: cfg.Enabled && cfg.LastTestSuccess, }, Notice: r.URL.Query().Get("notice"), Error: r.URL.Query().Get("error"), }, }) } func (a *App) AdminStorageTestsJSON(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } cfg, err := a.uploadService.Storage().BackendConfig(r.PathValue("backendID")) if err != nil { http.NotFound(w, r) return } tests, err := a.uploadService.Storage().ListSpeedTests(cfg.ID, 100) if err != nil { http.Error(w, "unable to load storage tests", http.StatusInternalServerError) return } payload := struct { Tests []adminStorageSpeedTestJSON `json:"tests"` }{Tests: adminStorageSpeedTestsJSON(tests)} w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(payload) } func (a *App) renderStorageForm(w http.ResponseWriter, r *http.Request, status int, form adminStorageFormView, message string) { a.renderPage(w, r, status, "admin_storage_form.html", web.PageData{ Title: form.ProviderLabel + " storage", Description: "Configure Warpbox storage.", CurrentUser: a.currentPublicUser(r), Data: adminPageData{ Section: "storage", PageTitle: form.ProviderLabel + " storage", StorageForm: form, Error: message, }, }) } func (a *App) AdminCreateStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } rawProvider := adminStorageProviderFromRequest(r) if !validAdminStorageProvider(rawProvider) { http.NotFound(w, r) return } provider := normalizeAdminStorageProvider(rawProvider) if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/storage/new/"+provider, http.StatusSeeOther) return } _, err := a.uploadService.Storage().CreateBackend(a.storageConfigFromForm(r, provider)) if err != nil { http.Redirect(w, r, "/admin/storage/new/"+provider+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape("Storage backend added."), http.StatusSeeOther) } func (a *App) AdminEditStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/storage", http.StatusSeeOther) return } provider := normalizeAdminStorageProvider(r.FormValue("provider")) _, err := a.uploadService.Storage().UpdateBackend(r.PathValue("backendID"), a.storageConfigFromForm(r, provider)) if err != nil { http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/edit?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape("Storage backend updated."), http.StatusSeeOther) } func (a *App) AdminTestStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } next := "/admin/storage" if r.FormValue("next") == "tests" { next = "/admin/storage/" + r.PathValue("backendID") + "/tests" } if _, err := a.uploadService.Storage().TestBackend(r.PathValue("backendID")); err != nil { http.Redirect(w, r, next+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, next+"?notice="+url.QueryEscape("Storage connection test passed."), http.StatusSeeOther) } func (a *App) AdminStartStorageSpeedTest(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape("Unable to read speed test form."), http.StatusSeeOther) return } options := services.StorageSpeedTestOptions{ Mode: r.FormValue("mode"), CustomFileCount: parsePositiveInt(r.FormValue("custom_file_count")), CustomFileSizeMB: parsePositiveFloat(r.FormValue("custom_file_size_mb")), } test, err := a.uploadService.Storage().StartSpeedTestWithOptions(r.PathValue("backendID"), options) if err != nil { http.Redirect(w, r, "/admin/storage/"+r.PathValue("backendID")+"/tests?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } go a.uploadService.Storage().RunSpeedTest(context.Background(), test.ID) 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 } 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 { http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/storage", http.StatusSeeOther) } func (a *App) AdminRunStorageCleanup(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } cleaned, err := jobs.RunCleanupNow(a.uploadService, a.logger) if err != nil { http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(fmt.Sprintf("Cleanup finished. Removed %d unavailable boxes.", cleaned)), http.StatusSeeOther) } func (a *App) AdminRunStorageThumbnails(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } result, err := jobs.RunThumbnailsNow(a.uploadService, a.logger) if err != nil { http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } message := fmt.Sprintf("Thumbnail pass finished. Scanned %d files, generated %d, failed %d.", result.Scanned, result.Generated, result.Failed) http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(message), http.StatusSeeOther) } func (a *App) AdminVerifyStorageBackends(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } configs, err := a.uploadService.Storage().ListBackendConfigs() if err != nil { http.Redirect(w, r, "/admin/storage?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } passed := 0 failed := 0 for _, cfg := range configs { if !cfg.Enabled { continue } if _, err := a.uploadService.Storage().TestBackend(cfg.ID); err != nil { failed++ continue } passed++ } message := fmt.Sprintf("Storage verification finished. %d passed, %d failed.", passed, failed) http.Redirect(w, r, "/admin/storage?notice="+url.QueryEscape(message), http.StatusSeeOther) } func (a *App) AdminUpdateUserQuota(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } var quota *float64 if r.FormValue("storage_quota_mb") != "" { parsed, err := services.ParseMegabytesValue(r.FormValue("storage_quota_mb")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } quota = &parsed } if err := a.authService.SetUserStorageQuota(r.PathValue("userID"), quota); err != nil { http.Error(w, "unable to update quota", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/users", http.StatusSeeOther) } func (a *App) AdminUpdateUserPolicy(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } policy := services.UserPolicy{ MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")), DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")), StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")), MaxDays: optionalInt(r.FormValue("max_days")), DailyBoxes: optionalInt(r.FormValue("daily_boxes")), ActiveBoxes: optionalInt(r.FormValue("active_boxes")), ShortWindowRequests: optionalInt(r.FormValue("short_window_requests")), } if backendID := r.FormValue("storage_backend_id"); backendID != "" { if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil { http.Error(w, "storage backend not found", http.StatusBadRequest) return } policy.StorageBackendID = &backendID } if err := a.authService.SetUserPolicy(r.PathValue("userID"), policy); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } http.Redirect(w, r, "/admin/users", http.StatusSeeOther) } func (a *App) AdminUpdateUser(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit", http.StatusSeeOther) return } policy := services.UserPolicy{ MaxUploadMB: optionalMB(r.FormValue("max_upload_mb")), DailyUploadMB: optionalMB(r.FormValue("daily_upload_mb")), StorageQuotaMB: optionalMBAllowZero(r.FormValue("storage_quota_mb")), MaxDays: optionalInt(r.FormValue("max_days")), DailyBoxes: optionalInt(r.FormValue("daily_boxes")), ActiveBoxes: optionalInt(r.FormValue("active_boxes")), ShortWindowRequests: optionalInt(r.FormValue("short_window_requests")), } if backendID := r.FormValue("storage_backend_id"); backendID != "" { if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil { http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?error="+url.QueryEscape("storage backend not found"), http.StatusSeeOther) return } policy.StorageBackendID = &backendID } if _, err := a.authService.UpdateUserAdminFields( r.PathValue("userID"), r.FormValue("username"), r.FormValue("email"), r.FormValue("role"), r.FormValue("status"), policy, ); err != nil { http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?error="+url.QueryEscape(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit", http.StatusSeeOther) } func (a *App) AdminUpdateUserStorage(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } if backendID := r.FormValue("storage_backend_id"); backendID != "" { if _, err := a.uploadService.Storage().BackendConfig(backendID); err != nil { http.Error(w, "storage backend not found", http.StatusBadRequest) return } } if err := a.authService.SetUserStorageBackend(r.PathValue("userID"), r.FormValue("storage_backend_id")); err != nil { http.Error(w, "unable to update user storage", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/users", http.StatusSeeOther) } func (a *App) AdminCreateInvite(w http.ResponseWriter, r *http.Request) { admin, ok := a.requireAdminUser(w, r) if !ok || !a.validateCSRF(w, r) { return } if err := r.ParseForm(); err != nil { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } result, err := a.authService.CreateInvite(r.FormValue("email"), r.FormValue("role"), admin.ID, 7*24*time.Hour) if err != nil { http.Redirect(w, r, "/admin/users", http.StatusSeeOther) return } a.logger.Info("invite created", "source", "admin", "severity", "user_activity", "code", 2404, "admin_id", admin.ID) http.Redirect(w, r, "/admin/users?invite="+url.QueryEscape(result.URL), http.StatusSeeOther) } func (a *App) AdminDisableUser(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } disabled := r.URL.Query().Get("disabled") != "false" if err := a.authService.DisableUser(r.PathValue("userID"), disabled); err != nil { http.Error(w, "unable to update user", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/users", http.StatusSeeOther) } func (a *App) AdminResetUser(w http.ResponseWriter, r *http.Request) { admin, ok := a.requireAdminUser(w, r) if !ok || !a.validateCSRF(w, r) { return } result, err := a.authService.CreatePasswordResetInvite(r.PathValue("userID"), admin.ID) if err != nil { http.Error(w, "unable to create reset link", http.StatusInternalServerError) return } if r.URL.Query().Get("next") == "edit" { http.Redirect(w, r, "/admin/users/"+r.PathValue("userID")+"/edit?invite="+url.QueryEscape(result.URL), http.StatusSeeOther) return } http.Redirect(w, r, "/admin/users?invite="+url.QueryEscape(result.URL), http.StatusSeeOther) } func (a *App) AdminDeleteBox(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) { return } boxID := r.PathValue("boxID") if err := a.uploadService.DeleteBox(boxID); err != nil { a.logger.Warn("admin delete failed", "source", "admin", "severity", "warn", "code", 4302, "box_id", boxID, "error", err.Error()) http.Error(w, "unable to delete box", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin/files", http.StatusSeeOther) } func (a *App) AdminViewBox(w http.ResponseWriter, r *http.Request) { if !a.requireAdmin(w, r) { return } box, err := a.uploadService.GetBox(r.PathValue("boxID")) if err != nil { http.NotFound(w, r) return } if a.uploadService.IsProtected(box) { http.SetCookie(w, &http.Cookie{ Name: unlockCookieName(box.ID), Value: a.uploadService.UnlockToken(box), Path: "/d/" + box.ID, HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: box.ExpiresAt, }) a.logger.Info("admin bypassed box password", "source", "admin", "severity", "user_activity", "code", 2302, "box_id", box.ID) } http.Redirect(w, r, "/d/"+box.ID, http.StatusSeeOther) } func (a *App) renderAdminLogin(w http.ResponseWriter, r *http.Request, status int, message string) { a.renderPage(w, r, status, "admin_login.html", web.PageData{ Title: "Admin login", Description: "Sign in to the Warpbox admin console.", Data: adminPageData{ Error: message, }, }) } func (a *App) adminBoxes(limit int) ([]adminBoxView, error) { boxes, err := a.uploadService.AdminBoxes(limit) if err != nil { return nil, err } rows := make([]adminBoxView, 0, len(boxes)) for _, box := range boxes { owner := "Anonymous" if box.OwnerID != "" { if user, err := a.authService.UserByID(box.OwnerID); err == nil { owner = user.Email } else { owner = "User" } } rows = append(rows, adminBoxView{ ID: box.ID, Owner: owner, CreatedAt: box.CreatedAt.Format("Jan 2 15:04"), ExpiresAt: box.ExpiresAt.Format("Jan 2 15:04"), FileCount: box.FileCount, TotalSizeLabel: box.TotalSizeLabel, DownloadCount: box.DownloadCount, MaxDownloads: box.MaxDownloads, Protected: box.Protected, Expired: box.Expired, }) } return rows, nil } func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool { if a.isAdmin(r) { return true } http.Redirect(w, r, "/admin/login", http.StatusSeeOther) return false } func (a *App) isAdmin(r *http.Request) bool { if user, ok := a.currentUser(r); ok && user.Role == services.UserRoleAdmin { return true } if a.cfg.AdminToken == "" { return false } cookie, err := r.Cookie(adminCookieName) if err != nil { return false } return cookie.Value == adminCookieValue(a.cfg.AdminToken) } func (a *App) requireAdminUser(w http.ResponseWriter, r *http.Request) (services.User, bool) { user, ok := a.currentUser(r) if ok && user.Role == services.UserRoleAdmin { return user, true } if a.cfg.AdminToken != "" && a.isAdmin(r) { return services.User{ID: "env-admin", Role: services.UserRoleAdmin, Status: services.UserStatusActive}, true } http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther) return services.User{}, false } func (a *App) currentPublicUser(r *http.Request) any { if user, ok := a.currentUser(r); ok { return a.authService.PublicUser(user) } return nil } func adminCookieValue(token string) string { sum := sha256.Sum256([]byte("warpbox-admin:" + token)) return hex.EncodeToString(sum[:]) } func parsePositiveInt(value string) int { parsed, err := strconv.Atoi(value) if err != nil { return 0 } return parsed } func parsePositiveFloat(value string) float64 { parsed, err := strconv.ParseFloat(value, 64) if err != nil { return 0 } return parsed } func optionalMB(value string) *float64 { if value == "" { return nil } parsed, err := services.ParseMegabytesLimitValue(value) if err != nil { return nil } return &parsed } func optionalMBAllowZero(value string) *float64 { if value == "" { return nil } parsed, err := strconv.ParseFloat(value, 64) if err != nil || parsed < 0 { return nil } return &parsed } func optionalInt(value string) *int { if value == "" { return nil } parsed, err := strconv.Atoi(value) if err != nil || parsed <= 0 { return nil } return &parsed } func formatMB(value float64) string { return strconv.FormatFloat(value, 'f', -1, 64) + " MB" } func adminStorageSpeedTestsJSON(tests []services.StorageSpeedTest) []adminStorageSpeedTestJSON { rows := make([]adminStorageSpeedTestJSON, 0, len(tests)) for _, test := range tests { rows = append(rows, adminStorageSpeedTestJSON{ ID: test.ID, Mode: test.Mode, ModeLabel: test.ModeLabel(), Status: test.Status, Stage: test.Stage, Progress: test.ProgressPercent, CustomLabel: storageSpeedCustomLabel(test), StartedLabel: test.StartedLabel(), FinishedLabel: test.FinishedLabel(), Files: test.FilesWritten, SizeLabel: test.TotalSizeLabel(), WriteSpeed: test.WriteSpeedLabel(), ReadSpeed: test.ReadSpeedLabel(), Error: test.Error, }) } return rows } func storageSpeedCustomLabel(test services.StorageSpeedTest) string { if test.Mode != services.StorageSpeedModeCustom { return "" } return fmt.Sprintf("%d files × %s each", test.CustomFileCount, services.FormatMegabytesLabel(test.CustomFileSizeMB)) } func (a *App) storageConfigFromForm(r *http.Request, provider string) services.StorageBackendConfig { cfg := services.StorageBackendConfig{ Provider: provider, Name: r.FormValue("name"), } switch provider { case services.StorageProviderSFTP: cfg.Host = r.FormValue("host") cfg.Port = parsePositiveInt(r.FormValue("port")) cfg.Username = r.FormValue("username") cfg.Password = r.FormValue("password") cfg.PrivateKey = r.FormValue("private_key") cfg.HostKey = r.FormValue("host_key") cfg.RemotePath = r.FormValue("remote_path") case services.StorageProviderSMB: cfg.Host = r.FormValue("host") cfg.Port = parsePositiveInt(r.FormValue("port")) cfg.Share = r.FormValue("share") cfg.Domain = r.FormValue("domain") cfg.Username = r.FormValue("username") cfg.Password = r.FormValue("password") cfg.RemotePath = r.FormValue("remote_path") case services.StorageProviderWebDAV: cfg.Endpoint = r.FormValue("endpoint") cfg.Username = r.FormValue("username") cfg.Password = r.FormValue("password") cfg.RemotePath = r.FormValue("remote_path") case services.StorageProviderContabo: cfg.Endpoint = r.FormValue("endpoint") cfg.Region = r.FormValue("region") cfg.Bucket = r.FormValue("bucket") cfg.AccessKey = r.FormValue("access_key") cfg.SecretKey = r.FormValue("secret_key") cfg.UseSSL = true cfg.PathStyle = true default: cfg.Endpoint = r.FormValue("endpoint") cfg.Region = r.FormValue("region") cfg.Bucket = r.FormValue("bucket") cfg.AccessKey = r.FormValue("access_key") cfg.SecretKey = r.FormValue("secret_key") cfg.UseSSL = r.FormValue("use_ssl") == "on" cfg.PathStyle = r.FormValue("path_style") == "on" } return cfg } func adminStorageProviderOptions() []adminStorageProviderView { return []adminStorageProviderView{ {Provider: services.StorageProviderS3, Label: "S3 Bucket", Description: "Generic S3-compatible object storage.", Icon: "cloud"}, {Provider: services.StorageProviderContabo, Label: "Contabo Object Storage", Description: "Contabo COS with TLS and path-style lookup locked on.", Icon: "cloud"}, {Provider: services.StorageProviderSFTP, Label: "SFTP", Description: "SSH file transfer to a server or NAS.", Icon: "database"}, {Provider: services.StorageProviderSMB, Label: "Samba / SMB", Description: "Windows share or network attached storage.", Icon: "folder"}, {Provider: services.StorageProviderWebDAV, Label: "WebDAV", Description: "Nextcloud, ownCloud, or any WebDAV endpoint.", Icon: "sync"}, } } func normalizeAdminStorageProvider(provider string) string { switch provider { case services.StorageProviderContabo: return services.StorageProviderContabo case services.StorageProviderSFTP: return services.StorageProviderSFTP case services.StorageProviderSMB: return services.StorageProviderSMB case services.StorageProviderWebDAV: return services.StorageProviderWebDAV default: return services.StorageProviderS3 } } func adminStorageProviderFromRequest(r *http.Request) string { if provider := r.PathValue("provider"); provider != "" { return provider } return path.Base(r.URL.Path) } func validAdminStorageProvider(provider string) bool { switch provider { case services.StorageProviderS3, services.StorageProviderContabo, services.StorageProviderSFTP, services.StorageProviderSMB, services.StorageProviderWebDAV: return true default: return false } } func adminStorageProviderLabel(provider string) string { switch normalizeAdminStorageProvider(provider) { case services.StorageProviderContabo: return "Contabo Object Storage" case services.StorageProviderSFTP: return "SFTP" case services.StorageProviderSMB: return "Samba / SMB" case services.StorageProviderWebDAV: return "WebDAV" default: return "S3 Bucket" } } func adminStorageTypeForProvider(provider string) string { switch normalizeAdminStorageProvider(provider) { case services.StorageProviderSFTP: return services.StorageBackendSFTP case services.StorageProviderSMB: return services.StorageBackendSMB case services.StorageProviderWebDAV: return services.StorageBackendWebDAV default: return services.StorageBackendS3 } } func (a *App) storageBackendViews() ([]services.StorageBackendView, error) { configs, err := a.uploadService.Storage().ListBackendConfigs() if err != nil { return nil, err } views := make([]services.StorageBackendView, 0, len(configs)) for _, cfg := range configs { var usage int64 if backend, err := a.uploadService.Storage().BackendConfig(cfg.ID); err == nil && backend.Enabled { if concrete, err := a.uploadService.Storage().Backend(cfg.ID); err == nil { usage, _ = concrete.Usage(context.Background()) } } inUse, _ := a.storageBackendInUse(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, SpeedTests: speedTests, CanSpeedTest: cfg.LastTestSuccess, }) } return views, nil } func (a *App) adminUserEdit(user services.User, settings services.UploadPolicySettings) (adminUserEditView, error) { storageUsed, err := a.uploadService.UserActiveStorageUsed(user.ID) if err != nil { return adminUserEditView{}, err } usage, err := a.settingsService.UsageForUser(user.ID, time.Now().UTC()) if err != nil { return adminUserEditView{}, err } effective := a.settingsService.EffectivePolicyForUser(settings, user) view := adminUserEditView{ ID: user.ID, Username: user.Username, Email: user.Email, Role: user.Role, Status: user.Status, StorageUsed: services.FormatMegabytesFromBytes(storageUsed), DailyUsed: services.FormatMegabytesFromBytes(usage.UploadedBytes), EffectiveDaily: services.FormatMegabytesLabel(effective.DailyUploadMB), EffectiveMaxDays: effective.MaxDays, EffectiveDailyBoxes: effective.DailyBoxes, EffectiveActiveBoxes: effective.ActiveBoxes, EffectiveBackend: effective.StorageBackendID, MaxUploadMB: floatPtrString(user.Policy.MaxUploadMB), DailyUploadMB: floatPtrString(user.Policy.DailyUploadMB), StorageQuotaMB: floatPtrString(user.Policy.StorageQuotaMB), MaxDays: intPtrString(user.Policy.MaxDays), DailyBoxes: intPtrString(user.Policy.DailyBoxes), ActiveBoxes: intPtrString(user.Policy.ActiveBoxes), ShortWindowRequests: intPtrString(user.Policy.ShortWindowRequests), StorageBackendID: stringPtrString(user.Policy.StorageBackendID), } if effective.StorageQuotaSet { view.EffectiveStorage = services.FormatMegabytesLabel(effective.StorageQuotaMB) } else { view.EffectiveStorage = "unlimited" } return view, nil } func (a *App) storageBackendInUse(id string) (bool, error) { settings, err := a.settingsService.UploadPolicy() if err != nil { return false, err } if settings.AnonymousStorageBackend == id || settings.UserStorageBackend == id { return true, nil } boxes, err := a.uploadService.ListBoxes(0) if err != nil { return false, err } for _, box := range boxes { if a.uploadService.BoxStorageBackendID(box) == id { return true, nil } } users, err := a.authService.ListUsers() if err != nil { return false, err } for _, user := range users { if user.Policy.StorageBackendID != nil && *user.Policy.StorageBackendID == id { return true, nil } } return false, nil } func floatPtrString(value *float64) string { if value == nil { return "" } return strconv.FormatFloat(*value, 'f', -1, 64) } func intPtrString(value *int) string { if value == nil { return "" } return strconv.Itoa(*value) } func stringPtrString(value *string) string { if value == nil { return "" } return *value }