Files
backend/internal/service/auth_service.go
2025-12-13 22:34:01 +05:00

247 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}