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 для создания нового аккаунта
|
||||
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 для ответа о сбросе пароля
|
||||
|
||||
@@ -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
|
||||
|
||||
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 <token>"
|
||||
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
|
||||
}
|
||||
@@ -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/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("Настройка маршрутов завершена")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user