From 4b18834da8a0c8c342b70293636fb4aa126756fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 15:21:00 +0000 Subject: [PATCH 1/5] feat: upgrade Echo v4 to v5 - Update go.mod: github.com/labstack/echo/v4 -> v5, remove labstack/gommon - Remove github.com/swaggo/echo-swagger (no v5 support); replace with hand-rolled Swagger UI handler using swaggo/files/v2 and swag.ReadDoc() - Mass import path replacement across 55 Go files (v4 -> v5) - Fix echo.Context interface -> *echo.Context struct pointer (v5 change) - Fix HTTPErrorHandler signature: func(err, ctx) -> func(ctx, err) - Fix NewHTTPError 3-arg calls -> 2-arg with .Wrap(err) (v5 API change) - Fix e.Routes() -> e.Router().Routes() (v5 API change) - Fix e.Shutdown() -> echo.StartConfig with GracefulTimeout (v5 API change) - Fix e.Debug field removed -> use config-based debug flag - Fix c.Response().Status/Committed -> echo.UnwrapResponse() (v5 change) - Fix RecoverConfig: remove LogLevel/LogErrorFunc/DisableErrorHandler fields - Fix gommon/random -> math/rand for RequestID fallback generation - Fix Validator interface: Validate(interface{}) -> Validate(any) https://claude.ai/code/session_012HXUTcemrXNF2PbH2kSQ7m --- cmd/main.go | 38 +++++---- cmd/server/rest_server.go | 81 +++++++++++++++++-- cmd/server/web_server.go | 8 +- config/config.go | 13 +-- go.mod | 20 ++--- go.sum | 28 ++----- .../auth/controller/auth_controller.go | 12 +-- .../auth/controller/auth_controller_test.go | 2 +- .../auth/controller/auth_routes.go | 2 +- internal/applications/auth/registry.go | 2 +- .../auth/service/auth_service_test.go | 2 +- .../health/controller/health_controller.go | 8 +- internal/applications/health/registry.go | 2 +- .../controller/notification_controller.go | 4 +- .../controller/notification_routes.go | 2 +- .../applications/notification/registry.go | 2 +- .../rbac/controllers/audit_controller.go | 12 +-- .../controllers/enforcement_controller.go | 10 +-- .../rbac/controllers/policy_controller.go | 12 +-- .../rbac/controllers/role_controller.go | 14 ++-- .../rbac/controllers/user_role_controller.go | 12 +-- internal/applications/rbac/registry.go | 2 +- .../user/controller/user_controller.go | 12 +-- .../user/controller/user_routes.go | 2 +- internal/applications/user/registry.go | 2 +- internal/middlewares/cors_middleware.go | 4 +- internal/middlewares/log_middleware.go | 6 +- internal/middlewares/rbac_audit_middleware.go | 45 ++++------- .../rbac_enforcement_middleware.go | 12 +-- internal/middlewares/registry.go | 18 ++--- .../middlewares/req_context_middleware.go | 4 +- .../middlewares/req_injector_middleware.go | 4 +- internal/middlewares/requestid_middleware.go | 12 +-- .../middlewares/tenant_context_middleware.go | 14 ++-- internal/middlewares/timeout_middleware.go | 4 +- internal/middlewares/validator_middleware.go | 6 +- internal/middlewares/version_middleware.go | 10 +-- .../middlewares/version_middleware_test.go | 20 ++--- pkg/authenticator/auth.go | 8 +- pkg/authenticator/auth_middleware.go | 4 +- pkg/authenticator/jwt.go | 4 +- pkg/authenticator/jwt_test.go | 2 +- pkg/authenticator/jwt_token.go | 4 +- pkg/errors/bun_handler.go | 13 ++- pkg/errors/echo_handler.go | 4 +- pkg/errors/generic_handler.go | 8 +- pkg/errors/handler.go | 8 +- pkg/errors/oops_handler.go | 6 +- pkg/errors/registry.go | 2 +- pkg/logger/logger.go | 12 ++- pkg/logger/logger_test.go | 2 +- pkg/utils/response/response_json_builder.go | 10 +-- pkg/validator/helper.go | 10 +-- pkg/validator/mocks/mock_context_validator.go | 16 ++-- pkg/validator/translator.go | 4 +- pkg/versioning/version.go | 2 +- pkg/versioning/version_test.go | 2 +- 57 files changed, 310 insertions(+), 274 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 03ee8d6..dc73741 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,14 @@ import ( "context" "errors" "fmt" + "net/http" + "os" + "os/signal" + "time" + + "github.com/labstack/echo/v5" + "github.com/samber/do/v2" + "ichi-go/cmd/server" "ichi-go/config" _ "ichi-go/docs" // Import generated docs @@ -12,14 +20,6 @@ import ( "ichi-go/internal/middlewares" errors2 "ichi-go/pkg/errors" "ichi-go/pkg/logger" - "net/http" - "os" - "os/signal" - "time" - - "github.com/samber/do/v2" - - "github.com/labstack/echo/v4" ) // @title Ichi-Go API @@ -72,7 +72,7 @@ func main() { errors2.Setup(e) // Log all routes - for _, route := range e.Routes() { + for _, route := range e.Router().Routes() { if route.Method == "" && route.Path == "" { continue } @@ -90,11 +90,20 @@ func main() { go server.StartQueueWorkers(ctx, msgConfig, msgConn, injector) } - // Start the server + // Start the server with context-based graceful shutdown + serverDone := make(chan struct{}) go func() { + defer close(serverDone) address := fmt.Sprintf(":%d", cfg.Http().Port) logger.Infof("starting http server at %s", address) - if err := e.Start(address); err != nil && !errors.Is(err, http.ErrServerClosed) { + sc := echo.StartConfig{ + Address: address, + GracefulTimeout: 10 * time.Second, + OnShutdownError: func(err error) { + logger.Errorf("error during server shutdown: %v", err) + }, + } + if err := sc.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Errorf("http server error: %v", err) } }() @@ -104,12 +113,9 @@ func main() { // Graceful shutdown logger.Infof("Received shutdown signal...") - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if err := e.Shutdown(shutdownCtx); err != nil { - logger.Fatalf("error during server shutdown: %v", err) - } + // Wait for server graceful shutdown to complete + <-serverDone // Shutdown all services in reverse dependency order logger.Infof("shutting down services...") diff --git a/cmd/server/rest_server.go b/cmd/server/rest_server.go index 46ce267..678a435 100644 --- a/cmd/server/rest_server.go +++ b/cmd/server/rest_server.go @@ -2,11 +2,15 @@ package server import ( "encoding/json" + "html/template" + "net/http" "os" + "regexp" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/do/v2" - echoSwagger "github.com/swaggo/echo-swagger" + swaggerFiles "github.com/swaggo/files/v2" + "github.com/swaggo/swag" "ichi-go/config" "ichi-go/internal/applications/auth" @@ -28,8 +32,8 @@ func SetupRestRoutes(injector do.Injector, e *echo.Echo, cfg *config.Config) { // Register application domains user.Register(injector, cfg.App().Name, e, appAuth) auth.Register(injector, cfg.App().Name, e, appAuth) - rbacapp.Register(injector, cfg.App().Name, e, appAuth) // RBAC domain - notificationapp.Register(injector, cfg.App().Name, e, appAuth) // Notification domain + rbacapp.Register(injector, cfg.App().Name, e, appAuth) // RBAC domain + notificationapp.Register(injector, cfg.App().Name, e, appAuth) // Notification domain healthapp.Register(injector, cfg.App().Name, e, cfg) } @@ -38,7 +42,7 @@ func GetServiceName(configApp config.AppConfig) string { } func generateRouteList(e *echo.Echo) { - data, err := json.MarshalIndent(e.Routes(), "", " ") + data, err := json.MarshalIndent(e.Router().Routes(), "", " ") if err != nil { panic(err) } @@ -48,8 +52,71 @@ func generateRouteList(e *echo.Echo) { } } +// swaggerIndexTemplate is the HTML template served at /docs/index.html. +const swaggerIndexTemplate = ` + + + Swagger UI + + + + + +
+ + + + +` + func openOpenAPIDocs(e *echo.Echo, cfg *config.Config) { - // Swagger documentation endpoint - e.GET("/docs/*", echoSwagger.WrapHandler) + indexTmpl := template.Must(template.New("swagger_index.html").Parse(swaggerIndexTemplate)) + re := regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`) + + handler := func(c *echo.Context) error { + if c.Request().Method != http.MethodGet { + return echo.ErrMethodNotAllowed + } + + matches := re.FindStringSubmatch(c.Request().RequestURI) + if len(matches) != 3 { + return echo.ErrNotFound + } + + path := matches[2] + switch path { + case "": + return c.Redirect(http.StatusMovedPermanently, matches[1]+"index.html") + case "index.html": + c.Response().Header().Set("Content-Type", "text/html; charset=utf-8") + return indexTmpl.Execute(c.Response(), nil) + case "doc.json": + doc, err := swag.ReadDoc() + if err != nil { + return echo.ErrNotFound + } + c.Response().Header().Set("Content-Type", "application/json; charset=utf-8") + _, err = c.Response().Write([]byte(doc)) + return err + default: + c.Request().URL.Path = path + http.FileServer(http.FS(swaggerFiles.FS)).ServeHTTP(c.Response(), c.Request()) + return nil + } + } + + e.GET("/docs/*", handler) logger.Infof("Swagger UI available at http://localhost:%d/docs/index.html", cfg.Http().Port) } diff --git a/cmd/server/web_server.go b/cmd/server/web_server.go index fabd1ba..6cf146d 100644 --- a/cmd/server/web_server.go +++ b/cmd/server/web_server.go @@ -5,27 +5,27 @@ import ( "ichi-go/config" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type TemplateRenderer struct { templates *template.Template } -func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}, c echo.Context) error { +func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}, c *echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } func SetupWebRoutes(e *echo.Echo, config *config.Schema) { g := e.Group(config.App.Name) - g.GET("", func(c echo.Context) error { + g.GET("", func(c *echo.Context) error { return c.HTML(http.StatusOK, "Hello, World!") }) //templates := template.Must(template.ParseGlob(filepath.Join("web/templates", "*.html"))) //e.Renderer = &TemplateRenderer{templates} // //e.Static("/static", "web/static") - //e.GET("/", func(c echo.Context) error { + //e.GET("/", func(c *echo.Context) error { // data := map[string]interface{}{ // "title": "Home Page", // "message": "Welcome to the Web Server!", diff --git a/config/config.go b/config/config.go index 97ef141..51ced74 100644 --- a/config/config.go +++ b/config/config.go @@ -13,8 +13,7 @@ import ( "os" "sync" - "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" + "github.com/labstack/echo/v5" "github.com/spf13/viper" pkgClientConfig "ichi-go/pkg/clients" @@ -212,12 +211,6 @@ func setDefault() { rbac.SetDefault() } -func SetDebugMode(e *echo.Echo, debug bool) { - e.Debug = debug - if debug { - log.SetLevel(log.DEBUG) - } else { - log.SetLevel(log.INFO) - } - log.Debugf("Debug mode set to %v", debug) +func SetDebugMode(_ *echo.Echo, debug bool) { + logger.Debugf("Debug mode set to %v", debug) } diff --git a/go.mod b/go.mod index ec8d241..df347f5 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module ichi-go -go 1.25 - -toolchain go1.25.0 +go 1.25.0 require ( firebase.google.com/go/v4 v4.19.0 @@ -15,8 +13,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/labstack/echo/v4 v4.15.0 - github.com/labstack/gommon v0.4.2 + github.com/labstack/echo/v5 v5.1.0 github.com/pierrec/lz4/v4 v4.1.23 github.com/pressly/goose/v3 v3.26.0 github.com/rabbitmq/amqp091-go v1.10.0 @@ -27,12 +24,12 @@ require ( github.com/samber/oops/loggers/zerolog v0.0.0-20260101195714-ee59589fe6d3 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/swaggo/echo-swagger v1.4.1 + github.com/swaggo/files/v2 v2.0.2 github.com/swaggo/swag v1.16.6 github.com/uptrace/bun v1.2.16 github.com/uptrace/bun/dialect/mysqldialect v1.2.16 github.com/vmihailenco/msgpack/v5 v5.4.1 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.47.0 google.golang.org/api v0.231.0 resty.dev/v3 v3.0.0-beta.5 ) @@ -65,7 +62,6 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -106,10 +102,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/files/v2 v2.0.2 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -124,11 +117,11 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect @@ -137,6 +130,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/grpc v1.72.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 70fa6b0..a79bc31 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -155,10 +153,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.15.0 h1:hoRTKWcnR5STXZFe9BmYun9AMTNeSbjHi2vtDuADJ24= -github.com/labstack/echo/v4 v4.15.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/echo/v5 v5.1.0 h1:MvIRydoN+p9cx/zq8Lff6YXqUW2ZaEsOMISzEGSMrBI= +github.com/labstack/echo/v5 v5.1.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -231,8 +227,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= -github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= @@ -243,10 +237,6 @@ github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc= github.com/uptrace/bun v1.2.16/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM= github.com/uptrace/bun/dialect/mysqldialect v1.2.16 h1:ok06dAS094cEKvKg38SVAnXMroNHNaM5ZtpRkPE/Oz0= github.com/uptrace/bun/dialect/mysqldialect v1.2.16/go.mod h1:fjbFYeJZCK8z0m0ACvdgs+dbFdDIaLYWDr+jvaPLedQ= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -282,8 +272,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -292,8 +282,8 @@ golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -316,8 +306,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -346,8 +336,6 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= diff --git a/internal/applications/auth/controller/auth_controller.go b/internal/applications/auth/controller/auth_controller.go index 5b262b8..49b5fbd 100644 --- a/internal/applications/auth/controller/auth_controller.go +++ b/internal/applications/auth/controller/auth_controller.go @@ -10,7 +10,7 @@ import ( appValidator "ichi-go/pkg/validator" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type AuthController struct { @@ -35,7 +35,7 @@ func NewAuthController(service *authService.ServiceImpl) *AuthController { // @Failure 401 {object} response.ErrorResponse "Invalid credentials" // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /202601/auth/login [post] -func (c *AuthController) Login(eCtx echo.Context) error { +func (c *AuthController) Login(eCtx *echo.Context) error { var req authDto.LoginRequest // Bind and validate with automatic language detection @@ -67,7 +67,7 @@ func (c *AuthController) Login(eCtx echo.Context) error { // @Failure 400 {object} response.ErrorResponse "Invalid request, validation error, or user already exists" // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /auth/register [post] -func (c *AuthController) Register(eCtx echo.Context) error { +func (c *AuthController) Register(eCtx *echo.Context) error { var req authDto.RegisterRequest err := appValidator.BindAndValidate(eCtx, &req) @@ -98,7 +98,7 @@ func (c *AuthController) Register(eCtx echo.Context) error { // @Failure 401 {object} response.ErrorResponse "Invalid or expired refresh token" // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /auth/refresh [post] -func (c *AuthController) RefreshToken(eCtx echo.Context) error { +func (c *AuthController) RefreshToken(eCtx *echo.Context) error { var req authDto.RefreshTokenRequest // Bind and validate with automatic language detection @@ -129,7 +129,7 @@ func (c *AuthController) RefreshToken(eCtx echo.Context) error { // @Success 200 {object} response.SuccessResponse{data=map[string]string} "Logged out successfully" // @Failure 401 {object} response.ErrorResponse "Unauthorized - invalid or missing token" // @Router /auth/logout [post] -func (c *AuthController) Logout(eCtx echo.Context) error { +func (c *AuthController) Logout(eCtx *echo.Context) error { logger.Infof("User logged out") return response.Success(eCtx, map[string]string{ "message": "Logged out successfully", @@ -148,7 +148,7 @@ func (c *AuthController) Logout(eCtx echo.Context) error { // @Failure 401 {object} response.ErrorResponse "Unauthorized - invalid or missing token" // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /auth/me [get] -func (c *AuthController) Me(eCtx echo.Context) error { +func (c *AuthController) Me(eCtx *echo.Context) error { fmt.Println("AuthController.Me called") // Get auth context from middleware authCtx, ok := eCtx.Get("auth").(*authenticator.AuthContext) diff --git a/internal/applications/auth/controller/auth_controller_test.go b/internal/applications/auth/controller/auth_controller_test.go index 8a749f5..e6895d7 100644 --- a/internal/applications/auth/controller/auth_controller_test.go +++ b/internal/applications/auth/controller/auth_controller_test.go @@ -13,7 +13,7 @@ import ( "ichi-go/pkg/testutil" "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/internal/applications/auth/controller/auth_routes.go b/internal/applications/auth/controller/auth_routes.go index 55bc195..a8f4b77 100644 --- a/internal/applications/auth/controller/auth_routes.go +++ b/internal/applications/auth/controller/auth_routes.go @@ -4,7 +4,7 @@ import ( "ichi-go/pkg/authenticator" "ichi-go/pkg/versioning" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) var Domain = "auth" diff --git a/internal/applications/auth/registry.go b/internal/applications/auth/registry.go index 21aa21b..5b1173d 100644 --- a/internal/applications/auth/registry.go +++ b/internal/applications/auth/registry.go @@ -5,7 +5,7 @@ import ( authController "ichi-go/internal/applications/auth/controller" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/do/v2" ) diff --git a/internal/applications/auth/service/auth_service_test.go b/internal/applications/auth/service/auth_service_test.go index 9d42350..c816bf9 100644 --- a/internal/applications/auth/service/auth_service_test.go +++ b/internal/applications/auth/service/auth_service_test.go @@ -14,7 +14,7 @@ import ( dbModel "ichi-go/pkg/db/model" pkgErrors "ichi-go/pkg/errors" - _ "github.com/labstack/echo/v4" + _ "github.com/labstack/echo/v5" "github.com/samber/oops" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/applications/health/controller/health_controller.go b/internal/applications/health/controller/health_controller.go index 216e017..38de90b 100644 --- a/internal/applications/health/controller/health_controller.go +++ b/internal/applications/health/controller/health_controller.go @@ -6,7 +6,7 @@ import ( "ichi-go/internal/applications/health/service" "ichi-go/pkg/health" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) var Domain = "health" @@ -36,7 +36,7 @@ func (c *HealthController) RegisterRoutes(e *echo.Echo, serviceName string) { // @Produce json // @Success 200 {object} health.HealthResponse // @Router /health [get] -func (c *HealthController) GetHealth(ctx echo.Context) error { +func (c *HealthController) GetHealth(ctx *echo.Context) error { response := c.service.GetHealth(ctx.Request().Context()) return ctx.JSON(http.StatusOK, response) } @@ -49,7 +49,7 @@ func (c *HealthController) GetHealth(ctx echo.Context) error { // @Produce json // @Success 200 {object} health.HealthResponse // @Router /health/live [get] -func (c *HealthController) GetLiveness(ctx echo.Context) error { +func (c *HealthController) GetLiveness(ctx *echo.Context) error { response := c.service.GetLiveness(ctx.Request().Context()) return ctx.JSON(http.StatusOK, response) } @@ -63,7 +63,7 @@ func (c *HealthController) GetLiveness(ctx echo.Context) error { // @Success 200 {object} health.HealthResponse "All dependencies healthy" // @Failure 503 {object} health.HealthResponse "One or more dependencies unhealthy" // @Router /health/ready [get] -func (c *HealthController) GetReadiness(ctx echo.Context) error { +func (c *HealthController) GetReadiness(ctx *echo.Context) error { response := c.service.GetReadiness(ctx.Request().Context()) statusCode := http.StatusOK diff --git a/internal/applications/health/registry.go b/internal/applications/health/registry.go index 819a961..030a032 100644 --- a/internal/applications/health/registry.go +++ b/internal/applications/health/registry.go @@ -1,7 +1,7 @@ package health import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/redis/go-redis/v9" "github.com/samber/do/v2" "github.com/uptrace/bun" diff --git a/internal/applications/notification/controller/notification_controller.go b/internal/applications/notification/controller/notification_controller.go index c30e8dc..7b244ec 100644 --- a/internal/applications/notification/controller/notification_controller.go +++ b/internal/applications/notification/controller/notification_controller.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "ichi-go/internal/applications/notification/dto" "ichi-go/internal/applications/notification/services" @@ -28,7 +28,7 @@ func NewNotificationController(campaignService *services.CampaignService) *Notif // Returns 201 Created on success with campaign_id, status, and published_at. // Returns 422 Unprocessable Entity when event_slug is not registered or channels are unsupported. // Returns 400 Bad Request for validation failures (missing fields, bad delay, etc.). -func (c *NotificationController) Send(eCtx echo.Context) error { +func (c *NotificationController) Send(eCtx *echo.Context) error { var req dto.SendNotificationRequest if err := eCtx.Bind(&req); err != nil { return response.Error(eCtx, http.StatusBadRequest, err) diff --git a/internal/applications/notification/controller/notification_routes.go b/internal/applications/notification/controller/notification_routes.go index 3d1ec36..0e26fe6 100644 --- a/internal/applications/notification/controller/notification_routes.go +++ b/internal/applications/notification/controller/notification_routes.go @@ -1,7 +1,7 @@ package controller import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "ichi-go/pkg/authenticator" ) diff --git a/internal/applications/notification/registry.go b/internal/applications/notification/registry.go index 6b121d3..8c8af42 100644 --- a/internal/applications/notification/registry.go +++ b/internal/applications/notification/registry.go @@ -1,7 +1,7 @@ package notification import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/do/v2" notifController "ichi-go/internal/applications/notification/controller" diff --git a/internal/applications/rbac/controllers/audit_controller.go b/internal/applications/rbac/controllers/audit_controller.go index 516704a..2efa272 100644 --- a/internal/applications/rbac/controllers/audit_controller.go +++ b/internal/applications/rbac/controllers/audit_controller.go @@ -13,7 +13,7 @@ import ( "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // AuditController handles audit log endpoints @@ -51,7 +51,7 @@ func NewAuditController(auditService *services.AuditService) *AuditController { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/audit/logs [get] -func (c *AuditController) QueryAuditLogs(ctx echo.Context) error { +func (c *AuditController) QueryAuditLogs(ctx *echo.Context) error { var req dto.AuditQueryRequest if err := ctx.Bind(&req); err != nil { @@ -139,7 +139,7 @@ func (c *AuditController) QueryAuditLogs(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/audit/stats [get] -func (c *AuditController) GetAuditStats(ctx echo.Context) error { +func (c *AuditController) GetAuditStats(ctx *echo.Context) error { var req dto.AuditStatsRequest if err := ctx.Bind(&req); err != nil { @@ -196,7 +196,7 @@ func (c *AuditController) GetAuditStats(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/audit/export [post] -func (c *AuditController) ExportAuditLogs(ctx echo.Context) error { +func (c *AuditController) ExportAuditLogs(ctx *echo.Context) error { var req dto.ExportAuditLogsRequest if err := ctx.Bind(&req); err != nil { @@ -277,7 +277,7 @@ func (c *AuditController) ExportAuditLogs(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/audit/mutations [get] -func (c *AuditController) GetRecentMutations(ctx echo.Context) error { +func (c *AuditController) GetRecentMutations(ctx *echo.Context) error { tenantID := ctx.QueryParam("tenant_id") limit := 50 // Default limit @@ -330,7 +330,7 @@ func (c *AuditController) GetRecentMutations(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/audit/decisions [get] -func (c *AuditController) GetRecentDecisions(ctx echo.Context) error { +func (c *AuditController) GetRecentDecisions(ctx *echo.Context) error { tenantID := ctx.QueryParam("tenant_id") limit := 50 // Default limit diff --git a/internal/applications/rbac/controllers/enforcement_controller.go b/internal/applications/rbac/controllers/enforcement_controller.go index dc1a2ec..55579ee 100644 --- a/internal/applications/rbac/controllers/enforcement_controller.go +++ b/internal/applications/rbac/controllers/enforcement_controller.go @@ -8,7 +8,7 @@ import ( "ichi-go/pkg/requestctx" "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // EnforcementController handles permission check endpoints @@ -36,7 +36,7 @@ func NewEnforcementController(enforcementService *services.EnforcementService) * // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/enforce/check [post] -func (c *EnforcementController) CheckPermission(ctx echo.Context) error { +func (c *EnforcementController) CheckPermission(ctx *echo.Context) error { var req dto.CheckPermissionRequest if err := ctx.Bind(&req); err != nil { @@ -85,7 +85,7 @@ func (c *EnforcementController) CheckPermission(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/enforce/batch [post] -func (c *EnforcementController) CheckBatchPermissions(ctx echo.Context) error { +func (c *EnforcementController) CheckBatchPermissions(ctx *echo.Context) error { var req dto.BatchCheckPermissionRequest if err := ctx.Bind(&req); err != nil { @@ -137,7 +137,7 @@ func (c *EnforcementController) CheckBatchPermissions(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/enforce/my-permissions [get] -func (c *EnforcementController) GetMyPermissions(ctx echo.Context) error { +func (c *EnforcementController) GetMyPermissions(ctx *echo.Context) error { // Get user ID from request context userID := requestctx.GetUserIDAsInt64(ctx.Request().Context()) if userID == 0 { @@ -174,7 +174,7 @@ func (c *EnforcementController) GetMyPermissions(ctx echo.Context) error { // RequirePermission is a helper method for middleware/handlers // It checks permission and returns error if denied -func (c *EnforcementController) RequirePermission(ctx echo.Context, resource, action string) error { +func (c *EnforcementController) RequirePermission(ctx *echo.Context, resource, action string) error { userID := requestctx.GetUserIDAsInt64(ctx.Request().Context()) if userID == 0 { return echo.NewHTTPError(http.StatusUnauthorized, "User not authenticated") diff --git a/internal/applications/rbac/controllers/policy_controller.go b/internal/applications/rbac/controllers/policy_controller.go index 3b42c4a..346b0d4 100644 --- a/internal/applications/rbac/controllers/policy_controller.go +++ b/internal/applications/rbac/controllers/policy_controller.go @@ -9,7 +9,7 @@ import ( "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // PolicyController handles policy management endpoints @@ -40,7 +40,7 @@ func NewPolicyController(policyService *services.PolicyService) *PolicyControlle // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/policies [get] -func (c *PolicyController) GetPolicies(ctx echo.Context) error { +func (c *PolicyController) GetPolicies(ctx *echo.Context) error { var req dto.GetPoliciesRequest if err := ctx.Bind(&req); err != nil { @@ -127,7 +127,7 @@ func (c *PolicyController) GetPolicies(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/policies [post] -func (c *PolicyController) AddPolicy(ctx echo.Context) error { +func (c *PolicyController) AddPolicy(ctx *echo.Context) error { var req dto.AddPolicyRequest if err := ctx.Bind(&req); err != nil { @@ -175,7 +175,7 @@ func (c *PolicyController) AddPolicy(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/policies [delete] -func (c *PolicyController) RemovePolicy(ctx echo.Context) error { +func (c *PolicyController) RemovePolicy(ctx *echo.Context) error { var req dto.RemovePolicyRequest if err := ctx.Bind(&req); err != nil { @@ -221,7 +221,7 @@ func (c *PolicyController) RemovePolicy(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/policies/count [get] -func (c *PolicyController) GetPolicyCount(ctx echo.Context) error { +func (c *PolicyController) GetPolicyCount(ctx *echo.Context) error { tenantID := ctx.QueryParam("tenant_id") var count int @@ -262,7 +262,7 @@ func (c *PolicyController) GetPolicyCount(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/policies/reload [post] -func (c *PolicyController) ReloadPolicies(ctx echo.Context) error { +func (c *PolicyController) ReloadPolicies(ctx *echo.Context) error { // Get actor ID from context actorID := requestctx.GetUserIDAsInt64(ctx.Request().Context()) if actorID == 0 { diff --git a/internal/applications/rbac/controllers/role_controller.go b/internal/applications/rbac/controllers/role_controller.go index a1a936b..0d16340 100644 --- a/internal/applications/rbac/controllers/role_controller.go +++ b/internal/applications/rbac/controllers/role_controller.go @@ -10,7 +10,7 @@ import ( "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // RoleController handles role management endpoints @@ -41,7 +41,7 @@ func NewRoleController(roleService *services.RoleService) *RoleController { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles [get] -func (c *RoleController) GetRoles(ctx echo.Context) error { +func (c *RoleController) GetRoles(ctx *echo.Context) error { var req dto.GetRolesRequest if err := ctx.Bind(&req); err != nil { @@ -116,7 +116,7 @@ func (c *RoleController) GetRoles(ctx echo.Context) error { // @Failure 404 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles/{id} [get] -func (c *RoleController) GetRole(ctx echo.Context) error { +func (c *RoleController) GetRole(ctx *echo.Context) error { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid role ID")) @@ -143,7 +143,7 @@ func (c *RoleController) GetRole(ctx echo.Context) error { // @Failure 404 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles/{id}/permissions [get] -func (c *RoleController) GetRoleWithPermissions(ctx echo.Context) error { +func (c *RoleController) GetRoleWithPermissions(ctx *echo.Context) error { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid role ID")) @@ -176,7 +176,7 @@ func (c *RoleController) GetRoleWithPermissions(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles [post] -func (c *RoleController) CreateRole(ctx echo.Context) error { +func (c *RoleController) CreateRole(ctx *echo.Context) error { var req dto.CreateRoleRequest if err := ctx.Bind(&req); err != nil { @@ -218,7 +218,7 @@ func (c *RoleController) CreateRole(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles/{id} [put] -func (c *RoleController) UpdateRole(ctx echo.Context) error { +func (c *RoleController) UpdateRole(ctx *echo.Context) error { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid role ID")) @@ -269,7 +269,7 @@ func (c *RoleController) UpdateRole(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles/{id} [delete] -func (c *RoleController) DeleteRole(ctx echo.Context) error { +func (c *RoleController) DeleteRole(ctx *echo.Context) error { id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid role ID")) diff --git a/internal/applications/rbac/controllers/user_role_controller.go b/internal/applications/rbac/controllers/user_role_controller.go index 9f525f5..a894875 100644 --- a/internal/applications/rbac/controllers/user_role_controller.go +++ b/internal/applications/rbac/controllers/user_role_controller.go @@ -12,7 +12,7 @@ import ( "ichi-go/pkg/utils/response" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // UserRoleController handles user role assignment endpoints @@ -41,7 +41,7 @@ func NewUserRoleController(userRoleService *services.UserRoleService) *UserRoleC // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/users/{userId}/roles [get] -func (c *UserRoleController) GetUserRoles(ctx echo.Context) error { +func (c *UserRoleController) GetUserRoles(ctx *echo.Context) error { userID, err := strconv.ParseInt(ctx.Param("userId"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")) @@ -90,7 +90,7 @@ func (c *UserRoleController) GetUserRoles(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/users/{userId}/roles/active [get] -func (c *UserRoleController) GetActiveUserRoles(ctx echo.Context) error { +func (c *UserRoleController) GetActiveUserRoles(ctx *echo.Context) error { userID, err := strconv.ParseInt(ctx.Param("userId"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")) @@ -140,7 +140,7 @@ func (c *UserRoleController) GetActiveUserRoles(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/users/{userId}/roles [post] -func (c *UserRoleController) AssignRole(ctx echo.Context) error { +func (c *UserRoleController) AssignRole(ctx *echo.Context) error { userID, err := strconv.ParseInt(ctx.Param("userId"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")) @@ -197,7 +197,7 @@ func (c *UserRoleController) AssignRole(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/users/{userId}/roles/{roleSlug} [delete] -func (c *UserRoleController) RevokeRole(ctx echo.Context) error { +func (c *UserRoleController) RevokeRole(ctx *echo.Context) error { userID, err := strconv.ParseInt(ctx.Param("userId"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")) @@ -263,7 +263,7 @@ func (c *UserRoleController) RevokeRole(ctx echo.Context) error { // @Failure 500 {object} response.ErrorResponse // @Security BearerAuth // @Router /v1/rbac/roles/{roleId}/users [get] -func (c *UserRoleController) GetUsersWithRole(ctx echo.Context) error { +func (c *UserRoleController) GetUsersWithRole(ctx *echo.Context) error { roleID, err := strconv.ParseInt(ctx.Param("roleId"), 10, 64) if err != nil { return response.Error(ctx, http.StatusBadRequest, echo.NewHTTPError(http.StatusBadRequest, "Invalid role ID")) diff --git a/internal/applications/rbac/registry.go b/internal/applications/rbac/registry.go index c6114a3..b6c2aca 100644 --- a/internal/applications/rbac/registry.go +++ b/internal/applications/rbac/registry.go @@ -4,7 +4,7 @@ import ( "ichi-go/internal/applications/rbac/controllers" "ichi-go/pkg/authenticator" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/do/v2" ) diff --git a/internal/applications/user/controller/user_controller.go b/internal/applications/user/controller/user_controller.go index c6474f1..1851128 100644 --- a/internal/applications/user/controller/user_controller.go +++ b/internal/applications/user/controller/user_controller.go @@ -13,7 +13,7 @@ import ( "strconv" dtoMapper "github.com/dranikpg/dto-mapper" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type UserController struct { @@ -24,7 +24,7 @@ func NewUserController(service *userService.ServiceImpl) *UserController { return &UserController{service: service} } -func (c *UserController) GetUser(eCtx echo.Context) error { +func (c *UserController) GetUser(eCtx *echo.Context) error { var userGetReq userDto.UserGetRequest logger.Debugf("GetUser request: %+v", requestctx.FromContext(eCtx.Request().Context())) err := eCtx.Bind(&userGetReq) @@ -47,7 +47,7 @@ func (c *UserController) GetUser(eCtx echo.Context) error { return response.Success(eCtx, user) } -func (c *UserController) CreateUser(eCtx echo.Context) error { +func (c *UserController) CreateUser(eCtx *echo.Context) error { var userCreateReq userDto.UserCreateRequest err := eCtx.Bind(&userCreateReq) if err != nil { @@ -67,7 +67,7 @@ func (c *UserController) CreateUser(eCtx echo.Context) error { return response.Created(eCtx, map[string]interface{}{"new_user_id": user}) } -func (c *UserController) UpdateUser(eCtx echo.Context) error { +func (c *UserController) UpdateUser(eCtx *echo.Context) error { var userUpdateReq userDto.UserUpdateRequest err := eCtx.Bind(&userUpdateReq) if err != nil { @@ -87,11 +87,11 @@ func (c *UserController) UpdateUser(eCtx echo.Context) error { return response.Success(eCtx, user) } -func (c *UserController) GetUserPage(eCtx echo.Context) error { +func (c *UserController) GetUserPage(eCtx *echo.Context) error { return eCtx.HTML(http.StatusOK, "

This is User Page

") } -func (c *UserController) GetPokemon(eCtx echo.Context) error { +func (c *UserController) GetPokemon(eCtx *echo.Context) error { name := eCtx.Param("name") var pokemonGetResponseDto pokeDto.PokemonGetResponseDto result, err := c.service.GetPokemon(eCtx.Request().Context(), name) diff --git a/internal/applications/user/controller/user_routes.go b/internal/applications/user/controller/user_routes.go index 4b46155..335eb7e 100644 --- a/internal/applications/user/controller/user_routes.go +++ b/internal/applications/user/controller/user_routes.go @@ -4,7 +4,7 @@ import ( "ichi-go/internal/middlewares" "ichi-go/pkg/authenticator" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) var Domain = "users" diff --git a/internal/applications/user/registry.go b/internal/applications/user/registry.go index ee952ee..7729b64 100644 --- a/internal/applications/user/registry.go +++ b/internal/applications/user/registry.go @@ -1,7 +1,7 @@ package user import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/do/v2" user "ichi-go/internal/applications/user/controller" diff --git a/internal/middlewares/cors_middleware.go b/internal/middlewares/cors_middleware.go index 9c1d736..d043a2d 100644 --- a/internal/middlewares/cors_middleware.go +++ b/internal/middlewares/cors_middleware.go @@ -4,8 +4,8 @@ import ( httpConfig "ichi-go/pkg/http" "net/http" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func GetCorsConfig(configCors *httpConfig.CorsConfig) middleware.CORSConfig { diff --git a/internal/middlewares/log_middleware.go b/internal/middlewares/log_middleware.go index 690383f..335752f 100644 --- a/internal/middlewares/log_middleware.go +++ b/internal/middlewares/log_middleware.go @@ -4,8 +4,8 @@ import ( "ichi-go/config" "ichi-go/pkg/logger" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func Logger(config *config.Config) echo.MiddlewareFunc { @@ -13,7 +13,7 @@ func Logger(config *config.Config) echo.MiddlewareFunc { LogRequestID: true, LogURI: true, LogStatus: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { logger.RequestLogging(c, "%s %s %d", v.Method, v.URI, v.Status) return nil }, diff --git a/internal/middlewares/rbac_audit_middleware.go b/internal/middlewares/rbac_audit_middleware.go index 80931c8..b2a47bf 100644 --- a/internal/middlewares/rbac_audit_middleware.go +++ b/internal/middlewares/rbac_audit_middleware.go @@ -12,7 +12,7 @@ import ( "ichi-go/pkg/logger" "ichi-go/pkg/requestctx" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // RBACAuditMiddleware logs RBAC-related operations to the audit log @@ -20,7 +20,7 @@ import ( // on RBAC resources and logs them for compliance and security auditing func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { ctx := c.Request().Context() rc := requestctx.FromContext(ctx) @@ -43,12 +43,8 @@ func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditCo requestBody = captureRequestBody(c) } - // Create response writer wrapper to capture status code - resWrapper := &responseWrapper{ - ResponseWriter: c.Response().Writer, - statusCode: http.StatusOK, - } - c.Response().Writer = resWrapper + // Capture echo Response to read status code after the request + echoResp, _ := echo.UnwrapResponse(c.Response()) // Execute request err := next(c) @@ -81,7 +77,13 @@ func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditCo } // Add response status - statusCode := resWrapper.statusCode + statusCode := http.StatusOK + if echoResp != nil { + statusCode = echoResp.Status + if statusCode == 0 { + statusCode = http.StatusOK + } + } if err != nil { if he, ok := err.(*echo.HTTPError); ok { statusCode = he.Code @@ -147,32 +149,13 @@ func DefaultAuditConfig() AuditConfig { } } -// responseWrapper wraps http.ResponseWriter to capture status code -type responseWrapper struct { - http.ResponseWriter - statusCode int - body *bytes.Buffer -} - -func (w *responseWrapper) WriteHeader(statusCode int) { - w.statusCode = statusCode - w.ResponseWriter.WriteHeader(statusCode) -} - -func (w *responseWrapper) Write(b []byte) (int, error) { - if w.body != nil { - w.body.Write(b) - } - return w.ResponseWriter.Write(b) -} - // shouldCaptureBody determines if request body should be captured func shouldCaptureBody(method string) bool { return method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch } // captureRequestBody reads and captures the request body -func captureRequestBody(c echo.Context) map[string]interface{} { +func captureRequestBody(c *echo.Context) map[string]interface{} { if c.Request().Body == nil { return nil } @@ -199,7 +182,7 @@ func captureRequestBody(c echo.Context) map[string]interface{} { } // shouldAudit determines if a request should be audited -func shouldAudit(c echo.Context, config AuditConfig) bool { +func shouldAudit(c *echo.Context, config AuditConfig) bool { path := c.Path() // Check included paths first (if specified) @@ -291,7 +274,7 @@ func extractResourceType(path string) string { } // extractResourceID extracts the resource ID from the URL path or params -func extractResourceID(c echo.Context) string { +func extractResourceID(c *echo.Context) string { // Try common ID parameter names params := []string{"id", "roleId", "userId", "permissionId"} diff --git a/internal/middlewares/rbac_enforcement_middleware.go b/internal/middlewares/rbac_enforcement_middleware.go index a686ed8..2a3c970 100644 --- a/internal/middlewares/rbac_enforcement_middleware.go +++ b/internal/middlewares/rbac_enforcement_middleware.go @@ -7,7 +7,7 @@ import ( "ichi-go/pkg/logger" "ichi-go/pkg/requestctx" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // RBACEnforcementMiddleware enforces RBAC permissions on routes @@ -15,7 +15,7 @@ import ( // to access the requested resource/action combination func RBACEnforcementMiddleware(enforcementService *services.EnforcementService, config RBACConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // Skip enforcement for excluded paths if isExcludedPath(c.Path(), config.ExcludedPaths) { return next(c) @@ -100,7 +100,7 @@ type RBACConfig struct { ExcludedPaths []string // ResourceMapper is a custom function to map routes to resources - ResourceMapper func(c echo.Context) (resource string, action string) + ResourceMapper func(c *echo.Context) (resource string, action string) // DefaultResource is used when ResourceMapper returns empty string DefaultResource string @@ -126,7 +126,7 @@ func DefaultRBACConfig() RBACConfig { // This is useful for protecting individual routes with explicit permissions func RequirePermission(enforcementService *services.EnforcementService, resource, action string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { ctx := c.Request().Context() // Get user ID @@ -162,7 +162,7 @@ func RequirePermission(enforcementService *services.EnforcementService, resource } // resolveResourceAction determines the resource and action from the request -func resolveResourceAction(c echo.Context, config RBACConfig) (resource, action string) { +func resolveResourceAction(c *echo.Context, config RBACConfig) (resource, action string) { // Use custom mapper if provided if config.ResourceMapper != nil { resource, action = config.ResourceMapper(c) @@ -181,7 +181,7 @@ func resolveResourceAction(c echo.Context, config RBACConfig) (resource, action // defaultResourceMapper is the default resource/action mapper // Maps HTTP methods to RBAC actions and extracts resource from path -func defaultResourceMapper(c echo.Context) (resource, action string) { +func defaultResourceMapper(c *echo.Context) (resource, action string) { // Map HTTP method to RBAC action switch c.Request().Method { case http.MethodGet: diff --git a/internal/middlewares/registry.go b/internal/middlewares/registry.go index beb10e1..cefe9eb 100644 --- a/internal/middlewares/registry.go +++ b/internal/middlewares/registry.go @@ -1,12 +1,12 @@ package middlewares import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/log" + "strings" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "ichi-go/config" "ichi-go/pkg/logger" - "strings" ) func Init(e *echo.Echo, mainConfig *config.Config) { @@ -28,17 +28,11 @@ func Init(e *echo.Echo, mainConfig *config.Config) { } e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ - LogLevel: log.ERROR, - DisablePrintStack: !e.Debug, - LogErrorFunc: func(c echo.Context, err error, stack []byte) error { - logger.Errorf("PANIC RECOVER: %v, stack trace: %s", err, stack) - return nil - }, - DisableErrorHandler: true, + DisablePrintStack: !mainConfig.App().Debug, })) e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Skipper: func(c echo.Context) bool { + Skipper: func(c *echo.Context) bool { if strings.Contains(c.Request().URL.Path, "swagger") { return true } diff --git a/internal/middlewares/req_context_middleware.go b/internal/middlewares/req_context_middleware.go index 931c167..088ec43 100644 --- a/internal/middlewares/req_context_middleware.go +++ b/internal/middlewares/req_context_middleware.go @@ -3,7 +3,7 @@ package middlewares import ( "ichi-go/pkg/requestctx" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type ContextKey string @@ -17,7 +17,7 @@ const ( func RequestContextMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { rc := requestctx.FromRequest(c.Request()) c.SetRequest(c.Request().WithContext(requestctx.NewContext(c.Request().Context(), rc))) return next(c) diff --git a/internal/middlewares/req_injector_middleware.go b/internal/middlewares/req_injector_middleware.go index 0c68eb3..9812ea3 100644 --- a/internal/middlewares/req_injector_middleware.go +++ b/internal/middlewares/req_injector_middleware.go @@ -1,7 +1,7 @@ package middlewares import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type RequestFields struct { @@ -10,7 +10,7 @@ type RequestFields struct { func RequestInjector(fields RequestFields) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { c.Request().Header.Set("Domain", fields.Domain) return next(c) } diff --git a/internal/middlewares/requestid_middleware.go b/internal/middlewares/requestid_middleware.go index ed9dc65..382f052 100644 --- a/internal/middlewares/requestid_middleware.go +++ b/internal/middlewares/requestid_middleware.go @@ -2,10 +2,12 @@ package middlewares import ( "context" + "fmt" + "math/rand" + "github.com/google/uuid" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/random" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" ) func AppRequestID() echo.MiddlewareFunc { @@ -18,13 +20,13 @@ func AppRequestID() echo.MiddlewareFunc { func RequestIDGenerator() string { requestID, err := uuid.NewRandom() if err != nil { - return random.String(32) + return fmt.Sprintf("%016x%016x", rand.Int63(), rand.Int63()) } return requestID.String() } func copyRequestID(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { requestID := c.Request().Header.Get(echo.HeaderXRequestID) if requestID == "" { requestID = c.Response().Header().Get(echo.HeaderXRequestID) diff --git a/internal/middlewares/tenant_context_middleware.go b/internal/middlewares/tenant_context_middleware.go index a45bff1..aa97562 100644 --- a/internal/middlewares/tenant_context_middleware.go +++ b/internal/middlewares/tenant_context_middleware.go @@ -7,7 +7,7 @@ import ( "ichi-go/pkg/logger" "ichi-go/pkg/requestctx" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // TenantContextMiddleware extracts and validates tenant context from requests @@ -18,7 +18,7 @@ import ( // 4. Query parameter (?tenant_id=...) func TenantContextMiddleware(config TenantConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // Get request context rc := requestctx.FromContext(c.Request().Context()) if rc == nil { @@ -96,7 +96,7 @@ func DefaultTenantConfig() TenantConfig { } // resolveTenantID resolves tenant ID based on configured strategy -func resolveTenantID(c echo.Context, config TenantConfig) string { +func resolveTenantID(c *echo.Context, config TenantConfig) string { var tenantID string switch config.Strategy { @@ -141,7 +141,7 @@ func resolveTenantID(c echo.Context, config TenantConfig) string { } // resolveTenantFromHeader extracts tenant from HTTP header -func resolveTenantFromHeader(c echo.Context, config TenantConfig) string { +func resolveTenantFromHeader(c *echo.Context, config TenantConfig) string { headerName := config.HeaderName if headerName == "" { headerName = "X-Tenant-Id" @@ -151,7 +151,7 @@ func resolveTenantFromHeader(c echo.Context, config TenantConfig) string { // resolveTenantFromSubdomain extracts tenant from subdomain // Example: tenant1.example.com -> tenant1 -func resolveTenantFromSubdomain(c echo.Context, config TenantConfig) string { +func resolveTenantFromSubdomain(c *echo.Context, config TenantConfig) string { host := c.Request().Host // Remove port if present @@ -177,7 +177,7 @@ func resolveTenantFromSubdomain(c echo.Context, config TenantConfig) string { // resolveTenantFromPath extracts tenant from URL path // Example: /tenants/acme/users -> acme -func resolveTenantFromPath(c echo.Context, config TenantConfig) string { +func resolveTenantFromPath(c *echo.Context, config TenantConfig) string { path := c.Request().URL.Path if !strings.HasPrefix(path, config.PathPrefix) { @@ -197,7 +197,7 @@ func resolveTenantFromPath(c echo.Context, config TenantConfig) string { } // resolveTenantFromQuery extracts tenant from query parameter -func resolveTenantFromQuery(c echo.Context, config TenantConfig) string { +func resolveTenantFromQuery(c *echo.Context, config TenantConfig) string { paramName := config.QueryParam if paramName == "" { paramName = "tenant_id" diff --git a/internal/middlewares/timeout_middleware.go b/internal/middlewares/timeout_middleware.go index 8e2c877..a858693 100644 --- a/internal/middlewares/timeout_middleware.go +++ b/internal/middlewares/timeout_middleware.go @@ -1,8 +1,8 @@ package middlewares import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "ichi-go/pkg/http" "time" ) diff --git a/internal/middlewares/validator_middleware.go b/internal/middlewares/validator_middleware.go index a7661a7..573d4ce 100644 --- a/internal/middlewares/validator_middleware.go +++ b/internal/middlewares/validator_middleware.go @@ -6,7 +6,7 @@ import ( "ichi-go/pkg/logger" appValidator "ichi-go/pkg/validator" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // ValidatorMiddleware provides Echo with our custom validator @@ -22,7 +22,7 @@ func NewValidatorMiddleware(validator *appValidator.AppValidator) *ValidatorMidd } // Validate implements echo.Validator interface -func (vm *ValidatorMiddleware) Validate(i interface{}) error { +func (vm *ValidatorMiddleware) Validate(i any) error { // Note: Echo passes the bound struct, not the context // We'll use default language here, language detection happens in controller validationErr := vm.validator.ValidateStruct(i) @@ -42,7 +42,7 @@ func (vm *ValidatorMiddleware) ValidateWithLanguage(i interface{}, lang string) } // ValidateWithContext validates using language from Echo context -func (vm *ValidatorMiddleware) ValidateWithContext(c echo.Context, i interface{}) error { +func (vm *ValidatorMiddleware) ValidateWithContext(c *echo.Context, i interface{}) error { lang := appValidator.GetLanguageFromContext(c) return vm.ValidateWithLanguage(i, lang) } diff --git a/internal/middlewares/version_middleware.go b/internal/middlewares/version_middleware.go index 4450ffd..771cdaf 100644 --- a/internal/middlewares/version_middleware.go +++ b/internal/middlewares/version_middleware.go @@ -8,13 +8,13 @@ import ( "strings" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // VersionDeprecation creates a middleware that handles API version deprecation func VersionDeprecation() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { path := c.Request().URL.Path version := extractVersionFromPath(path) @@ -88,7 +88,7 @@ func extractVersionFromPath(path string) string { // VersionValidator creates a middleware that validates API versions against supported versions func VersionValidator(config *versioning.Config) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if !config.Enabled { return next(c) } @@ -122,7 +122,7 @@ func VersionValidator(config *versioning.Config) echo.MiddlewareFunc { // VersionLogger creates a middleware that logs API version usage func VersionLogger() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { path := c.Request().URL.Path version := extractVersionFromPath(path) @@ -150,7 +150,7 @@ func VersionMiddleware(config *versioning.Config) echo.MiddlewareFunc { strategy, _ := config.GetStrategy() return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if !config.Enabled { return next(c) } diff --git a/internal/middlewares/version_middleware_test.go b/internal/middlewares/version_middleware_test.go index 598c6a9..ea89125 100644 --- a/internal/middlewares/version_middleware_test.go +++ b/internal/middlewares/version_middleware_test.go @@ -8,7 +8,7 @@ import ( "ichi-go/pkg/testutil" "ichi-go/pkg/versioning" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -100,7 +100,7 @@ func TestVersionMiddleware_DetectVersionFromHeader(t *testing.T) { c := e.NewContext(req, rec) handlerCalled := false - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { handlerCalled = true // Check version was set in context @@ -165,7 +165,7 @@ func TestVersionMiddleware_VersionFromPath(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -193,7 +193,7 @@ func TestVersionMiddleware_Disabled(t *testing.T) { c := e.NewContext(req, rec) handlerCalled := false - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { handlerCalled = true // Version should not be set when middleware is disabled @@ -243,7 +243,7 @@ func TestVersionMiddleware_DeprecationWarning(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -289,7 +289,7 @@ func BenchmarkVersionMiddleware(b *testing.B) { req := httptest.NewRequest(http.MethodGet, "/api/users", nil) req.Header.Set("API-Version", "202601") - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -317,7 +317,7 @@ func BenchmarkVersionMiddleware_WithDeprecation(b *testing.B) { req := httptest.NewRequest(http.MethodGet, "/api/users", nil) req.Header.Set("API-Version", "202511") // Old version - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -352,7 +352,7 @@ func TestVersionMiddleware_Integration(t *testing.T) { e.Use(VersionMiddleware(config)) // Register test route - e.GET("/api/users", func(c echo.Context) error { + e.GET("/api/users", func(c *echo.Context) error { version := c.Get("api_version") return c.JSON(http.StatusOK, map[string]interface{}{ "version": version, @@ -417,7 +417,7 @@ func TestVersionMiddleware_TableDriven(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -435,7 +435,7 @@ func TestVersionMiddleware_TableDriven(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - handler := func(c echo.Context) error { + handler := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } diff --git a/pkg/authenticator/auth.go b/pkg/authenticator/auth.go index a6877a9..685f13a 100644 --- a/pkg/authenticator/auth.go +++ b/pkg/authenticator/auth.go @@ -3,7 +3,7 @@ package authenticator import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type Authenticator struct { @@ -39,7 +39,7 @@ type AuthContext struct { // // RequirePermission middleware checks ACL // func (a *Authenticator) RequirePermission(resource, action string) echo.MiddlewareFunc { // return func(next echo.HandlerFunc) echo.HandlerFunc { -// return func(c echo.Context) error { +// return func(c *echo.Context) error { // authCtx := c.Get("auth").(*AuthContext) // allowed, err := a.aclChecker.Check(authCtx.UserID, resource, action) @@ -74,6 +74,6 @@ func (a *Authenticator) shouldSkip(path string, skipPaths []string) bool { return false } -func handleAuthError(c echo.Context, err error) error { - return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized", err) +func handleAuthError(c *echo.Context, err error) error { + return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized").Wrap(err) } diff --git a/pkg/authenticator/auth_middleware.go b/pkg/authenticator/auth_middleware.go index 84c4c76..431a1f3 100644 --- a/pkg/authenticator/auth_middleware.go +++ b/pkg/authenticator/auth_middleware.go @@ -1,7 +1,7 @@ package authenticator import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // AuthenticateMiddleware Authenticate middleware tries all enabled auth methods @@ -12,7 +12,7 @@ func (a *Authenticator) AuthenticateMiddleware(options ...AuthOption) echo.Middl } return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { // Try each auth method var authCtx *AuthContext diff --git a/pkg/authenticator/jwt.go b/pkg/authenticator/jwt.go index 8a188b0..e6139ae 100644 --- a/pkg/authenticator/jwt.go +++ b/pkg/authenticator/jwt.go @@ -5,7 +5,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type JWTAuthenticator struct { @@ -48,7 +48,7 @@ type JWTConfig struct { type JWTClaimsValidator func(claims jwt.MapClaims) error -func (a *JWTAuthenticator) Authenticate(c echo.Context) (*AuthContext, error) { +func (a *JWTAuthenticator) Authenticate(c *echo.Context) (*AuthContext, error) { tokenString, err := ExtractToken(c, *a.config) if err != nil { return nil, pkgErrors.AuthService(pkgErrors.ErrCodeInvalidToken). diff --git a/pkg/authenticator/jwt_test.go b/pkg/authenticator/jwt_test.go index 8842edd..15b7bfe 100644 --- a/pkg/authenticator/jwt_test.go +++ b/pkg/authenticator/jwt_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/authenticator/jwt_token.go b/pkg/authenticator/jwt_token.go index 4b110c0..983fadd 100644 --- a/pkg/authenticator/jwt_token.go +++ b/pkg/authenticator/jwt_token.go @@ -6,12 +6,12 @@ import ( "strings" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // ExtractToken extracts JWT token from Echo context based on TokenLookup configuration // Supports multiple lookup strategies: header, query, cookie -func ExtractToken(c echo.Context, config JWTConfig) (string, error) { +func ExtractToken(c *echo.Context, config JWTConfig) (string, error) { // Parse TokenLookup string (e.g., "header:Authorization,query:token,cookie:jwt") lookups := parseTokenLookup(config.TokenLookup) diff --git a/pkg/errors/bun_handler.go b/pkg/errors/bun_handler.go index 730fab4..2b57368 100644 --- a/pkg/errors/bun_handler.go +++ b/pkg/errors/bun_handler.go @@ -5,7 +5,7 @@ import ( "errors" "ichi-go/pkg/logger" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type BunErrorHandler struct { @@ -16,15 +16,20 @@ func NewBunHandler() *BunErrorHandler { } // Handle maps Bun errors to HTTP responses; otherwise it returns the error -func (h *BunErrorHandler) Handle(err error, ctx echo.Context) error { +func (h *BunErrorHandler) Handle(err error, ctx *echo.Context) error { + resp, _ := echo.UnwrapResponse(ctx.Response()) switch { case errors.Is(err, sql.ErrNoRows): logger.Debugf("BunErrorHandler: record not found") - ctx.Response().Status = 404 + if resp != nil { + resp.Status = 404 + } return nil case errors.Is(err, sql.ErrTxDone): - ctx.Response().Status = 500 + if resp != nil { + resp.Status = 500 + } return nil } return err diff --git a/pkg/errors/echo_handler.go b/pkg/errors/echo_handler.go index 5d2b1fa..bc21fff 100644 --- a/pkg/errors/echo_handler.go +++ b/pkg/errors/echo_handler.go @@ -3,7 +3,7 @@ package errors import ( "errors" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type EchoHandler struct { @@ -13,7 +13,7 @@ func NewEchoHandler() *EchoHandler { return &EchoHandler{} } -func (h *EchoHandler) Handle(err error, c echo.Context) error { +func (h *EchoHandler) Handle(err error, c *echo.Context) error { var httpErr *echo.HTTPError if errors.As(err, &httpErr) { diff --git a/pkg/errors/generic_handler.go b/pkg/errors/generic_handler.go index ba2530a..d75ee12 100644 --- a/pkg/errors/generic_handler.go +++ b/pkg/errors/generic_handler.go @@ -1,7 +1,7 @@ package errors import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type GenericHandler struct { @@ -11,9 +11,11 @@ func NewGenericHandler() *GenericHandler { return &GenericHandler{} } -func (g *GenericHandler) Handle(err error, ctx echo.Context) error { +func (g *GenericHandler) Handle(err error, ctx *echo.Context) error { - ctx.Response().Status = 500 + if resp, unwrapErr := echo.UnwrapResponse(ctx.Response()); unwrapErr == nil { + resp.Status = 500 + } // addition code for body or header here return nil } diff --git a/pkg/errors/handler.go b/pkg/errors/handler.go index 3ca391e..09dc55e 100644 --- a/pkg/errors/handler.go +++ b/pkg/errors/handler.go @@ -1,9 +1,9 @@ package errors -import "github.com/labstack/echo/v4" +import "github.com/labstack/echo/v5" type ErrorHandler interface { - Handle(err error, ctx echo.Context) error + Handle(err error, ctx *echo.Context) error } type Chain []ErrorHandler @@ -12,7 +12,7 @@ func NewChain(handlers ...ErrorHandler) Chain { return handlers } -func (c Chain) Handle(err error, ctx echo.Context) error { +func (c Chain) Handle(err error, ctx *echo.Context) error { for _, h := range c { if h.Handle(err, ctx) == nil { return nil @@ -21,7 +21,7 @@ func (c Chain) Handle(err error, ctx echo.Context) error { return err } -func (c Chain) EchoHandler(err error, ctx echo.Context) { +func (c Chain) EchoHandler(ctx *echo.Context, err error) { if remaining := c.Handle(err, ctx); remaining != nil { NewGenericHandler().Handle(remaining, ctx) } diff --git a/pkg/errors/oops_handler.go b/pkg/errors/oops_handler.go index 9706516..70bab65 100644 --- a/pkg/errors/oops_handler.go +++ b/pkg/errors/oops_handler.go @@ -7,7 +7,7 @@ import ( appValidator "ichi-go/pkg/validator" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/oops" ) @@ -18,8 +18,8 @@ func NewOppsHandler() *OopsErrorHandler { return &OopsErrorHandler{} } -func (h *OopsErrorHandler) Handle(err error, c echo.Context) error { - if c.Response().Committed { +func (h *OopsErrorHandler) Handle(err error, c *echo.Context) error { + if resp, unwrapErr := echo.UnwrapResponse(c.Response()); unwrapErr == nil && resp.Committed { return nil } code := http.StatusInternalServerError diff --git a/pkg/errors/registry.go b/pkg/errors/registry.go index 93183c8..f888028 100644 --- a/pkg/errors/registry.go +++ b/pkg/errors/registry.go @@ -3,7 +3,7 @@ package errors import ( "ichi-go/pkg/logger" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) //func Setup(e *echo.Echo) { diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index f92b26d..9ca7e35 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/rs/zerolog" oopszerolog "github.com/samber/oops/loggers/zerolog" "github.com/spf13/viper" @@ -106,7 +106,7 @@ func GetInstance() *Logger { return instance } -func RequestLogging(eCtx echo.Context, format string, v ...interface{}) { +func RequestLogging(eCtx *echo.Context, format string, v ...interface{}) { WithRequestStamp(eCtx).Info(). Msgf(format, v...) } @@ -141,12 +141,16 @@ func WithContext(ctx context.Context) *Logger { return &Logger{contextualLogger} } -func WithRequestStamp(eCtx echo.Context) *Logger { +func WithRequestStamp(eCtx *echo.Context) *Logger { baseInstance := GetInstance() + var statusCode int + if resp, err := echo.UnwrapResponse(eCtx.Response()); err == nil { + statusCode = resp.Status + } contextualLogger := baseInstance.With(). Str("method", eCtx.Request().Method). Str("path", eCtx.Request().URL.Path). - Int("status", eCtx.Response().Status). + Int("status", statusCode). Str("ip", eCtx.RealIP()). Str("user_agent", eCtx.Request().UserAgent()). Str(echo.HeaderXRequestID, eCtx.Response().Header().Get(echo.HeaderXRequestID)). diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go index 8e8e649..fd7c0f3 100644 --- a/pkg/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -8,7 +8,7 @@ import ( "sync" "testing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/rs/zerolog" "github.com/spf13/viper" ) diff --git a/pkg/utils/response/response_json_builder.go b/pkg/utils/response/response_json_builder.go index 37d8836..2037f5e 100644 --- a/pkg/utils/response/response_json_builder.go +++ b/pkg/utils/response/response_json_builder.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type body struct { @@ -17,7 +17,7 @@ type body struct { ServerTime string `json:"serverTime"` } -func Base(ctx echo.Context, httpCode int, message string, data interface{}, errorCode int, err error) error { +func Base(ctx *echo.Context, httpCode int, message string, data interface{}, errorCode int, err error) error { date := time.Now().Format(time.RFC1123) bodyResponse := body{ Code: httpCode, @@ -44,7 +44,7 @@ func Base(ctx echo.Context, httpCode int, message string, data interface{}, erro return ctx.JSON(httpCode, bodyResponse) } -func Created(ctx echo.Context, data interface{}) error { +func Created(ctx *echo.Context, data interface{}) error { if data == nil { panic(errors.New("success response : data on body is mandatory")) } @@ -52,7 +52,7 @@ func Created(ctx echo.Context, data interface{}) error { return Base(ctx, http.StatusCreated, http.StatusText(http.StatusCreated), data, http.StatusCreated, nil) } -func Success(ctx echo.Context, data interface{}) error { +func Success(ctx *echo.Context, data interface{}) error { if data == nil { panic(errors.New("success response : data on body is mandatory")) } @@ -61,6 +61,6 @@ func Success(ctx echo.Context, data interface{}) error { } //goland:noinspection GoUnusedExportedFunction -func Error(ctx echo.Context, httpCode int, err error) error { +func Error(ctx *echo.Context, httpCode int, err error) error { return Base(ctx, httpCode, http.StatusText(httpCode), nil, httpCode, err) } diff --git a/pkg/validator/helper.go b/pkg/validator/helper.go index 193d164..2704821 100644 --- a/pkg/validator/helper.go +++ b/pkg/validator/helper.go @@ -4,20 +4,20 @@ import ( "errors" "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // ErrValidationFailed is a sentinel error for validation failures var ErrValidationFailed = errors.New("validation failed") type ContextValidator interface { - ValidateWithContext(c echo.Context, i interface{}) error + ValidateWithContext(c *echo.Context, i interface{}) error ValidateWithLanguage(i interface{}, lang string) error } // BindAndValidate binds request body and validates with automatic language detection // Returns ValidationErrors that can be passed to response.Error() -func BindAndValidate(c echo.Context, req interface{}) error { +func BindAndValidate(c *echo.Context, req interface{}) error { // Bind request body if err := c.Bind(req); err != nil { return err @@ -29,7 +29,7 @@ func BindAndValidate(c echo.Context, req interface{}) error { // ValidateWithTranslation validates a struct with automatic language detection from headers // Returns ValidationErrors without sending response (compatible with response builder) -func ValidateWithTranslation(c echo.Context, req interface{}) error { +func ValidateWithTranslation(c *echo.Context, req interface{}) error { // Get validator from Echo echoValidator := c.Echo().Validator if echoValidator == nil { @@ -55,7 +55,7 @@ func ValidateWithTranslation(c echo.Context, req interface{}) error { // ValidateWithLanguage validates a struct with a specific language // Returns ValidationErrors without sending response (compatible with response builder) -func ValidateWithLanguage(c echo.Context, req interface{}, lang string) error { +func ValidateWithLanguage(c *echo.Context, req interface{}, lang string) error { // Get validator from Echo echoValidator := c.Echo().Validator if echoValidator == nil { diff --git a/pkg/validator/mocks/mock_context_validator.go b/pkg/validator/mocks/mock_context_validator.go index 4ab9c77..0a3ded2 100644 --- a/pkg/validator/mocks/mock_context_validator.go +++ b/pkg/validator/mocks/mock_context_validator.go @@ -5,7 +5,7 @@ package validator import ( - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" mock "github.com/stretchr/testify/mock" ) @@ -37,7 +37,7 @@ func (_m *MockContextValidator) EXPECT() *MockContextValidator_Expecter { } // ValidateWithContext provides a mock function for the type MockContextValidator -func (_mock *MockContextValidator) ValidateWithContext(c echo.Context, i interface{}) error { +func (_mock *MockContextValidator) ValidateWithContext(c *echo.Context, i interface{}) error { ret := _mock.Called(c, i) if len(ret) == 0 { @@ -45,7 +45,7 @@ func (_mock *MockContextValidator) ValidateWithContext(c echo.Context, i interfa } var r0 error - if returnFunc, ok := ret.Get(0).(func(echo.Context, interface{}) error); ok { + if returnFunc, ok := ret.Get(0).(func(*echo.Context, interface{}) error); ok { r0 = returnFunc(c, i) } else { r0 = ret.Error(0) @@ -59,17 +59,17 @@ type MockContextValidator_ValidateWithContext_Call struct { } // ValidateWithContext is a helper method to define mock.On call -// - c echo.Context +// - c *echo.Context // - i interface{} func (_e *MockContextValidator_Expecter) ValidateWithContext(c interface{}, i interface{}) *MockContextValidator_ValidateWithContext_Call { return &MockContextValidator_ValidateWithContext_Call{Call: _e.mock.On("ValidateWithContext", c, i)} } -func (_c *MockContextValidator_ValidateWithContext_Call) Run(run func(c echo.Context, i interface{})) *MockContextValidator_ValidateWithContext_Call { +func (_c *MockContextValidator_ValidateWithContext_Call) Run(run func(c *echo.Context, i interface{})) *MockContextValidator_ValidateWithContext_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 echo.Context + var arg0 *echo.Context if args[0] != nil { - arg0 = args[0].(echo.Context) + arg0 = args[0].(*echo.Context) } var arg1 interface{} if args[1] != nil { @@ -88,7 +88,7 @@ func (_c *MockContextValidator_ValidateWithContext_Call) Return(err error) *Mock return _c } -func (_c *MockContextValidator_ValidateWithContext_Call) RunAndReturn(run func(c echo.Context, i interface{}) error) *MockContextValidator_ValidateWithContext_Call { +func (_c *MockContextValidator_ValidateWithContext_Call) RunAndReturn(run func(c *echo.Context, i interface{}) error) *MockContextValidator_ValidateWithContext_Call { _c.Call.Return(run) return _c } diff --git a/pkg/validator/translator.go b/pkg/validator/translator.go index de69e7c..20f0a7e 100644 --- a/pkg/validator/translator.go +++ b/pkg/validator/translator.go @@ -3,11 +3,11 @@ package validator import ( "strings" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // GetLanguageFromContext extracts language from Accept-Language or X-Language header -func GetLanguageFromContext(c echo.Context) string { +func GetLanguageFromContext(c *echo.Context) string { // Try X-Language custom header first (highest priority) customLang := c.Request().Header.Get("X-Language") if customLang == "id" || customLang == "en" { diff --git a/pkg/versioning/version.go b/pkg/versioning/version.go index e7ae96b..924c9de 100644 --- a/pkg/versioning/version.go +++ b/pkg/versioning/version.go @@ -3,7 +3,7 @@ package versioning import ( "fmt" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) // APIVersion represents an API version diff --git a/pkg/versioning/version_test.go b/pkg/versioning/version_test.go index 8efde44..2c55286 100644 --- a/pkg/versioning/version_test.go +++ b/pkg/versioning/version_test.go @@ -3,7 +3,7 @@ package versioning import ( "testing" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) From 6d9e5018fa983da3b216d53e05f8c8d21d5ee31e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 03:15:06 +0000 Subject: [PATCH 2/5] refactor: replace hand-rolled Swagger handler with echo-swagger/v2 echo-swagger/v2 now supports Echo v5 (github.com/swaggo/echo-swagger/v2 v2.0.1). Replacing the custom swagger handler with echoSwagger.WrapHandler also removes the last indirect pulls of echo/v4 and labstack/gommon. https://claude.ai/code/session_012HXUTcemrXNF2PbH2kSQ7m --- cmd/server/rest_server.go | 72 ++------------------------------------- go.mod | 8 ++++- go.sum | 14 +++++++- 3 files changed, 22 insertions(+), 72 deletions(-) diff --git a/cmd/server/rest_server.go b/cmd/server/rest_server.go index 678a435..7d544e5 100644 --- a/cmd/server/rest_server.go +++ b/cmd/server/rest_server.go @@ -2,15 +2,11 @@ package server import ( "encoding/json" - "html/template" - "net/http" "os" - "regexp" "github.com/labstack/echo/v5" "github.com/samber/do/v2" - swaggerFiles "github.com/swaggo/files/v2" - "github.com/swaggo/swag" + echoSwagger "github.com/swaggo/echo-swagger/v2" "ichi-go/config" "ichi-go/internal/applications/auth" @@ -52,71 +48,7 @@ func generateRouteList(e *echo.Echo) { } } -// swaggerIndexTemplate is the HTML template served at /docs/index.html. -const swaggerIndexTemplate = ` - - - Swagger UI - - - - - -
- - - - -` - func openOpenAPIDocs(e *echo.Echo, cfg *config.Config) { - indexTmpl := template.Must(template.New("swagger_index.html").Parse(swaggerIndexTemplate)) - re := regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`) - - handler := func(c *echo.Context) error { - if c.Request().Method != http.MethodGet { - return echo.ErrMethodNotAllowed - } - - matches := re.FindStringSubmatch(c.Request().RequestURI) - if len(matches) != 3 { - return echo.ErrNotFound - } - - path := matches[2] - switch path { - case "": - return c.Redirect(http.StatusMovedPermanently, matches[1]+"index.html") - case "index.html": - c.Response().Header().Set("Content-Type", "text/html; charset=utf-8") - return indexTmpl.Execute(c.Response(), nil) - case "doc.json": - doc, err := swag.ReadDoc() - if err != nil { - return echo.ErrNotFound - } - c.Response().Header().Set("Content-Type", "application/json; charset=utf-8") - _, err = c.Response().Write([]byte(doc)) - return err - default: - c.Request().URL.Path = path - http.FileServer(http.FS(swaggerFiles.FS)).ServeHTTP(c.Response(), c.Request()) - return nil - } - } - - e.GET("/docs/*", handler) + e.GET("/docs/*", echoSwagger.WrapHandler) logger.Infof("Swagger UI available at http://localhost:%d/docs/index.html", cfg.Http().Port) } diff --git a/go.mod b/go.mod index df347f5..56490e7 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/samber/oops/loggers/zerolog v0.0.0-20260101195714-ee59589fe6d3 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - github.com/swaggo/files/v2 v2.0.2 + github.com/swaggo/echo-swagger/v2 v2.0.1 github.com/swaggo/swag v1.16.6 github.com/uptrace/bun v1.2.16 github.com/uptrace/bun/dialect/mysqldialect v1.2.16 @@ -88,6 +88,7 @@ require ( github.com/mfridman/interpolate v0.0.2 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect @@ -102,6 +103,9 @@ require ( github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/sv-tools/openapi v0.2.1 // indirect + github.com/swaggo/files/v2 v2.0.2 // indirect + github.com/swaggo/swag/v2 v2.0.0-rc4 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zeebo/errs v1.4.0 // indirect @@ -130,5 +134,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/grpc v1.72.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index a79bc31..1cc222c 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -90,7 +91,7 @@ github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmG github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= @@ -175,6 +176,7 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU= github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= @@ -227,10 +229,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= +github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= +github.com/swaggo/echo-swagger/v2 v2.0.1 h1:jKR3QiK+ciGjxE0+7qZ/azjtlx/pTVls7pJFJqdJoJI= +github.com/swaggo/echo-swagger/v2 v2.0.1/go.mod h1:BbgiO9XKX6yYU5Rq4ejqVlQI0mVRv6ziFKd0XgdztnQ= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY= +github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc= @@ -336,6 +344,8 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= @@ -348,3 +358,5 @@ modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ= resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From f9656929f0333d3d98a20b6e1df19920bd30b0d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 02:24:08 +0000 Subject: [PATCH 3/5] fix: address coderabbitai review findings Inline comments: - web_server.go: fix TemplateRenderer.Render signature to match Echo v5 Renderer interface (c *echo.Context, w io.Writer, templateName string, data any) - bun_handler.go: propagate error when echo.UnwrapResponse returns nil resp instead of silently returning nil and swallowing the error - logger.go: use echo.ResolveResponseStatus instead of UnwrapResponse to normalise uncommitted responses to 200 (avoids logging status=0) Outside diff comments: - policy_controller.go: guard *req.TenantID dereference behind nil check to prevent panic when Role is set but TenantID is nil - user_controller.go: check strconv.Atoi error before calling GetById; check dtoMapper.Map error in GetPokemon before returning success response - rbac_audit_middleware.go: replace manual status code resolution with echo.ResolveResponseStatus(c.Response(), err); use detached context.WithTimeout(context.Background(), 5s) in audit goroutine to avoid cancellation when the request context is done Nitpick comments: - auth_controller.go: remove fmt.Println debug print from Me handler and drop now-unused fmt import - audit_controller.go: replace direct type assertions on PolicyBefore/PolicyAfter with safe toMapOrEmpty helper to prevent runtime panics on nil/type mismatch - generic_handler.go: fall back to WriteHeader(500) when echo.UnwrapResponse fails instead of returning nil without setting any status https://claude.ai/code/session_012HXUTcemrXNF2PbH2kSQ7m --- cmd/server/web_server.go | 5 ++-- .../auth/controller/auth_controller.go | 2 -- .../rbac/controllers/audit_controller.go | 12 ++++++++-- .../rbac/controllers/policy_controller.go | 2 +- .../user/controller/user_controller.go | 11 ++++++--- internal/middlewares/rbac_audit_middleware.go | 24 ++++++------------- pkg/errors/bun_handler.go | 10 ++++---- pkg/errors/generic_handler.go | 5 +++- pkg/logger/logger.go | 5 +--- 9 files changed, 40 insertions(+), 36 deletions(-) diff --git a/cmd/server/web_server.go b/cmd/server/web_server.go index 6cf146d..9219d29 100644 --- a/cmd/server/web_server.go +++ b/cmd/server/web_server.go @@ -3,6 +3,7 @@ package server import ( "html/template" "ichi-go/config" + "io" "net/http" "github.com/labstack/echo/v5" @@ -12,8 +13,8 @@ type TemplateRenderer struct { templates *template.Template } -func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}, c *echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, templateName string, data any) error { + return t.templates.ExecuteTemplate(w, templateName, data) } func SetupWebRoutes(e *echo.Echo, config *config.Schema) { diff --git a/internal/applications/auth/controller/auth_controller.go b/internal/applications/auth/controller/auth_controller.go index 49b5fbd..9178b52 100644 --- a/internal/applications/auth/controller/auth_controller.go +++ b/internal/applications/auth/controller/auth_controller.go @@ -1,7 +1,6 @@ package auth import ( - "fmt" authDto "ichi-go/internal/applications/auth/dto" authService "ichi-go/internal/applications/auth/service" "ichi-go/pkg/authenticator" @@ -149,7 +148,6 @@ func (c *AuthController) Logout(eCtx *echo.Context) error { // @Failure 500 {object} response.ErrorResponse "Internal server error" // @Router /auth/me [get] func (c *AuthController) Me(eCtx *echo.Context) error { - fmt.Println("AuthController.Me called") // Get auth context from middleware authCtx, ok := eCtx.Get("auth").(*authenticator.AuthContext) logger.Debugf("AuthController.Me: authCtx=%+v, ok=%v", authCtx, ok) diff --git a/internal/applications/rbac/controllers/audit_controller.go b/internal/applications/rbac/controllers/audit_controller.go index 2efa272..c3c7d86 100644 --- a/internal/applications/rbac/controllers/audit_controller.go +++ b/internal/applications/rbac/controllers/audit_controller.go @@ -109,8 +109,8 @@ func (c *AuditController) QueryAuditLogs(ctx *echo.Context) error { TenantID: log.TenantID, Decision: log.Decision, DecisionReason: log.DecisionReason, - PolicyBefore: log.PolicyBefore.(map[string]interface{}), - PolicyAfter: log.PolicyAfter.(map[string]interface{}), + PolicyBefore: toMapOrEmpty(log.PolicyBefore), + PolicyAfter: toMapOrEmpty(log.PolicyAfter), Reason: log.Reason, LatencyMs: log.LatencyMs, }) @@ -124,6 +124,14 @@ func (c *AuditController) QueryAuditLogs(ctx *echo.Context) error { return response.Success(ctx, resp) } +// toMapOrEmpty safely casts v to map[string]interface{}; returns an empty map on nil or type mismatch. +func toMapOrEmpty(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return map[string]interface{}{} +} + // GetAuditStats godoc // // @Summary Get audit statistics diff --git a/internal/applications/rbac/controllers/policy_controller.go b/internal/applications/rbac/controllers/policy_controller.go index 346b0d4..fefdf7a 100644 --- a/internal/applications/rbac/controllers/policy_controller.go +++ b/internal/applications/rbac/controllers/policy_controller.go @@ -75,7 +75,7 @@ func (c *PolicyController) GetPolicies(ctx *echo.Context) error { Action: p.V3, }) } - } else if req.Role != nil { + } else if req.Role != nil && req.TenantID != nil { casbinPolicies, err := c.policyService.GetPoliciesByRole(ctx.Request().Context(), *req.Role, *req.TenantID) if err != nil { return response.Error(ctx, http.StatusInternalServerError, err) diff --git a/internal/applications/user/controller/user_controller.go b/internal/applications/user/controller/user_controller.go index 1851128..0612b3c 100644 --- a/internal/applications/user/controller/user_controller.go +++ b/internal/applications/user/controller/user_controller.go @@ -32,8 +32,11 @@ func (c *UserController) GetUser(eCtx *echo.Context) error { return response.Error(eCtx, http.StatusBadRequest, err) } - idString, err := strconv.Atoi(userGetReq.ID) - user, err := c.service.GetById(eCtx.Request().Context(), uint32(idString)) + idInt, err := strconv.Atoi(userGetReq.ID) + if err != nil { + return response.Error(eCtx, http.StatusBadRequest, err) + } + user, err := c.service.GetById(eCtx.Request().Context(), uint32(idInt)) if err != nil { return response.Error(eCtx, http.StatusBadRequest, err) } @@ -98,6 +101,8 @@ func (c *UserController) GetPokemon(eCtx *echo.Context) error { if err != nil { return response.Error(eCtx, http.StatusInternalServerError, err) } - err = dtoMapper.Map(&pokemonGetResponseDto, result) + if err = dtoMapper.Map(&pokemonGetResponseDto, result); err != nil { + return response.Error(eCtx, http.StatusInternalServerError, err) + } return response.Success(eCtx, pokemonGetResponseDto) } diff --git a/internal/middlewares/rbac_audit_middleware.go b/internal/middlewares/rbac_audit_middleware.go index b2a47bf..3bbd0fc 100644 --- a/internal/middlewares/rbac_audit_middleware.go +++ b/internal/middlewares/rbac_audit_middleware.go @@ -2,6 +2,7 @@ package middlewares import ( "bytes" + "context" "encoding/json" "io" "net/http" @@ -43,9 +44,6 @@ func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditCo requestBody = captureRequestBody(c) } - // Capture echo Response to read status code after the request - echoResp, _ := echo.UnwrapResponse(c.Response()) - // Execute request err := next(c) @@ -77,18 +75,7 @@ func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditCo } // Add response status - statusCode := http.StatusOK - if echoResp != nil { - statusCode = echoResp.Status - if statusCode == 0 { - statusCode = http.StatusOK - } - } - if err != nil { - if he, ok := err.(*echo.HTTPError); ok { - statusCode = he.Code - } - } + _, statusCode := echo.ResolveResponseStatus(c.Response(), err) // Set decision based on HTTP status if statusCode >= 200 && statusCode < 300 { @@ -103,9 +90,12 @@ func RBACAuditMiddleware(auditRepo *repositories.AuditRepository, config AuditCo } } - // Save audit log asynchronously + // Save audit log asynchronously with a detached context to avoid + // cancellation when the request context is done. go func() { - if err := auditRepo.Create(ctx, log); err != nil { + auditCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := auditRepo.Create(auditCtx, log); err != nil { logger.Errorf("Failed to save audit log: %v", err) } }() diff --git a/pkg/errors/bun_handler.go b/pkg/errors/bun_handler.go index 2b57368..f022cda 100644 --- a/pkg/errors/bun_handler.go +++ b/pkg/errors/bun_handler.go @@ -21,15 +21,17 @@ func (h *BunErrorHandler) Handle(err error, ctx *echo.Context) error { switch { case errors.Is(err, sql.ErrNoRows): logger.Debugf("BunErrorHandler: record not found") - if resp != nil { - resp.Status = 404 + if resp == nil { + return err } + resp.Status = 404 return nil case errors.Is(err, sql.ErrTxDone): - if resp != nil { - resp.Status = 500 + if resp == nil { + return err } + resp.Status = 500 return nil } return err diff --git a/pkg/errors/generic_handler.go b/pkg/errors/generic_handler.go index d75ee12..b0f5fb0 100644 --- a/pkg/errors/generic_handler.go +++ b/pkg/errors/generic_handler.go @@ -1,6 +1,8 @@ package errors import ( + "net/http" + "github.com/labstack/echo/v5" ) @@ -12,9 +14,10 @@ func NewGenericHandler() *GenericHandler { } func (g *GenericHandler) Handle(err error, ctx *echo.Context) error { - if resp, unwrapErr := echo.UnwrapResponse(ctx.Response()); unwrapErr == nil { resp.Status = 500 + } else { + ctx.Response().WriteHeader(http.StatusInternalServerError) } // addition code for body or header here return nil diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 9ca7e35..1c8d9ef 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -143,10 +143,7 @@ func WithContext(ctx context.Context) *Logger { func WithRequestStamp(eCtx *echo.Context) *Logger { baseInstance := GetInstance() - var statusCode int - if resp, err := echo.UnwrapResponse(eCtx.Response()); err == nil { - statusCode = resp.Status - } + _, statusCode := echo.ResolveResponseStatus(eCtx.Response(), nil) contextualLogger := baseInstance.With(). Str("method", eCtx.Request().Method). Str("path", eCtx.Request().URL.Path). From 66e97334b47a8d0860d28e7a904507bb12657d42 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 09:54:16 +0000 Subject: [PATCH 4/5] fix: address coderabbitai review findings (round 2) - auth_controller.go: fix @Router annotations for Register, RefreshToken, Logout, and Me to include the /202601 version prefix, matching the actual mount paths from RegisterRoutesV1 (base path is /ichi-go/api per @BasePath) - audit_controller.go: fix path traversal in export filename handling; introduce sanitizeExportFileName that rejects absolute paths, "..", path separators, and non-allowlisted characters (only [a-zA-Z0-9._-] permitted), falling back to a timestamped default name on any violation - audit_controller.go: replace direct type assertions on PolicyBefore/PolicyAfter in GetRecentMutations response loop with toMapOrEmpty to prevent runtime panics on nil or unexpected types https://claude.ai/code/session_012HXUTcemrXNF2PbH2kSQ7m --- .../auth/controller/auth_controller.go | 8 ++--- .../rbac/controllers/audit_controller.go | 35 +++++++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/internal/applications/auth/controller/auth_controller.go b/internal/applications/auth/controller/auth_controller.go index 9178b52..fcd7165 100644 --- a/internal/applications/auth/controller/auth_controller.go +++ b/internal/applications/auth/controller/auth_controller.go @@ -65,7 +65,7 @@ func (c *AuthController) Login(eCtx *echo.Context) error { // @Success 201 {object} response.SuccessResponse{data=authDto.RegisterResponse} "User registered successfully" // @Failure 400 {object} response.ErrorResponse "Invalid request, validation error, or user already exists" // @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /auth/register [post] +// @Router /202601/auth/register [post] func (c *AuthController) Register(eCtx *echo.Context) error { var req authDto.RegisterRequest @@ -96,7 +96,7 @@ func (c *AuthController) Register(eCtx *echo.Context) error { // @Failure 400 {object} response.ErrorResponse "Invalid request or validation error" // @Failure 401 {object} response.ErrorResponse "Invalid or expired refresh token" // @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /auth/refresh [post] +// @Router /202601/auth/refresh [post] func (c *AuthController) RefreshToken(eCtx *echo.Context) error { var req authDto.RefreshTokenRequest @@ -127,7 +127,7 @@ func (c *AuthController) RefreshToken(eCtx *echo.Context) error { // @Security BearerAuth // @Success 200 {object} response.SuccessResponse{data=map[string]string} "Logged out successfully" // @Failure 401 {object} response.ErrorResponse "Unauthorized - invalid or missing token" -// @Router /auth/logout [post] +// @Router /202601/auth/logout [post] func (c *AuthController) Logout(eCtx *echo.Context) error { logger.Infof("User logged out") return response.Success(eCtx, map[string]string{ @@ -146,7 +146,7 @@ func (c *AuthController) Logout(eCtx *echo.Context) error { // @Success 200 {object} response.SuccessResponse{data=authDto.UserInfo} "User profile retrieved successfully" // @Failure 401 {object} response.ErrorResponse "Unauthorized - invalid or missing token" // @Failure 500 {object} response.ErrorResponse "Internal server error" -// @Router /auth/me [get] +// @Router /202601/auth/me [get] func (c *AuthController) Me(eCtx *echo.Context) error { // Get auth context from middleware authCtx, ok := eCtx.Get("auth").(*authenticator.AuthContext) diff --git a/internal/applications/rbac/controllers/audit_controller.go b/internal/applications/rbac/controllers/audit_controller.go index c3c7d86..1f976ab 100644 --- a/internal/applications/rbac/controllers/audit_controller.go +++ b/internal/applications/rbac/controllers/audit_controller.go @@ -5,6 +5,8 @@ import ( "net/http" "os" "path/filepath" + "regexp" + "strings" "time" "ichi-go/internal/applications/rbac/dto" @@ -132,6 +134,28 @@ func toMapOrEmpty(v interface{}) map[string]interface{} { return map[string]interface{}{} } +// safeFileNameRe permits only alphanumerics, dots, underscores, and hyphens. +var safeFileNameRe = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + +// sanitizeExportFileName returns a safe filename for export files. If the +// caller-supplied name is empty, absolute, contains path separators, "..", +// or characters outside the allowlist, it falls back to a timestamped default. +func sanitizeExportFileName(name, format string) string { + fallback := fmt.Sprintf("audit_logs_%s.%s", time.Now().Format("20060102_150405"), format) + if name == "" { + return fallback + } + // Strip directory component; reject anything that still looks dangerous. + base := filepath.Base(name) + if filepath.IsAbs(name) || + strings.Contains(name, "..") || + strings.ContainsAny(name, `/\`) || + !safeFileNameRe.MatchString(base) { + return fallback + } + return base +} + // GetAuditStats godoc // // @Summary Get audit statistics @@ -227,10 +251,7 @@ func (c *AuditController) ExportAuditLogs(ctx *echo.Context) error { } // Generate filename - fileName := req.FileName - if fileName == "" { - fileName = fmt.Sprintf("audit_logs_%s.%s", time.Now().Format("20060102_150405"), req.Format) - } + safeFileName := sanitizeExportFileName(req.FileName, req.Format) // Create export directory exportDir := "./exports/audit" @@ -238,7 +259,7 @@ func (c *AuditController) ExportAuditLogs(ctx *echo.Context) error { return response.Error(ctx, http.StatusInternalServerError, err) } - filePath := filepath.Join(exportDir, fileName) + filePath := filepath.Join(exportDir, safeFileName) // Export based on format var err error @@ -312,8 +333,8 @@ func (c *AuditController) GetRecentMutations(ctx *echo.Context) error { ResourceID: log.ResourceID, SubjectID: log.SubjectID, TenantID: log.TenantID, - PolicyBefore: log.PolicyBefore.(map[string]interface{}), - PolicyAfter: log.PolicyAfter.(map[string]interface{}), + PolicyBefore: toMapOrEmpty(log.PolicyBefore), + PolicyAfter: toMapOrEmpty(log.PolicyAfter), Reason: log.Reason, }) } From 5a623d1221e731320dc36a784943949ddbe87b3c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 10:05:01 +0000 Subject: [PATCH 5/5] fix: address coderabbitai review findings (round 3) - audit_controller.go: toMapOrEmpty now returns {"__raw": v} for unexpected non-nil types instead of silently returning an empty map, and logs a Warnf so deserialization mismatches are visible; nil still maps to empty map as nil policy is an expected/normal case - audit_controller.go: sanitizeExportFileName fallback name now uses nanosecond precision (now.Nanosecond() as 9-digit suffix) to prevent filename collisions when multiple exports are triggered within the same second https://claude.ai/code/session_012HXUTcemrXNF2PbH2kSQ7m --- .../rbac/controllers/audit_controller.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/applications/rbac/controllers/audit_controller.go b/internal/applications/rbac/controllers/audit_controller.go index 1f976ab..27e4837 100644 --- a/internal/applications/rbac/controllers/audit_controller.go +++ b/internal/applications/rbac/controllers/audit_controller.go @@ -12,7 +12,7 @@ import ( "ichi-go/internal/applications/rbac/dto" "ichi-go/internal/applications/rbac/repositories" "ichi-go/internal/applications/rbac/services" - + "ichi-go/pkg/logger" "ichi-go/pkg/utils/response" "github.com/labstack/echo/v5" @@ -126,12 +126,20 @@ func (c *AuditController) QueryAuditLogs(ctx *echo.Context) error { return response.Success(ctx, resp) } -// toMapOrEmpty safely casts v to map[string]interface{}; returns an empty map on nil or type mismatch. +// toMapOrEmpty casts v to map[string]interface{}. +// - nil → empty map (no policy recorded is expected and normal) +// - correct type → returned as-is +// - unexpected non-nil type → wrapped under "__raw" so the value is +// preserved in the response and a warning is logged for investigation. func toMapOrEmpty(v interface{}) map[string]interface{} { + if v == nil { + return map[string]interface{}{} + } if m, ok := v.(map[string]interface{}); ok { return m } - return map[string]interface{}{} + logger.Warnf("toMapOrEmpty: unexpected type %T for audit policy field; wrapping as __raw", v) + return map[string]interface{}{"__raw": v} } // safeFileNameRe permits only alphanumerics, dots, underscores, and hyphens. @@ -140,8 +148,11 @@ var safeFileNameRe = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) // sanitizeExportFileName returns a safe filename for export files. If the // caller-supplied name is empty, absolute, contains path separators, "..", // or characters outside the allowlist, it falls back to a timestamped default. +// The fallback includes nanosecond precision to avoid collisions between +// concurrent exports. func sanitizeExportFileName(name, format string) string { - fallback := fmt.Sprintf("audit_logs_%s.%s", time.Now().Format("20060102_150405"), format) + now := time.Now() + fallback := fmt.Sprintf("audit_logs_%s_%09d.%s", now.Format("20060102_150405"), now.Nanosecond(), format) if name == "" { return fallback }