diff --git a/cmd/main.go b/cmd/main.go index fd4c50b..14cd0bd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,6 +8,8 @@ import ( "merch-parser-api/internal/api/user" "merch-parser-api/internal/app" "merch-parser-api/internal/interfaces" + "merch-parser-api/internal/provider/auth" + "merch-parser-api/internal/provider/token" "merch-parser-api/internal/router" "merch-parser-api/pkg/db" "merch-parser-api/pkg/utils" @@ -15,45 +17,58 @@ import ( // @Title Merch Parser // @BasePath /api/v2 -// @Version 2.0.0-alpha +// @Version 2.0.0-alpha // @SecurityDefinitions.apikey BearerAuth // @In header // @Name Authorization -// @Description Введите "Bearer {your_token}" для аутентификации +// @Description Введите "Bearer {your_token}" для аутентификации func main() { + log.Debug("Starting merch-parser-api") //setup config //c := config.NewConfig() c := config.DevConfig() ctx := context.Background() + //log level + config.LogSetup(c.AppConf.LogLvl) + database, err := db.Connection(c) if err != nil { log.WithError(err).Fatal("Main | Error connecting to database") } - _ = database + //base providers + jwtProvider := token.NewJWT(token.Deps{ + SecretKey: c.JWTConf.Secret, + Issuer: c.JWTConf.Issuer, + AccessExpire: c.JWTConf.AccessExpire, + RefreshExpire: c.JWTConf.RefreshExpire, + }) + log.Debug("JWT provider initialized") + utilsProvider := utils.NewUtils() + log.Debug("Utils provider initialized") + + //deps providers routerHandler := router.NewRouter(router.Deps{ ApiPrefix: c.AppConf.ApiPrefix, GinMode: c.AppConf.GinMode, + TokenProv: jwtProvider, }) + log.Debug("Router handler initialized") - //base providers - //jwtProv := token.NewJWT(token.Deps{ - // SecretKey: c.JWTConf.Secret, - // Issuer: c.JWTConf.Issuer, - // AccessExpire: c.JWTConf.AccessExpire, - // RefreshExpire: c.JWTConf.RefreshExpire, - //}) - - utilsProv := utils.NewUtils() - - //deps providers + authProvider := auth.NewHandler(auth.Deps{ + DB: database, + JwtProvider: jwtProvider, + Utils: utilsProvider, + }) + log.Debug("Auth provider initialized") //register app modules users := user.NewHandler(user.Deps{ + Auth: authProvider, DB: database, - Utils: utilsProv, + Utils: utilsProvider, }) //collect modules diff --git a/internal/api/user/controller.go b/internal/api/user/controller.go index 59b32b0..91a3056 100644 --- a/internal/api/user/controller.go +++ b/internal/api/user/controller.go @@ -28,23 +28,28 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { userGroup.GET("/", h.controller.get) userGroup.PUT("/", h.controller.update) userGroup.DELETE("/", h.controller.delete) + + //auth + userGroup.POST("/login", h.controller.login) + userGroup.POST("/logout", h.controller.logout) + userGroup.POST("/refresh", h.controller.refresh) } func (h *Handler) ExcludeRoutes() []shared.ExcludeRoute { return []shared.ExcludeRoute{ {Route: "/user", Method: http.MethodPost}, + {Route: "/user/login", Method: http.MethodPost}, } } -// @Summary Регистрация нового пользователя -// @Description Регистрация нового пользователя +// @Summary Регистрация нового пользователя +// @Description Регистрация нового пользователя // @Tags Users -// @Security BearerAuth // @Accept json // @Param body body Register true "новый пользователь" -// @Success 200 -// @Failure 400 {object} responses.ErrorResponse400 -// @Failure 500 {object} responses.ErrorResponse500 +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 // @Router /user [post] func (co *controller) register(c *gin.Context) { var register Register @@ -63,14 +68,14 @@ func (co *controller) register(c *gin.Context) { c.Status(http.StatusOK) } -// @Summary Получить информацию о пользователе -// @Description Получает информацию о пользователе по его uuid из токена +// @Summary Получить информацию о пользователе +// @Description Получает информацию о пользователе по его uuid из токена // @Tags Users // @Security BearerAuth -// @Success 200 {object} Info -// @Failure 400 {object} responses.ErrorResponse400 -// @Failure 401 {object} responses.ErrorResponse401 -// @Failure 500 {object} responses.ErrorResponse500 +// @Success 200 {object} Info +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 401 {object} responses.ErrorResponse401 +// @Failure 500 {object} responses.ErrorResponse500 // @Router /user [get] func (co *controller) get(c *gin.Context) { userUuid, err := co.utils.GetUserUuidFromContext(c) @@ -90,16 +95,16 @@ func (co *controller) get(c *gin.Context) { c.JSON(http.StatusOK, response) } -// @Summary Обновить информацию о пользователе -// @Description Обновить информацию о пользователе по его uuid из токена +// @Summary Обновить информацию о пользователе +// @Description Обновить информацию о пользователе по его uuid из токена // @Tags Users // @Security BearerAuth // @Accept json // @Param body body Update true "изменения" -// @Success 200 -// @Failure 400 {object} responses.ErrorResponse400 -// @Failure 401 {object} responses.ErrorResponse401 -// @Failure 500 {object} responses.ErrorResponse500 +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 401 {object} responses.ErrorResponse401 +// @Failure 500 {object} responses.ErrorResponse500 // @Router /user [put] func (co *controller) update(c *gin.Context) { userUuid, err := co.utils.GetUserUuidFromContext(c) @@ -125,14 +130,14 @@ func (co *controller) update(c *gin.Context) { c.Status(http.StatusOK) } -// @Summary Удалить пользователя -// @Description Помечает пользователя как удаленного по его uuid из токена +// @Summary Удалить пользователя +// @Description Помечает пользователя как удаленного по его uuid из токена // @Tags Users // @Security BearerAuth -// @Success 200 -// @Failure 400 {object} responses.ErrorResponse400 -// @Failure 401 {object} responses.ErrorResponse401 -// @Failure 500 {object} responses.ErrorResponse500 +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 401 {object} responses.ErrorResponse401 +// @Failure 500 {object} responses.ErrorResponse500 // @Router /user [delete] func (co *controller) delete(c *gin.Context) { userUuid, err := co.utils.GetUserUuidFromContext(c) @@ -150,3 +155,82 @@ func (co *controller) delete(c *gin.Context) { c.Status(http.StatusOK) } + +// @Summary Логин +// @Description Логин +// @Tags Users - auth +// @Accept json +// @Param body body Login true "логин" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /user/login [post] +func (co *controller) login(c *gin.Context) { + var login Login + if err := c.ShouldBindJSON(&login); err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error("User | Failed to bind JSON on login") + return + } + + response, err := co.service.login(login) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error("User | Failed to login") + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Логаут +// @Description Логаут. Для логаута надо передать refresh token, он будет инвалидирован. +// @Tags Users - auth +// @Security BearerAuth +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /user/logout [post] +func (co *controller) logout(c *gin.Context) { + userUuid, tokenUuid, err := co.utils.GetUserAndTokenUuidFromContext(c) + if err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error("User | Failed to get uuids from context on refresh") + return + } + + if err = co.service.logout(userUuid, tokenUuid); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error("User | Failed to logout") + return + } + + c.Status(http.StatusOK) +} + +// @Summary Обновление аксесс токена по рефреш токену. +// @Description Принимает рефреш токен в заголовке Authorization +// @Tags Users - auth +// @Security BearerAuth +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400s +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /user/refresh [post] +func (co *controller) refresh(c *gin.Context) { + //токены будут помещены в контекст при срабатывании мидлвари авторизации + userUuid, tokenUuid, err := co.utils.GetUserAndTokenUuidFromContext(c) + if err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error("User | Failed to get uuids from context on refresh") + return + } + + response, err := co.service.refresh(userUuid, tokenUuid) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error("User | Failed to refresh user info") + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/api/user/handler.go b/internal/api/user/handler.go index 295d8d3..2b0c143 100644 --- a/internal/api/user/handler.go +++ b/internal/api/user/handler.go @@ -12,13 +12,14 @@ type Handler struct { } type Deps struct { + Auth interfaces.Auth DB *gorm.DB Utils interfaces.Utils } func NewHandler(deps Deps) *Handler { r := newRepo(deps.DB) - s := newService(r) + s := newService(deps.Auth, r, deps.Utils) c := newController(s, deps.Utils) return &Handler{ diff --git a/internal/api/user/repository.go b/internal/api/user/repository.go index d7f0808..c9cd5c3 100644 --- a/internal/api/user/repository.go +++ b/internal/api/user/repository.go @@ -14,12 +14,22 @@ func newRepo(db *gorm.DB) *repo { } type UserRepo interface { + userCrud + userAuth +} + +type userCrud interface { register(user User) error getByUuid(userUuid string) (User, error) update(user map[string]any) error delete(userUuid string) error } +type userAuth interface { + getUserByEmail(email string) (User, error) +} + +// user CRUD methods func (r *repo) register(user User) error { return r.db.Create(&user).Error } @@ -36,3 +46,9 @@ func (r *repo) update(user map[string]any) error { func (r *repo) delete(userUuid string) error { return r.db.Model(&User{}).Where("uuid = ?", userUuid).Update("deleted_at", time.Now().UTC()).Error } + +// methods for auth +func (r *repo) getUserByEmail(email string) (user User, err error) { + err = r.db.Where("email = ?", email).First(&user).Error + return user, err +} diff --git a/internal/api/user/service.go b/internal/api/user/service.go index b217fed..e0ce38b 100644 --- a/internal/api/user/service.go +++ b/internal/api/user/service.go @@ -2,27 +2,54 @@ package user import ( "database/sql" + "errors" "github.com/google/uuid" log "github.com/sirupsen/logrus" + "merch-parser-api/internal/interfaces" + "merch-parser-api/internal/shared" "time" ) type service struct { - repo UserRepo + auth interfaces.Auth + repo UserRepo + utils interfaces.Utils } -func newService(repo UserRepo) *service { - return &service{repo: repo} +func newService(auth interfaces.Auth, repo UserRepo, utils interfaces.Utils) *service { + return &service{ + auth: auth, + repo: repo, + utils: utils, + } } func (s *service) register(dto Register) error { + if !s.utils.IsEmail(dto.Email) { + return errors.New("email isn't valid") + } + + if len(dto.Password) < 1 { + return errors.New("password can't be empty") + } + + if len(dto.Username) > 255 { + return errors.New("username can't be longer than 255 characters") + } + + hashedPass, err := s.utils.HashPassword(dto.Password) + if err != nil { + log.WithError(err).Error("User | Failed to hash password on register") + return err + } + user := User{ CreatedAt: time.Now().UTC(), UpdatedAt: sql.NullTime{Valid: false}, DeletedAt: sql.NullTime{Valid: false}, Uuid: uuid.NewString(), Username: dto.Username, - Password: dto.Password, + Password: hashedPass, Email: dto.Email, Verified: 0, } @@ -64,3 +91,39 @@ func (s *service) update(userUuid string, update Update) error { func (s *service) delete(userUuid string) error { return s.repo.delete(userUuid) } + +func (s *service) login(login Login) (shared.AuthData, error) { + if !s.utils.IsEmail(login.Email) { + return shared.AuthData{}, errors.New("email isn't valid") + } + + if len(login.Password) < 1 { + return shared.AuthData{}, errors.New("password can't be empty") + } + + user, err := s.repo.getUserByEmail(login.Email) + if err != nil { + log.WithError(err).Error("User | Failed to get user by email") + return shared.AuthData{}, errors.New("invalid email or password") + } + + if err = s.utils.ComparePasswords(user.Password, login.Password); err != nil { + return shared.AuthData{}, errors.New("invalid email or password") + } + + authData, err := s.auth.Login(user.Uuid) + if err != nil { + log.WithError(err).Error("User | Failed to generate auth data") + return shared.AuthData{}, err + } + + return authData, nil +} + +func (s *service) logout(userUuid string, refreshUuid string) error { + return s.auth.Logout(userUuid, refreshUuid) +} + +func (s *service) refresh(userUuid string, refreshUuid string) (shared.AuthData, error) { + return s.auth.Refresh(userUuid, refreshUuid) +}