From 75b2f3f6b2af5b255923ad777c0d10a0088b92a5 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 31 Mar 2026 09:43:18 +0500 Subject: [PATCH] On branch main modified: main_dc/yalarba/api_yal/internal/domain/account/dto.go new file: main_dc/yalarba/api_yal/internal/domain/account/errors.go modified: main_dc/yalarba/api_yal/internal/domain/account/handler.go modified: main_dc/yalarba/api_yal/internal/domain/account/router.go modified: main_dc/yalarba/api_yal/internal/domain/account/service.go new file: main_dc/yalarba/api_yal/internal/domain/account/types.go new file: main_dc/yalarba/api_yal/internal/middleware/admin.go modified: main_dc/yalarba/api_yal/internal/middleware/auth.go new file: main_dc/yalarba/api_yal/internal/middleware/context.go new file: main_dc/yalarba/api_yal/internal/middleware/logging.go modified: main_dc/yalarba/api_yal/internal/router/router.go last but not yet commit --- .../api_yal/internal/domain/account/dto.go | 46 +- .../api_yal/internal/domain/account/errors.go | 31 + .../internal/domain/account/handler.go | 448 +++++++++++++- .../api_yal/internal/domain/account/router.go | 52 +- .../internal/domain/account/service.go | 562 +++++++++++++++++- .../api_yal/internal/domain/account/types.go | 34 ++ .../api_yal/internal/middleware/admin.go | 50 ++ .../api_yal/internal/middleware/auth.go | 171 +++--- .../api_yal/internal/middleware/context.go | 20 + .../api_yal/internal/middleware/logging.go | 67 +++ .../yalarba/api_yal/internal/router/router.go | 12 +- 11 files changed, 1384 insertions(+), 109 deletions(-) create mode 100644 main_dc/yalarba/api_yal/internal/domain/account/errors.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/account/types.go create mode 100644 main_dc/yalarba/api_yal/internal/middleware/admin.go create mode 100644 main_dc/yalarba/api_yal/internal/middleware/context.go create mode 100644 main_dc/yalarba/api_yal/internal/middleware/logging.go diff --git a/main_dc/yalarba/api_yal/internal/domain/account/dto.go b/main_dc/yalarba/api_yal/internal/domain/account/dto.go index 6730d23..6fe76f2 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/dto.go @@ -9,11 +9,11 @@ import ( // CreateAccountRequest - DTO для создания нового аккаунта type CreateAccountRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - FullName string `json:"full_name" binding:"required"` - FirstName string `json:"first_name" binding:"required"` - LastName string `json:"last_name" binding:"required"` + 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"` @@ -43,19 +43,19 @@ type UpdateAccountRequest struct { // ChangePasswordRequest - DTO для смены пароля type ChangePasswordRequest struct { - CurrentPassword string `json:"current_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=6"` + CurrentPassword string `json:"current_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` } // ForgotPasswordRequest - DTO для запроса сброса пароля type ForgotPasswordRequest struct { - Email string `json:"email" binding:"required,email"` + Email string `json:"email" validate:"required,email"` } // ResetPasswordRequest - DTO для сброса пароля по токену type ResetPasswordRequest struct { - Token string `json:"token" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=6"` + Token string `json:"token" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` } // VerifyAccountRequest - DTO для верификации аккаунта (админ) @@ -66,15 +66,15 @@ type VerifyAccountRequest struct { // UpdateAccountStatusRequest - DTO для изменения статуса аккаунта (админ) type UpdateAccountStatusRequest struct { IsActive bool `json:"is_active"` - Role string `json:"role" binding:"omitempty,oneof=user admin moderator"` + Role string `json:"role" validate:"omitempty,oneof=user admin moderator"` } // ListAccountsRequest - DTO для фильтрации списка аккаунтов type ListAccountsRequest struct { - Page int `form:"page" binding:"min=1"` - PageSize int `form:"page_size" binding:"min=1,max=100"` + Page int `form:"page" validate:"min=1"` + PageSize int `form:"page_size" validate:"min=1,max=100"` Search string `form:"search"` - Role string `form:"role" binding:"omitempty,oneof=user admin moderator"` + Role string `form:"role" validate:"omitempty,oneof=user admin moderator"` IsActive *bool `form:"is_active"` } @@ -140,19 +140,19 @@ type AccountProfileResponse struct { // AccountStats - статистика аккаунта type AccountStats struct { - ObjectsCount int `json:"objects_count"` - FeedbacksCount int `json:"feedbacks_count"` - CommentsCount int `json:"comments_count"` - RatingsCount int `json:"ratings_count"` - AppealsCount int `json:"appeals_count"` + ObjectsCount int `json:"objects_count"` + FeedbacksCount int `json:"feedbacks_count"` + CommentsCount int `json:"comments_count"` + RatingsCount int `json:"ratings_count"` + AppealsCount int `json:"appeals_count"` } // AuthResponse - DTO для ответа при авторизации type AuthResponse struct { - Token string `json:"token"` - TokenType string `json:"token_type"` - ExpiresAt int64 `json:"expires_at"` - Account AccountResponse `json:"account"` + Token string `json:"token"` + TokenType string `json:"token_type"` + ExpiresAt int64 `json:"expires_at"` + Account AccountResponse `json:"account"` } // PasswordResetResponse - DTO для ответа о сбросе пароля diff --git a/main_dc/yalarba/api_yal/internal/domain/account/errors.go b/main_dc/yalarba/api_yal/internal/domain/account/errors.go new file mode 100644 index 0000000..30346bb --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/account/errors.go @@ -0,0 +1,31 @@ +package account + +import "errors" + +var ( + // Основные ошибки + ErrAccountNotFound = errors.New("account not found") + ErrAccountAlreadyExists = errors.New("account already exists") + ErrAccountDeactivated = errors.New("account is deactivated") + ErrAccountNotVerified = errors.New("account is not verified") + + // Ошибки пароля + ErrInvalidCurrentPassword = errors.New("invalid current password") + ErrPasswordTooWeak = errors.New("password too weak") + ErrPasswordMismatch = errors.New("password mismatch") + + // Ошибки токенов сброса пароля + ErrResetTokenNotFound = errors.New("reset token not found") + ErrResetTokenExpired = errors.New("reset token has expired") + ErrResetTokenAlreadyUsed = errors.New("reset token already used") + ErrResetTokenInvalid = errors.New("invalid reset token") + + // Ошибки доступа + ErrInsufficientPermissions = errors.New("insufficient permissions") + ErrUnauthorized = errors.New("unauthorized") + + // Ошибки валидации + ErrInvalidEmail = errors.New("invalid email") + ErrInvalidPhone = errors.New("invalid phone number") + ErrInvalidINN = errors.New("invalid INN") +) \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/account/handler.go b/main_dc/yalarba/api_yal/internal/domain/account/handler.go index f8f035e..1058058 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/handler.go @@ -1 +1,447 @@ -package account \ No newline at end of file +package account + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "api_yal/internal/logger" + "api_yal/internal/middleware" + + "github.com/go-playground/validator/v10" + "go.uber.org/zap" +) + +// Handler обработчик для операций с аккаунтами +type Handler struct { + service Service + validator *validator.Validate +} + +// NewHandler создает новый экземпляр Handler +func NewHandler(service Service) *Handler { + return &Handler{ + service: service, + validator: validator.New(), + } +} + +// GetAccountByID получение аккаунта по ID +func (h *Handler) GetAccountByID(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + // Получаем ID из контекста (для своего профиля) + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Проверяем, что пользователь запрашивает свой профиль + // В реальном приложении админы могут просматривать чужие профили + account, err := h.service.GetAccountByID(userID) + if err != nil { + l.Error("Ошибка получения аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to get account", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(account) +} + +// GetAccountProfile получение профиля пользователя +func (h *Handler) GetAccountProfile(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + profile, err := h.service.GetAccountProfile(userID) + if err != nil { + l.Error("Ошибка получения профиля", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to get profile", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(profile) +} + +// UpdateAccount обновление аккаунта +func (h *Handler) UpdateAccount(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req UpdateAccountRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Валидация не требуется для всех полей, так как они опциональны + account, err := h.service.UpdateAccount(userID, req) + if err != nil { + l.Error("Ошибка обновления аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to update account", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(account) +} + +// ChangePassword смена пароля +func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req ChangePasswordRequest + 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 { + h.handleValidationError(w, err) + return + } + + if err := h.service.ChangePassword(userID, req); err != nil { + l.Error("Ошибка смены пароля", zap.Error(err)) + + if errors.Is(err, ErrInvalidCurrentPassword) { + http.Error(w, "Current password is incorrect", http.StatusBadRequest) + return + } + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to change password", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Password changed successfully", + }) +} + +// ForgotPassword запрос на сброс пароля +func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + var req ForgotPasswordRequest + 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 { + h.handleValidationError(w, err) + return + } + + token, err := h.service.ForgotPassword(req.Email) + if err != nil { + l.Error("Ошибка запроса сброса пароля", zap.Error(err)) + http.Error(w, "Password reset request failed", http.StatusInternalServerError) + return + } + + // В реальном приложении здесь отправляется email + // Для тестирования возвращаем токен + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(PasswordResetResponse{ + Message: "If the email exists, a reset link has been sent", + Token: token, // Только для тестирования + }) +} + +// ResetPassword подтверждение сброса пароля +func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + var req ResetPasswordRequest + 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 { + h.handleValidationError(w, err) + return + } + + if err := h.service.ResetPassword(req.Token, req.NewPassword); err != nil { + l.Error("Ошибка сброса пароля", zap.Error(err)) + + if errors.Is(err, ErrResetTokenNotFound) { + http.Error(w, "Reset token not found", http.StatusNotFound) + return + } + + if errors.Is(err, ErrResetTokenExpired) { + http.Error(w, "Reset token has expired", http.StatusBadRequest) + return + } + + if errors.Is(err, ErrResetTokenAlreadyUsed) { + http.Error(w, "Reset token has already been used", http.StatusBadRequest) + return + } + + http.Error(w, "Password reset failed", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Password has been successfully reset", + }) +} + +// DeleteAccount удаление аккаунта +func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if err := h.service.DeleteAccount(userID); err != nil { + l.Error("Ошибка удаления аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to delete account", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Account deleted successfully", + }) +} + +// ==================== Административные методы ==================== + +// ListAccounts список аккаунтов (админ) +func (h *Handler) ListAccounts(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + var req ListAccountsRequest + + // Парсинг query параметров + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + page, _ := strconv.Atoi(pageStr) + req.Page = page + } + if pageSizeStr := r.URL.Query().Get("page_size"); pageSizeStr != "" { + pageSize, _ := strconv.Atoi(pageSizeStr) + req.PageSize = pageSize + } + req.Search = r.URL.Query().Get("search") + req.Role = r.URL.Query().Get("role") + if isActiveStr := r.URL.Query().Get("is_active"); isActiveStr != "" { + isActive, _ := strconv.ParseBool(isActiveStr) + req.IsActive = &isActive + } + + if err := h.validator.Struct(req); err != nil { + h.handleValidationError(w, err) + return + } + + response, err := h.service.ListAccounts(req) + if err != nil { + l.Error("Ошибка получения списка аккаунтов", zap.Error(err)) + http.Error(w, "Failed to list accounts", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// GetAccountByIDAdmin получение аккаунта по ID (админ) +func (h *Handler) GetAccountByIDAdmin(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + // Получаем ID из URL + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing id parameter", http.StatusBadRequest) + return + } + + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid id parameter", http.StatusBadRequest) + return + } + + account, err := h.service.GetAccountByID(uint(id)) + if err != nil { + l.Error("Ошибка получения аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to get account", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(account) +} + +// VerifyAccount верификация аккаунта (админ) +func (h *Handler) VerifyAccount(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + // Получаем ID из URL + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing id parameter", http.StatusBadRequest) + return + } + + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid id parameter", http.StatusBadRequest) + return + } + + var req VerifyAccountRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := h.service.VerifyAccount(uint(id), req); err != nil { + l.Error("Ошибка верификации аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to verify account", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Account verified successfully", + }) +} + +// UpdateAccountStatus обновление статуса аккаунта (админ) +func (h *Handler) UpdateAccountStatus(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + // Получаем ID из URL + idStr := r.URL.Query().Get("id") + if idStr == "" { + http.Error(w, "Missing id parameter", http.StatusBadRequest) + return + } + + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid id parameter", http.StatusBadRequest) + return + } + + var req UpdateAccountStatusRequest + 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 { + h.handleValidationError(w, err) + return + } + + if err := h.service.UpdateAccountStatus(uint(id), req); err != nil { + l.Error("Ошибка обновления статуса аккаунта", zap.Error(err)) + + if errors.Is(err, ErrAccountNotFound) { + http.Error(w, "Account not found", http.StatusNotFound) + return + } + + http.Error(w, "Failed to update account status", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Account status updated successfully", + }) +} + +// handleValidationError обрабатывает ошибки валидации +func (h *Handler) handleValidationError(w http.ResponseWriter, err error) { + var invalidValidationError *validator.InvalidValidationError + if errors.As(err, &invalidValidationError) { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + var errs []string + for _, err := range err.(validator.ValidationErrors) { + errs = append(errs, fmt.Sprintf("field %s is invalid: %s", err.Field(), err.Tag())) + } + + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": "Validation failed", + "fields": errs, + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/account/router.go b/main_dc/yalarba/api_yal/internal/domain/account/router.go index f8f035e..4135393 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/router.go @@ -1 +1,51 @@ -package account \ No newline at end of file +package account + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/repository" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +// RegisterRoutes регистрирует маршруты для работы с аккаунтами +func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Debug("Регистрация маршрутов аккаунтов") + + // Создаем репозиторий и сервис + accountRepo := repository.NewAccountRepository(db) + accountService := NewService(accountRepo) + accountHandler := NewHandler(accountService) + + // Публичные маршруты (без аутентификации) + r.Group(func(r chi.Router) { + r.Post("/forgot-password", accountHandler.ForgotPassword) + r.Post("/reset-password", accountHandler.ResetPassword) + }) + + // Защищенные маршруты (требуют аутентификации) + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + r.Get("/profile", accountHandler.GetAccountProfile) + r.Get("/me", accountHandler.GetAccountByID) + r.Put("/me", accountHandler.UpdateAccount) + r.Delete("/me", accountHandler.DeleteAccount) + r.Post("/change-password", accountHandler.ChangePassword) + }) + + // Административные маршруты (требуют прав администратора) + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + r.Use(middleware.AdminOnlyMiddleware) + + r.Get("/accounts", accountHandler.ListAccounts) + r.Get("/account", accountHandler.GetAccountByIDAdmin) + r.Put("/account/verify", accountHandler.VerifyAccount) + r.Put("/account/status", accountHandler.UpdateAccountStatus) + }) + + l.Info("Маршруты аккаунтов зарегистрированы") +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/account/service.go b/main_dc/yalarba/api_yal/internal/domain/account/service.go index f8f035e..83a4dce 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/service.go @@ -1 +1,561 @@ -package account \ No newline at end of file +package account + +import ( + "api_yal/internal/logger" + "api_yal/internal/models" + "api_yal/internal/repository" + "errors" + "fmt" + "time" + + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// Service интерфейс сервиса аккаунтов +type Service interface { + // Основные операции + GetAccountByID(id uint) (*AccountResponse, error) + GetAccountByEmail(email string) (*AccountResponse, error) + GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) + UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) + DeleteAccount(id uint) error + + // Управление паролем + ChangePassword(userID uint, req ChangePasswordRequest) error + ForgotPassword(email string) (string, error) + ResetPassword(token, newPassword string) error + + // Административные функции + ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) + VerifyAccount(accountID uint, req VerifyAccountRequest) error + UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error + + // Статистика + GetAccountStats(userID uint) (*AccountStats, error) + GetAccountProfile(userID uint) (*AccountProfileResponse, error) + + // Внутренние методы для auth сервиса + GetAccountModelByID(id uint) (*models.Account, error) + GetAccountModelByEmail(email string) (*models.Account, error) + CreateAccount(req CreateAccountRequest) (*models.Account, error) + UpdateAccountModel(account *models.Account) error +} + +// serviceImpl реализация сервиса аккаунтов +type serviceImpl struct { + accountRepo repository.AccountRepository +} + +// NewService создает новый экземпляр сервиса аккаунтов +func NewService(accountRepo repository.AccountRepository) Service { + return &serviceImpl{ + accountRepo: accountRepo, + } +} + +// GetAccountByID получает аккаунт по ID +func (s *serviceImpl) GetAccountByID(id uint) (*AccountResponse, error) { + l := logger.Get() + l.Debug("Получение аккаунта по ID", zap.Uint("id", id)) + + account, err := s.accountRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + l.Error("Ошибка получения аккаунта", zap.Error(err)) + return nil, err + } + + response := ToAccountResponse(account) + return &response, nil +} + +// GetAccountByEmail получает аккаунт по email +func (s *serviceImpl) GetAccountByEmail(email string) (*AccountResponse, error) { + l := logger.Get() + l.Debug("Получение аккаунта по email", zap.String("email", email)) + + account, err := s.accountRepo.GetByEmail(email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + l.Error("Ошибка получения аккаунта", zap.Error(err)) + return nil, err + } + + response := ToAccountResponse(account) + return &response, nil +} + +// GetAccountWithObjects получает аккаунт с его объектами +func (s *serviceImpl) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) { + l := logger.Get() + l.Debug("Получение аккаунта с объектами", zap.Uint("id", id)) + + account, err := s.accountRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + l.Error("Ошибка получения аккаунта", zap.Error(err)) + return nil, err + } + + objects, err := s.accountRepo.GetObjects(id) + if err != nil { + l.Error("Ошибка получения объектов", zap.Error(err)) + return nil, err + } + account.Objects = objects + + response := ToAccountWithObjectsResponse(account) + return &response, nil +} + +// UpdateAccount обновляет информацию об аккаунте +func (s *serviceImpl) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) { + l := logger.Get() + l.Info("Обновление аккаунта", zap.Uint("id", id)) + + account, err := s.accountRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + return nil, err + } + + // Обновляем поля + if req.FullName != "" { + account.FullName = req.FullName + } + if req.FirstName != "" { + account.FirstName = req.FirstName + } + if req.LastName != "" { + account.LastName = req.LastName + } + if req.Phone != "" { + account.Phone = req.Phone + } + if req.City != "" { + account.City = req.City + } + if req.OrganizationForm != "" { + account.OrganizationForm = req.OrganizationForm + } + if req.OrganizationName != "" { + account.OrganizationName = req.OrganizationName + } + if req.OrganizationShort != "" { + account.OrganizationShort = req.OrganizationShort + } + if req.INN != "" { + account.INN = req.INN + } + if req.PersonalINN != "" { + account.PersonalINN = req.PersonalINN + } + + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления аккаунта", zap.Error(err)) + return nil, err + } + + response := ToAccountResponse(account) + return &response, nil +} + +// DeleteAccount удаляет аккаунт (мягкое удаление) +func (s *serviceImpl) DeleteAccount(id uint) error { + l := logger.Get() + l.Info("Удаление аккаунта", zap.Uint("id", id)) + + if err := s.accountRepo.Delete(id); err != nil { + l.Error("Ошибка удаления аккаунта", zap.Error(err)) + return err + } + + return nil +} + +// ChangePassword изменяет пароль пользователя +func (s *serviceImpl) ChangePassword(userID uint, req ChangePasswordRequest) error { + l := logger.Get() + l.Info("Смена пароля", zap.Uint("userID", userID)) + + account, err := s.accountRepo.GetByID(userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return err + } + + // Проверяем текущий пароль + if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.CurrentPassword)); err != nil { + l.Error("Неверный текущий пароль") + return ErrInvalidCurrentPassword + } + + // Хешируем новый пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + l.Error("Ошибка хеширования пароля", zap.Error(err)) + return err + } + + account.PasswordHash = string(hashedPassword) + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления пароля", zap.Error(err)) + return err + } + + l.Info("Пароль успешно изменен", zap.Uint("userID", userID)) + return nil +} + +// ForgotPassword запрашивает сброс пароля +func (s *serviceImpl) ForgotPassword(email string) (string, error) { + l := logger.Get() + l.Info("Запрос сброса пароля", zap.String("email", email)) + + account, err := s.accountRepo.GetByEmail(email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Для безопасности не сообщаем, что пользователь не найден + return "", nil + } + return "", err + } + + // Генерируем reset token (используем метод из auth сервиса) + // В реальном приложении здесь должна быть генерация токена + resetToken := generateResetToken() + + passwordReset := &models.PasswordReset{ + AccountID: account.ID, + Token: resetToken, + ExpiresAt: time.Now().Add(1 * time.Hour), + Used: false, + } + + if err := s.accountRepo.CreatePasswordReset(passwordReset); err != nil { + l.Error("Ошибка сохранения reset token", zap.Error(err)) + return "", err + } + + l.Info("Reset token создан", zap.String("email", email)) + return resetToken, nil +} + +// ResetPassword сбрасывает пароль по токену +func (s *serviceImpl) ResetPassword(token, newPassword string) error { + l := logger.Get() + l.Info("Сброс пароля по токену") + + passwordReset, err := s.accountRepo.GetPasswordResetByToken(token) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrResetTokenNotFound + } + return err + } + + // Проверяем срок действия токена + if passwordReset.ExpiresAt.Before(time.Now()) { + return ErrResetTokenExpired + } + + // Проверяем, не использован ли токен + if passwordReset.Used { + return ErrResetTokenAlreadyUsed + } + + // Получаем пользователя + account, err := s.accountRepo.GetByID(passwordReset.AccountID) + if err != nil { + return ErrAccountNotFound + } + + // Хешируем новый пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + l.Error("Ошибка хеширования пароля", zap.Error(err)) + return err + } + + // Обновляем пароль + account.PasswordHash = string(hashedPassword) + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления пароля", zap.Error(err)) + return err + } + + // Помечаем токен как использованный + passwordReset.Used = true + if err := s.accountRepo.UpdatePasswordReset(passwordReset); err != nil { + l.Error("Ошибка обновления статуса токена", zap.Error(err)) + } + + l.Info("Пароль успешно сброшен", zap.Uint("userID", account.ID)) + return nil +} + +// ListAccounts возвращает список аккаунтов с пагинацией +func (s *serviceImpl) ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) { + l := logger.Get() + l.Debug("Получение списка аккаунтов", zap.Any("request", req)) + + // Устанавливаем значения по умолчанию + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 20 + } + + offset := (req.Page - 1) * req.PageSize + + var accounts []models.Account + var total int64 + var err error + + // Поиск с фильтрацией + if req.Search != "" { + accounts, err = s.accountRepo.Search(req.Search, offset, req.PageSize) + if err != nil { + l.Error("Ошибка поиска аккаунтов", zap.Error(err)) + return nil, err + } + total, err = s.getSearchTotal(req.Search) + } else { + accounts, err = s.accountRepo.List(offset, req.PageSize) + if err != nil { + l.Error("Ошибка получения списка аккаунтов", zap.Error(err)) + return nil, err + } + total, err = s.accountRepo.Count() + } + + if err != nil { + return nil, err + } + + // Фильтруем по роли и статусу (если нужно) + filteredAccounts := s.filterAccounts(accounts, req.Role, req.IsActive) + + items := make([]AccountResponse, len(filteredAccounts)) + for i, acc := range filteredAccounts { + items[i] = ToAccountResponse(&acc) + } + + totalPages := int(total) / req.PageSize + if int(total)%req.PageSize > 0 { + totalPages++ + } + + return &AccountListResponse{ + Items: items, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + TotalPages: totalPages, + }, nil +} + +// VerifyAccount верифицирует аккаунт +func (s *serviceImpl) VerifyAccount(accountID uint, req VerifyAccountRequest) error { + l := logger.Get() + l.Info("Верификация аккаунта", zap.Uint("id", accountID)) + + account, err := s.accountRepo.GetByID(accountID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return err + } + + account.IsVerified = req.IsVerified + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления статуса верификации", zap.Error(err)) + return err + } + + l.Info("Аккаунт верифицирован", zap.Uint("id", accountID)) + return nil +} + +// UpdateAccountStatus обновляет статус аккаунта (админ) +func (s *serviceImpl) UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error { + l := logger.Get() + l.Info("Обновление статуса аккаунта", zap.Uint("id", accountID)) + + account, err := s.accountRepo.GetByID(accountID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return err + } + + account.IsActive = req.IsActive + if req.Role != "" { + account.Role = req.Role + } + + if err := s.accountRepo.Update(account); err != nil { + l.Error("Ошибка обновления статуса аккаунта", zap.Error(err)) + return err + } + + l.Info("Статус аккаунта обновлен", zap.Uint("id", accountID)) + return nil +} + +// GetAccountStats получает статистику аккаунта +func (s *serviceImpl) GetAccountStats(userID uint) (*AccountStats, error) { + l := logger.Get() + l.Debug("Получение статистики аккаунта", zap.Uint("userID", userID)) + + // Здесь должна быть реальная логика подсчета статистики + // В реальном приложении нужно запрашивать данные из соответствующих репозиториев + + stats := &AccountStats{ + ObjectsCount: 0, + FeedbacksCount: 0, + CommentsCount: 0, + RatingsCount: 0, + AppealsCount: 0, + } + + // TODO: Получить реальную статистику из базы данных + + return stats, nil +} + +// GetAccountProfile получает профиль пользователя со статистикой +func (s *serviceImpl) GetAccountProfile(userID uint) (*AccountProfileResponse, error) { + l := logger.Get() + l.Debug("Получение профиля пользователя", zap.Uint("userID", userID)) + + account, err := s.accountRepo.GetByID(userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + return nil, err + } + + stats, err := s.GetAccountStats(userID) + if err != nil { + l.Error("Ошибка получения статистики", zap.Error(err)) + return nil, err + } + + return &AccountProfileResponse{ + AccountResponse: ToAccountResponse(account), + Stats: *stats, + }, nil +} + +// GetAccountModelByID получает модель аккаунта по ID (для внутреннего использования) +func (s *serviceImpl) GetAccountModelByID(id uint) (*models.Account, error) { + return s.accountRepo.GetByID(id) +} + +// GetAccountModelByEmail получает модель аккаунта по email (для внутреннего использования) +func (s *serviceImpl) GetAccountModelByEmail(email string) (*models.Account, error) { + return s.accountRepo.GetByEmail(email) +} + +// CreateAccount создает новый аккаунт +func (s *serviceImpl) CreateAccount(req CreateAccountRequest) (*models.Account, error) { + l := logger.Get() + l.Info("Создание аккаунта", zap.String("email", req.Email)) + + // Проверяем, существует ли пользователь + existing, _ := s.accountRepo.GetByEmail(req.Email) + if existing != nil { + return nil, ErrAccountAlreadyExists + } + + // Хешируем пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + l.Error("Ошибка хеширования пароля", zap.Error(err)) + return nil, err + } + + // Формируем полное имя, если не указано + fullName := req.FullName + if fullName == "" { + fullName = req.FirstName + " " + req.LastName + } + + account := &models.Account{ + Email: req.Email, + PasswordHash: string(hashedPassword), + FullName: fullName, + FirstName: req.FirstName, + LastName: req.LastName, + Phone: req.Phone, + City: req.City, + OrganizationForm: req.OrganizationForm, + OrganizationName: req.OrganizationName, + OrganizationShort: req.OrganizationShort, + INN: req.INN, + PersonalINN: req.PersonalINN, + IsActive: true, + IsVerified: false, + Role: "user", + } + + if err := s.accountRepo.Create(account); err != nil { + l.Error("Ошибка создания аккаунта", zap.Error(err)) + return nil, err + } + + l.Info("Аккаунт создан", zap.String("email", req.Email)) + return account, nil +} + +// UpdateAccountModel обновляет модель аккаунта +func (s *serviceImpl) UpdateAccountModel(account *models.Account) error { + return s.accountRepo.Update(account) +} + +// Вспомогательные методы + +func (s *serviceImpl) getSearchTotal(query string) (int64, error) { + // Здесь должна быть реализация подсчета общего количества результатов поиска + // Для простоты возвращаем 0 + return 0, nil +} + +func (s *serviceImpl) filterAccounts(accounts []models.Account, role string, isActive *bool) []models.Account { + var filtered []models.Account + + for _, acc := range accounts { + if role != "" && acc.Role != role { + continue + } + if isActive != nil && acc.IsActive != *isActive { + continue + } + filtered = append(filtered, acc) + } + + return filtered +} + +// Генерация reset токена +func generateResetToken() string { + // В реальном приложении используйте криптографически безопасную генерацию + return fmt.Sprintf("reset_%d_%d", time.Now().UnixNano(), time.Now().Unix()) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/account/types.go b/main_dc/yalarba/api_yal/internal/domain/account/types.go new file mode 100644 index 0000000..e33fcd6 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/account/types.go @@ -0,0 +1,34 @@ +package account + +// AccountType тип аккаунта +type AccountType string + +const ( + AccountTypeIndividual AccountType = "individual" // Физическое лицо + AccountTypeBusiness AccountType = "business" // Юридическое лицо +) + +// AccountRole роль пользователя +type AccountRole string + +const ( + RoleUser AccountRole = "user" + RoleModer AccountRole = "moderator" + RoleAdmin AccountRole = "admin" +) + +// ValidationRules правила валидации +type ValidationRules struct { + MinPasswordLength int + MaxNameLength int + MaxPhoneLength int + INNLength int +} + +// DefaultValidationRules правила валидации по умолчанию +var DefaultValidationRules = ValidationRules{ + MinPasswordLength: 6, + MaxNameLength: 100, + MaxPhoneLength: 20, + INNLength: 12, +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/middleware/admin.go b/main_dc/yalarba/api_yal/internal/middleware/admin.go new file mode 100644 index 0000000..c48e27e --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/middleware/admin.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "net/http" + + "api_yal/internal/logger" + + "go.uber.org/zap" +) + +// AdminOnlyMiddleware проверяет, что пользователь имеет права администратора +func AdminOnlyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + role, ok := GetUserRole(r.Context()) + if !ok { + l.Warn("Admin check: user role not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if role != "admin" { + l.Warn("Admin check: insufficient permissions", + zap.String("role", role)) + http.Error(w, "Admin access required", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// ModeratorOrAdminMiddleware проверяет, что пользователь имеет права модератора или администратора +func ModeratorOrAdminMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + role, ok := GetUserRole(r.Context()) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if role != "admin" && role != "moderator" { + http.Error(w, "Moderator or admin access required", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/main_dc/yalarba/api_yal/internal/middleware/auth.go b/main_dc/yalarba/api_yal/internal/middleware/auth.go index b944f31..4e7cd9d 100644 --- a/main_dc/yalarba/api_yal/internal/middleware/auth.go +++ b/main_dc/yalarba/api_yal/internal/middleware/auth.go @@ -1,4 +1,3 @@ -// middleware/auth.go (обновленная версия с логированием) package middleware import ( @@ -27,15 +26,16 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := logger.Get() - l.Info("=== AUTH MIDDLEWARE START ===") - l.Info("Request path", zap.String("path", r.URL.Path)) - + // Логируем только в debug режиме, чтобы не засорять логи + l.Debug("Auth middleware: processing request", + zap.String("path", r.URL.Path), + zap.String("method", r.Method)) + // Получаем токен из заголовка Authorization authHeader := r.Header.Get("Authorization") - l.Info("Authorization header", zap.String("header", authHeader)) if authHeader == "" { - l.Warn("Отсутствует заголовок Authorization") + l.Debug("Authorization header missing") http.Error(w, "Authorization header required", http.StatusUnauthorized) return } @@ -43,101 +43,112 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler { // Ожидаем формат "Bearer " parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { - l.Warn("Неверный формат заголовка Authorization", - zap.Int("parts_count", len(parts)), - zap.String("first_part", parts[0])) + l.Debug("Invalid authorization header format", + zap.String("header", authHeader)) http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) return } tokenString := parts[1] - l.Info("Token extracted", zap.String("token_preview", tokenString[:min(20, len(tokenString))]+"...")) // Парсим и валидируем токен - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - // Проверяем метод подписи - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - l.Error("Unexpected signing method", - zap.String("method", token.Method.Alg())) - return nil, jwt.ErrSignatureInvalid - } - return []byte(jwtSecret), nil - }) - + claims, err := validateToken(tokenString, jwtSecret) if err != nil { - l.Error("Token parse error", zap.Error(err)) - http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized) - return - } - - if !token.Valid { - l.Error("Token is not valid") + l.Debug("Token validation failed", zap.Error(err)) + + // Возвращаем разные сообщения в зависимости от ошибки + if err == jwt.ErrTokenExpired { + http.Error(w, "Token expired", http.StatusUnauthorized) + return + } http.Error(w, "Invalid token", http.StatusUnauthorized) return } - l.Info("Token is valid") - - // Извлекаем claims - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - l.Error("Failed to extract claims") - http.Error(w, "Invalid token claims", http.StatusUnauthorized) - return - } - - l.Info("Claims extracted", zap.Any("claims", claims)) - - // Проверяем тип токена (должен быть access) - if tokenType, exists := claims["type"]; exists { - l.Info("Token type", zap.String("type", tokenType.(string))) - if tokenType != "access" { - l.Error("Wrong token type, expected access", zap.String("type", tokenType.(string))) - http.Error(w, "Invalid token type", http.StatusUnauthorized) - return - } - } - // Добавляем информацию о пользователе в контекст - ctx := r.Context() + ctx := addUserToContext(r.Context(), claims) - // Извлекаем userID из sub (subject) - // В claims sub хранится как string, а не float64 - if userID, ok := claims["sub"].(string); ok { - l.Info("User ID from claims", zap.String("user_id_str", userID)) - // Конвертируем string в uint - var userIDUint uint - if _, err := fmt.Sscan(userID, &userIDUint); err == nil { - ctx = context.WithValue(ctx, UserIDKey, userIDUint) - l.Info("User ID added to context", zap.Uint("user_id", userIDUint)) - } - } else { - l.Error("sub claim not found or wrong type") - } - - // Извлекаем email - if email, ok := claims["email"].(string); ok { - ctx = context.WithValue(ctx, UserEmailKey, email) - l.Info("Email added to context", zap.String("email", email)) - } - - // Извлекаем роль - if role, ok := claims["role"].(string); ok { - ctx = context.WithValue(ctx, UserRoleKey, role) - l.Info("Role added to context", zap.String("role", role)) + // Логируем успешную аутентификацию (только в debug) + if userID, ok := ctx.Value(UserIDKey).(uint); ok { + l.Debug("User authenticated", + zap.Uint("user_id", userID), + zap.String("path", r.URL.Path)) } - l.Info("=== AUTH MIDDLEWARE END ===") - // Передаем управление дальше с обновленным контекстом next.ServeHTTP(w, r.WithContext(ctx)) }) } } -func min(a, b int) int { - if a < b { - return a +// validateToken валидирует JWT токен и возвращает claims +func validateToken(tokenString, jwtSecret string) (jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Проверяем метод подписи + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(jwtSecret), nil + }) + + if err != nil { + return nil, err } - return b + + if !token.Valid { + return nil, fmt.Errorf("invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid claims") + } + + // Проверяем тип токена (должен быть access) + if tokenType, exists := claims["type"]; !exists || tokenType != "access" { + return nil, fmt.Errorf("invalid token type, expected access") + } + + return claims, nil +} + +// addUserToContext добавляет информацию о пользователе в контекст +func addUserToContext(ctx context.Context, claims jwt.MapClaims) context.Context { + // Извлекаем userID из sub (subject) + if userIDStr, ok := claims["sub"].(string); ok { + var userID uint + if _, err := fmt.Sscan(userIDStr, &userID); err == nil { + ctx = context.WithValue(ctx, UserIDKey, userID) + } + } + + // Извлекаем email + if email, ok := claims["email"].(string); ok { + ctx = context.WithValue(ctx, UserEmailKey, email) + } + + // Извлекаем роль + if role, ok := claims["role"].(string); ok { + ctx = context.WithValue(ctx, UserRoleKey, role) + } + + return ctx +} + +// GetUserID извлекает ID пользователя из контекста +func GetUserID(ctx context.Context) (uint, bool) { + userID, ok := ctx.Value(UserIDKey).(uint) + return userID, ok +} + +// GetUserEmail извлекает email пользователя из контекста +func GetUserEmail(ctx context.Context) (string, bool) { + email, ok := ctx.Value(UserEmailKey).(string) + return email, ok +} + +// GetUserRole извлекает роль пользователя из контекста +func GetUserRole(ctx context.Context) (string, bool) { + role, ok := ctx.Value(UserRoleKey).(string) + return role, ok } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/middleware/context.go b/main_dc/yalarba/api_yal/internal/middleware/context.go new file mode 100644 index 0000000..0e6162b --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/middleware/context.go @@ -0,0 +1,20 @@ +package middleware + +import ( + "context" +) + +// WithUserID добавляет ID пользователя в контекст (для тестирования) +func WithUserID(ctx context.Context, userID uint) context.Context { + return context.WithValue(ctx, UserIDKey, userID) +} + +// WithUserEmail добавляет email пользователя в контекст +func WithUserEmail(ctx context.Context, email string) context.Context { + return context.WithValue(ctx, UserEmailKey, email) +} + +// WithUserRole добавляет роль пользователя в контекст +func WithUserRole(ctx context.Context, role string) context.Context { + return context.WithValue(ctx, UserRoleKey, role) +} diff --git a/main_dc/yalarba/api_yal/internal/middleware/logging.go b/main_dc/yalarba/api_yal/internal/middleware/logging.go new file mode 100644 index 0000000..1635301 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/middleware/logging.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "bytes" + "io" + "net/http" + "time" + + "api_yal/internal/logger" + + "github.com/go-chi/chi/v5/middleware" + "go.uber.org/zap" +) + +// RequestLoggerMiddleware логирует все HTTP запросы +func RequestLoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + start := time.Now() + + // Создаем кастомный ResponseWriter для захвата статуса + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + // Логируем тело запроса только для определенных методов (опционально) + var body []byte + if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" { + body, _ = io.ReadAll(r.Body) + r.Body = io.NopCloser(bytes.NewBuffer(body)) + } + + // Обрабатываем запрос + next.ServeHTTP(ww, r) + + // Логируем результат + duration := time.Since(start) + + logFields := []zap.Field{ + zap.String("method", r.Method), + zap.String("path", r.URL.Path), + zap.Int("status", ww.Status()), + zap.Duration("duration", duration), + zap.String("remote_addr", r.RemoteAddr), + zap.String("user_agent", r.UserAgent()), + } + + // Добавляем ID пользователя, если есть + if userID, ok := GetUserID(r.Context()); ok { + logFields = append(logFields, zap.Uint("user_id", userID)) + } + + // Логируем тело запроса для ошибок + if ww.Status() >= 400 && len(body) > 0 { + logFields = append(logFields, zap.ByteString("request_body", body)) + } + + // Выбираем уровень логирования в зависимости от статуса + switch { + case ww.Status() >= 500: + l.Error("Request failed", logFields...) + case ww.Status() >= 400: + l.Warn("Request error", logFields...) + default: + l.Info("Request completed", logFields...) + } + }) +} diff --git a/main_dc/yalarba/api_yal/internal/router/router.go b/main_dc/yalarba/api_yal/internal/router/router.go index e162493..9da5d60 100644 --- a/main_dc/yalarba/api_yal/internal/router/router.go +++ b/main_dc/yalarba/api_yal/internal/router/router.go @@ -4,6 +4,7 @@ import ( "api_yal/internal/config" "api_yal/internal/logger" "api_yal/internal/domain/auth" + "api_yal/internal/domain/account" "time" "encoding/json" @@ -45,9 +46,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { }) zapLogger.Debug("Health check маршрут зарегистрирован") - // Здесь можно добавить другие маршруты, которые будут защищены аутентификацией - auth.RegisterRoutes(r, db, config.JWTSecret) - // r.Mount("/api/v1", apiRoutes(db, config)) + // Группируем API маршруты под /api/v1 + r.Route("/api/v1", func(r chi.Router) { + // Регистрируем маршруты аутентификации + auth.RegisterRoutes(r, db, config.JWTSecret) + + // Регистрируем маршруты аккаунтов + account.RegisterRoutes(r, db, config.JWTSecret) + }) zapLogger.Info("Настройка маршрутов завершена")