diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index 612a022..45fd6ac 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -15,17 +15,17 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-// @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
+// @in header
+// @name Authorization
package main
@@ -46,7 +46,6 @@ import (
"easywish/internal/database"
"easywish/internal/logger"
redisclient "easywish/internal/redisClient"
- "easywish/internal/routes"
"easywish/internal/services"
"easywish/internal/validation"
@@ -74,7 +73,6 @@ func main() {
validation.Module,
controllers.Module,
- routes.Module,
fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine, syncLogger *logger.SyncLogger) {
diff --git a/backend/docs/docs.go b/backend/docs/docs.go
index e1df226..eb5e1dd 100644
--- a/backend/docs/docs.go
+++ b/backend/docs/docs.go
@@ -413,7 +413,7 @@ const docTemplate = `{
"200": {
"description": "Says whether it's healthy or not",
"schema": {
- "$ref": "#/definitions/controllers.HealthStatus"
+ "$ref": "#/definitions/models.HealthStatusResponse"
}
}
}
@@ -421,14 +421,6 @@ const docTemplate = `{
}
},
"definitions": {
- "controllers.HealthStatus": {
- "type": "object",
- "properties": {
- "healthy": {
- "type": "boolean"
- }
- }
- },
"models.ChangePasswordRequest": {
"type": "object",
"required": [
@@ -447,6 +439,14 @@ const docTemplate = `{
}
}
},
+ "models.HealthStatusResponse": {
+ "type": "object",
+ "properties": {
+ "healthy": {
+ "type": "boolean"
+ }
+ }
+ },
"models.LoginRequest": {
"type": "object",
"required": [
@@ -530,7 +530,8 @@ const docTemplate = `{
],
"properties": {
"refresh_token": {
- "type": "string"
+ "type": "string",
+ "maxLength": 2000
}
}
},
diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json
index af5b4dd..4f37e72 100644
--- a/backend/docs/swagger.json
+++ b/backend/docs/swagger.json
@@ -409,7 +409,7 @@
"200": {
"description": "Says whether it's healthy or not",
"schema": {
- "$ref": "#/definitions/controllers.HealthStatus"
+ "$ref": "#/definitions/models.HealthStatusResponse"
}
}
}
@@ -417,14 +417,6 @@
}
},
"definitions": {
- "controllers.HealthStatus": {
- "type": "object",
- "properties": {
- "healthy": {
- "type": "boolean"
- }
- }
- },
"models.ChangePasswordRequest": {
"type": "object",
"required": [
@@ -443,6 +435,14 @@
}
}
},
+ "models.HealthStatusResponse": {
+ "type": "object",
+ "properties": {
+ "healthy": {
+ "type": "boolean"
+ }
+ }
+ },
"models.LoginRequest": {
"type": "object",
"required": [
@@ -526,7 +526,8 @@
],
"properties": {
"refresh_token": {
- "type": "string"
+ "type": "string",
+ "maxLength": 2000
}
}
},
diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml
index 4454738..39f35c1 100644
--- a/backend/docs/swagger.yaml
+++ b/backend/docs/swagger.yaml
@@ -1,10 +1,5 @@
basePath: /api/
definitions:
- controllers.HealthStatus:
- properties:
- healthy:
- type: boolean
- type: object
models.ChangePasswordRequest:
properties:
old_password:
@@ -17,6 +12,11 @@ definitions:
- old_password
- password
type: object
+ models.HealthStatusResponse:
+ properties:
+ healthy:
+ type: boolean
+ type: object
models.LoginRequest:
properties:
password:
@@ -71,6 +71,7 @@ definitions:
models.RefreshRequest:
properties:
refresh_token:
+ maxLength: 2000
type: string
required:
- refresh_token
@@ -372,7 +373,7 @@ paths:
"200":
description: Says whether it's healthy or not
schema:
- $ref: '#/definitions/controllers.HealthStatus'
+ $ref: '#/definitions/models.HealthStatusResponse'
summary: Get health status
tags:
- Service
diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go
index 8e82813..0c264fc 100644
--- a/backend/internal/controllers/auth.go
+++ b/backend/internal/controllers/auth.go
@@ -19,7 +19,6 @@ package controllers
import (
errs "easywish/internal/errors"
- "easywish/internal/middleware"
"easywish/internal/models"
"easywish/internal/services"
"easywish/internal/utils"
@@ -31,73 +30,91 @@ import (
"go.uber.org/zap"
)
-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)
- ChangePassword(c *gin.Context)
- Router
-}
-
-type authControllerImpl struct {
- log *zap.Logger
+type AuthController struct {
auth services.AuthService
+ log *zap.Logger
}
-func NewAuthController(_log *zap.Logger, _auth services.AuthService) AuthController {
- return &authControllerImpl{log: _log, auth: _auth}
-}
-
-// @Summary Acquire tokens via login credentials (and 2FA code if needed)
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.LoginRequest true " "
-// @Success 200 {object} models.LoginResponse " "
-// @Failure 403 "Invalid login credentials"
-// @Router /auth/login [post]
-func (a *authControllerImpl) Login(c *gin.Context) {
- request, ok := utils.GetRequest[models.LoginRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
- return
+func NewAuthController(log *zap.Logger, auth services.AuthService) Controller {
+ ctrl := &AuthController{auth: auth, log: log}
+
+ return &controllerImpl{
+ Path: "/auth",
+ Middleware: []gin.HandlerFunc{},
+ Methods: []ControllerMethod{
+ {
+ HttpMethod: POST,
+ Path: "/registrationBegin",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.registrationBeginHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/registrationComplete",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.registrationCompleteHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/login",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.loginHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/refresh",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.refreshHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/passwordResetBegin",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.passwordResetBeginHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/passwordResetComplete",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.passwordResetCompleteHandler,
+ },
+ {
+ HttpMethod: POST,
+ Path: "/changePassword",
+ Authorization: enums.UserRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.changePasswordHandler,
+ },
+ },
}
+}
- response, err := a.auth.Login(request.User, request.Body)
-
+// @Summary Register an account
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RegistrationBeginRequest true " "
+// @Success 200 "Account is created and awaiting verification"
+// @Failure 409 "Username or email is already taken"
+// @Failure 429 "Too many recent registration attempts for this email"
+// @Router /auth/registrationBegin [post]
+func (ctrl *AuthController) registrationBeginHandler(c *gin.Context) {
+ request, err := GetRequest[models.RegistrationBeginRequest](c)
if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else {
- c.Status(http.StatusInternalServerError)
- }
return
}
- c.JSON(http.StatusOK, response)
-}
-
-// @Summary Request password reset email
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.PasswordResetBeginRequest true " "
-// @Router /auth/passwordResetBegin [post]
-// @Success 200 "Reset code sent to the email if it is attached to an account"
-// @Failure 429 "Too many recent requests for this email"
-func (a *authControllerImpl) PasswordResetBegin(c *gin.Context) {
- request, ok := utils.GetRequest[models.PasswordResetBeginRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
- return
- }
-
- response, err := a.auth.PasswordResetBegin(request.Body)
+ _, err = ctrl.auth.RegistrationBegin(request.Body)
if err != nil {
- if errors.Is(err, errs.ErrTooManyRequests) {
+ if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
+ c.Status(http.StatusConflict)
+ } else if errors.Is(err, errs.ErrTooManyRequests) {
c.Status(http.StatusTooManyRequests)
} else {
c.Status(http.StatusInternalServerError)
@@ -105,26 +122,53 @@ func (a *authControllerImpl) PasswordResetBegin(c *gin.Context) {
return
}
+ c.Status(http.StatusOK)
+}
+
+// @Summary Confirm with code, finish creating the account
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RegistrationCompleteRequest true " "
+// @Success 200 {object} models.RegistrationCompleteResponse " "
+// @Failure 403 "Invalid email or verification code"
+// @Router /auth/registrationComplete [post]
+func (ctrl *AuthController) registrationCompleteHandler(c *gin.Context) {
+ request, err := GetRequest[models.RegistrationCompleteRequest](c)
+ if err != nil {
+ return
+ }
+
+ response, err := ctrl.auth.RegistrationComplete(request.User, request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else if errors.Is(err, errs.ErrUnauthorized) {
+ c.Status(http.StatusUnauthorized)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
c.JSON(http.StatusOK, response)
}
-// @Summary Complete password reset via email code
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.PasswordResetCompleteRequest true " "
-// @Router /auth/passwordResetComplete [post]
-// @Success 200 {object} models.PasswordResetCompleteResponse " "
-// @Success 403 "Wrong verification code or username"
-func (a *authControllerImpl) PasswordResetComplete(c *gin.Context) {
-
- request, ok := utils.GetRequest[models.PasswordResetCompleteRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
+// @Summary Acquire tokens via login credentials (and 2FA code if needed)
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.LoginRequest true " "
+// @Success 200 {object} models.LoginResponse " "
+// @Failure 403 "Invalid login credentials"
+// @Router /auth/login [post]
+func (ctrl *AuthController) loginHandler(c *gin.Context) {
+ request, err := GetRequest[models.LoginRequest](c)
+ if err != nil {
return
}
-
- response, err := a.auth.PasswordResetComplete(request.Body)
+
+ response, err := ctrl.auth.Login(request.User, request.Body)
if err != nil {
if errors.Is(err, errs.ErrForbidden) {
c.Status(http.StatusForbidden)
@@ -133,28 +177,25 @@ func (a *authControllerImpl) PasswordResetComplete(c *gin.Context) {
}
return
}
-
+
c.JSON(http.StatusOK, response)
}
-
-// @Summary Receive new tokens via refresh token
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.RefreshRequest true " "
-// @Router /auth/refresh [post]
-// @Success 200 {object} models.RefreshResponse " "
-// @Failure 401 "Invalid refresh token"
-func (a *authControllerImpl) Refresh(c *gin.Context) {
-
- request, ok := utils.GetRequest[models.RefreshRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
+// @Summary Receive new tokens via refresh token
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.RefreshRequest true " "
+// @Router /auth/refresh [post]
+// @Success 200 {object} models.RefreshResponse " "
+// @Failure 401 "Invalid refresh token"
+func (ctrl *AuthController) refreshHandler(c *gin.Context) {
+ request, err := GetRequest[models.RefreshRequest](c)
+ if err != nil {
return
}
-
- response, err := a.auth.Refresh(request.Body)
+
+ response, err := ctrl.auth.Refresh(request.Body)
if err != nil {
if utils.ErrorIsOneOf(
err,
@@ -167,36 +208,87 @@ func (a *authControllerImpl) Refresh(c *gin.Context) {
) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
} else {
- c.JSON(http.StatusInternalServerError, err.Error())
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
return
}
-
+
c.JSON(http.StatusOK, response)
}
-// @Summary Register an account
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.RegistrationBeginRequest true " "
-// @Success 200 "Account is created and awaiting verification"
-// @Failure 409 "Username or email is already taken"
-// @Failure 429 "Too many recent registration attempts for this email"
-// @Router /auth/registrationBegin [post]
-func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
+// @Summary Request password reset email
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.PasswordResetBeginRequest true " "
+// @Router /auth/passwordResetBegin [post]
+// @Success 200 "Reset code sent to the email if it is attached to an account"
+// @Failure 429 "Too many recent requests for this email"
+func (ctrl *AuthController) passwordResetBeginHandler(c *gin.Context) {
+ request, err := GetRequest[models.PasswordResetBeginRequest](c)
+ if err != nil {
+ return
+ }
+
+ _, err = ctrl.auth.PasswordResetBegin(request.Body)
+ if err != nil {
+ if errors.Is(err, errs.ErrTooManyRequests) {
+ c.Status(http.StatusTooManyRequests)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.Status(http.StatusOK)
+}
- request, ok := utils.GetRequest[models.RegistrationBeginRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
+// @Summary Complete password reset via email code
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Param request body models.PasswordResetCompleteRequest true " "
+// @Router /auth/passwordResetComplete [post]
+// @Success 200 {object} models.PasswordResetCompleteResponse " "
+// @Success 403 "Wrong verification code or username"
+func (ctrl *AuthController) passwordResetCompleteHandler(c *gin.Context) {
+ request, err := GetRequest[models.PasswordResetCompleteRequest](c)
+ if err != nil {
return
}
- _, err := a.auth.RegistrationBegin(request.Body)
-
+ response, err := ctrl.auth.PasswordResetComplete(request.Body)
if err != nil {
- if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
- c.Status(http.StatusConflict)
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ c.JSON(http.StatusOK, response)
+}
+
+// @Summary Set new password using the old password
+// @Tags Auth
+// @Accept json
+// @Produce json
+// @Security JWT
+// @Param request body models.ChangePasswordRequest true " "
+// @Success 200 "Password successfully changed"
+// @Failure 403 "Invalid old password"
+// @Router /auth/changePassword [post]
+func (ctrl *AuthController) changePasswordHandler(c *gin.Context) {
+ request, err := GetRequest[models.ChangePasswordRequest](c)
+ if err != nil {
+ return
+ }
+
+ _, err = ctrl.auth.ChangePassword(request.Body, request.User)
+ if err != nil {
+ if errors.Is(err, errs.ErrForbidden) {
+ c.Status(http.StatusForbidden)
} else {
c.Status(http.StatusInternalServerError)
}
@@ -204,76 +296,4 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
}
c.Status(http.StatusOK)
- return
-}
-
-// @Summary Confirm with code, finish creating the account
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Param request body models.RegistrationCompleteRequest true " "
-// @Success 200 {object} models.RegistrationCompleteResponse " "
-// @Failure 403 "Invalid email or verification code"
-// @Router /auth/registrationComplete [post]
-func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
- request, ok := utils.GetRequest[models.RegistrationCompleteRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
- return
- }
-
- response, err := a.auth.RegistrationComplete(request.User, request.Body)
-
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else if errors.Is(err, errs.ErrUnauthorized) {
- c.Status(http.StatusUnauthorized)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
-}
-
-// @Summary Set new password using the old password
-// @Tags Auth
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Param request body models.ChangePasswordRequest true " "
-// @Success 200 "Password successfully changed"
-// @Failure 403 "Invalid old password"
-// @Router /auth/changePassword [post]
-func (a *authControllerImpl) ChangePassword(c *gin.Context) {
- request, ok := utils.GetRequest[models.ChangePasswordRequest](c)
- if !ok {
- c.Status(http.StatusBadRequest)
- return
- }
-
- response, err := a.auth.ChangePassword(request.Body, request.User)
-
- if err != nil {
- if errors.Is(err, errs.ErrForbidden) {
- c.Status(http.StatusForbidden)
- } else {
- c.Status(http.StatusInternalServerError)
- }
- return
- }
-
- c.JSON(http.StatusOK, response)
-}
-
-func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
- group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
- group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
- group.POST("/login", middleware.RequestMiddleware[models.LoginRequest](enums.GuestRole), a.Login)
- group.POST("/refresh", middleware.RequestMiddleware[models.RefreshRequest](enums.GuestRole), a.Refresh)
- group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.PasswordResetBeginRequest](enums.GuestRole), a.PasswordResetBegin)
- group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.PasswordResetCompleteRequest](enums.GuestRole), a.PasswordResetComplete)
- group.POST("/changePassword", middleware.RequestMiddleware[models.ChangePasswordRequest](enums.UserRole), a.ChangePassword)
}
diff --git a/backend/internal/controllers/controller.go b/backend/internal/controllers/controller.go
new file mode 100644
index 0000000..e9d3a85
--- /dev/null
+++ b/backend/internal/controllers/controller.go
@@ -0,0 +1,112 @@
+// Copyright (c) 2025 Nikolai Papin
+//
+// This file is part of Easywish
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+// the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package controllers
+
+import (
+ "easywish/internal/dto"
+ "easywish/internal/services"
+ "easywish/internal/utils/enums"
+ "easywish/internal/validation"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-playground/validator/v10"
+ "go.uber.org/zap"
+)
+
+var (
+ GET = "GET"
+ POST = "POST"
+ PUT = "PUT"
+ PATCH = "PATCH"
+ DELETE = "DELETE"
+)
+
+type ControllerMethod struct {
+ HttpMethod string
+ Path string
+ Authorization enums.Role
+ Middleware []gin.HandlerFunc
+ Function func (c *gin.Context)
+}
+
+type controllerImpl struct {
+ Path string
+ Middleware []gin.HandlerFunc
+ Methods []ControllerMethod
+}
+
+type Controller interface {
+ Setup(group *gin.RouterGroup, log *zap.Logger, auth services.AuthService)
+}
+
+func (ctrl *controllerImpl) Setup(group *gin.RouterGroup, log *zap.Logger, auth services.AuthService) {
+ ctrlGroup := group.Group(ctrl.Path)
+ ctrlGroup.Use(ctrl.Middleware...)
+
+ for _, method := range ctrl.Methods {
+ ctrlGroup.Handle(
+ method.HttpMethod,
+ method.Path,
+ append(
+ method.Middleware,
+ gin.HandlerFunc(func(c *gin.Context) {
+ clientInfo, _ := c.Get("client_info")
+ if clientInfo.(dto.ClientInfo).Role < method.Authorization {
+ c.AbortWithStatusJSON(
+ http.StatusForbidden,
+ gin.H{"error": "Insufficient authorization for this method"})
+ return
+ }
+ }),
+ method.Function)...,
+ )}
+}
+
+func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) {
+
+ var body ModelT
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return nil, err
+ }
+
+ // TODO: Think hard on a singleton for better performance
+ validate := validation.NewValidator()
+
+ if err := validate.Struct(body); err != nil {
+ errorList := err.(validator.ValidationErrors)
+ c.AbortWithStatusJSON(
+ http.StatusBadRequest,
+ gin.H{"error": errorList})
+ return nil, err
+ }
+
+ cinfoFromCtx, ok := c.Get("client_info"); if !ok {
+ c.AbortWithStatusJSON(
+ http.StatusInternalServerError,
+ gin.H{"error": "Client info was not found"})
+ panic("No client_info found in gin context. Does the handler use AuthMiddleware?")
+ }
+ cinfo := cinfoFromCtx.(dto.ClientInfo)
+
+ return &dto.Request[ModelT]{
+ Body: body,
+ User: cinfo,
+ }, nil
+}
diff --git a/backend/internal/controllers/profile.go b/backend/internal/controllers/profile.go
deleted file mode 100644
index 5a4fbd8..0000000
--- a/backend/internal/controllers/profile.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (c) 2025 Nikolai Papin
-//
-// This file is part of Easywish
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
-// the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-package controllers
-
-import (
- "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
-// @Produce json
-// @Param username path string true "Username"
-// @Security JWT
-// @Router /profile/{username} [get]
-func (p *profileControllerImpl) GetProfile(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Get own profile when authorized
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/me [get]
-func (p *profileControllerImpl) GetOwnProfile(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @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
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/privacy [get]
-func (p *profileControllerImpl) GetPrivacySettings(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-// @Summary Update profile privacy settings
-// @Tags Profile
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /profile/privacy [patch]
-func (p *profileControllerImpl) UpdatePrivacySettings(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
-}
-
-func (p *profileControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
-}
diff --git a/backend/internal/controllers/service.go b/backend/internal/controllers/service.go
index d0012a4..38b959a 100644
--- a/backend/internal/controllers/service.go
+++ b/backend/internal/controllers/service.go
@@ -1,56 +1,60 @@
// Copyright (c) 2025 Nikolai Papin
-//
+//
// This file is part of Easywish
-//
+//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
-//
+//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
// the GNU General Public License for more details.
-//
+//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
package controllers
import (
+ "easywish/internal/models"
+ "easywish/internal/utils/enums"
"net/http"
"github.com/gin-gonic/gin"
)
-type ServiceController interface {
- HealthCheck(c *gin.Context)
- Router
+type ServiceController struct {}
+
+func NewServiceController() Controller {
+
+ ctrl := &ServiceController{}
+
+ return &controllerImpl{
+ Path: "/service",
+ Middleware: []gin.HandlerFunc{},
+ Methods: []ControllerMethod{
+
+ {
+ HttpMethod: GET,
+ Path: "/health",
+ Authorization: enums.GuestRole,
+ Middleware: []gin.HandlerFunc{},
+ Function: ctrl.healthHandler,
+ },
+
+ },
+ }
}
-type serviceControllerImpl struct{}
-
-func NewServiceController() ServiceController {
- return &serviceControllerImpl{}
-}
-
-// HealthCheck implements ServiceController.
-// @Summary Get health status
-// @Description Used internally for checking service health
-// @Tags Service
-// @Accept json
-// @Produce json
-// @Success 200 {object} HealthStatus "Says whether it's healthy or not"
-// @Router /service/health [get]
-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"`
+// @Summary Get health status
+// @Description Used internally for checking service health
+// @Tags Service
+// @Accept json
+// @Produce json
+// @Success 200 {object} models.HealthStatusResponse "Says whether it's healthy or not"
+// @Router /service/health [get]
+func (ctrl *ServiceController) healthHandler(c *gin.Context) {
+ c.JSON(http.StatusOK, models.HealthStatusResponse{Healthy: true,})
}
diff --git a/backend/internal/controllers/setup.go b/backend/internal/controllers/setup.go
index 5f4ba0a..9816a9f 100644
--- a/backend/internal/controllers/setup.go
+++ b/backend/internal/controllers/setup.go
@@ -1,30 +1,72 @@
// Copyright (c) 2025 Nikolai Papin
-//
+//
// This file is part of Easywish
-//
+//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
-//
+//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
// the GNU General Public License for more details.
-//
+//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
package controllers
import (
+ "easywish/internal/dto"
+ "easywish/internal/middleware"
+ "easywish/internal/services"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
"go.uber.org/fx"
+ "go.uber.org/zap"
)
+type SetupControllersParams struct {
+ fx.In
+ Controllers []Controller `group:"controllers"`
+ Log *zap.Logger
+ Auth services.AuthService
+ Group *gin.Engine
+}
+
+func setupControllers(p SetupControllersParams) {
+
+ apiGroup := p.Group.Group("/api")
+ apiGroup.Use(middleware.AuthMiddleware(p.Log, p.Auth))
+ apiGroup.Use(gin.HandlerFunc(func(c *gin.Context) {
+ ip := c.ClientIP()
+ userAgent := c.Request.UserAgent()
+ sessionInfoFromCtx, ok := c.Get("session_info"); if !ok {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid or missing session data"})
+ return
+ }
+
+ sessionInfo := sessionInfoFromCtx.(dto.SessionInfo)
+
+ c.Set("client_info", dto.ClientInfo{
+ SessionInfo: sessionInfo,
+ IP: ip,
+ UserAgent: userAgent,
+ })
+
+ c.Next()
+ }))
+ for _, ctrl := range p.Controllers {
+ ctrl.Setup(apiGroup, p.Log, p.Auth)
+ }
+}
+
var Module = fx.Module("controllers",
fx.Provide(
- NewServiceController,
- NewAuthController,
- NewProfileController,
- ),
+ fx.Annotate(NewAuthController, fx.ResultTags(`group:"controllers"`)),
+ fx.Annotate(NewServiceController, fx.ResultTags(`group:"controllers"`)),
+ ),
+ fx.Invoke(setupControllers),
)
diff --git a/backend/internal/controllers/router.go b/backend/internal/errors/controller.go
similarity index 83%
rename from backend/internal/controllers/router.go
rename to backend/internal/errors/controller.go
index 20731b5..4489316 100644
--- a/backend/internal/controllers/router.go
+++ b/backend/internal/errors/controller.go
@@ -1,26 +1,24 @@
// Copyright (c) 2025 Nikolai Papin
-//
+//
// This file is part of Easywish
-//
+//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
-//
+//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
// the GNU General Public License for more details.
-//
+//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-package controllers
+package errors
-import (
- "github.com/gin-gonic/gin"
+import "errors"
+
+var (
+ ErrClientInfoNotProvided = errors.New("No client info provded")
)
-
-type Router interface {
- RegisterRoutes(group *gin.RouterGroup)
-}
diff --git a/backend/internal/middleware/request.go b/backend/internal/middleware/request.go
index a729644..7ebfa2a 100644
--- a/backend/internal/middleware/request.go
+++ b/backend/internal/middleware/request.go
@@ -30,6 +30,7 @@ import (
const requestKey = "request"
+// Deprecated: no longer used, embedded into controllers.GetRequest instead
func ClientInfoFromContext(c *gin.Context) (*dto.ClientInfo, bool) {
var ok bool
@@ -58,10 +59,12 @@ func ClientInfoFromContext(c *gin.Context) (*dto.ClientInfo, bool) {
}, true
}
+// Deprecated: no longer used, see controllers.GetRequest
func RequestFromContext[T any](c *gin.Context) dto.Request[T] {
return c.Value(requestKey).(dto.Request[T])
}
+// Deprecated: no longer used, see controllers.GetRequest
func RequestMiddleware[T any](role enums.Role) gin.HandlerFunc {
return gin.HandlerFunc(func(c *gin.Context) {
diff --git a/backend/internal/controllers/account.go b/backend/internal/models/service.go
similarity index 69%
rename from backend/internal/controllers/account.go
rename to backend/internal/models/service.go
index 3f2e1d3..373b050 100644
--- a/backend/internal/controllers/account.go
+++ b/backend/internal/models/service.go
@@ -1,34 +1,22 @@
// Copyright (c) 2025 Nikolai Papin
-//
+//
// This file is part of Easywish
-//
+//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
-//
+//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
// the GNU General Public License for more details.
-//
+//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-package controllers
+package models
-import (
- "net/http"
-
- "github.com/gin-gonic/gin"
-)
-
-// @Summary Change account password
-// @Tags Account
-// @Accept json
-// @Produce json
-// @Security JWT
-// @Router /account/changePassword [put]
-func ChangePassword(c *gin.Context) {
- c.Status(http.StatusNotImplemented)
+type HealthStatusResponse struct {
+ Healthy bool `json:"healthy"`
}
diff --git a/backend/internal/routes/router.go b/backend/internal/routes/router.go
index 7a263f7..e211a14 100644
--- a/backend/internal/routes/router.go
+++ b/backend/internal/routes/router.go
@@ -26,6 +26,7 @@ import (
"go.uber.org/zap"
)
+// Deprecated: no longer used, see controllers
func NewRouter(engine *gin.Engine, log *zap.Logger, auth services.AuthService, groups []RouteGroup) *gin.Engine {
apiGroup := engine.Group("/api")
apiGroup.Use(middleware.AuthMiddleware(log, auth))
@@ -37,29 +38,30 @@ func NewRouter(engine *gin.Engine, log *zap.Logger, auth services.AuthService, g
return engine
}
+// Deprecated: no longer used, see controllers
type RouteGroup struct {
BasePath string
Middleware []gin.HandlerFunc
Router controllers.Router
}
-func NewRouteGroups(
- authController controllers.AuthController,
- serviceController controllers.ServiceController,
- profileController controllers.ProfileController,
-) []RouteGroup {
- return []RouteGroup{
- {
- BasePath: "/auth",
- Router: authController,
- },
- {
- BasePath: "/service",
- Router: serviceController,
- },
- {
- BasePath: "/profile",
- Router: profileController,
- },
- }
-}
+// func NewRouteGroups(
+// authController controllers.AuthController,
+// serviceController controllers.ServiceController,
+// profileController controllers.ProfileController,
+// ) []RouteGroup {
+// return []RouteGroup{
+// {
+// BasePath: "/auth",
+// Router: authController,
+// },
+// {
+// BasePath: "/service",
+// Router: serviceController,
+// },
+// {
+// BasePath: "/profile",
+// Router: profileController,
+// },
+// }
+// }
diff --git a/backend/internal/routes/setup.go b/backend/internal/routes/setup.go
index 5a1e8f7..03a8c37 100644
--- a/backend/internal/routes/setup.go
+++ b/backend/internal/routes/setup.go
@@ -21,9 +21,10 @@ import (
"go.uber.org/fx"
)
+// Deprecated: no longer used, see controllers
var Module = fx.Module("routes",
- fx.Provide(
- NewRouteGroups,
- ),
- fx.Invoke(NewRouter),
+ // fx.Provide(
+ // NewRouteGroups,
+ // ),
+ // fx.Invoke(NewRouter),
)
diff --git a/backend/internal/utils/request.go b/backend/internal/utils/request.go
index 81e3195..dcec554 100644
--- a/backend/internal/utils/request.go
+++ b/backend/internal/utils/request.go
@@ -22,6 +22,7 @@ import (
"github.com/gin-gonic/gin"
)
+// Deprecated: use controllers.GetRequest method for the new controllers
func GetRequest[T any](c *gin.Context) (*dto.Request[T], bool) {
req, ok := c.Get("request")