package handlers import ( "errors" "log/slog" "net/http" "strings" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/services/session" "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) const ( tokenContextKey = "edgeguard_token" cookieName = "edgeguard_session" ) // Recover catches panics and returns a 500 with the response envelope // shape (gin.Recovery defaults to bare HTML). Logs the panic. func Recover() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if rec := recover(); rec != nil { slog.Error("panic in handler", "path", c.Request.URL.Path, "method", c.Request.Method, "panic", rec, ) response.Internal(c, errors.New("internal server error")) c.Abort() } }() c.Next() } } // SetupGate aborts with 503 setup_required for any non-setup route // while setup.Completed is false. Lets the UI's axios client redirect // to /setup on first boot. // // Pass-through prefixes: /api/v1/setup/*, /api/v1/system/health, // /api/health, /healthz. func SetupGate(store *setup.Store) gin.HandlerFunc { allow := []string{ "/api/v1/setup/", "/api/v1/system/health", "/api/health", "/healthz", } return func(c *gin.Context) { path := c.Request.URL.Path for _, p := range allow { if strings.HasPrefix(path, p) { c.Next() return } } st, err := store.Load() if err != nil { response.Internal(c, err) c.Abort() return } if !st.Completed { c.AbortWithStatusJSON(http.StatusServiceUnavailable, response.Envelope{Data: nil, Error: ptr("setup_required"), Message: "setup_required"}) return } c.Next() } } // RequireAuth verifies the JWT from the cookie or Authorization Bearer // header, stashes the Token in the gin context. Endpoints that don't // need auth must be mounted before this middleware in the route tree. func RequireAuth(signer *session.Signer) gin.HandlerFunc { return func(c *gin.Context) { raw := tokenFromRequest(c) if raw == "" { response.Unauthorized(c, errors.New("missing session")) c.Abort() return } tok, err := signer.Verify(raw) if err != nil { response.Unauthorized(c, err) c.Abort() return } c.Set(tokenContextKey, tok) c.Next() } } // CurrentToken returns the verified session token previously stashed // by RequireAuth, or nil for unauthenticated routes. func CurrentToken(c *gin.Context) *session.Token { v, ok := c.Get(tokenContextKey) if !ok { return nil } t, _ := v.(*session.Token) return t } func tokenFromRequest(c *gin.Context) string { if cookie, err := c.Cookie(cookieName); err == nil && cookie != "" { return cookie } auth := c.GetHeader("Authorization") if strings.HasPrefix(auth, "Bearer ") { return strings.TrimPrefix(auth, "Bearer ") } return "" } func ptr[T any](v T) *T { return &v }