package server import ( "net/http" "strconv" "strings" "syscall" "time" "github.com/gin-gonic/gin" "warpbox/lib/activity" "warpbox/lib/alerts" "warpbox/lib/config" "warpbox/lib/helpers" "warpbox/lib/security" "warpbox/lib/userstore" ) const adminSessionCookie = "warpbox_admin_session" const adminSessionMarker = "1" type adminDashboardView struct { ActiveBoxes int `json:"active_boxes"` BoxesCreatedToday int `json:"boxes_created_today"` PasswordedBoxes int `json:"passworded_boxes"` StorageUsedLabel string `json:"storage_used_label"` StorageFreeLabel string `json:"storage_free_label"` StorageCapLabel string `json:"storage_cap_label"` StorageMeter string `json:"storage_meter"` StorageBackend string `json:"storage_backend"` OpenAlerts int `json:"open_alerts"` HighAlerts int `json:"high_alerts"` MediumAlerts int `json:"medium_alerts"` LowAlerts int `json:"low_alerts"` TotalUsers int `json:"total_users"` ActiveUsers int `json:"active_users"` DisabledUsers int `json:"disabled_users"` APIKeyCount int `json:"api_key_count"` GuestUploadsLabel string `json:"guest_uploads_label"` APIUploadsLabel string `json:"api_uploads_label"` ZipDownloadsLabel string `json:"zip_downloads_label"` Alerts []adminDashboardAlert `json:"alerts"` Events []adminDashboardActivity `json:"events"` Boxes []adminDashboardBox `json:"boxes"` } type adminDashboardAlert struct { ID string `json:"id"` Title string `json:"title"` Severity string `json:"severity"` Status string `json:"status"` Group string `json:"group"` Code string `json:"code"` Trace string `json:"trace"` Message string `json:"message"` CreatedAt string `json:"created_at"` CreatedAtLabel string `json:"created_at_label"` Meta map[string]string `json:"meta,omitempty"` } type adminDashboardActivity struct { ID string `json:"id"` Kind string `json:"kind"` Severity string `json:"severity"` Message string `json:"message"` IP string `json:"ip"` Path string `json:"path"` Method string `json:"method"` CreatedAt string `json:"created_at"` CreatedAtLabel string `json:"created_at_label"` Meta map[string]string `json:"meta,omitempty"` TagClass string `json:"tag_class"` TagLabel string `json:"tag_label"` } type adminDashboardBox struct { ID string `json:"id"` Status string `json:"status"` StatusLabel string `json:"status_label"` StatusClass string `json:"status_class"` FileCount int `json:"file_count"` CompleteFiles int `json:"complete_files"` TotalSizeLabel string `json:"total_size_label"` CreatedAtLabel string `json:"created_at_label"` ExpiresAtLabel string `json:"expires_at_label"` PasswordProtected bool `json:"password_protected"` OneTimeDownload bool `json:"one_time_download"` OpenURL string `json:"open_url"` ZipURL string `json:"zip_url"` Flags []string `json:"flags"` } func (app *App) adminLoginEnabled() bool { return app.config.AdminLoginEnabled(app.config.AdminPassword != "") } func (app *App) adminAuthMiddleware(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") ctx.Abort() return } token, err := ctx.Cookie(adminSessionCookie) if err != nil || token != app.adminSessionToken() { ctx.Redirect(http.StatusSeeOther, "/admin/login") ctx.Abort() return } ctx.Next() } func (app *App) adminSessionToken() string { // A simple deterministic token derived from the admin credentials. // This will improve when proper user/session storage is added. return app.config.AdminUsername + ":" + app.config.AdminPassword } func (app *App) handleAdminLogin(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") return } // Already logged in. if token, err := ctx.Cookie(adminSessionCookie); err == nil && token == app.adminSessionToken() { ctx.Redirect(http.StatusSeeOther, "/admin/dashboard") return } ctx.HTML(http.StatusOK, "admin/login.html", gin.H{}) } func (app *App) handleAdminLoginPost(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") return } ip := app.clientIP(ctx) guard := app.securityGuard if app.securityFeaturesEnabled() && guard == nil { guard = security.NewGuard() app.securityGuard = guard } if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) { app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil) ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{ "ErrorMessage": "Too many failed attempts. Try again later.", }) return } username := strings.TrimSpace(ctx.PostForm("username")) password := ctx.PostForm("password") if username != app.config.AdminUsername || password != app.config.AdminPassword { if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) { banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds) app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)}) if banned { app.createAlert("Admin login brute-force blocked", "high", "security", "401", "auth.admin.bruteforce", "Too many failed admin logins triggered temporary ban.", map[string]string{"ip": ip, "attempts": strconv.Itoa(attempts)}) app.logActivity("security.ban", "high", "Auto-banned IP after admin login failures", ctx, map[string]string{"attempts": strconv.Itoa(attempts)}) } } ctx.HTML(http.StatusUnauthorized, "admin/login.html", gin.H{ "ErrorMessage": "Invalid username or password.", }) return } app.logActivity("auth.admin.success", "low", "Admin login successful", ctx, nil) secure := app.config.AdminCookieSecure maxAge := int(app.config.SessionTTLSeconds) ctx.SetCookie(adminSessionCookie, app.adminSessionToken(), maxAge, "/admin", "", secure, true) ctx.Redirect(http.StatusSeeOther, "/admin/dashboard") } func (app *App) handleAdminLogout(ctx *gin.Context) { secure := app.config.AdminCookieSecure ctx.SetCookie(adminSessionCookie, "", -1, "/admin", "", secure, true) ctx.Redirect(http.StatusSeeOther, "/admin/login") } func (app *App) handleAdminDashboard(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") return } dashboardEnabled := config.AdminEnabledTrue if cfgVal := app.config.AdminEnabled; cfgVal == config.AdminEnabledAuto || cfgVal == config.AdminEnabledTrue { dashboardEnabled = cfgVal } dashboard := app.buildAdminDashboardView() ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "dashboard", "DashboardEnabled": string(dashboardEnabled), "Dashboard": dashboard, "AlertChipClass": adminAlertChipClass(dashboard.OpenAlerts, dashboard.HighAlerts, dashboard.MediumAlerts), "AlertChipLabel": adminAlertChipLabel(dashboard.OpenAlerts), }) } func (app *App) buildAdminDashboardView() adminDashboardView { boxes, _ := app.listAdminBoxes() alertsList := []alerts.Alert{} if app.alertStore != nil { alertsList, _ = app.alertStore.List(500) } events := []activity.Event{} if app.activityStore != nil { events, _ = app.activityStore.List(80, app.config.ActivityRetentionSeconds) } users := []userstore.User{} if app.userStore != nil { users = app.userStore.List() } view := adminDashboardView{ StorageBackend: "local", GuestUploadsLabel: adminBoolLabel(app.config.GuestUploadsEnabled && app.config.APIEnabled), APIUploadsLabel: adminBoolLabel(app.config.APIEnabled), ZipDownloadsLabel: adminBoolLabel(app.config.ZipDownloadsEnabled), Alerts: make([]adminDashboardAlert, 0, 12), Events: make([]adminDashboardActivity, 0, 15), Boxes: make([]adminDashboardBox, 0, 12), } now := time.Now().UTC() dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) usedBytes := int64(0) for _, box := range boxes { usedBytes += box.TotalSizeBytes createdAt, _ := time.Parse(time.RFC3339, box.CreatedAtISO) if !createdAt.IsZero() && createdAt.After(dayStart) { view.BoxesCreatedToday++ } if box.PasswordProtected { view.PasswordedBoxes++ } if box.Status != "expired" && box.Status != "consumed" { view.ActiveBoxes++ } if len(view.Boxes) < 12 { view.Boxes = append(view.Boxes, adminDashboardBox{ ID: box.ID, Status: box.Status, StatusLabel: box.StatusLabel, StatusClass: adminDashboardStatusClass(box.Status), FileCount: box.FileCount, CompleteFiles: box.CompleteFiles, TotalSizeLabel: box.TotalSizeLabel, CreatedAtLabel: box.CreatedAtLabel, ExpiresAtLabel: box.ExpiresAtLabel, PasswordProtected: box.PasswordProtected, OneTimeDownload: box.OneTimeDownload, OpenURL: box.OpenURL, ZipURL: box.ZipURL, Flags: box.Flags, }) } } view.StorageUsedLabel = helpers.FormatBytes(usedBytes) view.StorageCapLabel = "unknown" view.StorageFreeLabel = "unknown" view.StorageMeter = "0%" if diskTotal, diskFree, ok := adminDiskCapacity(app.config.UploadsDir); ok && diskTotal > 0 { diskUsed := diskTotal - diskFree if diskUsed < 0 { diskUsed = 0 } view.StorageUsedLabel = helpers.FormatBytes(diskUsed) view.StorageFreeLabel = helpers.FormatBytes(diskFree) view.StorageCapLabel = helpers.FormatBytes(diskTotal) percent := float64(diskUsed) / float64(diskTotal) * 100 if percent > 100 { percent = 100 } view.StorageMeter = strconv.FormatFloat(percent, 'f', 1, 64) + "%" } for _, alert := range alertsList { if alert.Status != alerts.StatusClosed { view.OpenAlerts++ switch alert.Severity { case "high": view.HighAlerts++ case "medium": view.MediumAlerts++ default: view.LowAlerts++ } if len(view.Alerts) < 12 { view.Alerts = append(view.Alerts, adminDashboardAlert{ ID: alert.ID, Title: alert.Title, Severity: adminFallback(alert.Severity, "low"), Status: adminFallback(string(alert.Status), "open"), Group: alert.Group, Code: alert.Code, Trace: alert.Trace, Message: alert.Message, CreatedAt: formatBrowserTime(alert.CreatedAt), CreatedAtLabel: adminShortTimeLabel(alert.CreatedAt), Meta: alert.Meta, }) } } } for _, event := range events { if len(view.Events) >= 15 { break } view.Events = append(view.Events, adminDashboardActivity{ ID: event.ID, Kind: event.Kind, Severity: adminFallback(event.Severity, "low"), Message: event.Message, IP: event.IP, Path: event.Path, Method: event.Method, CreatedAt: formatBrowserTime(event.CreatedAt), CreatedAtLabel: adminShortTimeLabel(event.CreatedAt), Meta: event.Meta, TagClass: adminSeverityTagClass(event.Severity), TagLabel: adminActivityTagLabel(event.Kind), }) } for _, user := range users { view.TotalUsers++ if user.Status == userstore.StatusDisabled { view.DisabledUsers++ } else { view.ActiveUsers++ } for _, key := range user.APIKeys { if key.RevokedAt == nil { view.APIKeyCount++ } } } return view } func adminBoolLabel(enabled bool) string { if enabled { return "enabled" } return "disabled" } func adminFallback(value string, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } func adminShortTimeLabel(value time.Time) string { if value.IsZero() { return "-" } return value.UTC().Format("15:04") } func adminDashboardStatusClass(status string) string { switch status { case "ready": return "ok" case "uploading", "legacy": return "warn" case "attention", "expired", "consumed": return "danger" default: return "info" } } func adminSeverityTagClass(severity string) string { switch severity { case "high": return "danger" case "medium": return "warn" case "low": return "ok" default: return "info" } } func adminActivityTagLabel(kind string) string { parts := strings.Split(kind, ".") if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" { return "event" } return parts[0] } func adminDiskCapacity(path string) (int64, int64, bool) { if strings.TrimSpace(path) == "" { return 0, 0, false } var stats syscall.Statfs_t if err := syscall.Statfs(path, &stats); err != nil { return 0, 0, false } blockSize := int64(stats.Bsize) if blockSize <= 0 { return 0, 0, false } total := int64(stats.Blocks) * blockSize free := int64(stats.Bavail) * blockSize return total, free, true } func adminAlertChipClass(total int, high int, medium int) string { score := high*5 + medium*2 + (total - high - medium) switch { case high > 0 || score >= 12: return "is-danger" case medium >= 2 || score >= 5: return "is-warning" case total > 0: return "is-info" default: return "is-ok" } } func adminAlertChipLabel(total int) string { if total == 0 { return "OK no alerts" } return "! " + strconv.Itoa(total) + " alerts" } func (app *App) handleAdminAlerts(ctx *gin.Context) { if !app.adminLoginEnabled() { ctx.Redirect(http.StatusSeeOther, "/") return } alertsList := []alerts.Alert{} if app.alertStore != nil { var err error alertsList, err = app.alertStore.List(500) if err != nil { ctx.String(http.StatusInternalServerError, "Could not load alerts") return } } openCount := 0 highCount := 0 mediumCount := 0 ackedCount := 0 closedCount := 0 for _, alert := range alertsList { switch string(alert.Status) { case "open": openCount++ case "acked": ackedCount++ case "closed": closedCount++ } if alert.Severity == "high" && string(alert.Status) != "closed" { highCount++ } if alert.Severity == "medium" && string(alert.Status) != "closed" { mediumCount++ } } ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "alerts", "Alerts": alertsList, "OpenCount": strconv.Itoa(openCount), "HighCount": strconv.Itoa(highCount), "AckCount": strconv.Itoa(ackedCount), "ClosedCount": strconv.Itoa(closedCount), "AlertChipClass": adminAlertChipClass(openCount, highCount, mediumCount), "AlertChipLabel": adminAlertChipLabel(openCount), }) }