Initial commit: Эфир мессенджер

This commit is contained in:
2026-04-06 14:57:36 +03:00
commit ff93679b6d
50 changed files with 5642 additions and 0 deletions

View 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)
}