From aab55a143fa33c59ae82f48850ee15898fdecd72 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 16:14:55 +0300 Subject: [PATCH] experiment: successfully implemented dependency injections for controllers and services --- backend/cmd/main.go | 71 ++++++++++---- backend/go.mod | 2 + backend/go.sum | 4 + backend/internal/controllers/auth.go | 122 ++++++++++++------------ backend/internal/controllers/router.go | 9 ++ backend/internal/controllers/service.go | 23 ++++- backend/internal/routes/router.go | 39 ++++++++ backend/internal/routes/setup.go | 54 ++--------- backend/internal/services/auth.go | 28 +----- backend/internal/services/setup.go | 12 +++ 10 files changed, 208 insertions(+), 156 deletions(-) create mode 100644 backend/internal/controllers/router.go create mode 100644 backend/internal/routes/router.go create mode 100644 backend/internal/services/setup.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index f910425..fa8f2ea 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,27 +1,31 @@ - // @title Easywish client API - // @version 1.0 - // @description Easy and feature-rich wishlist. - // @license.name GPL 3.0 +// @title Easywish client API +// @version 1.0 +// @description Easy and feature-rich wishlist. +// @license.name GPL 3.0 - // @BasePath /api/ - // @Schemes http +// @BasePath /api/ +// @Schemes http - // @securityDefinitions.apikey JWT - // @in header - // @name Authorization +// @securityDefinitions.apikey JWT +// @in header +// @name Authorization package main import ( + "time" + "net/http" + "context" "github.com/gin-gonic/gin" + "go.uber.org/fx" "easywish/config" + "easywish/internal/controllers" "easywish/internal/logger" "easywish/internal/routes" - - docs "easywish/docs" - swaggerfiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" + "easywish/internal/services" + // swaggerfiles "github.com/swaggo/files" + // ginSwagger "github.com/swaggo/gin-swagger" ) func main() { @@ -32,12 +36,43 @@ func main() { defer logger.Sync() - r := gin.Default() - r = routes.SetupRoutes(r) + fx.New( + services.Module, + fx.Provide( + // func() *gin.Engine { + // engine := gin.Default() + // engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + // return engine + // }, + gin.New, + controllers.NewAuthController, + controllers.NewServiceController, + ), + routes.Module, - docs.SwaggerInfo.Schemes = []string{"http"} - r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) - r.Run(":8080") + fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine) { + server := &http.Server{ + Addr: ":8080", + Handler: router, + } + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return server.Shutdown(shutdownCtx) + }, + }) + }), + + ).Run() } diff --git a/backend/go.mod b/backend/go.mod index 7f2369a..3bc4aa7 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -49,6 +49,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/fx v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.18.0 // indirect golang.org/x/crypto v0.39.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 1223490..ae394c4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -120,6 +120,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2 github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go index 941516c..23edbf7 100644 --- a/backend/internal/controllers/auth.go +++ b/backend/internal/controllers/auth.go @@ -1,50 +1,31 @@ package controllers import ( - "easywish/internal/models" "easywish/internal/services" "net/http" "github.com/gin-gonic/gin" ) -// @Summary Register an account -// @Tags Auth -// @Accept json -// @Produce json -// @Router /auth/registrationBegin [post] -func RegistrationBegin(c *gin.Context) { - - var request models.RegistrationBeginRequest - - if err := c.ShouldBindJSON(&request); err != nil { - c.Status(http.StatusBadRequest) - return - } - - auths := services.NewAuthService() - - result, err := auths.Login(request) - - if err != nil { - c.Status(http.StatusTeapot) - return - } - - c.JSON(http.StatusFound, result) - - c.Status(http.StatusNotImplemented) +type AuthController interface { + RegistrationBegin(c *gin.Context) + RegistrationComplete(c *gin.Context) + Login(c *gin.Context) + Refresh(c *gin.Context) + PasswordResetBegin(c *gin.Context) + PasswordResetComplete(c *gin.Context) + Router } -// @Summary Confirm with code, finish creating the account -// @Tags Auth -// @Accept json -// @Produce json -// @Router /auth/registrationComplete [post] -func RegistrationComplete(c *gin.Context) { - c.Status(http.StatusNotImplemented) +type authControllerImpl struct { + authService services.AuthService } +func NewAuthController(as services.AuthService) AuthController { + return &authControllerImpl{authService: as} +} + +// Login implements AuthController. // @Summary Acquire tokens via login credentials (and 2FA code if needed) // @Tags Auth // @Accept json @@ -52,50 +33,65 @@ func RegistrationComplete(c *gin.Context) { // @Param request body models.LoginRequest true "desc" // @Success 200 {object} models.LoginResponse "desc" // @Router /auth/login [post] -func Login(c *gin.Context) { - - var request models.LoginRequest - - if err := c.ShouldBindJSON(&request); err != nil { - c.Status(http.StatusBadRequest) - return - } - - auths := services.NewAuthService() - - result, err := auths.Login(request) - - if err != nil { - c.Status(http.StatusTeapot) - return - } - - c.JSON(http.StatusFound, result) -} - -// @Summary Receive new tokens via refresh token -// @Tags Auth -// @Accept json -// @Produce json -// @Router /auth/refresh [post] -func Refresh(c *gin.Context) { +func (a *authControllerImpl) Login(c *gin.Context) { c.Status(http.StatusNotImplemented) } +// PasswordResetBegin implements AuthController. // @Summary Request password reset email // @Tags Auth // @Accept json // @Produce json // @Router /auth/passwordResetBegin [post] -func PasswordResetBegin(c *gin.Context) { +func (a *authControllerImpl) PasswordResetBegin(c *gin.Context) { c.Status(http.StatusNotImplemented) } +// PasswordResetComplete implements AuthController. // @Summary Complete password reset with email code and provide 2FA code or backup code if needed // @Tags Auth // @Accept json // @Produce json // @Router /auth/passwordResetComplete [post] -func PasswordResetComplete(c *gin.Context) { +func (a *authControllerImpl) PasswordResetComplete(c *gin.Context) { c.Status(http.StatusNotImplemented) } + +// Refresh implements AuthController. +// @Summary Receive new tokens via refresh token +// @Tags Auth +// @Accept json +// @Produce json +// @Router /auth/refresh [post] +func (a *authControllerImpl) Refresh(c *gin.Context) { + c.Status(http.StatusNotImplemented) +} + +// RegistrationComplete implements AuthController. +// @Summary Register an account +// @Tags Auth +// @Accept json +// @Produce json +// @Router /auth/registrationBegin [post] +func (a *authControllerImpl) RegistrationBegin(c *gin.Context) { + c.Status(http.StatusNotImplemented) +} + +// RegistrationBegin implements AuthController. +// @Summary Confirm with code, finish creating the account +// @Tags Auth +// @Accept json +// @Produce json +// @Router /auth/registrationComplete [post] +func (a *authControllerImpl) RegistrationComplete(c *gin.Context) { + c.Status(http.StatusNotImplemented) +} + +func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) { + group.POST("/registrationBegin", a.RegistrationBegin) + group.POST("/registrationComplete", a.RegistrationComplete) + group.POST("/login", a.Login) + group.POST("/refresh", a.Refresh) + group.POST("/passwordResetBegin", a.PasswordResetBegin) + group.POST("/passwordResetComplete", a.PasswordResetComplete) +} diff --git a/backend/internal/controllers/router.go b/backend/internal/controllers/router.go new file mode 100644 index 0000000..1256a3b --- /dev/null +++ b/backend/internal/controllers/router.go @@ -0,0 +1,9 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" +) + +type Router interface { + RegisterRoutes(group *gin.RouterGroup) +} diff --git a/backend/internal/controllers/service.go b/backend/internal/controllers/service.go index 698a3c5..923856a 100644 --- a/backend/internal/controllers/service.go +++ b/backend/internal/controllers/service.go @@ -6,10 +6,18 @@ import ( "github.com/gin-gonic/gin" ) -type HealthStatus struct { - Healthy bool `json:"healthy"` +type ServiceController interface { + HealthCheck(c *gin.Context) + Router } +type serviceControllerImpl struct{} + +func NewServiceController() ServiceController { + return &serviceControllerImpl{} +} + +// HealthCheck implements ServiceController. // @Summary Get health status // @Description Used internally for checking service health // @Tags Service @@ -17,6 +25,15 @@ type HealthStatus struct { // @Produce json // @Success 200 {object} HealthStatus "Says whether it's healthy or not" // @Router /service/health [get] -func HealthCheck(c *gin.Context) { +func (s *serviceControllerImpl) HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": true}) } + +// RegisterRoutes implements ServiceController. +func (s *serviceControllerImpl) RegisterRoutes(group *gin.RouterGroup) { + group.GET("/health", s.HealthCheck) +} + +type HealthStatus struct { + Healthy bool `json:"healthy"` +} diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go new file mode 100644 index 0000000..ad1a538 --- /dev/null +++ b/backend/internal/routes/router.go @@ -0,0 +1,39 @@ +package routes + +import ( + "easywish/internal/controllers" + + "github.com/gin-gonic/gin" +) + +func NewRouter(engine *gin.Engine, groups []RouteGroup) *gin.Engine { + apiGroup := engine.Group("/api") + for _, group := range groups { + subgroup := apiGroup.Group(group.BasePath) + subgroup.Use(group.Middleware...) + group.Router.RegisterRoutes(subgroup) + } + return engine +} + +type RouteGroup struct { + BasePath string + Middleware []gin.HandlerFunc + Router controllers.Router +} + +func NewRouteGroups( + authController controllers.AuthController, + serviceController controllers.ServiceController, +) []RouteGroup { + return []RouteGroup{ + { + BasePath: "/auth", + Router: authController, + }, + { + BasePath: "/service", + Router: serviceController, + }, + } +} diff --git a/backend/internal/routes/setup.go b/backend/internal/routes/setup.go index 8fe0445..3bfc0c0 100644 --- a/backend/internal/routes/setup.go +++ b/backend/internal/routes/setup.go @@ -1,52 +1,12 @@ package routes import ( - "easywish/internal/controllers" - "easywish/internal/middleware" - - "github.com/gin-gonic/gin" + "go.uber.org/fx" ) -func SetupRoutes(r *gin.Engine) *gin.Engine { - apiGroup := r.Group("/api") - { - serviceGroup := apiGroup.Group("/service") - { - serviceGroup.GET("/health", controllers.HealthCheck) - } - - authGroup := apiGroup.Group("/auth") - { - authGroup.POST("/registrationBegin", controllers.RegistrationBegin) - authGroup.POST("/registrationComplete", controllers.RegistrationComplete) - authGroup.POST("/login", controllers.Login) - authGroup.POST("/refresh", controllers.Refresh) - authGroup.POST("/passwordResetBegin", controllers.PasswordResetBegin) - authGroup.POST("/passwordResetComplete", controllers.PasswordResetComplete) - } - - profileGroup := apiGroup.Group("/profile") - { - profileGroup.GET("/:username", controllers.GetProfile) - } - - protected := apiGroup.Group("") - protected.Use(middleware.JWTAuthMiddleware()) - { - accountGroup := protected.Group("/account") - { - accountGroup.PUT("/changePassword", controllers.ChangePassword) - } - - profileGroup := protected.Group("/profile") - { - profileGroup.GET("/me", controllers.GetOwnProfile) - profileGroup.GET("/privacy", controllers.GetPrivacySettings) - profileGroup.PATCH("/privacy", controllers.UpdatePrivacySettings) - } - } - - } - - return r -} +var Module = fx.Module("routes", + fx.Provide( + NewRouteGroups, + ), + fx.Invoke(NewRouter), +) diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go index b8d0f72..55dafef 100644 --- a/backend/internal/services/auth.go +++ b/backend/internal/services/auth.go @@ -2,7 +2,6 @@ package services import ( "easywish/internal/database" - "easywish/internal/errors" errs "easywish/internal/errors" "easywish/internal/models" "easywish/internal/utils" @@ -23,32 +22,11 @@ func NewAuthService() AuthService { } func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) { - conn, ctx, err := utils.GetDbConn() - if err != nil { - return false, err - } - defer conn.Close(ctx) - - queries := database.New(conn) - - tx, err := database.Begin() - if err != nil { - return err - } - defer tx.Rollback() - qtx := queries.WithTx(tx) - - tx.Commit() - - return + return false, errs.ErrNotImplemented } func (a *authServiceImpl) RegistrationComplete(request models.RegistrationBeginRequest) (*models.RegistrationCompleteResponse, error) { - conn, ctx, err := utils.GetDbConn() - if err != nil { - return nil, err - } - defer conn.Close(ctx) + return nil, errs.ErrNotImplemented } func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginResponse, error) { @@ -75,5 +53,5 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo } func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) { - return nil, errors.ErrNotImplemented + return nil, errs.ErrNotImplemented } diff --git a/backend/internal/services/setup.go b/backend/internal/services/setup.go new file mode 100644 index 0000000..d93e5b0 --- /dev/null +++ b/backend/internal/services/setup.go @@ -0,0 +1,12 @@ +package services + +import ( + "go.uber.org/fx" +) + +var Module = fx.Module("services", + fx.Provide( + NewAuthService, + ), +) +