From 654c1eb7b593ec89d265d576c641fb095c4e0f41 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 13:33:06 +0300 Subject: [PATCH 1/8] feat: prototyping possible interaction of database, controllers, services with auth service --- backend/internal/controllers/auth.go | 19 +++++++++++++++ backend/internal/errors/auth.go | 3 +++ backend/internal/models/auth.go | 18 ++++++++++++++ backend/internal/services/auth.go | 35 +++++++++++++++++++++++++--- sqlc/query.sql | 2 +- 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go index fb11f20..941516c 100644 --- a/backend/internal/controllers/auth.go +++ b/backend/internal/controllers/auth.go @@ -14,6 +14,25 @@ import ( // @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) } diff --git a/backend/internal/errors/auth.go b/backend/internal/errors/auth.go index d7f6c90..a3d0959 100644 --- a/backend/internal/errors/auth.go +++ b/backend/internal/errors/auth.go @@ -6,4 +6,7 @@ import ( var ( ErrUnauthorized = errors.New("User is not authorized") + ErrUsernameTaken = errors.New("Provided username is already in use") + ErrInvalidCredentials = errors.New("Invalid username, password or TOTP code") + ErrInvalidToken = errors.New("Token is invalid or expired") ) diff --git a/backend/internal/models/auth.go b/backend/internal/models/auth.go index 97fb8df..e9bdecc 100644 --- a/backend/internal/models/auth.go +++ b/backend/internal/models/auth.go @@ -5,6 +5,24 @@ type Tokens struct { RefreshToken string `json:"refresh_token"` } +type RegistrationBeginRequest struct { + Username string `json:"username"` + Email *string `json:"email"` + Password string `json:"password"` // TODO: password checking +} + +type RegistrationCompleteRequest struct { + Username string `json:"username"` + VerificationCode string `json:"verification_code"` + Name string `json:"name"` + Birthday *string `json:"birthday"` + AvatarUrl *string `json:"avatar_url"` +} + +type RegistrationCompleteResponse struct { + Tokens +} + type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go index 1b9636c..b8d0f72 100644 --- a/backend/internal/services/auth.go +++ b/backend/internal/services/auth.go @@ -9,18 +9,48 @@ import ( ) type AuthService interface { + RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) + RegistrationComplete(request models.RegistrationBeginRequest) (*models.RegistrationCompleteResponse, error) Login(request models.LoginRequest) (*models.LoginResponse, error) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) } type authServiceImpl struct { - } func NewAuthService() AuthService { return &authServiceImpl{} } +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 +} + +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) +} + func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginResponse, error) { conn, ctx, err := utils.GetDbConn() if err != nil { @@ -44,7 +74,6 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo return &models.LoginResponse{Tokens: models.Tokens{AccessToken: accessToken, RefreshToken: refreshToken}}, nil } - func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) { - return nil, errors.ErrNotImplemented + return nil, errors.ErrNotImplemented } diff --git a/sqlc/query.sql b/sqlc/query.sql index 87fb266..4ceb2e2 100644 --- a/sqlc/query.sql +++ b/sqlc/query.sql @@ -74,7 +74,7 @@ WHERE users.username = $1; --: Login Information Object {{{ -;-- name: CreateLoginInformation :one +;-- name: CreateLoginInformation :on INSERT INTO login_informations(user_id, email, password_hash) VALUES ( $1, $2, crypt(@password::text, gen_salt('bf')) ) RETURNING *; -- 2.49.1 From aab55a143fa33c59ae82f48850ee15898fdecd72 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 16:14:55 +0300 Subject: [PATCH 2/8] 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, + ), +) + -- 2.49.1 From 9b7335a72efca5ede322edd4ffc9ab7d3d7f6e49 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 16:25:46 +0300 Subject: [PATCH 3/8] fix: got the gin logs back --- backend/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index fa8f2ea..5ec975b 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -44,7 +44,7 @@ func main() { // engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) // return engine // }, - gin.New, + gin.Default, controllers.NewAuthController, controllers.NewServiceController, ), -- 2.49.1 From 7ad1336c88c408a5f44d43abbf1c8d6c6367facd Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 16:34:41 +0300 Subject: [PATCH 4/8] experiment: swagger is back --- backend/cmd/main.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 5ec975b..35a416e 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -24,8 +24,9 @@ import ( "easywish/internal/logger" "easywish/internal/routes" "easywish/internal/services" - // swaggerfiles "github.com/swaggo/files" - // ginSwagger "github.com/swaggo/gin-swagger" + docs "easywish/docs" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" ) func main() { @@ -39,11 +40,6 @@ func main() { fx.New( services.Module, fx.Provide( - // func() *gin.Engine { - // engine := gin.Default() - // engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) - // return engine - // }, gin.Default, controllers.NewAuthController, controllers.NewServiceController, @@ -52,6 +48,11 @@ func main() { fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine) { + + // Swagger + docs.SwaggerInfo.Schemes = []string{"http"} + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + server := &http.Server{ Addr: ":8080", Handler: router, -- 2.49.1 From 8007b1173153f21c1e5da471b46ed17674386d3c Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 16:52:52 +0300 Subject: [PATCH 5/8] refactor: made logger a dependency --- backend/cmd/main.go | 21 +++++++++++---------- backend/internal/controllers/setup.go | 12 ++++++++++++ backend/internal/logger/logger.go | 20 +++++++++++++++++--- 3 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 backend/internal/controllers/setup.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 35a416e..57612c0 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -13,18 +13,21 @@ package main import ( - "time" - "net/http" "context" + "fmt" + "net/http" + "time" + "github.com/gin-gonic/gin" "go.uber.org/fx" "easywish/config" + docs "easywish/docs" "easywish/internal/controllers" "easywish/internal/logger" "easywish/internal/routes" "easywish/internal/services" - docs "easywish/docs" + swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) @@ -35,26 +38,24 @@ func main() { panic(err) } - defer logger.Sync() - fx.New( - services.Module, fx.Provide( + logger.NewLogger, gin.Default, - controllers.NewAuthController, - controllers.NewServiceController, ), + services.Module, + controllers.Module, routes.Module, - fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine) { // Swagger docs.SwaggerInfo.Schemes = []string{"http"} router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + // Gin server := &http.Server{ - Addr: ":8080", + Addr: fmt.Sprintf(":%s", config.GetConfig().Port), Handler: router, } diff --git a/backend/internal/controllers/setup.go b/backend/internal/controllers/setup.go new file mode 100644 index 0000000..a4bf298 --- /dev/null +++ b/backend/internal/controllers/setup.go @@ -0,0 +1,12 @@ +package controllers + +import ( + "go.uber.org/fx" +) + +var Module = fx.Module("controllers", + fx.Provide( + NewServiceController, + NewAuthController, + ), +) diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go index 051297e..d2586bd 100644 --- a/backend/internal/logger/logger.go +++ b/backend/internal/logger/logger.go @@ -1,17 +1,31 @@ package logger + import ( "sync" - "go.uber.org/zap" "easywish/config" + + "go.uber.org/zap" ) +type Logger interface { + Get() *zap.Logger + Sync() error +} + +type loggerImpl struct { +} + +func NewLogger() Logger { + return &loggerImpl{} +} + var ( logger *zap.Logger once sync.Once ) -func GetLogger() *zap.Logger { +func (l *loggerImpl) Get() *zap.Logger { once.Do(func() { var err error cfg := config.GetConfig() @@ -28,6 +42,6 @@ func GetLogger() *zap.Logger { return logger } -func Sync() error { +func (l *loggerImpl) Sync() error { return logger.Sync() } -- 2.49.1 From b72645852bbda3b3bd637a0e8ef6c122ed368508 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 17:53:11 +0300 Subject: [PATCH 6/8] refactor: profile controller; experiment: figured out a way to add auth middleware to individual methods in controllers, bypassing route group middleware if needed --- backend/internal/controllers/profile.go | 64 ++++++++++++++++--------- backend/internal/controllers/setup.go | 3 +- backend/internal/routes/router.go | 5 ++ 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go index 5cd8321..08aeeaf 100644 --- a/backend/internal/controllers/profile.go +++ b/backend/internal/controllers/profile.go @@ -1,11 +1,28 @@ package controllers import ( + "easywish/internal/middleware" "net/http" "github.com/gin-gonic/gin" ) +type ProfileController interface { + GetProfile(c *gin.Context) + GetOwnProfile(c *gin.Context) + UpdateProfile(c *gin.Context) + GetPrivacySettings(c *gin.Context) + UpdatePrivacySettings(c *gin.Context) + Router +} + +type profileControllerImpl struct { +} + +func NewProfileController() ProfileController { + return &profileControllerImpl{} +} + // @Summary Get someone's profile details // @Tags Profile // @Accept json @@ -13,20 +30,8 @@ import ( // @Param username path string true "Username" // @Security JWT // @Router /profile/{username} [get] -func GetProfile(c *gin.Context) { - - username := c.Param("username") - - if username == "" { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Username cannot be empty", - }) - return - } - - c.JSON(http.StatusNotImplemented, gin.H{ - "username": username, - }) +func (p *profileControllerImpl) GetProfile(c *gin.Context) { + c.Status(http.StatusNotImplemented) } // @Summary Get own profile when authorized @@ -35,13 +40,18 @@ func GetProfile(c *gin.Context) { // @Produce json // @Security JWT // @Router /profile/me [get] -func GetOwnProfile(c *gin.Context) { +func (p *profileControllerImpl) GetOwnProfile(c *gin.Context) { + c.Status(http.StatusNotImplemented) +} - username := "Gregory House" - - c.JSON(http.StatusNotImplemented, gin.H{ - "username": username, - }) +// @Summary Update profile +// @Tags Profile +// @Accept json +// @Produce json +// @Security JWT +// @Router /profile [patch] +func (p *profileControllerImpl) UpdateProfile(c *gin.Context) { + c.Status(http.StatusNotImplemented) } // @Summary Get profile privacy settings @@ -50,7 +60,7 @@ func GetOwnProfile(c *gin.Context) { // @Produce json // @Security JWT // @Router /profile/privacy [get] -func GetPrivacySettings(c *gin.Context) { +func (p *profileControllerImpl) GetPrivacySettings(c *gin.Context) { c.Status(http.StatusNotImplemented) } @@ -60,6 +70,16 @@ func GetPrivacySettings(c *gin.Context) { // @Produce json // @Security JWT // @Router /profile/privacy [patch] -func UpdatePrivacySettings(c *gin.Context) { +func (p *profileControllerImpl) UpdatePrivacySettings(c *gin.Context) { c.Status(http.StatusNotImplemented) } + +func (p *profileControllerImpl) RegisterRoutes(group *gin.RouterGroup) { + protected := group.Group("") + protected.Use(middleware.JWTAuthMiddleware()) + { + protected.GET("/me", p.GetOwnProfile) + protected.GET("/:username", p.GetProfile) + protected.GET("/privacy", p.GetPrivacySettings) + } +} diff --git a/backend/internal/controllers/setup.go b/backend/internal/controllers/setup.go index a4bf298..5b64227 100644 --- a/backend/internal/controllers/setup.go +++ b/backend/internal/controllers/setup.go @@ -1,12 +1,13 @@ package controllers import ( - "go.uber.org/fx" + "go.uber.org/fx" ) var Module = fx.Module("controllers", fx.Provide( NewServiceController, NewAuthController, + NewProfileController, ), ) diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go index ad1a538..abda7f2 100644 --- a/backend/internal/routes/router.go +++ b/backend/internal/routes/router.go @@ -25,6 +25,7 @@ type RouteGroup struct { func NewRouteGroups( authController controllers.AuthController, serviceController controllers.ServiceController, + profileController controllers.ProfileController, ) []RouteGroup { return []RouteGroup{ { @@ -35,5 +36,9 @@ func NewRouteGroups( BasePath: "/service", Router: serviceController, }, + { + BasePath: "/profile", + Router: profileController, + }, } } -- 2.49.1 From c57b37e88fe6d84080e165af6c94574f38a3e76d Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 17:55:10 +0300 Subject: [PATCH 7/8] fix: damaged sqlc query annotation --- sqlc/query.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlc/query.sql b/sqlc/query.sql index 4ceb2e2..87fb266 100644 --- a/sqlc/query.sql +++ b/sqlc/query.sql @@ -74,7 +74,7 @@ WHERE users.username = $1; --: Login Information Object {{{ -;-- name: CreateLoginInformation :on +;-- name: CreateLoginInformation :one INSERT INTO login_informations(user_id, email, password_hash) VALUES ( $1, $2, crypt(@password::text, gen_salt('bf')) ) RETURNING *; -- 2.49.1 From 0621bad6f8f75524f9a4309a64d1f6efc740a9a7 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Fri, 20 Jun 2025 17:55:42 +0300 Subject: [PATCH 8/8] chore: updated docs --- backend/docs/docs.go | 20 ++++++++++++++++++++ backend/docs/swagger.json | 20 ++++++++++++++++++++ backend/docs/swagger.yaml | 12 ++++++++++++ 3 files changed, 52 insertions(+) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 42b259c..044b645 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -146,6 +146,26 @@ const docTemplate = `{ "responses": {} } }, + "/profile": { + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update profile", + "responses": {} + } + }, "/profile/me": { "get": { "security": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 3679bc5..2133584 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -142,6 +142,26 @@ "responses": {} } }, + "/profile": { + "patch": { + "security": [ + { + "JWT": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Profile" + ], + "summary": "Update profile", + "responses": {} + } + }, "/profile/me": { "get": { "security": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index e66c72b..507b388 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -113,6 +113,18 @@ paths: summary: Confirm with code, finish creating the account tags: - Auth + /profile: + patch: + consumes: + - application/json + produces: + - application/json + responses: {} + security: + - JWT: [] + summary: Update profile + tags: + - Profile /profile/{username}: get: consumes: -- 2.49.1