diff --git a/backend/application/auth/resetpassword/usecase.go b/backend/application/auth/resetpassword/usecase.go index 55104bba..b1e99b9b 100644 --- a/backend/application/auth/resetpassword/usecase.go +++ b/backend/application/auth/resetpassword/usecase.go @@ -57,7 +57,7 @@ func (uc *UseCase) ResetPassword(request Request) (*Response, error) { return nil, err } - salt := make([]byte, 1024) + salt := make([]byte, 64) if _, err := rand.Read(salt); err != nil { return nil, err } diff --git a/backend/application/dashboard/profile/changepassword/request.go b/backend/application/dashboard/profile/changepassword/request.go new file mode 100644 index 00000000..cfcc3d65 --- /dev/null +++ b/backend/application/dashboard/profile/changepassword/request.go @@ -0,0 +1,13 @@ +package changepassword + +type validationErrors map[string]string + +type Request struct { + UserUUID string `json:"-"` + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` +} + +func (r *Request) Validate() (bool, validationErrors) { + return true, nil +} diff --git a/backend/application/dashboard/profile/changepassword/response.go b/backend/application/dashboard/profile/changepassword/response.go new file mode 100644 index 00000000..125e8e4c --- /dev/null +++ b/backend/application/dashboard/profile/changepassword/response.go @@ -0,0 +1,5 @@ +package changepassword + +type ChangePasswordResponse struct { + ValidationErrors validationErrors `json:"errors,omitempty"` +} diff --git a/backend/application/dashboard/profile/changepassword/usecase.go b/backend/application/dashboard/profile/changepassword/usecase.go new file mode 100644 index 00000000..e8ce09b5 --- /dev/null +++ b/backend/application/dashboard/profile/changepassword/usecase.go @@ -0,0 +1,61 @@ +package changepassword + +import ( + "crypto/rand" + + "github.com/khanzadimahdi/testproject/domain/password" + "github.com/khanzadimahdi/testproject/domain/user" +) + +type UseCase struct { + userRepository user.Repository + hasher password.Hasher +} + +func NewUseCase(userRepository user.Repository, hasher password.Hasher) *UseCase { + return &UseCase{ + userRepository: userRepository, + hasher: hasher, + } +} + +func (uc *UseCase) ChangePassword(request Request) (*ChangePasswordResponse, error) { + if ok, validation := request.Validate(); !ok { + return &ChangePasswordResponse{ + ValidationErrors: validation, + }, nil + } + + u, err := uc.userRepository.GetOne(request.UserUUID) + if err != nil { + return nil, err + } + + if !uc.passwordIsValid(u, []byte(request.CurrentPassword)) { + return &ChangePasswordResponse{ + ValidationErrors: validationErrors{ + "password": "password is not valid", + }, + }, nil + } + + salt := make([]byte, 64) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + u.PasswordHash = password.Hash{ + Value: uc.hasher.Hash([]byte(request.NewPassword), salt), + Salt: salt, + } + + if err := uc.userRepository.Save(&u); err != nil { + return nil, err + } + + return &ChangePasswordResponse{}, err +} + +func (uc *UseCase) passwordIsValid(u user.User, password []byte) bool { + return uc.hasher.Equal(password, u.PasswordHash.Value, u.PasswordHash.Salt) +} diff --git a/backend/application/dashboard/profile/changepassword/usecase_test.go b/backend/application/dashboard/profile/changepassword/usecase_test.go new file mode 100644 index 00000000..24f3fe22 --- /dev/null +++ b/backend/application/dashboard/profile/changepassword/usecase_test.go @@ -0,0 +1 @@ +package changepassword diff --git a/backend/application/dashboard/profile/getprofile/response.go b/backend/application/dashboard/profile/getprofile/response.go new file mode 100644 index 00000000..961e66f1 --- /dev/null +++ b/backend/application/dashboard/profile/getprofile/response.go @@ -0,0 +1,9 @@ +package getprofile + +type GetProfileResponse struct { + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + Email string `json:"email,omitempty"` + Username string `json:"username,omitempty"` +} diff --git a/backend/application/dashboard/profile/getprofile/usecase.go b/backend/application/dashboard/profile/getprofile/usecase.go new file mode 100644 index 00000000..598148d2 --- /dev/null +++ b/backend/application/dashboard/profile/getprofile/usecase.go @@ -0,0 +1,30 @@ +package getprofile + +import ( + "github.com/khanzadimahdi/testproject/domain/user" +) + +type UseCase struct { + userRepository user.Repository +} + +func NewUseCase(userRepository user.Repository) *UseCase { + return &UseCase{ + userRepository: userRepository, + } +} + +func (uc *UseCase) Profile(UUID string) (*GetProfileResponse, error) { + u, err := uc.userRepository.GetOne(UUID) + if err != nil { + return nil, err + } + + return &GetProfileResponse{ + UUID: UUID, + Name: u.Name, + Avatar: u.Avatar, + Email: u.Email, + Username: u.Username, + }, err +} diff --git a/backend/application/dashboard/profile/getprofile/usecase_test.go b/backend/application/dashboard/profile/getprofile/usecase_test.go new file mode 100644 index 00000000..c17a165a --- /dev/null +++ b/backend/application/dashboard/profile/getprofile/usecase_test.go @@ -0,0 +1 @@ +package getprofile diff --git a/backend/application/dashboard/profile/updateprofile/request.go b/backend/application/dashboard/profile/updateprofile/request.go new file mode 100644 index 00000000..8ac71d69 --- /dev/null +++ b/backend/application/dashboard/profile/updateprofile/request.go @@ -0,0 +1,15 @@ +package updateprofile + +type validationErrors map[string]string + +type Request struct { + UserUUID string `json:"-"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Email string `json:"email"` + Username string `json:"username"` +} + +func (r *Request) Validate() (bool, validationErrors) { + return true, nil +} diff --git a/backend/application/dashboard/profile/updateprofile/response.go b/backend/application/dashboard/profile/updateprofile/response.go new file mode 100644 index 00000000..47a79b27 --- /dev/null +++ b/backend/application/dashboard/profile/updateprofile/response.go @@ -0,0 +1,5 @@ +package updateprofile + +type UpdateProfileResponse struct { + ValidationErrors validationErrors `json:"errors,omitempty"` +} diff --git a/backend/application/dashboard/profile/updateprofile/usecase.go b/backend/application/dashboard/profile/updateprofile/usecase.go new file mode 100644 index 00000000..cd75bf9e --- /dev/null +++ b/backend/application/dashboard/profile/updateprofile/usecase.go @@ -0,0 +1,76 @@ +package updateprofile + +import ( + "github.com/khanzadimahdi/testproject/domain" + "github.com/khanzadimahdi/testproject/domain/user" +) + +type UseCase struct { + userRepository user.Repository +} + +func NewUseCase(userRepository user.Repository) *UseCase { + return &UseCase{ + userRepository: userRepository, + } +} + +func (uc *UseCase) UpdateProfile(request Request) (*UpdateProfileResponse, error) { + if ok, validation := request.Validate(); !ok { + return &UpdateProfileResponse{ + ValidationErrors: validation, + }, nil + } + + // make sure email is unique + exists, err := uc.anotherUserExists(request.Email, request.UserUUID) + if err != nil { + return nil, err + } else if exists { + return &UpdateProfileResponse{ + ValidationErrors: map[string]string{ + "email": "another user with this email already exists", + }, + }, nil + } + + // make sure username is unique + exists, err = uc.anotherUserExists(request.Username, request.UserUUID) + if err != nil { + return nil, err + } else if exists { + return &UpdateProfileResponse{ + ValidationErrors: map[string]string{ + "username": "another user with this email already exists", + }, + }, nil + } + + user, err := uc.userRepository.GetOne(request.UserUUID) + if err != nil { + return nil, err + } + + user.Name = request.Name + user.Avatar = request.Avatar + user.Email = request.Email + user.Username = request.Username + + err = uc.userRepository.Save(&user) + if err != nil { + return nil, err + } + + return &UpdateProfileResponse{}, err +} + +func (uc *UseCase) anotherUserExists(identity string, currentUserUUID string) (bool, error) { + u, err := uc.userRepository.GetOneByIdentity(identity) + if err == domain.ErrNotExists { + return false, nil + } else if err != nil { + return false, err + } + + return u.UUID != currentUserUUID, nil +} diff --git a/backend/application/dashboard/profile/updateprofile/usecase_test.go b/backend/application/dashboard/profile/updateprofile/usecase_test.go new file mode 100644 index 00000000..d533c8d6 --- /dev/null +++ b/backend/application/dashboard/profile/updateprofile/usecase_test.go @@ -0,0 +1 @@ +package updateprofile diff --git a/backend/main.go b/backend/main.go index 98a9017e..a43fc645 100644 --- a/backend/main.go +++ b/backend/main.go @@ -34,6 +34,9 @@ import ( dashboardGetFile "github.com/khanzadimahdi/testproject/application/dashboard/file/getFile" dashboardGetFiles "github.com/khanzadimahdi/testproject/application/dashboard/file/getFiles" dashboardUploadFile "github.com/khanzadimahdi/testproject/application/dashboard/file/uploadFile" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/changepassword" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/getprofile" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/updateprofile" getFile "github.com/khanzadimahdi/testproject/application/file/getFile" "github.com/khanzadimahdi/testproject/application/home" "github.com/khanzadimahdi/testproject/infrastructure/console" @@ -52,6 +55,7 @@ import ( dashboardArticleAPI "github.com/khanzadimahdi/testproject/presentation/http/api/dashboard/article" dashboardElementAPI "github.com/khanzadimahdi/testproject/presentation/http/api/dashboard/element" dashboardFileAPI "github.com/khanzadimahdi/testproject/presentation/http/api/dashboard/file" + "github.com/khanzadimahdi/testproject/presentation/http/api/dashboard/profile" fileAPI "github.com/khanzadimahdi/testproject/presentation/http/api/file" hashtagAPI "github.com/khanzadimahdi/testproject/presentation/http/api/hashtag" homeapi "github.com/khanzadimahdi/testproject/presentation/http/api/home" @@ -163,6 +167,10 @@ func httpHandler() http.Handler { router.Handler(http.MethodGet, "/files/:uuid", fileAPI.NewShowHandler(getFileUseCase)) // -------------------- dashboard -------------------- // + getProfile := getprofile.NewUseCase(userRepository) + updateProfile := updateprofile.NewUseCase(userRepository) + dashboardChangePassword := changepassword.NewUseCase(userRepository, hasher) + dashboardCreateArticleUsecase := dashboardCreateArticle.NewUseCase(articlesRepository) dashboardDeleteArticleUsecase := dashboardDeleteArticle.NewUseCase(articlesRepository) dashboardGetArticleUsecase := dashboardGetArticle.NewUseCase(articlesRepository) @@ -179,6 +187,11 @@ func httpHandler() http.Handler { dashboardGetElementsUsecase := dashboardGetElements.NewUseCase(elementsRepository) dashboardUpdateElementUsecase := dashboardUpdateElement.NewUseCase(elementsRepository) + // profile + router.Handler(http.MethodGet, "/api/dashboard/profile", middleware.NewAuthoriseMiddleware(profile.NewGetProfileHandler(getProfile), j, userRepository)) + router.Handler(http.MethodPut, "/api/dashboard/profile", middleware.NewAuthoriseMiddleware(profile.NewUpdateProfileHandler(updateProfile), j, userRepository)) + router.Handler(http.MethodPut, "/api/dashboard/password", middleware.NewAuthoriseMiddleware(profile.NewChangePasswordHandler(dashboardChangePassword), j, userRepository)) + // articles router.Handler(http.MethodPost, "/api/dashboard/articles", middleware.NewAuthoriseMiddleware(dashboardArticleAPI.NewCreateHandler(dashboardCreateArticleUsecase), j, userRepository)) router.Handler(http.MethodDelete, "/api/dashboard/articles/:uuid", middleware.NewAuthoriseMiddleware(dashboardArticleAPI.NewDeleteHandler(dashboardDeleteArticleUsecase), j, userRepository)) diff --git a/backend/presentation/http/api/dashboard/profile/changepassword.go b/backend/presentation/http/api/dashboard/profile/changepassword.go new file mode 100644 index 00000000..21135003 --- /dev/null +++ b/backend/presentation/http/api/dashboard/profile/changepassword.go @@ -0,0 +1,44 @@ +package profile + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/khanzadimahdi/testproject/application/auth" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/changepassword" + "github.com/khanzadimahdi/testproject/domain" +) + +type changePasswordHandler struct { + userCase *changepassword.UseCase +} + +func NewChangePasswordHandler(userCase *changepassword.UseCase) *changePasswordHandler { + return &changePasswordHandler{ + userCase: userCase, + } +} + +func (h *changePasswordHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var request changepassword.Request + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + request.UserUUID = auth.FromContext(r.Context()).UUID + + response, err := h.userCase.ChangePassword(request) + + switch true { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + case len(response.ValidationErrors) > 0: + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(response) + default: + rw.WriteHeader(http.StatusNoContent) + } +} diff --git a/backend/presentation/http/api/dashboard/profile/getprofile.go b/backend/presentation/http/api/dashboard/profile/getprofile.go new file mode 100644 index 00000000..b51040db --- /dev/null +++ b/backend/presentation/http/api/dashboard/profile/getprofile.go @@ -0,0 +1,37 @@ +package profile + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/khanzadimahdi/testproject/application/auth" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/getprofile" + "github.com/khanzadimahdi/testproject/domain" +) + +type getProfileHandler struct { + useCase *getprofile.UseCase +} + +func NewGetProfileHandler(useCase *getprofile.UseCase) *getProfileHandler { + return &getProfileHandler{ + useCase: useCase, + } +} + +func (h *getProfileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + userUUID := auth.FromContext(r.Context()).UUID + + response, err := h.useCase.Profile(userUUID) + + switch true { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + default: + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(response) + } +} diff --git a/backend/presentation/http/api/dashboard/profile/updateprofile.go b/backend/presentation/http/api/dashboard/profile/updateprofile.go new file mode 100644 index 00000000..f4e2870f --- /dev/null +++ b/backend/presentation/http/api/dashboard/profile/updateprofile.go @@ -0,0 +1,44 @@ +package profile + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/khanzadimahdi/testproject/application/auth" + "github.com/khanzadimahdi/testproject/application/dashboard/profile/updateprofile" + "github.com/khanzadimahdi/testproject/domain" +) + +type updateProfileHandler struct { + useCase *updateprofile.UseCase +} + +func NewUpdateProfileHandler(useCase *updateprofile.UseCase) *updateProfileHandler { + return &updateProfileHandler{ + useCase: useCase, + } +} + +func (h *updateProfileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var request updateprofile.Request + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + request.UserUUID = auth.FromContext(r.Context()).UUID + + response, err := h.useCase.UpdateProfile(request) + + switch true { + case errors.Is(err, domain.ErrNotExists): + rw.WriteHeader(http.StatusNotFound) + case err != nil: + rw.WriteHeader(http.StatusInternalServerError) + case len(response.ValidationErrors) > 0: + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(response) + default: + rw.WriteHeader(http.StatusNoContent) + } +}