Initial commit: Эфир мессенджер
This commit is contained in:
199
internal/service/auth_service.go
Normal file
199
internal/service/auth_service.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user