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
This commit is contained in:
@@ -9,11 +9,11 @@ import (
|
|||||||
|
|
||||||
// CreateAccountRequest - DTO для создания нового аккаунта
|
// CreateAccountRequest - DTO для создания нового аккаунта
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" binding:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
FullName string `json:"full_name" binding:"required"`
|
FullName string `json:"full_name" validate:"required"`
|
||||||
FirstName string `json:"first_name" binding:"required"`
|
FirstName string `json:"first_name" validate:"required"`
|
||||||
LastName string `json:"last_name" binding:"required"`
|
LastName string `json:"last_name" validate:"required"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
|
|
||||||
@@ -43,19 +43,19 @@ type UpdateAccountRequest struct {
|
|||||||
|
|
||||||
// ChangePasswordRequest - DTO для смены пароля
|
// ChangePasswordRequest - DTO для смены пароля
|
||||||
type ChangePasswordRequest struct {
|
type ChangePasswordRequest struct {
|
||||||
CurrentPassword string `json:"current_password" binding:"required"`
|
CurrentPassword string `json:"current_password" validate:"required"`
|
||||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForgotPasswordRequest - DTO для запроса сброса пароля
|
// ForgotPasswordRequest - DTO для запроса сброса пароля
|
||||||
type ForgotPasswordRequest struct {
|
type ForgotPasswordRequest struct {
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPasswordRequest - DTO для сброса пароля по токену
|
// ResetPasswordRequest - DTO для сброса пароля по токену
|
||||||
type ResetPasswordRequest struct {
|
type ResetPasswordRequest struct {
|
||||||
Token string `json:"token" binding:"required"`
|
Token string `json:"token" validate:"required"`
|
||||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyAccountRequest - DTO для верификации аккаунта (админ)
|
// VerifyAccountRequest - DTO для верификации аккаунта (админ)
|
||||||
@@ -66,15 +66,15 @@ type VerifyAccountRequest struct {
|
|||||||
// UpdateAccountStatusRequest - DTO для изменения статуса аккаунта (админ)
|
// UpdateAccountStatusRequest - DTO для изменения статуса аккаунта (админ)
|
||||||
type UpdateAccountStatusRequest struct {
|
type UpdateAccountStatusRequest struct {
|
||||||
IsActive bool `json:"is_active"`
|
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 для фильтрации списка аккаунтов
|
// ListAccountsRequest - DTO для фильтрации списка аккаунтов
|
||||||
type ListAccountsRequest struct {
|
type ListAccountsRequest struct {
|
||||||
Page int `form:"page" binding:"min=1"`
|
Page int `form:"page" validate:"min=1"`
|
||||||
PageSize int `form:"page_size" binding:"min=1,max=100"`
|
PageSize int `form:"page_size" validate:"min=1,max=100"`
|
||||||
Search string `form:"search"`
|
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"`
|
IsActive *bool `form:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,19 +140,19 @@ type AccountProfileResponse struct {
|
|||||||
|
|
||||||
// AccountStats - статистика аккаунта
|
// AccountStats - статистика аккаунта
|
||||||
type AccountStats struct {
|
type AccountStats struct {
|
||||||
ObjectsCount int `json:"objects_count"`
|
ObjectsCount int `json:"objects_count"`
|
||||||
FeedbacksCount int `json:"feedbacks_count"`
|
FeedbacksCount int `json:"feedbacks_count"`
|
||||||
CommentsCount int `json:"comments_count"`
|
CommentsCount int `json:"comments_count"`
|
||||||
RatingsCount int `json:"ratings_count"`
|
RatingsCount int `json:"ratings_count"`
|
||||||
AppealsCount int `json:"appeals_count"`
|
AppealsCount int `json:"appeals_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthResponse - DTO для ответа при авторизации
|
// AuthResponse - DTO для ответа при авторизации
|
||||||
type AuthResponse struct {
|
type AuthResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
ExpiresAt int64 `json:"expires_at"`
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
Account AccountResponse `json:"account"`
|
Account AccountResponse `json:"account"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasswordResetResponse - DTO для ответа о сбросе пароля
|
// PasswordResetResponse - DTO для ответа о сбросе пароля
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
@@ -1 +1,447 @@
|
|||||||
package account
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1 +1,51 @@
|
|||||||
package account
|
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("Маршруты аккаунтов зарегистрированы")
|
||||||
|
}
|
||||||
@@ -1 +1,561 @@
|
|||||||
package account
|
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())
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// middleware/auth.go (обновленная версия с логированием)
|
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -27,15 +26,16 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
l := logger.Get()
|
l := logger.Get()
|
||||||
|
|
||||||
l.Info("=== AUTH MIDDLEWARE START ===")
|
// Логируем только в debug режиме, чтобы не засорять логи
|
||||||
l.Info("Request path", zap.String("path", r.URL.Path))
|
l.Debug("Auth middleware: processing request",
|
||||||
|
zap.String("path", r.URL.Path),
|
||||||
|
zap.String("method", r.Method))
|
||||||
|
|
||||||
// Получаем токен из заголовка Authorization
|
// Получаем токен из заголовка Authorization
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
l.Info("Authorization header", zap.String("header", authHeader))
|
|
||||||
|
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
l.Warn("Отсутствует заголовок Authorization")
|
l.Debug("Authorization header missing")
|
||||||
http.Error(w, "Authorization header required", http.StatusUnauthorized)
|
http.Error(w, "Authorization header required", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,101 +43,112 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
|
|||||||
// Ожидаем формат "Bearer <token>"
|
// Ожидаем формат "Bearer <token>"
|
||||||
parts := strings.Split(authHeader, " ")
|
parts := strings.Split(authHeader, " ")
|
||||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
l.Warn("Неверный формат заголовка Authorization",
|
l.Debug("Invalid authorization header format",
|
||||||
zap.Int("parts_count", len(parts)),
|
zap.String("header", authHeader))
|
||||||
zap.String("first_part", parts[0]))
|
|
||||||
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
|
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenString := parts[1]
|
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) {
|
claims, err := validateToken(tokenString, jwtSecret)
|
||||||
// Проверяем метод подписи
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error("Token parse error", zap.Error(err))
|
l.Debug("Token validation failed", zap.Error(err))
|
||||||
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
// Возвращаем разные сообщения в зависимости от ошибки
|
||||||
}
|
if err == jwt.ErrTokenExpired {
|
||||||
|
http.Error(w, "Token expired", http.StatusUnauthorized)
|
||||||
if !token.Valid {
|
return
|
||||||
l.Error("Token is not valid")
|
}
|
||||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||||
return
|
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)
|
// Логируем успешную аутентификацию (только в debug)
|
||||||
// В claims sub хранится как string, а не float64
|
if userID, ok := ctx.Value(UserIDKey).(uint); ok {
|
||||||
if userID, ok := claims["sub"].(string); ok {
|
l.Debug("User authenticated",
|
||||||
l.Info("User ID from claims", zap.String("user_id_str", userID))
|
zap.Uint("user_id", userID),
|
||||||
// Конвертируем string в uint
|
zap.String("path", r.URL.Path))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info("=== AUTH MIDDLEWARE END ===")
|
|
||||||
|
|
||||||
// Передаем управление дальше с обновленным контекстом
|
// Передаем управление дальше с обновленным контекстом
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
// validateToken валидирует JWT токен и возвращает claims
|
||||||
if a < b {
|
func validateToken(tokenString, jwtSecret string) (jwt.MapClaims, error) {
|
||||||
return a
|
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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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...)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"api_yal/internal/config"
|
"api_yal/internal/config"
|
||||||
"api_yal/internal/logger"
|
"api_yal/internal/logger"
|
||||||
"api_yal/internal/domain/auth"
|
"api_yal/internal/domain/auth"
|
||||||
|
"api_yal/internal/domain/account"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -45,9 +46,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
|||||||
})
|
})
|
||||||
zapLogger.Debug("Health check маршрут зарегистрирован")
|
zapLogger.Debug("Health check маршрут зарегистрирован")
|
||||||
|
|
||||||
// Здесь можно добавить другие маршруты, которые будут защищены аутентификацией
|
// Группируем API маршруты под /api/v1
|
||||||
auth.RegisterRoutes(r, db, config.JWTSecret)
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
// r.Mount("/api/v1", apiRoutes(db, config))
|
// Регистрируем маршруты аутентификации
|
||||||
|
auth.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
|
||||||
|
// Регистрируем маршруты аккаунтов
|
||||||
|
account.RegisterRoutes(r, db, config.JWTSecret)
|
||||||
|
})
|
||||||
|
|
||||||
zapLogger.Info("Настройка маршрутов завершена")
|
zapLogger.Info("Настройка маршрутов завершена")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user