go-api/options.go
Snider f5ce02d661 feat: add WithSlog structured request logging middleware
Adds WithSlog(logger) option wrapping gin-contrib/slog for structured
request logging via Go's standard log/slog package. Logs method, path,
status code, latency, and client IP for every request. Falls back to
slog.Default() when nil is passed.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:14:15 +00:00

136 lines
4.1 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import (
"log/slog"
"net/http"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/secure"
ginslog "github.com/gin-contrib/slog"
"github.com/gin-gonic/gin"
)
// Option configures an Engine during construction.
type Option func(*Engine)
// WithAddr sets the listen address for the server.
func WithAddr(addr string) Option {
return func(e *Engine) {
e.addr = addr
}
}
// WithBearerAuth adds bearer token authentication middleware.
// Requests to /health and paths starting with /swagger are exempt.
func WithBearerAuth(token string) Option {
return func(e *Engine) {
skip := []string{"/health", "/swagger"}
e.middlewares = append(e.middlewares, bearerAuthMiddleware(token, skip))
}
}
// WithRequestID adds middleware that assigns an X-Request-ID to every response.
// Client-provided IDs are preserved; otherwise a random hex ID is generated.
func WithRequestID() Option {
return func(e *Engine) {
e.middlewares = append(e.middlewares, requestIDMiddleware())
}
}
// WithCORS configures Cross-Origin Resource Sharing via gin-contrib/cors.
// Pass "*" to allow all origins, or supply specific origin URLs.
// Standard methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) and common
// headers (Authorization, Content-Type, X-Request-ID) are permitted.
func WithCORS(allowOrigins ...string) Option {
return func(e *Engine) {
cfg := cors.Config{
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Authorization", "Content-Type", "X-Request-ID"},
MaxAge: 12 * time.Hour,
}
for _, o := range allowOrigins {
if o == "*" {
cfg.AllowAllOrigins = true
break
}
}
if !cfg.AllowAllOrigins {
cfg.AllowOrigins = allowOrigins
}
e.middlewares = append(e.middlewares, cors.New(cfg))
}
}
// WithMiddleware appends arbitrary Gin middleware to the engine.
func WithMiddleware(mw ...gin.HandlerFunc) Option {
return func(e *Engine) {
e.middlewares = append(e.middlewares, mw...)
}
}
// WithWSHandler registers a WebSocket handler at GET /ws.
// Typically this wraps a go-ws Hub.Handler().
func WithWSHandler(h http.Handler) Option {
return func(e *Engine) {
e.wsHandler = h
}
}
// WithAuthentik adds Authentik forward-auth middleware that extracts user
// identity from X-authentik-* headers set by a trusted reverse proxy.
// The middleware is permissive: unauthenticated requests are allowed through.
func WithAuthentik(cfg AuthentikConfig) Option {
return func(e *Engine) {
e.middlewares = append(e.middlewares, authentikMiddleware(cfg))
}
}
// WithSwagger enables the Swagger UI at /swagger/.
// The title, description, and version populate the OpenAPI info block.
func WithSwagger(title, description, version string) Option {
return func(e *Engine) {
e.swaggerTitle = title
e.swaggerDesc = description
e.swaggerVersion = version
e.swaggerEnabled = true
}
}
// WithSecure adds security headers middleware via gin-contrib/secure.
// Default policy sets HSTS (1 year, includeSubDomains), X-Frame-Options DENY,
// X-Content-Type-Options nosniff, and Referrer-Policy strict-origin-when-cross-origin.
// SSL redirect is disabled (IsDevelopment=true) so the middleware works behind
// a reverse proxy that terminates TLS.
func WithSecure() Option {
return func(e *Engine) {
e.middlewares = append(e.middlewares, secure.New(secure.Config{
STSSeconds: 31536000,
STSIncludeSubdomains: true,
FrameDeny: true,
ContentTypeNosniff: true,
ReferrerPolicy: "strict-origin-when-cross-origin",
IsDevelopment: false,
}))
}
}
// WithSlog adds structured request logging middleware via gin-contrib/slog.
// Each request is logged with method, path, status code, latency, and client IP.
// If logger is nil, slog.Default() is used.
func WithSlog(logger *slog.Logger) Option {
return func(e *Engine) {
if logger == nil {
logger = slog.Default()
}
e.middlewares = append(e.middlewares, ginslog.SetLogger(
ginslog.WithLogger(func(_ *gin.Context, l *slog.Logger) *slog.Logger {
return logger
}),
))
}
}