feat: add upload policies, daily limits, and storage quotas
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m8s
- Add environment variables to configure anonymous uploads, daily upload caps, and default user storage limits. - Update config loader to parse and validate the new settings. - Implement backend logic to track daily usage and active storage per user. - Update README and `.env.example` to document the new settings and admin panels.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"warpbox.dev/backend/libs/services"
|
||||
@@ -17,6 +18,9 @@ type adminPageData struct {
|
||||
Stats services.AdminStats
|
||||
Boxes []adminBoxView
|
||||
Users []adminUserView
|
||||
Settings services.UploadPolicySettings
|
||||
Section string
|
||||
PageTitle string
|
||||
LastInviteURL string
|
||||
Error string
|
||||
}
|
||||
@@ -35,12 +39,15 @@ type adminBoxView struct {
|
||||
}
|
||||
|
||||
type adminUserView struct {
|
||||
ID string
|
||||
Username string
|
||||
Email string
|
||||
Role string
|
||||
Status string
|
||||
CreatedAt string
|
||||
ID string
|
||||
Username string
|
||||
Email string
|
||||
Role string
|
||||
Status string
|
||||
StorageUsed string
|
||||
StorageQuota string
|
||||
DailyUsed string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -48,17 +55,17 @@ func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
a.renderAdminLogin(w, http.StatusOK, "")
|
||||
a.renderAdminLogin(w, r, http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
a.renderAdminLogin(w, http.StatusBadRequest, "Unable to read login form.")
|
||||
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, http.StatusUnauthorized, "Invalid admin token.")
|
||||
a.renderAdminLogin(w, r, http.StatusUnauthorized, "Invalid admin token.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,13 +111,15 @@ func (a *App) AdminDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
||||
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,
|
||||
Stats: stats,
|
||||
Boxes: boxes,
|
||||
Section: "overview",
|
||||
PageTitle: "Admin overview",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -131,13 +140,15 @@ func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
||||
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,
|
||||
Stats: stats,
|
||||
Boxes: boxes,
|
||||
Section: "files",
|
||||
PageTitle: "Admin files",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -157,28 +168,129 @@ func (a *App) AdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||
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())
|
||||
quotaMB := settings.DefaultUserStorageMB
|
||||
if user.StorageQuotaMB != nil {
|
||||
quotaMB = *user.StorageQuotaMB
|
||||
}
|
||||
rows = append(rows, adminUserView{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
CreatedAt: user.CreatedAt.Format("Jan 2 15:04"),
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
StorageUsed: services.FormatMegabytesFromBytes(storageUsed),
|
||||
StorageQuota: formatMB(quotaMB),
|
||||
DailyUsed: services.FormatMegabytesFromBytes(usage.UploadedBytes),
|
||||
CreatedAt: user.CreatedAt.Format("Jan 2 15:04"),
|
||||
})
|
||||
}
|
||||
a.renderer.Render(w, http.StatusOK, "admin_users.html", web.PageData{
|
||||
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) 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
|
||||
}
|
||||
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,
|
||||
Section: "settings",
|
||||
PageTitle: "Settings",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) AdminSettingsPost(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/admin/settings", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
settings := services.UploadPolicySettings{
|
||||
AnonymousUploadsEnabled: r.FormValue("anonymous_uploads_enabled") == "on",
|
||||
UsageRetentionDays: parsePositiveInt(r.FormValue("usage_retention_days")),
|
||||
}
|
||||
var err error
|
||||
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
|
||||
}
|
||||
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) AdminUpdateUserQuota(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireAdmin(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) AdminCreateInvite(w http.ResponseWriter, r *http.Request) {
|
||||
admin, ok := a.requireAdminUser(w, r)
|
||||
if !ok {
|
||||
@@ -263,8 +375,8 @@ func (a *App) AdminViewBox(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/d/"+box.ID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) renderAdminLogin(w http.ResponseWriter, status int, message string) {
|
||||
a.renderer.Render(w, status, "admin_login.html", web.PageData{
|
||||
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{
|
||||
@@ -350,3 +462,15 @@ 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 formatMB(value float64) string {
|
||||
return strconv.FormatFloat(value, 'f', -1, 64) + " MB"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user