package logic import ( "billit/internal/models" "crypto/rand" "encoding/hex" "errors" "os" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) var ( ErrInvalidCredentials = errors.New("invalid email or password") ErrInvalidToken = errors.New("invalid or expired token") ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") ) // AuthConfig holds auth configuration type AuthConfig struct { JWTSecret []byte TokenDuration time.Duration } // Claims represents JWT claims type Claims struct { UserID string `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } // AuthService handles authentication logic type AuthService struct { config AuthConfig users UserStore } // UserStore interface for user persistence (subset of database.Service) type UserStore interface { CreateUser(email, passwordHash string) (*models.User, error) GetUserByEmail(email string) (*models.User, error) GetUserByID(id string) (*models.User, error) } // NewAuthService creates a new auth service func NewAuthService(users UserStore) *AuthService { secret := os.Getenv("JWT_SECRET") if secret == "" { // Generate a random secret if not provided (not recommended for production) b := make([]byte, 32) rand.Read(b) secret = hex.EncodeToString(b) } return &AuthService{ config: AuthConfig{ JWTSecret: []byte(secret), TokenDuration: 24 * time.Hour, }, users: users, } } // HashPassword hashes a password using bcrypt with high cost func HashPassword(password string) (string, error) { // Use cost of 12 for good security/performance balance hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return "", err } return string(hash), nil } // CheckPassword verifies a password against a hash func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // Register creates a new user account func (s *AuthService) Register(email, password string) (*models.User, error) { // Check if user exists existing, _ := s.users.GetUserByEmail(email) if existing != nil { return nil, ErrUserExists } // Validate password strength if len(password) < 8 { return nil, errors.New("password must be at least 8 characters") } // Hash password hash, err := HashPassword(password) if err != nil { return nil, err } return s.users.CreateUser(email, hash) } // Login authenticates a user and returns a JWT token func (s *AuthService) Login(email, password string) (string, error) { user, err := s.users.GetUserByEmail(email) if err != nil || user == nil { return "", ErrInvalidCredentials } if !CheckPassword(password, user.Password) { return "", ErrInvalidCredentials } return s.generateToken(user) } // generateToken creates a new JWT token for a user func (s *AuthService) generateToken(user *models.User) (string, error) { now := time.Now() claims := &Claims{ UserID: user.ID, Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(s.config.TokenDuration)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: "billit", Subject: user.ID, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.config.JWTSecret) } // ValidateToken validates a JWT token and returns the claims func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrInvalidToken } return s.config.JWTSecret, nil }) if err != nil { return nil, ErrInvalidToken } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, ErrInvalidToken } // GetUserFromToken retrieves the user from a valid token func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, error) { claims, err := s.ValidateToken(tokenString) if err != nil { return nil, err } return s.users.GetUserByID(claims.UserID) }