// Package api wires pansy's HTTP surface: a gin engine with structured logging // and panic recovery, the versioned JSON API under /api/v1, and (via spa.go) the // embedded single-page-app fallback. Handlers stay thin — decode, call the // service layer, encode — so all business logic and permission checks live in // internal/service (added by later issues). package api import ( "log/slog" "net/http" "github.com/gin-gonic/gin" sloggin "github.com/samber/slog-gin" "gitea.stevedudenhoeffer.com/steve/pansy/internal/config" "gitea.stevedudenhoeffer.com/steve/pansy/internal/service" ) // handlers carries the dependencies shared by every HTTP handler. Handlers stay // thin: decode the request, call a service method, encode the result. type handlers struct { cfg *config.Config svc *service.Service } // New builds the gin engine with the standard middleware stack and registers the // API routes against the given service. The embedded SPA fallback is registered // separately by the caller via RegisterSPA (see spa.go) so the API can be built // and tested without a web build present. func New(cfg *config.Config, svc *service.Service) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(sloggin.New(slog.Default()), gin.Recovery()) if err := r.SetTrustedProxies(cfg.TrustedProxies); err != nil { // Do not leave gin's trust-everyone default active on a parse failure — // that would let any client spoof X-Forwarded-For. Fall back to trusting // no proxies, which is also the behavior when none are configured. slog.Error("api: invalid trusted proxies, trusting none", "error", err) _ = r.SetTrustedProxies(nil) } h := &handlers{cfg: cfg, svc: svc} v1 := r.Group("/api/v1") v1.GET("/healthz", healthz) // Auth endpoints are exempt from requireAuth (you can't be logged in yet); // /me is the one that needs a session. Feature routers in later issues attach // h.requireAuth() to their own protected groups. auth := v1.Group("/auth") auth.POST("/register", h.register) auth.POST("/login", h.login) auth.POST("/logout", h.logout) auth.GET("/providers", h.providers) auth.GET("/me", h.requireAuth(), h.me) return r } // healthz is a liveness probe: always returns {"ok": true} when the server is up. func healthz(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) }