Files
warpbox/lib/server/admin.go

174 lines
5.0 KiB
Go

package server
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"warpbox/lib/alerts"
"warpbox/lib/config"
"warpbox/lib/security"
)
const adminSessionCookie = "warpbox_admin_session"
const adminSessionMarker = "1"
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 := clientIP(ctx)
guard := app.securityGuard
if guard == nil {
guard = security.NewGuard()
app.securityGuard = guard
}
if !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 !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
}
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
"AdminUsername": app.config.AdminUsername,
"AdminEmail": app.config.AdminEmail,
"ActivePage": "dashboard",
"DashboardEnabled": string(dashboardEnabled),
})
}
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
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++
}
}
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),
})
}