initial commit

This commit is contained in:
2025-11-29 00:28:21 +05:00
parent 46229acc82
commit ec3b03a935
76 changed files with 13492 additions and 0 deletions

View File

@@ -0,0 +1,246 @@
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
}