From 1c74d12df687475dfc8abda80ed2740672cb7da2 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Wed, 12 Nov 2025 05:19:26 +0500 Subject: [PATCH] modified: main_dc/yalarba/api_es/go.mod modified: main_dc/yalarba/api_es/go.sum new file: main_dc/yalarba/api_es/internal/dto/user.go new file: main_dc/yalarba/api_es/internal/handler/all_handlers.go new file: main_dc/yalarba/api_es/internal/handler/auth_handler.go new file: main_dc/yalarba/api_es/internal/handler/user_handler.go deleted: main_dc/yalarba/api_es/internal/handlers/all_handlers.go deleted: main_dc/yalarba/api_es/internal/handlers/auth_handler.go deleted: main_dc/yalarba/api_es/internal/handlers/user_handler.go new file: main_dc/yalarba/api_es/internal/middleware/auth.go deleted: main_dc/yalarba/api_es/internal/repositories/user_repository.go new file: main_dc/yalarba/api_es/internal/repository/user_repository.go new file: main_dc/yalarba/api_es/internal/service/user_service.go new file: main_dc/yalarba/api_es/internal/utils/jwt.go add service, handler, repository for user model --- main_dc/yalarba/api_es/go.mod | 17 +- main_dc/yalarba/api_es/go.sum | 34 ++- main_dc/yalarba/api_es/internal/dto/user.go | 85 ++++++ .../api_es/internal/handler/all_handlers.go | 1 + .../api_es/internal/handler/auth_handler.go | 1 + .../api_es/internal/handler/user_handler.go | 254 ++++++++++++++++++ .../api_es/internal/handlers/all_handlers.go | 1 - .../api_es/internal/handlers/auth_handler.go | 1 - .../api_es/internal/handlers/user_handler.go | 1 - .../api_es/internal/middleware/auth.go | 57 ++++ .../internal/repositories/user_repository.go | 1 - .../internal/repository/user_repository.go | 74 +++++ .../api_es/internal/service/user_service.go | 194 +++++++++++++ main_dc/yalarba/api_es/internal/utils/jwt.go | 53 ++++ 14 files changed, 758 insertions(+), 16 deletions(-) create mode 100644 main_dc/yalarba/api_es/internal/dto/user.go create mode 100644 main_dc/yalarba/api_es/internal/handler/all_handlers.go create mode 100644 main_dc/yalarba/api_es/internal/handler/auth_handler.go create mode 100644 main_dc/yalarba/api_es/internal/handler/user_handler.go delete mode 100644 main_dc/yalarba/api_es/internal/handlers/all_handlers.go delete mode 100644 main_dc/yalarba/api_es/internal/handlers/auth_handler.go delete mode 100644 main_dc/yalarba/api_es/internal/handlers/user_handler.go create mode 100644 main_dc/yalarba/api_es/internal/middleware/auth.go delete mode 100644 main_dc/yalarba/api_es/internal/repositories/user_repository.go create mode 100644 main_dc/yalarba/api_es/internal/repository/user_repository.go create mode 100644 main_dc/yalarba/api_es/internal/service/user_service.go create mode 100644 main_dc/yalarba/api_es/internal/utils/jwt.go diff --git a/main_dc/yalarba/api_es/go.mod b/main_dc/yalarba/api_es/go.mod index 8009eff..402c71e 100644 --- a/main_dc/yalarba/api_es/go.mod +++ b/main_dc/yalarba/api_es/go.mod @@ -8,9 +8,18 @@ require ( gorm.io/gorm v1.25.10 ) -require go.uber.org/multierr v1.10.0 // indirect +require ( + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.36.0 // indirect +) require ( + github.com/go-playground/validator/v10 v10.28.0 + github.com/golang-jwt/jwt/v4 v4.5.2 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -18,7 +27,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.42.0 + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect ) diff --git a/main_dc/yalarba/api_es/go.sum b/main_dc/yalarba/api_es/go.sum index 23b1350..eaaf6f6 100644 --- a/main_dc/yalarba/api_es/go.sum +++ b/main_dc/yalarba/api_es/go.sum @@ -1,8 +1,20 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -15,23 +27,29 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main_dc/yalarba/api_es/internal/dto/user.go b/main_dc/yalarba/api_es/internal/dto/user.go new file mode 100644 index 0000000..76efc39 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/dto/user.go @@ -0,0 +1,85 @@ +package dto + +import ( + "api_es/internal/models" + "time" +) + +// RegisterRequest - запрос на регистрацию +type RegisterRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=6"` + FullName string `json:"full_name" validate:"required"` + FirstName string `json:"first_name" validate:"required"` + LastName string `json:"last_name" validate:"required"` + Phone string `json:"phone"` + City string `json:"city"` +} + +// LoginRequest - запрос на вход +type LoginRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` +} + +// UpdateUserRequest - запрос на обновление пользователя +type UpdateUserRequest struct { + FullName string `json:"full_name"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Phone string `json:"phone"` + City string `json:"city"` + OrganizationForm string `json:"organization_form"` + OrganizationName string `json:"organization_name"` + OrganizationShort string `json:"organization_short"` + INN string `json:"inn"` + PersonalINN string `json:"personal_inn"` +} + +// UserResponse - ответ с данными пользователя +type UserResponse struct { + ID uint `json:"id"` + Email string `json:"email"` + FullName string `json:"full_name"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Phone string `json:"phone"` + City string `json:"city"` + OrganizationForm string `json:"organization_form"` + OrganizationName string `json:"organization_name"` + OrganizationShort string `json:"organization_short"` + INN string `json:"inn"` + PersonalINN string `json:"personal_inn"` + IsActive bool `json:"is_active"` + IsVerified bool `json:"is_verified"` + Role string `json:"role"` + CreatedAt time.Time `json:"created_at"` +} + +// AuthResponse - ответ с токеном +type AuthResponse struct { + Token string `json:"token"` + User UserResponse `json:"user"` +} + +// ToUserResponse преобразует модель в DTO +func ToUserResponse(user *models.User) UserResponse { + return UserResponse{ + ID: user.ID, + Email: user.Email, + FullName: user.FullName, + FirstName: user.FirstName, + LastName: user.LastName, + Phone: user.Phone, + City: user.City, + OrganizationForm: user.OrganizationForm, + OrganizationName: user.OrganizationName, + OrganizationShort: user.OrganizationShort, + INN: user.INN, + PersonalINN: user.PersonalINN, + IsActive: user.IsActive, + IsVerified: user.IsVerified, + Role: user.Role, + CreatedAt: user.CreatedAt, + } +} diff --git a/main_dc/yalarba/api_es/internal/handler/all_handlers.go b/main_dc/yalarba/api_es/internal/handler/all_handlers.go new file mode 100644 index 0000000..cd97792 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/handler/all_handlers.go @@ -0,0 +1 @@ +package handler \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/handler/auth_handler.go b/main_dc/yalarba/api_es/internal/handler/auth_handler.go new file mode 100644 index 0000000..cd97792 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/handler/auth_handler.go @@ -0,0 +1 @@ +package handler \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/handler/user_handler.go b/main_dc/yalarba/api_es/internal/handler/user_handler.go new file mode 100644 index 0000000..978283f --- /dev/null +++ b/main_dc/yalarba/api_es/internal/handler/user_handler.go @@ -0,0 +1,254 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + "api_es/internal/dto" + appMiddleware "api_es/internal/middleware" + "api_es/internal/service" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" +) + +type UserHandler struct { + userService service.UserService + validator *validator.Validate +} + +func NewUserHandler(userService service.UserService) *UserHandler { + return &UserHandler{ + userService: userService, + validator: validator.New(), + } +} + +// Register godoc +// @Summary Register new user +// @Description Create a new user account +// @Tags auth +// @Accept json +// @Produce json +// @Param request body dto.RegisterRequest true "Register request" +// @Success 201 {object} dto.AuthResponse +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /auth/register [post] +func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { + var req dto.RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := h.validator.Struct(req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + response, err := h.userService.Register(r.Context(), req) + if err != nil { + switch err { + case service.ErrUserAlreadyExists: + http.Error(w, "User already exists", http.StatusConflict) + default: + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(response) +} + +// Login godoc +// @Summary Login user +// @Description Authenticate user and get token +// @Tags auth +// @Accept json +// @Produce json +// @Param request body dto.LoginRequest true "Login request" +// @Success 200 {object} dto.AuthResponse +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /auth/login [post] +func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) { + var req dto.LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := h.validator.Struct(req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + response, err := h.userService.Login(r.Context(), req) + if err != nil { + switch err { + case service.ErrInvalidCredentials: + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + default: + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// GetProfile godoc +// @Summary Get user profile +// @Description Get current user profile +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.UserResponse +// @Failure 404 {object} map[string]string +// @Router /users/profile [get] +func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + user, err := h.userService.GetUserProfile(r.Context(), userID) + if err != nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + +// UpdateProfile godoc +// @Summary Update user profile +// @Description Update current user profile +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.UpdateUserRequest true "Update request" +// @Success 200 {object} dto.UserResponse +// @Failure 400 {object} map[string]string +// @Router /users/profile [put] +func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req dto.UpdateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + user, err := h.userService.UpdateUser(r.Context(), userID, req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + +// GetUser godoc +// @Summary Get user by ID +// @Description Get user details by ID (admin only) +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "User ID" +// @Success 200 {object} dto.UserResponse +// @Failure 404 {object} map[string]string +// @Router /users/{id} [get] +func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid user ID", http.StatusBadRequest) + return + } + + user, err := h.userService.GetUser(r.Context(), uint(id)) + if err != nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + +// ListUsers godoc +// @Summary List users +// @Description Get paginated list of users (admin only) +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {array} dto.UserResponse +// @Router /users [get] +func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) { + limitStr := r.URL.Query().Get("limit") + offsetStr := r.URL.Query().Get("offset") + + limit := 10 + offset := 0 + + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil { + limit = l + } + } + + if offsetStr != "" { + if o, err := strconv.Atoi(offsetStr); err == nil { + offset = o + } + } + + users, err := h.userService.ListUsers(r.Context(), limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) +} + +func (h *UserHandler) Routes() chi.Router { + r := chi.NewRouter() + + r.Route("/auth", func(r chi.Router) { + r.Post("/register", h.Register) + r.Post("/login", h.Login) + }) + + r.Route("/users", func(r chi.Router) { + r.Use(appMiddleware.AuthMiddleware) + + r.Get("/profile", h.GetProfile) + r.Put("/profile", h.UpdateProfile) + + // Admin routes + r.With(appMiddleware.AdminMiddleware).Get("/", h.ListUsers) + r.With(appMiddleware.AdminMiddleware).Get("/{id}", h.GetUser) + }) + + return r +} diff --git a/main_dc/yalarba/api_es/internal/handlers/all_handlers.go b/main_dc/yalarba/api_es/internal/handlers/all_handlers.go deleted file mode 100644 index 28ae6f5..0000000 --- a/main_dc/yalarba/api_es/internal/handlers/all_handlers.go +++ /dev/null @@ -1 +0,0 @@ -package handlers \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/handlers/auth_handler.go b/main_dc/yalarba/api_es/internal/handlers/auth_handler.go deleted file mode 100644 index 28ae6f5..0000000 --- a/main_dc/yalarba/api_es/internal/handlers/auth_handler.go +++ /dev/null @@ -1 +0,0 @@ -package handlers \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/handlers/user_handler.go b/main_dc/yalarba/api_es/internal/handlers/user_handler.go deleted file mode 100644 index 28ae6f5..0000000 --- a/main_dc/yalarba/api_es/internal/handlers/user_handler.go +++ /dev/null @@ -1 +0,0 @@ -package handlers \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/middleware/auth.go b/main_dc/yalarba/api_es/internal/middleware/auth.go new file mode 100644 index 0000000..6a0e807 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/middleware/auth.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + "api_es/internal/utils" +) + +type contextKey string + +const ( + UserIDKey contextKey = "userID" + UserEmailKey contextKey = "userEmail" + UserRoleKey contextKey = "userRole" +) + +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization header required", http.StatusUnauthorized) + return + } + + tokenString := strings.Replace(authHeader, "Bearer ", "", 1) + if tokenString == "" { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Здесь нужно использовать ваш JWT утилити + jwtUtil := utils.NewJWTUtil("your-secret-key") + claims, err := jwtUtil.ValidateToken(tokenString) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID) + ctx = context.WithValue(ctx, UserEmailKey, claims.Email) + ctx = context.WithValue(ctx, UserRoleKey, claims.Role) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func AdminMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + role, ok := r.Context().Value(UserRoleKey).(string) + if !ok || role != "admin" { + http.Error(w, "Admin access required", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/main_dc/yalarba/api_es/internal/repositories/user_repository.go b/main_dc/yalarba/api_es/internal/repositories/user_repository.go deleted file mode 100644 index 61385c7..0000000 --- a/main_dc/yalarba/api_es/internal/repositories/user_repository.go +++ /dev/null @@ -1 +0,0 @@ -package repositories \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/repository/user_repository.go b/main_dc/yalarba/api_es/internal/repository/user_repository.go new file mode 100644 index 0000000..1590ab1 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/repository/user_repository.go @@ -0,0 +1,74 @@ +package repository + +import ( + "api_es/internal/models" + "context" + + "gorm.io/gorm" +) + +type UserRepository interface { + Create(ctx context.Context, user *models.User) error + GetByID(ctx context.Context, id uint) (*models.User, error) + GetByEmail(ctx context.Context, email string) (*models.User, error) + Update(ctx context.Context, user *models.User) error + Delete(ctx context.Context, id uint) error + List(ctx context.Context, limit, offset int) ([]*models.User, error) + GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error) +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) Create(ctx context.Context, user *models.User) error { + return r.db.WithContext(ctx).Create(user).Error +} + +func (r *userRepository) GetByID(ctx context.Context, id uint) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx).First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *userRepository) Update(ctx context.Context, user *models.User) error { + return r.db.WithContext(ctx).Save(user).Error +} + +func (r *userRepository) Delete(ctx context.Context, id uint) error { + return r.db.WithContext(ctx).Delete(&models.User{}, id).Error +} + +func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) { + var users []*models.User + err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&users).Error + if err != nil { + return nil, err + } + return users, nil +} + +func (r *userRepository) GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error) { + var stats models.UserStats + err := r.db.WithContext(ctx).First(&stats, userID).Error + if err != nil { + return nil, err + } + return &stats, nil +} diff --git a/main_dc/yalarba/api_es/internal/service/user_service.go b/main_dc/yalarba/api_es/internal/service/user_service.go new file mode 100644 index 0000000..bf2de6b --- /dev/null +++ b/main_dc/yalarba/api_es/internal/service/user_service.go @@ -0,0 +1,194 @@ +package service + +import ( + "context" + "errors" + "api_es/internal/dto" + "api_es/internal/models" + "api_es/internal/repository" + "api_es/internal/utils" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrInvalidCredentials = errors.New("invalid credentials") + ErrUserAlreadyExists = errors.New("user already exists") + ErrInvalidPassword = errors.New("invalid password") +) + +type UserService interface { + Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error) + Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error) + GetUser(ctx context.Context, id uint) (*dto.UserResponse, error) + UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error) + DeleteUser(ctx context.Context, id uint) error + ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error) + GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error) +} + +type userService struct { + userRepo repository.UserRepository + jwtUtil *utils.JWTUtil +} + +func NewUserService(userRepo repository.UserRepository, jwtUtil *utils.JWTUtil) UserService { + return &userService{ + userRepo: userRepo, + jwtUtil: jwtUtil, + } +} + +func (s *userService) Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error) { + // Проверяем существование пользователя + existingUser, _ := s.userRepo.GetByEmail(ctx, req.Email) + if existingUser != nil { + return nil, ErrUserAlreadyExists + } + + // Хешируем пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + // Создаем пользователя + user := &models.User{ + Email: req.Email, + PasswordHash: string(hashedPassword), + FullName: req.FullName, + FirstName: req.FirstName, + LastName: req.LastName, + Phone: req.Phone, + City: req.City, + IsActive: true, + IsVerified: false, + Role: "user", + } + + if err := s.userRepo.Create(ctx, user); err != nil { + return nil, err + } + + // Генерируем токен + token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role) + if err != nil { + return nil, err + } + + userResponse := dto.ToUserResponse(user) + return &dto.AuthResponse{ + Token: token, + User: userResponse, + }, nil +} + +func (s *userService) Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error) { + // Находим пользователя по email + user, err := s.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return nil, ErrInvalidCredentials + } + + // Проверяем пароль + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { + return nil, ErrInvalidCredentials + } + + // Проверяем активность пользователя + if !user.IsActive { + return nil, errors.New("account is deactivated") + } + + // Генерируем токен + token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role) + if err != nil { + return nil, err + } + + userResponse := dto.ToUserResponse(user) + return &dto.AuthResponse{ + Token: token, + User: userResponse, + }, nil +} + +func (s *userService) GetUser(ctx context.Context, id uint) (*dto.UserResponse, error) { + user, err := s.userRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrUserNotFound + } + + response := dto.ToUserResponse(user) + return &response, nil +} + +func (s *userService) UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error) { + user, err := s.userRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrUserNotFound + } + + // Обновляем поля + if req.FullName != "" { + user.FullName = req.FullName + } + if req.FirstName != "" { + user.FirstName = req.FirstName + } + if req.LastName != "" { + user.LastName = req.LastName + } + if req.Phone != "" { + user.Phone = req.Phone + } + if req.City != "" { + user.City = req.City + } + if req.OrganizationForm != "" { + user.OrganizationForm = req.OrganizationForm + } + if req.OrganizationName != "" { + user.OrganizationName = req.OrganizationName + } + if req.OrganizationShort != "" { + user.OrganizationShort = req.OrganizationShort + } + if req.INN != "" { + user.INN = req.INN + } + if req.PersonalINN != "" { + user.PersonalINN = req.PersonalINN + } + + if err := s.userRepo.Update(ctx, user); err != nil { + return nil, err + } + + response := dto.ToUserResponse(user) + return &response, nil +} + +func (s *userService) DeleteUser(ctx context.Context, id uint) error { + return s.userRepo.Delete(ctx, id) +} + +func (s *userService) ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error) { + users, err := s.userRepo.List(ctx, limit, offset) + if err != nil { + return nil, err + } + + responses := make([]*dto.UserResponse, len(users)) + for i, user := range users { + response := dto.ToUserResponse(user) + responses[i] = &response + } + + return responses, nil +} + +func (s *userService) GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error) { + return s.GetUser(ctx, id) +} diff --git a/main_dc/yalarba/api_es/internal/utils/jwt.go b/main_dc/yalarba/api_es/internal/utils/jwt.go new file mode 100644 index 0000000..6f7596b --- /dev/null +++ b/main_dc/yalarba/api_es/internal/utils/jwt.go @@ -0,0 +1,53 @@ +package utils + +import ( + "time" + + "github.com/golang-jwt/jwt/v4" +) + +type JWTUtil struct { + secretKey string +} + +type Claims struct { + UserID uint `json:"user_id"` + Email string `json:"email"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func NewJWTUtil(secretKey string) *JWTUtil { + return &JWTUtil{secretKey: secretKey} +} + +func (j *JWTUtil) GenerateToken(userID uint, email, role string) (string, error) { + claims := Claims{ + UserID: userID, + Email: email, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(j.secretKey)) +} + +func (j *JWTUtil) ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(j.secretKey), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, jwt.ErrInvalidKey +}