75b2f3f6b2
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
561 lines
17 KiB
Go
561 lines
17 KiB
Go
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())
|
|
} |