package service import ( "context" "crypto/rand" "encoding/base64" "fmt" "time" "git.kirlllll.ru/volontery/backend/internal/database" "git.kirlllll.ru/volontery/backend/internal/pkg/jwt" "git.kirlllll.ru/volontery/backend/internal/pkg/password" "git.kirlllll.ru/volontery/backend/internal/repository" ) // AuthService предоставляет методы для аутентификации type AuthService struct { userRepo *repository.UserRepository authRepo *repository.AuthRepository rbacRepo *repository.RBACRepository jwtMgr *jwt.Manager } // NewAuthService создает новый AuthService func NewAuthService( userRepo *repository.UserRepository, authRepo *repository.AuthRepository, rbacRepo *repository.RBACRepository, jwtMgr *jwt.Manager, ) *AuthService { return &AuthService{ userRepo: userRepo, authRepo: authRepo, rbacRepo: rbacRepo, jwtMgr: jwtMgr, } } // RegisterRequest - запрос на регистрацию type RegisterRequest struct { Email string `json:"email"` Password string `json:"password"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Phone string `json:"phone,omitempty"` Latitude float64 `json:"latitude,omitempty"` Longitude float64 `json:"longitude,omitempty"` Address string `json:"address,omitempty"` City string `json:"city,omitempty"` Bio string `json:"bio,omitempty"` } // LoginRequest - запрос на вход type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } // AuthResponse - ответ с токенами type AuthResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int64 `json:"expires_in"` User *UserInfo `json:"user"` } // UserInfo - информация о пользователе type UserInfo struct { ID int64 `json:"id"` Email string `json:"email"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Verified bool `json:"email_verified"` } // Register регистрирует нового пользователя func (s *AuthService) Register(ctx context.Context, req RegisterRequest) (*AuthResponse, error) { // Валидация if req.Email == "" { return nil, fmt.Errorf("email is required") } if !password.IsValid(req.Password) { return nil, fmt.Errorf("password must be at least 8 characters") } if req.FirstName == "" || req.LastName == "" { return nil, fmt.Errorf("first name and last name are required") } // Проверка существования email exists, err := s.userRepo.EmailExists(ctx, req.Email) if err != nil { return nil, fmt.Errorf("failed to check email: %w", err) } if exists { return nil, fmt.Errorf("email already registered") } // Хеширование пароля hashedPassword, err := password.Hash(req.Password) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } // Создание пользователя user, err := s.userRepo.Create(ctx, database.CreateUserParams{ Email: req.Email, PasswordHash: hashedPassword, FirstName: req.FirstName, LastName: req.LastName, Phone: stringToPgText(req.Phone), StMakepoint: req.Longitude, StMakepoint_2: req.Latitude, Address: stringToPgText(req.Address), City: stringToPgText(req.City), }) if err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } // Назначение роли "requester" по умолчанию requesterRole, err := s.rbacRepo.GetRoleByName(ctx, "requester") if err == nil { _, _ = s.rbacRepo.AssignRoleToUser(ctx, database.AssignRoleToUserParams{ UserID: user.ID, RoleID: requesterRole.ID, AssignedBy: int64ToPgInt8(user.ID), // сам себе назначил }) } // Генерация токенов return s.generateTokens(ctx, user.ID, user.Email, "", "") } // Login выполняет вход пользователя func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*AuthResponse, error) { // Получение пользователя user, err := s.userRepo.GetByEmail(ctx, req.Email) if err != nil { return nil, fmt.Errorf("invalid email or password") } // Проверка пароля if err := password.Verify(user.PasswordHash, req.Password); err != nil { return nil, fmt.Errorf("invalid email or password") } // Проверка блокировки if user.IsBlocked.Bool { return nil, fmt.Errorf("user account is blocked") } // Обновление времени последнего входа _ = s.userRepo.UpdateLastLogin(ctx, user.ID) // Генерация токенов return s.generateTokens(ctx, user.ID, user.Email, "", "") } // RefreshTokens обновляет токены func (s *AuthService) RefreshTokens(ctx context.Context, refreshTokenString string) (*AuthResponse, error) { // Валидация refresh токена claims, err := s.jwtMgr.ValidateToken(refreshTokenString) if err != nil { return nil, fmt.Errorf("invalid refresh token") } // Проверка токена в БД storedToken, err := s.authRepo.GetRefreshToken(ctx, refreshTokenString) if err != nil { return nil, fmt.Errorf("refresh token not found or expired") } // Отзыв старого токена _ = s.authRepo.RevokeRefreshToken(ctx, storedToken.ID) // Получение пользователя user, err := s.userRepo.GetByID(ctx, claims.UserID) if err != nil { return nil, fmt.Errorf("user not found") } if user.IsBlocked.Bool { return nil, fmt.Errorf("user account is blocked") } // Генерация новых токенов return s.generateTokens(ctx, user.ID, user.Email, "", "") } // Logout выход пользователя func (s *AuthService) Logout(ctx context.Context, userID int64) error { return s.authRepo.RevokeAllUserTokens(ctx, userID) } // generateTokens генерирует access и refresh токены func (s *AuthService) generateTokens(ctx context.Context, userID int64, email, userAgent, ipAddress string) (*AuthResponse, error) { // Генерация access токена accessToken, err := s.jwtMgr.GenerateAccessToken(userID, email) if err != nil { return nil, fmt.Errorf("failed to generate access token: %w", err) } // Генерация refresh токена refreshToken, err := s.jwtMgr.GenerateRefreshToken(userID, email) if err != nil { return nil, fmt.Errorf("failed to generate refresh token: %w", err) } // Сохранение refresh токена в БД expiresAt := time.Now().Add(s.jwtMgr.GetRefreshTokenDuration()) _, err = s.authRepo.CreateRefreshToken(ctx, database.CreateRefreshTokenParams{ UserID: userID, Token: refreshToken, ExpiresAt: timeToPgTimestamptz(expiresAt), UserAgent: stringToPgText(userAgent), IpAddress: nil, // IP адрес не передается в текущей реализации }) if err != nil { return nil, fmt.Errorf("failed to save refresh token: %w", err) } // Получение информации о пользователе user, _ := s.userRepo.GetByID(ctx, userID) return &AuthResponse{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(s.jwtMgr.GetAccessTokenDuration().Seconds()), User: &UserInfo{ ID: userID, Email: email, FirstName: user.FirstName, LastName: user.LastName, Verified: user.EmailVerified.Bool, }, }, nil } // generateRandomToken генерирует случайный токен func generateRandomToken(length int) (string, error) { bytes := make([]byte, length) if _, err := rand.Read(bytes); err != nil { return "", err } return base64.URLEncoding.EncodeToString(bytes), nil }