package router
import (
"context"
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"allaboutapps.dev/aw/go-starter/internal/api"
"allaboutapps.dev/aw/go-starter/internal/api/handlers"
"allaboutapps.dev/aw/go-starter/internal/api/handlers/constants"
"allaboutapps.dev/aw/go-starter/internal/api/middleware"
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
"github.com/labstack/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
echoMiddleware "github.com/labstack/echo/v4/middleware"
"github.com/rs/zerolog/log"
// #nosec G108 - pprof handlers (conditionally made available via http.DefaultServeMux)
"net/http/pprof"
)
func Init(s *api.Server) error {
s.Echo = echo.New()
viewsRenderer := &echoRenderer{
templates: map[templates.ViewTemplate]*template.Template{},
}
files, err := os.ReadDir(s.Config.Echo.WebTemplatesViewsBaseDirAbs)
if err != nil {
return fmt.Errorf("failed to read views templates dir: %w", err)
}
for _, file := range files {
if file.IsDir() {
continue
}
templateName := file.Name()
t, err := template.New(templateName).ParseGlob(filepath.Join(s.Config.Echo.WebTemplatesViewsBaseDirAbs, templateName))
if err != nil {
return fmt.Errorf("failed to parse template file: %w", err)
}
viewsRenderer.templates[templates.ViewTemplate(templateName)] = t
}
s.Echo.Renderer = viewsRenderer
s.Echo.Debug = s.Config.Echo.Debug
s.Echo.HideBanner = true
s.Echo.Logger.SetOutput(&echoLogger{level: s.Config.Logger.RequestLevel, log: log.With().Str("component", "echo").Logger()})
echo.NotFoundHandler = NotFoundHandler(s.Config)
s.Echo.HTTPErrorHandler = HTTPErrorHandlerWithConfig(HTTPErrorHandlerConfig{
HideInternalServerErrorDetails: s.Config.Echo.HideInternalServerErrorDetails,
})
// ---
// General middleware
if s.Config.Management.EnableMetrics {
s.Echo.Use(echoprometheus.NewMiddleware(""))
err := s.Metrics.RegisterMetrics(context.Background())
if err != nil {
log.Err(err).Msg("Failed to register metrics")
return err
}
} else {
log.Warn().Msg("Disabling metrics middleware due to environment config")
}
if s.Config.Echo.EnableTrailingSlashMiddleware {
s.Echo.Pre(echoMiddleware.RemoveTrailingSlash())
} else {
log.Warn().Msg("Disabling trailing slash middleware due to environment config")
}
if s.Config.Echo.EnableRecoverMiddleware {
s.Echo.Use(echoMiddleware.RecoverWithConfig(echoMiddleware.RecoverConfig{
LogErrorFunc: middleware.LogErrorFuncWithRequestInfo,
}))
} else {
log.Warn().Msg("Disabling recover middleware due to environment config")
}
if s.Config.Echo.EnableSecureMiddleware {
s.Echo.Use(echoMiddleware.SecureWithConfig(echoMiddleware.SecureConfig{
Skipper: echoMiddleware.DefaultSecureConfig.Skipper,
XSSProtection: s.Config.Echo.SecureMiddleware.XSSProtection,
ContentTypeNosniff: s.Config.Echo.SecureMiddleware.ContentTypeNosniff,
XFrameOptions: s.Config.Echo.SecureMiddleware.XFrameOptions,
HSTSMaxAge: s.Config.Echo.SecureMiddleware.HSTSMaxAge,
HSTSExcludeSubdomains: s.Config.Echo.SecureMiddleware.HSTSExcludeSubdomains,
ContentSecurityPolicy: s.Config.Echo.SecureMiddleware.ContentSecurityPolicy,
CSPReportOnly: s.Config.Echo.SecureMiddleware.CSPReportOnly,
HSTSPreloadEnabled: s.Config.Echo.SecureMiddleware.HSTSPreloadEnabled,
ReferrerPolicy: s.Config.Echo.SecureMiddleware.ReferrerPolicy,
}))
} else {
log.Warn().Msg("Disabling secure middleware due to environment config")
}
if s.Config.Echo.EnableRequestIDMiddleware {
s.Echo.Use(echoMiddleware.RequestID())
} else {
log.Warn().Msg("Disabling request ID middleware due to environment config")
}
if s.Config.Echo.EnableLoggerMiddleware {
s.Echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Level: s.Config.Logger.RequestLevel,
LogRequestBody: s.Config.Logger.LogRequestBody,
LogRequestHeader: s.Config.Logger.LogRequestHeader,
LogRequestQuery: s.Config.Logger.LogRequestQuery,
LogResponseBody: s.Config.Logger.LogResponseBody,
LogResponseHeader: s.Config.Logger.LogResponseHeader,
LogCaller: s.Config.Logger.LogCaller,
RequestBodyLogSkipper: func(req *http.Request) bool {
// We skip all body logging for auth endpoints as these might contain credentials
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
return true
}
return middleware.DefaultRequestBodyLogSkipper(req)
},
ResponseBodyLogSkipper: func(req *http.Request, res *echo.Response) bool {
// We skip all body logging for auth endpoints as these might contain credentials
if strings.HasPrefix(req.URL.Path, "/api/v1/auth") {
return true
}
return middleware.DefaultResponseBodyLogSkipper(req, res)
},
Skipper: func(c echo.Context) bool {
// We skip logging of readiness and liveness endpoints
switch c.Path() {
case "/-/ready", "/-/healthy":
return true
}
return false
},
}))
} else {
log.Warn().Msg("Disabling logger middleware due to environment config")
}
if s.Config.Echo.EnableCORSMiddleware {
s.Echo.Use(echoMiddleware.CORS())
} else {
log.Warn().Msg("Disabling CORS middleware due to environment config")
}
if s.Config.Echo.EnableCacheControlMiddleware {
s.Echo.Use(middleware.CacheControl())
} else {
log.Warn().Msg("Disabling cache control middleware due to environment config")
}
if s.Config.Pprof.Enable {
pprofAuthMiddleware := middleware.Noop()
if s.Config.Pprof.EnableManagementKeyAuth {
pprofAuthMiddleware = echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
KeyLookup: "query:mgmt-secret",
Validator: func(key string, _ echo.Context) (bool, error) {
return key == s.Config.Management.Secret, nil
},
})
}
s.Echo.GET("/debug/pprof", echo.WrapHandler(http.HandlerFunc(pprof.Index)), pprofAuthMiddleware)
s.Echo.Any("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux), pprofAuthMiddleware)
log.Warn().Bool("EnableManagementKeyAuth", s.Config.Pprof.EnableManagementKeyAuth).Msg("Pprof http handlers are available at /debug/pprof")
if s.Config.Pprof.RuntimeBlockProfileRate != 0 {
runtime.SetBlockProfileRate(s.Config.Pprof.RuntimeBlockProfileRate)
log.Warn().Int("RuntimeBlockProfileRate", s.Config.Pprof.RuntimeBlockProfileRate).Msg("Pprof runtime.SetBlockProfileRate")
}
if s.Config.Pprof.RuntimeMutexProfileFraction != 0 {
runtime.SetMutexProfileFraction(s.Config.Pprof.RuntimeMutexProfileFraction)
log.Warn().Int("RuntimeMutexProfileFraction", s.Config.Pprof.RuntimeMutexProfileFraction).Msg("Pprof runtime.SetMutexProfileFraction")
}
}
// Add your custom / additional middlewares here.
// see https://echo.labstack.com/middleware
// ---
// Initialize our general groups and set middleware to use above them
s.Router = &api.Router{
Routes: nil, // will be populated by handlers.AttachAllRoutes(s)
// Unsecured base group available at /**
Root: s.Echo.Group(""),
// Management endpoints, uncacheable, secured by key auth (query param), available at /-/**
Management: s.Echo.Group("/-", echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{
KeyLookup: "query:mgmt-secret",
Validator: func(key string, _ echo.Context) (bool, error) {
return key == s.Config.Management.Secret, nil
},
Skipper: func(c echo.Context) bool {
//nolint:gocritic
switch c.Path() {
case "/-/ready":
return true
}
return false
},
}), middleware.NoCache()),
// OAuth2, unsecured or secured by bearer auth, available at /api/v1/auth/**
APIV1Auth: s.Echo.Group("/api/v1/auth", middleware.AuthWithConfig(middleware.AuthConfig{
S: s,
Mode: middleware.AuthModeRequired,
Skipper: func(c echo.Context) bool {
switch c.Path() {
case "/api/v1/auth/forgot-password",
"/api/v1/auth/forgot-password/complete",
"/api/v1/auth/login",
"/api/v1/auth/refresh",
"/api/v1/auth/register",
fmt.Sprintf("/api/v1/auth/register/:%s", constants.RegistrationTokenParam):
return true
}
return false
},
})),
WellKnown: s.Echo.Group("/.well-known"),
// Your other endpoints, typically secured by bearer auth, available at /api/v1/**
APIV1Push: s.Echo.Group("/api/v1/push", middleware.Auth(s)),
}
// ---
// Finally attach our handlers
handlers.AttachAllRoutes(s)
if s.Config.Management.EnableMetrics {
log.Info().Msg("Metrics enabled and available under /metrics")
s.Echo.GET("/metrics", echoprometheus.NewHandler())
}
return nil
}