Files
efir-api-server/internal/service/auth_service.go

199 lines
6.0 KiB
Go
Raw Permalink 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"
"errors"
"fmt"
"messenger/internal/models"
"messenger/internal/pkg/logger"
"messenger/internal/pkg/validator"
"messenger/internal/repository"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
type AuthService struct {
userRepo repository.UserRepository
profileRepo repository.ProfileRepository
jwtSecret []byte
jwtExpiry int64
}
type Claims struct {
UserID int64 `json:"user_id"`
Login string `json:"login"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func NewAuthService(userRepo repository.UserRepository, profileRepo repository.ProfileRepository, jwtSecret []byte, jwtExpiry int64) *AuthService {
return &AuthService{
userRepo: userRepo,
profileRepo: profileRepo,
jwtSecret: jwtSecret,
jwtExpiry: jwtExpiry,
}
}
// Register регистрирует нового пользователя
func (s *AuthService) Register(ctx context.Context, login, password string) (*models.User, string, error) {
// Валидация входных данных
if !validator.ValidateLogin(login) {
return nil, "", errors.New("invalid login: must be 3-32 characters, only letters, numbers and underscore")
}
if !validator.ValidatePassword(password) {
return nil, "", errors.New("invalid password: must be at least 6 characters")
}
// Проверка существования пользователя
exists, err := s.userRepo.Exists(ctx, login)
if err != nil {
logger.Error("Failed to check user existence", "error", err)
return nil, "", errors.New("internal server error")
}
if exists {
return nil, "", errors.New("user already exists")
}
// Хэширование пароля
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
logger.Error("Failed to hash password", "error", err)
return nil, "", errors.New("internal server error")
}
// Создание пользователя
user := &models.User{
Login: login,
PasswordHash: string(passwordHash),
Role: models.RoleUser,
CreatedAt: time.Now(),
}
if err := s.userRepo.Create(ctx, user); err != nil {
logger.Error("Failed to create user", "error", err)
return nil, "", errors.New("internal server error")
}
// Создание профиля
profile := &models.Profile{
UserID: user.ID,
}
if err := s.profileRepo.Create(ctx, profile); err != nil {
logger.Error("Failed to create profile", "error", err)
// Не фатально, профиль можно создать позже
}
// Генерация JWT токена
token, err := s.generateToken(user)
if err != nil {
logger.Error("Failed to generate token", "error", err)
return nil, "", errors.New("internal server error")
}
logger.Info("User registered successfully", "user_id", user.ID, "login", user.Login)
return user, token, nil
}
// Login аутентифицирует пользователя
func (s *AuthService) Login(ctx context.Context, login, password string) (*models.User, string, error) {
// Поиск пользователя
user, err := s.userRepo.FindByLogin(ctx, login)
if err != nil {
logger.Error("Failed to find user", "error", err)
return nil, "", errors.New("internal server error")
}
if user == nil {
return nil, "", errors.New("invalid credentials")
}
// Проверка пароля
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, "", errors.New("invalid credentials")
}
// Обновление last_seen
go func() {
if err := s.userRepo.UpdateLastSeen(context.Background(), user.ID); err != nil {
logger.Error("Failed to update last_seen", "error", err)
}
}()
// Генерация JWT токена
token, err := s.generateToken(user)
if err != nil {
logger.Error("Failed to generate token", "error", err)
return nil, "", errors.New("internal server error")
}
logger.Info("User logged in", "user_id", user.ID, "login", user.Login)
return user, token, nil
}
// ValidateToken проверяет валидность JWT токена и возвращает пользователя
func (s *AuthService) ValidateToken(tokenString string) (*models.User, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return s.jwtSecret, nil
})
if err != nil {
return nil, errors.New("invalid token")
}
if !token.Valid {
return nil, errors.New("invalid token")
}
// Получаем пользователя из БД (на случай, если роль изменилась)
ctx := context.Background()
user, err := s.userRepo.FindByID(ctx, claims.UserID)
if err != nil {
return nil, errors.New("user not found")
}
if user == nil {
return nil, errors.New("user not found")
}
return user, nil
}
// generateToken генерирует JWT токен для пользователя
func (s *AuthService) generateToken(user *models.User) (string, error) {
expirationTime := time.Now().Add(time.Duration(s.jwtExpiry) * time.Hour)
claims := &Claims{
UserID: user.ID,
Login: user.Login,
Role: string(user.Role),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.jwtSecret)
}
// GetUserByID возвращает пользователя по ID
func (s *AuthService) GetUserByID(ctx context.Context, userID int64) (*models.User, error) {
return s.userRepo.FindByID(ctx, userID)
}
// GetUserByLogin возвращает пользователя по логину
func (s *AuthService) GetUserByLogin(ctx context.Context, login string) (*models.User, error) {
return s.userRepo.FindByLogin(ctx, login)
}