2026-05-01 00:29:06 +03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
2026-05-04 00:00:36 +03:00
|
|
|
"strconv"
|
2026-05-01 00:29:06 +03:00
|
|
|
"strings"
|
2026-05-04 10:54:44 +03:00
|
|
|
"syscall"
|
|
|
|
|
"time"
|
2026-05-01 00:29:06 +03:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
"warpbox/lib/activity"
|
2026-05-04 00:00:36 +03:00
|
|
|
"warpbox/lib/alerts"
|
2026-05-01 00:29:06 +03:00
|
|
|
"warpbox/lib/config"
|
2026-05-04 10:54:44 +03:00
|
|
|
"warpbox/lib/helpers"
|
2026-05-04 00:00:36 +03:00
|
|
|
"warpbox/lib/security"
|
2026-05-04 10:54:44 +03:00
|
|
|
"warpbox/lib/userstore"
|
2026-05-01 00:29:06 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const adminSessionCookie = "warpbox_admin_session"
|
|
|
|
|
const adminSessionMarker = "1"
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 00:29:06 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-04 00:00:36 +03:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
|
|
|
|
|
username := strings.TrimSpace(ctx.PostForm("username"))
|
|
|
|
|
password := ctx.PostForm("password")
|
|
|
|
|
|
|
|
|
|
if username != app.config.AdminUsername || password != app.config.AdminPassword {
|
2026-05-04 00:00:36 +03:00
|
|
|
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)})
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
ctx.HTML(http.StatusUnauthorized, "admin/login.html", gin.H{
|
|
|
|
|
"ErrorMessage": "Invalid username or password.",
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 00:00:36 +03:00
|
|
|
app.logActivity("auth.admin.success", "low", "Admin login successful", ctx, nil)
|
2026-05-01 00:29:06 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
dashboard := app.buildAdminDashboardView()
|
2026-05-01 00:29:06 +03:00
|
|
|
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
|
|
|
|
"AdminUsername": app.config.AdminUsername,
|
|
|
|
|
"AdminEmail": app.config.AdminEmail,
|
2026-05-01 00:46:10 +03:00
|
|
|
"ActivePage": "dashboard",
|
2026-05-01 00:29:06 +03:00
|
|
|
"DashboardEnabled": string(dashboardEnabled),
|
2026-05-04 10:54:44 +03:00
|
|
|
"Dashboard": dashboard,
|
|
|
|
|
"AlertChipClass": adminAlertChipClass(dashboard.OpenAlerts, dashboard.HighAlerts, dashboard.MediumAlerts),
|
|
|
|
|
"AlertChipLabel": adminAlertChipLabel(dashboard.OpenAlerts),
|
2026-05-01 00:29:06 +03:00
|
|
|
})
|
|
|
|
|
}
|
2026-05-01 00:46:10 +03:00
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 00:46:10 +03:00
|
|
|
func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
|
|
|
|
if !app.adminLoginEnabled() {
|
|
|
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 00:00:36 +03:00
|
|
|
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
|
2026-05-04 10:54:44 +03:00
|
|
|
mediumCount := 0
|
2026-05-04 00:00:36 +03:00
|
|
|
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++
|
|
|
|
|
}
|
2026-05-04 10:54:44 +03:00
|
|
|
if alert.Severity == "medium" && string(alert.Status) != "closed" {
|
|
|
|
|
mediumCount++
|
|
|
|
|
}
|
2026-05-04 00:00:36 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 00:46:10 +03:00
|
|
|
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
2026-05-04 10:54:44 +03:00
|
|
|
"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),
|
2026-05-01 00:46:10 +03:00
|
|
|
})
|
|
|
|
|
}
|