package services import ( "context" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "github.com/photoplaces/backend/internal/models" "github.com/photoplaces/backend/internal/repository" "golang.org/x/crypto/bcrypt" ) var ( ErrEmailExists = errors.New("email already exists") ErrInvalidCreds = errors.New("invalid email or password") ErrUserBanned = errors.New("account is banned") ErrInvalidToken = errors.New("invalid or expired token") ) type AuthService struct { userRepo *repository.UserRepo jwtSecret []byte refreshSecret []byte } func NewAuthService(userRepo *repository.UserRepo, jwtSecret, refreshSecret string) *AuthService { return &AuthService{ userRepo: userRepo, jwtSecret: []byte(jwtSecret), refreshSecret: []byte(refreshSecret), } } type AuthResult struct { User *models.User `json:"user"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } type RegisterInput struct { Email string Password string Role string Name string } func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, error) { existing, _ := s.userRepo.GetByEmail(ctx, input.Email) if existing != nil { return nil, ErrEmailExists } hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("hash password: %w", err) } user := &models.User{ Email: input.Email, PasswordHash: string(hash), Role: input.Role, Name: strPtr(input.Name), } if err := s.userRepo.Create(ctx, user); err != nil { return nil, fmt.Errorf("create user: %w", err) } return s.generateTokens(ctx, user) } type LoginInput struct { Email string Password string } func (s *AuthService) Login(ctx context.Context, input LoginInput) (*AuthResult, error) { user, err := s.userRepo.GetByEmail(ctx, input.Email) if err != nil { return nil, fmt.Errorf("get user: %w", err) } if user == nil { return nil, ErrInvalidCreds } if user.Status == "banned" { return nil, ErrUserBanned } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil { return nil, ErrInvalidCreds } return s.generateTokens(ctx, user) } type TokenClaims struct { UserID string `json:"user_id"` Role string `json:"role"` jwt.RegisteredClaims } func (s *AuthService) generateTokens(ctx context.Context, user *models.User) (*AuthResult, error) { now := time.Now() accessClaims := TokenClaims{ UserID: user.ID, Role: user.Role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)), IssuedAt: jwt.NewNumericDate(now), }, } accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.jwtSecret) if err != nil { return nil, fmt.Errorf("sign access token: %w", err) } refreshClaims := jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(30 * 24 * time.Hour)), IssuedAt: jwt.NewNumericDate(now), Subject: user.ID, } refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.refreshSecret) if err != nil { return nil, fmt.Errorf("sign refresh token: %w", err) } return &AuthResult{ User: user, AccessToken: accessToken, RefreshToken: refreshToken, }, nil } func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) { token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(t *jwt.Token) (interface{}, error) { return s.jwtSecret, nil }) if err != nil { return nil, ErrInvalidToken } claims, ok := token.Claims.(*TokenClaims) if !ok || !token.Valid { return nil, ErrInvalidToken } return claims, nil } func (s *AuthService) ValidateRefreshToken(tokenString string) (string, error) { token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) { return s.refreshSecret, nil }) if err != nil { return "", ErrInvalidToken } claims, ok := token.Claims.(*jwt.RegisteredClaims) if !ok || !token.Valid { return "", ErrInvalidToken } return claims.Subject, nil } func (s *AuthService) RefreshSession(ctx context.Context, refreshToken string) (*AuthResult, error) { userID, err := s.ValidateRefreshToken(refreshToken) if err != nil { return nil, err } user, err := s.userRepo.GetByID(ctx, userID) if err != nil { return nil, fmt.Errorf("get user: %w", err) } if user == nil { return nil, ErrInvalidToken } if user.Status == "banned" { return nil, ErrUserBanned } return s.generateTokens(ctx, user) } func strPtr(s string) *string { if s == "" { return nil } return &s }