Files
tp/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go
T
valitovgaziz 21c6c03b27 On branch main
modified:   internal/database/psql_db.go
	modified:   internal/domain/auth/dto.go
	modified:   internal/domain/auth/handler.go
	modified:   internal/domain/auth/router.go
	modified:   internal/domain/auth/servcie.go
	new file:   internal/models/password_reset.go
	modified:   internal/repository/account_repository.go
	modified:   internal/repository/account_repository_impl.go
auth domain is implemented but not tested
2026-03-31 05:29:46 +05:00

471 lines
15 KiB
Go

// service.go
package auth
import (
"api_yal/internal/logger"
"api_yal/internal/models"
"api_yal/internal/repository"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
// AuthService интерфейс сервиса аутентификации
type AuthService interface {
Register(req RegisterRequest) (*AuthResponse, error)
Login(req LoginRequest) (*AuthResponse, string, error)
RefreshToken(refreshToken string) (*RefreshTokenResponse, error)
Logout(userID uint) error
ValidateAccessToken(tokenString string) (*jwt.MapClaims, error)
GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error)
// Reset password methods
RequestPasswordReset(email string) (string, error) // Возвращает reset token
ConfirmPasswordReset(token, newPassword string) error
}
// authServiceImpl реализация сервиса аутентификации
type authServiceImpl struct {
accountRepo repository.AccountRepository
jwtSecret []byte
accessTokenTTL time.Duration
refreshTokenTTL time.Duration
resetTokenTTL time.Duration
}
// AuthServiceConfig конфигурация для сервиса аутентификации
type AuthServiceConfig struct {
JWTSecret string
AccessTokenTTL time.Duration // Рекомендуется 15-30 минут
RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней
ResetTokenTTL time.Duration // Рекомендуется 1 час
}
// NewAuthService создает новый экземпляр сервиса аутентификации
func NewAuthService(accountRepo repository.AccountRepository, config AuthServiceConfig) AuthService {
return &authServiceImpl{
accountRepo: accountRepo,
jwtSecret: []byte(config.JWTSecret),
accessTokenTTL: config.AccessTokenTTL,
refreshTokenTTL: config.RefreshTokenTTL,
resetTokenTTL: config.ResetTokenTTL,
}
}
// Register регистрирует нового пользователя
func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
l := logger.Get()
l.Info("Начало регистрации нового пользователя", zap.String("email", req.Email))
// Проверяем, существует ли пользователь с таким email
existingUser, err := s.accountRepo.GetByEmail(req.Email)
if err == nil && existingUser != nil {
return nil, ErrUserAlreadyExists
}
// Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
l.Error("Ошибка хеширования пароля", zap.Error(err))
return nil, err
}
// Формируем полное имя
fullName := req.FirstName + " " + req.LastName
// Создаем аккаунт
newAcc := &models.Account{
Email: req.Email,
PasswordHash: string(hashedPassword),
FirstName: req.FirstName,
LastName: req.LastName,
FullName: fullName,
IsActive: true,
IsVerified: false,
Role: "user",
}
// Сохраняем в базу данных
if err := s.accountRepo.Create(newAcc); err != nil {
l.Error("Ошибка создания аккаунта", zap.Error(err))
return nil, err
}
// Генерируем access token
accessToken, expiresAt, err := s.generateAccessToken(newAcc)
if err != nil {
return nil, err
}
// Формируем ответ
response := &AuthResponse{
Token: accessToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: newAcc.Base.ID,
Email: newAcc.Email,
FirstName: newAcc.FirstName,
LastName: newAcc.LastName,
FullName: newAcc.FullName,
Role: newAcc.Role,
},
}
l.Info("Пользователь успешно зарегистрирован", zap.String("email", req.Email))
return response, nil
}
// Login аутентифицирует пользователя
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) {
l := logger.Get()
l.Info("Начало входа пользователя", zap.String("email", req.Email))
// Ищем пользователя по email
account, err := s.accountRepo.GetByEmail(req.Email)
if err != nil {
l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err))
return nil, "", ErrUserNotFound
}
// Проверяем, активен ли аккаунт
if !account.IsActive {
l.Error("Аккаунт деактивирован", zap.String("email", req.Email))
return nil, "", errors.New("account is deactivated")
}
// Сравниваем пароли
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil {
l.Error("Неверный пароль для пользователя", zap.String("email", req.Email))
return nil, "", ErrInvalidPassword
}
// Генерируем токены
accessToken, expiresAt, err := s.generateAccessToken(account)
if err != nil {
return nil, "", err
}
refreshToken, err := s.generateRefreshToken(account)
if err != nil {
return nil, "", err
}
// Формируем ответ
response := &AuthResponse{
Token: accessToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
Email: account.Email,
FirstName: account.FirstName,
LastName: account.LastName,
FullName: account.FullName,
Role: account.Role,
},
}
l.Info("Пользователь успешно вошел", zap.String("email", req.Email))
return response, refreshToken, nil
}
// RequestPasswordReset запрашивает сброс пароля
func (s *authServiceImpl) RequestPasswordReset(email string) (string, error) {
l := logger.Get()
l.Info("Запрос сброса пароля", zap.String("email", email))
// Проверяем существование пользователя
account, err := s.accountRepo.GetByEmail(email)
if err != nil {
l.Error("Пользователь не найден", zap.String("email", email), zap.Error(err))
return "", ErrUserNotFound
}
// Генерируем reset token
resetToken, err := s.generateResetToken(account)
if err != nil {
l.Error("Ошибка генерации reset token", zap.Error(err))
return "", err
}
// Сохраняем reset token в базу данных
// Для этого нужно создать модель PasswordReset
passwordReset := &models.PasswordReset{
AccountID: account.Base.ID,
Token: resetToken,
ExpiresAt: time.Now().Add(s.resetTokenTTL),
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), zap.String("token", resetToken))
return resetToken, nil
}
// ConfirmPasswordReset подтверждает сброс пароля
func (s *authServiceImpl) ConfirmPasswordReset(token, newPassword string) error {
l := logger.Get()
l.Info("Подтверждение сброса пароля")
// Находим reset token в базе
passwordReset, err := s.accountRepo.GetPasswordResetByToken(token)
if err != nil {
l.Error("Reset token не найден", zap.Error(err))
return ErrResetTokenNotFound
}
// Проверяем, не истек ли токен
if passwordReset.ExpiresAt.Before(time.Now()) {
l.Error("Reset token истек")
return ErrResetTokenInvalid
}
// Проверяем, не использован ли токен
if passwordReset.Used {
l.Error("Reset token уже использован")
return ErrResetTokenInvalid
}
// Получаем пользователя
account, err := s.accountRepo.GetByID(passwordReset.AccountID)
if err != nil {
l.Error("Пользователь не найден", zap.Error(err))
return ErrUserNotFound
}
// Хешируем новый пароль
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.Base.ID))
return nil
}
// RefreshToken обновляет JWT токен по refresh token
func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) {
l := logger.Get()
l.Info("Начало обновления токена")
// Парсим и валидируем refresh token
claims, err := s.validateRefreshToken(refreshToken)
if err != nil {
l.Error("Ошибка валидации refresh token", zap.Error(err))
return nil, err
}
// Получаем ID пользователя из claims
userIDStr, ok := (*claims)["sub"].(string)
if !ok {
l.Error("Не удалось получить subject из токена")
return nil, ErrInvalidToken
}
// Конвертируем string в uint
var userID uint
if _, err := fmt.Sscan(userIDStr, &userID); err != nil {
l.Error("Ошибка конвертации user ID", zap.Error(err))
return nil, ErrInvalidToken
}
// Получаем пользователя из базы по ID
account, err := s.accountRepo.GetByID(userID)
if err != nil {
l.Error("Пользователь не найден", zap.Uint("userID", userID), zap.Error(err))
return nil, ErrUserNotFound
}
// Проверяем, активен ли аккаунт
if !account.IsActive {
l.Error("Аккаунт деактивирован", zap.Uint("userID", userID))
return nil, errors.New("account is deactivated")
}
// Генерируем новый access token
accessToken, expiresAt, err := s.generateAccessToken(account)
if err != nil {
l.Error("Ошибка генерации access token", zap.Error(err))
return nil, err
}
l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID))
return &RefreshTokenResponse{
Token: accessToken,
ExpiresAt: expiresAt,
User: UserInfo{
ID: account.Base.ID,
Email: account.Email,
FirstName: account.FirstName,
LastName: account.LastName,
FullName: account.FullName,
Role: account.Role,
},
}, nil
}
// Logout выход пользователя
func (s *authServiceImpl) Logout(userID uint) error {
l := logger.Get()
l.Info("Выход пользователя", zap.Uint("userID", userID))
// Здесь можно добавить логику инвалидации токенов (например, в Redis)
l.Info("Выход успешно завершен", zap.Uint("userID", userID))
return nil
}
// ValidateAccessToken валидирует access token
func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return s.jwtSecret, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidToken
}
if tokenType, exists := claims["type"]; !exists || tokenType != "access" {
return nil, ErrInvalidToken
}
return &claims, nil
}
// GetUserFromToken получает пользователя из claims
func (s *authServiceImpl) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) {
userIDStr, ok := (*claims)["sub"].(string)
if !ok {
return nil, ErrInvalidToken
}
var userID uint
if _, err := fmt.Sscan(userIDStr, &userID); err != nil {
return nil, ErrInvalidToken
}
return s.accountRepo.GetByID(userID)
}
// generateAccessToken генерирует access token
func (s *authServiceImpl) generateAccessToken(account *models.Account) (string, time.Time, error) {
expiresAt := time.Now().Add(s.accessTokenTTL)
claims := jwt.MapClaims{
"sub": fmt.Sprintf("%d", account.Base.ID),
"email": account.Email,
"role": account.Role,
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
"type": "access",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", time.Time{}, err
}
return tokenString, expiresAt, nil
}
// generateRefreshToken генерирует refresh token
func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string, error) {
expiresAt := time.Now().Add(s.refreshTokenTTL)
claims := jwt.MapClaims{
"sub": fmt.Sprintf("%d", account.Base.ID),
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
"type": "refresh",
"jti": fmt.Sprintf("refresh-%d-%d", account.Base.ID, time.Now().Unix()),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", err
}
return tokenString, nil
}
// generateResetToken генерирует reset token
func (s *authServiceImpl) generateResetToken(account *models.Account) (string, error) {
// Генерируем случайный токен
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
token := base64.URLEncoding.EncodeToString(bytes)
return token, nil
}
// validateRefreshToken валидирует refresh token
func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return s.jwtSecret, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidToken
}
if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" {
return nil, ErrInvalidToken
}
exp, ok := claims["exp"].(float64)
if ok && int64(exp) < time.Now().Unix() {
return nil, ErrTokenExpired
}
return &claims, nil
}