199 lines
6.0 KiB
Go
199 lines
6.0 KiB
Go
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)
|
||
} |