From cd0b5cdaa5035f1379d76926f5636bada03b6a71 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Wed, 8 May 2024 09:20:25 -0300 Subject: [PATCH 1/7] add listUserPets to users routes --- api/routes/routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/routes/routes.go b/api/routes/routes.go index fdb2d8b..485cb81 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -38,6 +38,7 @@ func InitRoutes(controllers Controllers, c *chi.Mux) { }) r.Route("/user", func(r chi.Router) { + r.Get("/{id}/my-pets", controllers.PetController.ListUserPets) r.Post("/token", controllers.UserController.GenerateToken) r.Post("/", controllers.UserController.Insert) r.Patch("/{id}", controllers.UserController.Update) From 46ca126366ef3caaf5854fa692e1d7e1c2630681 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Thu, 9 May 2024 17:35:30 -0300 Subject: [PATCH 2/7] feat: create Delete on user repository --- infra/db/user_repository.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/infra/db/user_repository.go b/infra/db/user_repository.go index f644e4d..07bae7c 100644 --- a/infra/db/user_repository.go +++ b/infra/db/user_repository.go @@ -24,6 +24,13 @@ func NewUserRepository(dbconn *sqlx.DB) interfaces.UserRepository { } func (ur *UserRepository) Delete(id uniqueEntityId.ID) error { + _, err := ur.dbconnection.Exec("UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?", id) + + if err != nil { + loggerUserRepository.Error(fmt.Errorf("#UserRepository.Delete error: %w", err)) + return fmt.Errorf("error on update user") + } + return nil } From b03fa76b502f124524bb5f0ce46aa47d97a985b0 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Mon, 13 May 2024 20:49:40 -0300 Subject: [PATCH 3/7] feat: create user delete endpoint --- api/controllers/user.go | 22 +++++++++++++++++++++- api/routes/routes.go | 1 + usecase/user.go | 12 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/api/controllers/user.go b/api/controllers/user.go index 72a8373..369edeb 100644 --- a/api/controllers/user.go +++ b/api/controllers/user.go @@ -23,7 +23,6 @@ func NewUserController(usecase *usecase.UserUsecase) *UserController { } } - func (uc *UserController) Insert(w http.ResponseWriter, r *http.Request) { var userDto dto.UserInsertDto err := json.NewDecoder(r.Body).Decode(&userDto) @@ -133,3 +132,24 @@ func (uc *UserController) FindByID(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +func (uc *UserController) Delete(w http.ResponseWriter, r *http.Request) { + IDStr := chi.URLParam(r, "id") + ID, err := uniqueEntityId.ParseID(IDStr) + + if err != nil { + uc.logger.Error("[#UserController.Delete] Erro ao tentar converter o body da requisiçao -> Erro: %v", err) + http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) + return + } + + err = uc.usecase.Delete(ID) + + if err != nil { + uc.logger.Error("[#UserController.Delete] Erro ao tentar deletar o usuário -> Erro: %v", err) + http.Error(w, "Erro ao tentar atualizar o usuário ", http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/api/routes/routes.go b/api/routes/routes.go index 485cb81..40431bd 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -43,6 +43,7 @@ func InitRoutes(controllers Controllers, c *chi.Mux) { r.Post("/", controllers.UserController.Insert) r.Patch("/{id}", controllers.UserController.Update) r.Get("/{id}", controllers.UserController.FindByID) + r.Delete("/{id}", controllers.UserController.Delete) }) }) } diff --git a/usecase/user.go b/usecase/user.go index 0e18e9b..b3b2c06 100644 --- a/usecase/user.go +++ b/usecase/user.go @@ -2,6 +2,7 @@ package usecase import ( "errors" + "fmt" "pet-dex-backend/v2/entity" "pet-dex-backend/v2/entity/dto" "pet-dex-backend/v2/infra/config" @@ -106,3 +107,14 @@ func (uc *UserUsecase) FindByID(ID uniqueEntityId.ID) (*entity.User, error) { return user, nil } + +func (uc *UserUsecase) Delete(userID uniqueEntityId.ID) error { + err := uc.repo.Delete(userID) + + if err != nil { + uc.logger.Error(fmt.Errorf("#UserUsecase.Delete error: %w", err)) + return err + } + + return nil +} From a40ac0a158ba6427ec689f360111a0a5ec47fbf4 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Fri, 24 May 2024 19:04:35 -0300 Subject: [PATCH 4/7] WIP --- api/controllers/user.go | 32 ++++++++++++- api/middlewares/auth.go | 17 +++++-- api/routes/routes.go | 46 +++++++++++-------- infra/db/user_repository.go | 17 +++++-- interfaces/user_repository.go | 2 +- .../v2/interfaces/mock_UserRepository.go | 4 +- usecase/user.go | 6 ++- 7 files changed, 91 insertions(+), 33 deletions(-) diff --git a/api/controllers/user.go b/api/controllers/user.go index 369edeb..a3cd449 100644 --- a/api/controllers/user.go +++ b/api/controllers/user.go @@ -3,8 +3,10 @@ package controllers import ( "encoding/json" "net/http" + "pet-dex-backend/v2/api/middlewares" "pet-dex-backend/v2/entity/dto" "pet-dex-backend/v2/infra/config" + "pet-dex-backend/v2/interfaces" "pet-dex-backend/v2/pkg/uniqueEntityId" "pet-dex-backend/v2/usecase" @@ -140,13 +142,41 @@ func (uc *UserController) Delete(w http.ResponseWriter, r *http.Request) { if err != nil { uc.logger.Error("[#UserController.Delete] Erro ao tentar converter o body da requisiçao -> Erro: %v", err) http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) + } + + userclaims, ok := r.Context().Value(middlewares.ContextKey("userClaims")).(interfaces.UserClaims) + if !ok { + logger.Error("[#UserController.Delete] Falha ao receber userclaims") + http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) return } err = uc.usecase.Delete(ID) + userIDFromUserclaims, err := uniqueEntityId.ParseID(userclaims.Id) + if err != nil { + logger.Error("[#UserController.Delete] Erro ao tentar receber o ID do token -> Erro: %v", err) + http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) + return + } + + IDStr := chi.URLParam(r, "id") + ID, err := uniqueEntityId.ParseID(IDStr) if err != nil { - uc.logger.Error("[#UserController.Delete] Erro ao tentar deletar o usuário -> Erro: %v", err) + logger.Error("[#UserController.Delete] Erro ao tentar converter o body da requisição -> Erro: %v", err) + http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) + return + } + + if userIDFromUserclaims != ID { + logger.Error("[#UserController.Delete] Erro ao tentar excluir outro usuário -> Erro: %v", err) + http.Error(w, "Usuário não autorizado a excluir este usuário", http.StatusUnauthorized) + return + } + + err = uc.usecase.Delete(ID) + if err != nil { + logger.Error("[#UserController.Delete] Erro ao tentar deletar o usuário -> Erro: %v", err) http.Error(w, "Erro ao tentar atualizar o usuário ", http.StatusBadRequest) return } diff --git a/api/middlewares/auth.go b/api/middlewares/auth.go index 9cf7ce3..2c0f2bc 100644 --- a/api/middlewares/auth.go +++ b/api/middlewares/auth.go @@ -1,6 +1,7 @@ package middlewares import ( + "context" "net/http" "pet-dex-backend/v2/infra/config" "pet-dex-backend/v2/pkg/encoder" @@ -8,30 +9,36 @@ import ( "time" ) +type ContextKey string + func AuthMiddleware(next http.Handler) http.Handler { + const UserClaimsContextKey ContextKey = "userClaims" + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { encoder := encoder.NewEncoderAdapter(config.GetEnvConfig().JWT_SECRET) authHeader := r.Header.Get("Authorization") if authHeader == "" { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } headerSplited := strings.Split(authHeader, " ") if len(headerSplited) != 2 { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } bearerToken := headerSplited[1] if bearerToken == "" { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } userclaims := encoder.ParseAccessToken(bearerToken) if userclaims.ExpiresAt != 0 && userclaims.ExpiresAt < time.Now().Unix() { - w.WriteHeader(401) + w.WriteHeader(http.StatusUnauthorized) return } - next.ServeHTTP(w, r) + + context := context.WithValue(r.Context(), UserClaimsContextKey, userclaims) + next.ServeHTTP(w, r.WithContext(context)) }) } diff --git a/api/routes/routes.go b/api/routes/routes.go index 40431bd..f721847 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -2,6 +2,7 @@ package routes import ( "pet-dex-backend/v2/api/controllers" + "pet-dex-backend/v2/api/middlewares" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -19,31 +20,36 @@ func InitRoutes(controllers Controllers, c *chi.Mux) { c.Route("/api", func(r chi.Router) { r.Use(middleware.AllowContentType("application/json")) - r.Route("/pets", func(r chi.Router) { - r.Route("/breeds", func(r chi.Router) { - r.Get("/", controllers.BreedController.List) - r.Get("/{breedID}", controllers.BreedController.FindBreed) + r.Group(func(private chi.Router) { + private.Use(middlewares.AuthMiddleware) + + private.Route("/pets", func(r chi.Router) { + r.Route("/breeds", func(r chi.Router) { + r.Get("/", controllers.BreedController.List) + }) + + r.Patch("/{petID}", controllers.PetController.Update) + r.Post("/", controllers.PetController.CreatePet) }) - r.Get("/breeds", controllers.BreedController.List) - r.Patch("/{petID}", controllers.PetController.Update) - r.Post("/", controllers.PetController.CreatePet) - }) + private.Route("/ongs", func(r chi.Router) { + r.Post("/", controllers.OngController.Insert) + r.Get("/", controllers.OngController.List) + r.Get("/{ongID}", controllers.OngController.FindByID) + r.Patch("/{ongID}", controllers.OngController.Update) + }) - r.Route("/ongs", func(r chi.Router) { - r.Post("/", controllers.OngController.Insert) - r.Get("/", controllers.OngController.List) - r.Get("/{ongID}", controllers.OngController.FindByID) - r.Patch("/{ongID}", controllers.OngController.Update) + private.Route("/user", func(r chi.Router) { + r.Get("/{id}/my-pets", controllers.PetController.ListUserPets) + r.Patch("/{id}", controllers.UserController.Update) + r.Get("/{id}", controllers.UserController.FindByID) + r.Delete("/{id}", controllers.UserController.Delete) + }) }) - r.Route("/user", func(r chi.Router) { - r.Get("/{id}/my-pets", controllers.PetController.ListUserPets) - r.Post("/token", controllers.UserController.GenerateToken) - r.Post("/", controllers.UserController.Insert) - r.Patch("/{id}", controllers.UserController.Update) - r.Get("/{id}", controllers.UserController.FindByID) - r.Delete("/{id}", controllers.UserController.Delete) + r.Group(func(public chi.Router) { + public.Post("/user", controllers.UserController.Insert) + public.Post("/user/token", controllers.UserController.GenerateToken) }) }) } diff --git a/infra/db/user_repository.go b/infra/db/user_repository.go index 07bae7c..02f9423 100644 --- a/infra/db/user_repository.go +++ b/infra/db/user_repository.go @@ -1,6 +1,7 @@ package db import ( + "database/sql" "fmt" "pet-dex-backend/v2/entity" "pet-dex-backend/v2/infra/config" @@ -28,7 +29,7 @@ func (ur *UserRepository) Delete(id uniqueEntityId.ID) error { if err != nil { loggerUserRepository.Error(fmt.Errorf("#UserRepository.Delete error: %w", err)) - return fmt.Errorf("error on update user") + return fmt.Errorf("error on delete user") } return nil @@ -163,8 +164,18 @@ func (ur *UserRepository) FindByID(ID uniqueEntityId.ID) (*entity.User, error) { return &user, nil } -func (ur *UserRepository) FindByEmail(email string) *entity.User { - return &entity.User{} +func (ur *UserRepository) FindByEmail(email string) (*entity.User, error) { + var user entity.User + err := ur.dbconnection.QueryRow("SELECT name, pass, email FROM users WHERE email = ?", email).Scan(&user.Name, &user.Pass, &user.Email) + + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("error retrieving user: %w", err) + } + + return &user, nil } func (ur *UserRepository) List() (users []entity.User, err error) { diff --git a/interfaces/user_repository.go b/interfaces/user_repository.go index 910222f..55342ab 100644 --- a/interfaces/user_repository.go +++ b/interfaces/user_repository.go @@ -10,7 +10,7 @@ type UserRepository interface { Update(userID uniqueEntityId.ID, user entity.User) error Delete(id uniqueEntityId.ID) error FindByID(ID uniqueEntityId.ID) (*entity.User, error) - FindByEmail(email string) *entity.User + FindByEmail(email string) (*entity.User, error) List() ([]entity.User, error) AdressRepo } diff --git a/mocks/pet-dex-backend/v2/interfaces/mock_UserRepository.go b/mocks/pet-dex-backend/v2/interfaces/mock_UserRepository.go index bb83e6d..dbccf38 100644 --- a/mocks/pet-dex-backend/v2/interfaces/mock_UserRepository.go +++ b/mocks/pet-dex-backend/v2/interfaces/mock_UserRepository.go @@ -128,7 +128,7 @@ func (_c *MockUserRepository_FindAddressByUserID_Call) RunAndReturn(run func(uui } // FindByEmail provides a mock function with given fields: email -func (_m *MockUserRepository) FindByEmail(email string) *entity.User { +func (_m *MockUserRepository) FindByEmail(email string) (*entity.User, error) { ret := _m.Called(email) if len(ret) == 0 { @@ -144,7 +144,7 @@ func (_m *MockUserRepository) FindByEmail(email string) *entity.User { } } - return r0 + return r0, nil } // MockUserRepository_FindByEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByEmail' diff --git a/usecase/user.go b/usecase/user.go index b3b2c06..54a2f83 100644 --- a/usecase/user.go +++ b/usecase/user.go @@ -57,7 +57,11 @@ func (uc *UserUsecase) Save(userDto dto.UserInsertDto) error { } func (uc *UserUsecase) GenerateToken(loginDto *dto.UserLoginDto) (string, error) { - user := uc.repo.FindByEmail(loginDto.Email) + user, err := uc.repo.FindByEmail(loginDto.Email) + if err != nil { + return "", errors.New("invalid credentials") + } + if user.Name == "" { return "", errors.New("invalid credentials") } From fea2cb55274cde0ffbe42e402b2edd9a0afadcc1 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Mon, 3 Jun 2024 17:16:38 -0300 Subject: [PATCH 5/7] feat: create user safe delete endpoint --- api/controllers/user.go | 24 +++--------------------- api/middlewares/auth.go | 18 ++++++------------ 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/api/controllers/user.go b/api/controllers/user.go index a3cd449..28549bb 100644 --- a/api/controllers/user.go +++ b/api/controllers/user.go @@ -3,10 +3,8 @@ package controllers import ( "encoding/json" "net/http" - "pet-dex-backend/v2/api/middlewares" "pet-dex-backend/v2/entity/dto" "pet-dex-backend/v2/infra/config" - "pet-dex-backend/v2/interfaces" "pet-dex-backend/v2/pkg/uniqueEntityId" "pet-dex-backend/v2/usecase" @@ -136,23 +134,8 @@ func (uc *UserController) FindByID(w http.ResponseWriter, r *http.Request) { } func (uc *UserController) Delete(w http.ResponseWriter, r *http.Request) { - IDStr := chi.URLParam(r, "id") - ID, err := uniqueEntityId.ParseID(IDStr) - - if err != nil { - uc.logger.Error("[#UserController.Delete] Erro ao tentar converter o body da requisiçao -> Erro: %v", err) - http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) - } - - userclaims, ok := r.Context().Value(middlewares.ContextKey("userClaims")).(interfaces.UserClaims) - if !ok { - logger.Error("[#UserController.Delete] Falha ao receber userclaims") - http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) - return - } - - err = uc.usecase.Delete(ID) - userIDFromUserclaims, err := uniqueEntityId.ParseID(userclaims.Id) + userIDFromTokenStr := r.Header.Get("UserId") + userIDFromToken, err := uniqueEntityId.ParseID(userIDFromTokenStr) if err != nil { logger.Error("[#UserController.Delete] Erro ao tentar receber o ID do token -> Erro: %v", err) http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) @@ -161,14 +144,13 @@ func (uc *UserController) Delete(w http.ResponseWriter, r *http.Request) { IDStr := chi.URLParam(r, "id") ID, err := uniqueEntityId.ParseID(IDStr) - if err != nil { logger.Error("[#UserController.Delete] Erro ao tentar converter o body da requisição -> Erro: %v", err) http.Error(w, "Erro ao converter a requisição ", http.StatusBadRequest) return } - if userIDFromUserclaims != ID { + if userIDFromToken != ID { logger.Error("[#UserController.Delete] Erro ao tentar excluir outro usuário -> Erro: %v", err) http.Error(w, "Usuário não autorizado a excluir este usuário", http.StatusUnauthorized) return diff --git a/api/middlewares/auth.go b/api/middlewares/auth.go index 2c0f2bc..caeafd4 100644 --- a/api/middlewares/auth.go +++ b/api/middlewares/auth.go @@ -1,7 +1,6 @@ package middlewares import ( - "context" "net/http" "pet-dex-backend/v2/infra/config" "pet-dex-backend/v2/pkg/encoder" @@ -9,36 +8,31 @@ import ( "time" ) -type ContextKey string - func AuthMiddleware(next http.Handler) http.Handler { - const UserClaimsContextKey ContextKey = "userClaims" - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { encoder := encoder.NewEncoderAdapter(config.GetEnvConfig().JWT_SECRET) authHeader := r.Header.Get("Authorization") if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(401) return } headerSplited := strings.Split(authHeader, " ") if len(headerSplited) != 2 { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(401) return } bearerToken := headerSplited[1] if bearerToken == "" { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(401) return } userclaims := encoder.ParseAccessToken(bearerToken) if userclaims.ExpiresAt != 0 && userclaims.ExpiresAt < time.Now().Unix() { - w.WriteHeader(http.StatusUnauthorized) + w.WriteHeader(401) return } - - context := context.WithValue(r.Context(), UserClaimsContextKey, userclaims) - next.ServeHTTP(w, r.WithContext(context)) + r.Header.Add("userId", userclaims.Id) + next.ServeHTTP(w, r) }) } From 037a389180f4b5f5bce9f4a4b47e65c731852816 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Mon, 3 Jun 2024 17:30:07 -0300 Subject: [PATCH 6/7] test: add tests for Delete method in UserUsecase --- usecase/user_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/usecase/user_test.go b/usecase/user_test.go index 5a2f9a9..2e14417 100644 --- a/usecase/user_test.go +++ b/usecase/user_test.go @@ -259,3 +259,53 @@ func TestErrorUpdate(t *testing.T) { }) } } + +func TestDelete(t *testing.T) { + tcases := map[string]struct { + repo *mockInterfaces.MockUserRepository + inputID uniqueEntityId.ID + expectOutput error + }{ + "success": { + repo: mockInterfaces.NewMockUserRepository(t), + inputID: uniqueEntityId.NewID(), + expectOutput: nil, + }, + } + + for name, tcase := range tcases { + t.Run(name, func(t *testing.T) { + tcase.repo.On("Delete", tcase.inputID).Return(tcase.expectOutput) + + usecase := NewUserUsecase(tcase.repo, nil, nil) + err := usecase.Delete(tcase.inputID) + + assert.Equal(t, tcase.expectOutput, err, "expected error mismatch") + }) + } +} + +func TestErrorDelete(t *testing.T) { + tcases := map[string]struct { + repo *mockInterfaces.MockUserRepository + inputID uniqueEntityId.ID + expectOutput error + }{ + "errorDelete": { + repo: mockInterfaces.NewMockUserRepository(t), + inputID: uniqueEntityId.NewID(), + expectOutput: fmt.Errorf("error on delete user"), + }, + } + + for name, tcase := range tcases { + t.Run(name, func(t *testing.T) { + tcase.repo.On("Delete", tcase.inputID).Return(tcase.expectOutput) + + usecase := NewUserUsecase(tcase.repo, nil, nil) + err := usecase.Delete(tcase.inputID) + + assert.Equal(t, tcase.expectOutput, err, "expected error mismatch") + }) + } +} From dff638ed58a470c7f73a5abf336408f0506d4808 Mon Sep 17 00:00:00 2001 From: David Ambrozio Date: Tue, 4 Jun 2024 21:20:52 -0300 Subject: [PATCH 7/7] fix: change logger call on delete --- infra/db/user_repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/db/user_repository.go b/infra/db/user_repository.go index 02f9423..00b8554 100644 --- a/infra/db/user_repository.go +++ b/infra/db/user_repository.go @@ -28,7 +28,7 @@ func (ur *UserRepository) Delete(id uniqueEntityId.ID) error { _, err := ur.dbconnection.Exec("UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?", id) if err != nil { - loggerUserRepository.Error(fmt.Errorf("#UserRepository.Delete error: %w", err)) + ur.logger.Error("#UserRepository.Delete error: %w", err) return fmt.Errorf("error on delete user") }