247 lines
7.7 KiB
Go
247 lines
7.7 KiB
Go
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
|
||
}
|