(Feat): Initial Commit
Some checks failed
Build & Test / build-test (push) Has been cancelled
Build & Test / swagger-codegen-cli (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled

This commit is contained in:
2026-07-03 19:41:31 +05:30
commit 7e940c83a7
461 changed files with 45002 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package router
import "github.com/rs/zerolog"
type echoLogger struct {
level zerolog.Level
log zerolog.Logger
}
func (l *echoLogger) Write(p []byte) (int, error) {
l.log.WithLevel(l.level).Msgf("%s", p)
return len(p), nil
}

View File

@@ -0,0 +1,27 @@
package router
import (
"fmt"
"html/template"
"io"
"allaboutapps.dev/aw/go-starter/internal/api/router/templates"
"github.com/labstack/echo/v4"
)
type echoRenderer struct {
templates map[templates.ViewTemplate]*template.Template
}
func (t *echoRenderer) Render(writer io.Writer, name string, data interface{}, _ echo.Context) error {
tmplHTML, ok := t.templates[templates.ViewTemplate(name)]
if !ok {
return fmt.Errorf("template not found: %s", name)
}
if err := tmplHTML.Execute(writer, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
return nil
}

View File

@@ -0,0 +1,150 @@
package router
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"allaboutapps.dev/aw/go-starter/internal/api/httperrors"
"allaboutapps.dev/aw/go-starter/internal/config"
"allaboutapps.dev/aw/go-starter/internal/types"
"allaboutapps.dev/aw/go-starter/internal/util"
"github.com/go-openapi/swag"
"github.com/labstack/echo/v4"
"github.com/timewasted/go-accept-headers"
)
var (
DefaultHTTPErrorHandlerConfig = HTTPErrorHandlerConfig{
HideInternalServerErrorDetails: true,
}
)
type HTTPErrorHandlerConfig struct {
HideInternalServerErrorDetails bool
}
func HTTPErrorHandler() echo.HTTPErrorHandler {
return HTTPErrorHandlerWithConfig(DefaultHTTPErrorHandlerConfig)
}
func HTTPErrorHandlerWithConfig(config HTTPErrorHandlerConfig) echo.HTTPErrorHandler {
return func(err error, c echo.Context) {
var code int64
var resultErr error
var httpError *httperrors.HTTPError
var httpValidationError *httperrors.HTTPValidationError
var echoHTTPError *echo.HTTPError
switch {
case errors.As(err, &httpError):
code = *httpError.Code
resultErr = httpError
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
if httpError.Internal == nil {
//nolint:errorlint
httpError.Internal = fmt.Errorf("internal error: %s", httpError)
}
httpError.Title = swag.String(http.StatusText(http.StatusInternalServerError))
}
case errors.As(err, &httpValidationError):
code = *httpValidationError.Code
resultErr = httpValidationError
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
if httpValidationError.Internal == nil {
//nolint:errorlint
httpValidationError.Internal = fmt.Errorf("internal error: %s", httpValidationError)
}
httpValidationError.Title = swag.String(http.StatusText(http.StatusInternalServerError))
}
case errors.As(err, &echoHTTPError):
code = int64(echoHTTPError.Code)
//nolint:nestif
if code == http.StatusInternalServerError && config.HideInternalServerErrorDetails {
if echoHTTPError.Internal == nil {
//nolint:errorlint
echoHTTPError.Internal = fmt.Errorf("internal error: %s", echoHTTPError)
}
resultErr = &httperrors.HTTPError{
PublicHTTPError: types.PublicHTTPError{
Code: swag.Int64(int64(echoHTTPError.Code)),
Title: swag.String(http.StatusText(http.StatusInternalServerError)),
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
},
Internal: echoHTTPError.Internal,
}
} else {
msg, ok := echoHTTPError.Message.(string)
if !ok {
if m, errr := json.Marshal(msg); err == nil {
msg = string(m)
} else {
msg = fmt.Sprintf("failed to marshal HTTP error message: %v", errr)
}
}
resultErr = &httperrors.HTTPError{
PublicHTTPError: types.PublicHTTPError{
Code: swag.Int64(int64(echoHTTPError.Code)),
Title: &msg,
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
},
Internal: echoHTTPError.Internal,
}
}
default:
code = http.StatusInternalServerError
if config.HideInternalServerErrorDetails {
resultErr = &httperrors.HTTPError{
PublicHTTPError: types.PublicHTTPError{
Code: swag.Int64(int64(http.StatusInternalServerError)),
Title: swag.String(http.StatusText(http.StatusInternalServerError)),
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
},
Internal: err,
}
} else {
resultErr = &httperrors.HTTPError{
PublicHTTPError: types.PublicHTTPError{
Code: swag.Int64(int64(http.StatusInternalServerError)),
Title: swag.String(err.Error()),
Type: types.PublicHTTPErrorTypeGeneric.Pointer(),
},
}
}
}
if !c.Response().Committed {
if c.Request().Method == http.MethodHead {
err = c.NoContent(int(code))
} else {
err = c.JSON(int(code), resultErr)
}
if err != nil {
util.LogFromEchoContext(c).Warn().Err(err).AnErr("http_err", err).Msg("Failed to handle HTTP error")
}
}
}
}
func NotFoundHandler(config config.Server) func(c echo.Context) error {
return func(c echo.Context) error {
accepted := accept.Parse(c.Request().Header.Get(echo.HeaderAccept))
if accepted.Accepts(echo.MIMETextHTML) {
return c.HTML(http.StatusNotFound, fmt.Sprintf(`<html><body><h1>Page Not Found</h1><p>The page you are looking for does not exist. Did you mean to visit <a href="%s">%s</a>?</p></body></html>`, config.Frontend.BaseURL, config.Frontend.BaseURL))
}
return echo.ErrNotFound
}
}

View File

@@ -0,0 +1,252 @@
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
}

View File

@@ -0,0 +1,128 @@
package router_test
import (
"fmt"
"net/http"
"testing"
"allaboutapps.dev/aw/go-starter/internal/api"
"allaboutapps.dev/aw/go-starter/internal/config"
"allaboutapps.dev/aw/go-starter/internal/metrics/users"
"allaboutapps.dev/aw/go-starter/internal/models"
"allaboutapps.dev/aw/go-starter/internal/test"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPprofEnabled(t *testing.T) {
config := config.DefaultServiceConfigFromEnv()
// these are typically our default values, however we force set them here to ensure those are set while test execution.
config.Pprof.Enable = true
config.Pprof.EnableManagementKeyAuth = true
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
// heap (test any)
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil)
require.Equal(t, 200, res.Result().StatusCode)
// index
res = test.PerformRequest(t, s, "GET", "/debug/pprof?mgmt-secret="+s.Config.Management.Secret, nil, nil)
require.Equal(t, 200, res.Result().StatusCode)
res = test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret=wrongsecret", nil, nil)
require.Equal(t, 401, res.Result().StatusCode)
})
}
func TestPprofEnabledNoAuth(t *testing.T) {
config := config.DefaultServiceConfigFromEnv()
// these are typically our default values, however we force set them here to ensure those are set while test execution.
config.Pprof.Enable = true
config.Pprof.EnableManagementKeyAuth = false
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?", nil, nil)
require.Equal(t, 200, res.Result().StatusCode)
})
}
func TestPprofDisabled(t *testing.T) {
config := config.DefaultServiceConfigFromEnv()
config.Pprof.Enable = false
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil)
require.Equal(t, 404, res.Result().StatusCode)
})
}
func TestMiddlewaresDisabled(t *testing.T) {
// disable all
config := config.DefaultServiceConfigFromEnv()
config.Echo.EnableCORSMiddleware = false
config.Echo.EnableLoggerMiddleware = false
config.Echo.EnableRecoverMiddleware = false
config.Echo.EnableRequestIDMiddleware = false
config.Echo.EnableSecureMiddleware = false
config.Echo.EnableTrailingSlashMiddleware = false
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil)
require.Equal(t, 200, res.Result().StatusCode)
})
}
func TestMetricsEnabled(t *testing.T) {
config := config.DefaultServiceConfigFromEnv()
config.Management.EnableMetrics = true
test.WithTestServerConfigurable(t, config, func(s *api.Server) {
res := test.PerformRequest(t, s, "GET", "/metrics", nil, nil)
require.Equal(t, http.StatusOK, res.Result().StatusCode)
result := res.Body.String()
// expect custom metric for the total user count
expectedTotalUserCount, err := models.Users().Count(t.Context(), s.DB)
require.NoError(t, err)
assert.Contains(t, result, fmt.Sprintf("%s %d", users.MetricNameTotalUsers, expectedTotalUserCount))
// expect sqlstats metrics
assert.Contains(t, result, "go_sql_stats_connections")
})
}
func TestMetricsDisabled(t *testing.T) {
test.WithTestServer(t, func(s *api.Server) {
res := test.PerformRequest(t, s, "GET", "/metrics", nil, nil)
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
})
}
func TestNotFound(t *testing.T) {
test.WithTestServer(t, func(s *api.Server) {
t.Run("AcceptApplicationJSON", func(t *testing.T) {
headers := http.Header{}
headers.Set(echo.HeaderAccept, echo.MIMEApplicationJSON)
res := test.PerformRequest(t, s, "GET", "/api/v1/unknown-path", nil, headers)
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
test.Snapshoter.Save(t, res.Body.String())
})
t.Run("AcceptTextHTML", func(t *testing.T) {
headers := http.Header{}
headers.Set(echo.HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
res := test.PerformRequest(t, s, "GET", "/api/v1/unknown-path", nil, headers)
require.Equal(t, http.StatusNotFound, res.Result().StatusCode)
test.Snapshoter.Save(t, res.Body.String())
})
})
}

View File

@@ -0,0 +1,11 @@
package templates
type ViewTemplate string
const (
ViewTemplateAccountConfirmation ViewTemplate = "account_confirmation.html.tmpl"
)
func (vt ViewTemplate) String() string {
return string(vt)
}