2026-05-25 16:52:57 +03:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-31 02:14:10 +03:00
|
|
|
"context"
|
2026-05-25 16:52:57 +03:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"net/http"
|
2026-05-30 15:42:35 +03:00
|
|
|
"net/url"
|
2026-05-30 17:23:20 +03:00
|
|
|
"strconv"
|
2026-05-25 16:52:57 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"warpbox.dev/backend/libs/services"
|
|
|
|
|
"warpbox.dev/backend/libs/web"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const adminCookieName = "warpbox_admin"
|
|
|
|
|
|
|
|
|
|
type adminPageData struct {
|
2026-05-30 15:42:35 +03:00
|
|
|
Stats services.AdminStats
|
|
|
|
|
Boxes []adminBoxView
|
|
|
|
|
Users []adminUserView
|
2026-05-30 17:23:20 +03:00
|
|
|
Settings services.UploadPolicySettings
|
2026-05-31 02:14:10 +03:00
|
|
|
Storage []services.StorageBackendView
|
|
|
|
|
UserEdit adminUserEditView
|
2026-05-30 17:23:20 +03:00
|
|
|
Section string
|
|
|
|
|
PageTitle string
|
2026-05-30 15:42:35 +03:00
|
|
|
LastInviteURL string
|
|
|
|
|
Error string
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type adminBoxView struct {
|
|
|
|
|
ID string
|
2026-05-30 15:42:35 +03:00
|
|
|
Owner string
|
2026-05-25 16:52:57 +03:00
|
|
|
CreatedAt string
|
|
|
|
|
ExpiresAt string
|
|
|
|
|
FileCount int
|
|
|
|
|
TotalSizeLabel string
|
|
|
|
|
DownloadCount int
|
|
|
|
|
MaxDownloads int
|
|
|
|
|
Protected bool
|
|
|
|
|
Expired bool
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:42:35 +03:00
|
|
|
type adminUserView struct {
|
2026-05-31 02:14:10 +03:00
|
|
|
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
|
2026-05-30 15:42:35 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if a.isAdmin(r) {
|
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderAdminLogin(w, r, http.StatusOK, "")
|
2026-05-25 16:52:57 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
2026-05-31 02:14:10 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
if err := r.ParseForm(); err != nil {
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderAdminLogin(w, r, http.StatusBadRequest, "Unable to read login form.")
|
2026-05-25 16:52:57 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.AdminToken == "" || r.FormValue("token") != a.cfg.AdminToken {
|
|
|
|
|
a.logger.Warn("admin login failed", "source", "admin", "severity", "warn", "code", 4301)
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderAdminLogin(w, r, http.StatusUnauthorized, "Invalid admin token.")
|
2026-05-25 16:52:57 +03:00
|
|
|
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) {
|
2026-05-31 02:14:10 +03:00
|
|
|
if !a.validateCSRF(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-30 15:42:35 +03:00
|
|
|
a.clearUserSessionCookie(w)
|
2026-05-25 16:52:57 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusOK, "admin.html", web.PageData{
|
2026-05-25 16:52:57 +03:00
|
|
|
Title: "Admin overview",
|
|
|
|
|
Description: "Warpbox admin overview.",
|
2026-05-30 15:42:35 +03:00
|
|
|
CurrentUser: a.currentPublicUser(r),
|
2026-05-25 16:52:57 +03:00
|
|
|
Data: adminPageData{
|
2026-05-30 17:23:20 +03:00
|
|
|
Stats: stats,
|
|
|
|
|
Boxes: boxes,
|
|
|
|
|
Section: "overview",
|
|
|
|
|
PageTitle: "Admin overview",
|
2026-05-25 16:52:57 +03:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusOK, "admin.html", web.PageData{
|
2026-05-25 16:52:57 +03:00
|
|
|
Title: "Admin files",
|
|
|
|
|
Description: "Manage Warpbox uploads.",
|
2026-05-30 15:42:35 +03:00
|
|
|
CurrentUser: a.currentPublicUser(r),
|
2026-05-25 16:52:57 +03:00
|
|
|
Data: adminPageData{
|
2026-05-30 17:23:20 +03:00
|
|
|
Stats: stats,
|
|
|
|
|
Boxes: boxes,
|
|
|
|
|
Section: "files",
|
|
|
|
|
PageTitle: "Admin files",
|
2026-05-25 16:52:57 +03:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:42:35 +03:00
|
|
|
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))
|
2026-05-30 17:23:20 +03:00
|
|
|
settings, err := a.settingsService.UploadPolicy()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load settings", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-30 15:42:35 +03:00
|
|
|
for _, user := range users {
|
2026-05-30 17:23:20 +03:00
|
|
|
storageUsed, _ := a.uploadService.UserActiveStorageUsed(user.ID)
|
|
|
|
|
usage, _ := a.settingsService.UsageForUser(user.ID, time.Now().UTC())
|
2026-05-31 02:14:10 +03:00
|
|
|
policy := a.settingsService.EffectivePolicyForUser(settings, user)
|
|
|
|
|
quota := "unlimited"
|
|
|
|
|
if policy.StorageQuotaSet {
|
|
|
|
|
quota = formatMB(policy.StorageQuotaMB)
|
2026-05-30 17:23:20 +03:00
|
|
|
}
|
2026-05-30 15:42:35 +03:00
|
|
|
rows = append(rows, adminUserView{
|
2026-05-31 02:14:10 +03:00
|
|
|
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"),
|
2026-05-30 15:42:35 +03:00
|
|
|
})
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
a.renderPage(w, r, http.StatusOK, "admin_users.html", web.PageData{
|
2026-05-30 15:42:35 +03:00
|
|
|
Title: "Admin users",
|
|
|
|
|
Description: "Manage Warpbox users and invites.",
|
|
|
|
|
CurrentUser: a.currentPublicUser(r),
|
|
|
|
|
Data: adminPageData{
|
|
|
|
|
Stats: stats,
|
|
|
|
|
Users: rows,
|
2026-05-30 17:23:20 +03:00
|
|
|
Section: "users",
|
|
|
|
|
PageTitle: "Users",
|
2026-05-30 15:42:35 +03:00
|
|
|
LastInviteURL: r.URL.Query().Get("invite"),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
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"),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
storage, err := a.storageBackendViews()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load storage", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
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,
|
2026-05-31 02:14:10 +03:00
|
|
|
Storage: storage,
|
2026-05-30 17:23:20 +03:00
|
|
|
Section: "settings",
|
|
|
|
|
PageTitle: "Settings",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminSettingsPost(w http.ResponseWriter, r *http.Request) {
|
2026-05-31 02:14:10 +03:00
|
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
2026-05-30 17:23:20 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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
|
2026-05-30 17:23:20 +03:00
|
|
|
}
|
|
|
|
|
if settings.AnonymousMaxUploadMB, err = services.ParseMegabytesValue(r.FormValue("anonymous_max_upload_mb")); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if settings.AnonymousDailyUploadMB, err = services.ParseMegabytesValue(r.FormValue("anonymous_daily_upload_mb")); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if settings.UserDailyUploadMB, err = services.ParseMegabytesValue(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
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
if err := a.settingsService.UpdateUploadPolicy(settings); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
func (a *App) AdminStorage(w http.ResponseWriter, r *http.Request) {
|
2026-05-30 17:23:20 +03:00
|
|
|
if !a.requireAdmin(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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",
|
|
|
|
|
Error: r.URL.Query().Get("error"),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminCreateS3Storage(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
|
|
|
|
|
}
|
|
|
|
|
_, err := a.uploadService.Storage().CreateS3Backend(services.StorageBackendConfig{
|
|
|
|
|
Provider: r.FormValue("provider"),
|
|
|
|
|
Name: r.FormValue("name"),
|
|
|
|
|
Endpoint: r.FormValue("endpoint"),
|
|
|
|
|
Region: r.FormValue("region"),
|
|
|
|
|
Bucket: r.FormValue("bucket"),
|
|
|
|
|
AccessKey: r.FormValue("access_key"),
|
|
|
|
|
SecretKey: r.FormValue("secret_key"),
|
|
|
|
|
UseSSL: r.FormValue("use_ssl") == "on",
|
|
|
|
|
PathStyle: r.FormValue("path_style") == "on",
|
|
|
|
|
})
|
|
|
|
|
if 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) 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
|
|
|
|
|
}
|
|
|
|
|
_, err := a.uploadService.Storage().UpdateS3Backend(r.PathValue("backendID"), services.StorageBackendConfig{
|
|
|
|
|
Provider: r.FormValue("provider"),
|
|
|
|
|
Name: r.FormValue("name"),
|
|
|
|
|
Endpoint: r.FormValue("endpoint"),
|
|
|
|
|
Region: r.FormValue("region"),
|
|
|
|
|
Bucket: r.FormValue("bucket"),
|
|
|
|
|
AccessKey: r.FormValue("access_key"),
|
|
|
|
|
SecretKey: r.FormValue("secret_key"),
|
|
|
|
|
UseSSL: r.FormValue("use_ssl") == "on",
|
|
|
|
|
PathStyle: r.FormValue("path_style") == "on",
|
|
|
|
|
})
|
|
|
|
|
if 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) AdminTestStorage(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if _, err := a.uploadService.Storage().TestBackend(r.PathValue("backendID")); 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) 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) AdminUpdateUserQuota(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:42:35 +03:00
|
|
|
func (a *App) AdminCreateInvite(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
admin, ok := a.requireAdminUser(w, r)
|
2026-05-31 02:14:10 +03:00
|
|
|
if !ok || !a.validateCSRF(w, r) {
|
2026-05-30 15:42:35 +03:00
|
|
|
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) {
|
2026-05-31 02:14:10 +03:00
|
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
2026-05-30 15:42:35 +03:00
|
|
|
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)
|
2026-05-31 02:14:10 +03:00
|
|
|
if !ok || !a.validateCSRF(w, r) {
|
2026-05-30 15:42:35 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-30 15:42:35 +03:00
|
|
|
http.Redirect(w, r, "/admin/users?invite="+url.QueryEscape(result.URL), http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func (a *App) AdminDeleteBox(w http.ResponseWriter, r *http.Request) {
|
2026-05-31 02:14:10 +03:00
|
|
|
if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
|
2026-05-25 16:52:57 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 17:05:59 +03:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
func (a *App) renderAdminLogin(w http.ResponseWriter, r *http.Request, status int, message string) {
|
|
|
|
|
a.renderPage(w, r, status, "admin_login.html", web.PageData{
|
2026-05-25 16:52:57 +03:00
|
|
|
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 {
|
2026-05-30 15:42:35 +03:00
|
|
|
owner := "Anonymous"
|
|
|
|
|
if box.OwnerID != "" {
|
|
|
|
|
if user, err := a.authService.UserByID(box.OwnerID); err == nil {
|
|
|
|
|
owner = user.Email
|
|
|
|
|
} else {
|
|
|
|
|
owner = "User"
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
rows = append(rows, adminBoxView{
|
|
|
|
|
ID: box.ID,
|
2026-05-30 15:42:35 +03:00
|
|
|
Owner: owner,
|
2026-05-25 16:52:57 +03:00
|
|
|
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 {
|
2026-05-30 15:42:35 +03:00
|
|
|
if user, ok := a.currentUser(r); ok && user.Role == services.UserRoleAdmin {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-05-25 16:52:57 +03:00
|
|
|
if a.cfg.AdminToken == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
cookie, err := r.Cookie(adminCookieName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return cookie.Value == adminCookieValue(a.cfg.AdminToken)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 15:42:35 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func adminCookieValue(token string) string {
|
|
|
|
|
sum := sha256.Sum256([]byte("warpbox-admin:" + token))
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|
2026-05-30 17:23:20 +03:00
|
|
|
|
|
|
|
|
func parsePositiveInt(value string) int {
|
|
|
|
|
parsed, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return parsed
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 02:14:10 +03:00
|
|
|
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.ParseMegabytesValue(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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 17:23:20 +03:00
|
|
|
func formatMB(value float64) string {
|
|
|
|
|
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
|
|
|
|
|
}
|
2026-05-31 02:14:10 +03:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
views = append(views, services.StorageBackendView{
|
|
|
|
|
Config: cfg,
|
|
|
|
|
UsageBytes: usage,
|
|
|
|
|
UsageLabel: services.FormatMegabytesFromBytes(usage),
|
|
|
|
|
InUse: inUse,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|