package user import ( "context" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) // JWTConfig holds JWT configuration type JWTConfig struct { Secret string ExpirationTime time.Duration Issuer string } // userServiceImpl implements the unified UserService interface type userServiceImpl struct { repo UserRepository jwtConfig JWTConfig masterPassword string } // NewUserService creates a new user service with all functionality func NewUserService(repo UserRepository, jwtConfig JWTConfig, masterPassword string) *userServiceImpl { return &userServiceImpl{ repo: repo, jwtConfig: jwtConfig, masterPassword: masterPassword, } } // Authenticate authenticates a user with username and password func (s *userServiceImpl) Authenticate(ctx context.Context, username, password string) (*User, error) { user, err := s.repo.GetUserByUsername(ctx, username) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } if user == nil { return nil, errors.New("invalid credentials") } // Check password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { return nil, errors.New("invalid credentials") } // Update last login time now := time.Now() user.LastLogin = &now if err := s.repo.UpdateUser(ctx, user); err != nil { // Don't fail authentication if we can't update last login // Just log it and continue } return user, nil } // GenerateJWT generates a JWT token for the given user func (s *userServiceImpl) GenerateJWT(ctx context.Context, user *User) (string, error) { // Create the claims claims := jwt.MapClaims{ "sub": user.ID, "name": user.Username, "admin": user.IsAdmin, "exp": time.Now().Add(s.jwtConfig.ExpirationTime).Unix(), "iat": time.Now().Unix(), "iss": s.jwtConfig.Issuer, } // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get the complete encoded token as a string tokenString, err := token.SignedString([]byte(s.jwtConfig.Secret)) if err != nil { return "", fmt.Errorf("failed to sign JWT: %w", err) } return tokenString, nil } // ValidateJWT validates a JWT token and returns the user func (s *userServiceImpl) ValidateJWT(ctx context.Context, tokenString string) (*User, error) { // Parse the token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Verify the signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(s.jwtConfig.Secret), nil }) if err != nil { return nil, fmt.Errorf("failed to parse JWT: %w", err) } // Check if token is valid if !token.Valid { return nil, errors.New("invalid JWT token") } // Get claims claims, ok := token.Claims.(jwt.MapClaims) if !ok { return nil, errors.New("invalid JWT claims") } // Get user ID from claims userIDFloat, ok := claims["sub"].(float64) if !ok { return nil, errors.New("invalid user ID in JWT") } userID := uint(userIDFloat) // Get user from repository user, err := s.repo.GetUserByID(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to get user from JWT: %w", err) } if user == nil { return nil, errors.New("user not found") } return user, nil } // HashPassword hashes a password using bcrypt (implements PasswordService interface) func (s *userServiceImpl) HashPassword(ctx context.Context, password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("failed to hash password: %w", err) } return string(hash), nil } // AdminAuthenticate authenticates an admin user with master password func (s *userServiceImpl) AdminAuthenticate(ctx context.Context, masterPassword string) (*User, error) { // Check if master password matches if masterPassword != s.masterPassword { return nil, errors.New("invalid admin credentials") } // Create a virtual admin user (not persisted) adminUser := &User{ ID: 0, // Special ID for admin Username: "admin", IsAdmin: true, } return adminUser, nil } // UserExists checks if a user exists by username func (s *userServiceImpl) UserExists(ctx context.Context, username string) (bool, error) { return s.repo.UserExists(ctx, username) } // CreateUser creates a new user in the database func (s *userServiceImpl) CreateUser(ctx context.Context, user *User) error { return s.repo.CreateUser(ctx, user) } // RequestPasswordReset requests a password reset for a user func (s *userServiceImpl) RequestPasswordReset(ctx context.Context, username string) error { // Check if user exists exists, err := s.repo.UserExists(ctx, username) if err != nil { return fmt.Errorf("failed to check if user exists: %w", err) } if !exists { return fmt.Errorf("user not found: %s", username) } // Allow password reset return s.repo.AllowPasswordReset(ctx, username) } // CompletePasswordReset completes the password reset process func (s *userServiceImpl) CompletePasswordReset(ctx context.Context, username, newPassword string) error { // Hash the new password hashedPassword, err := s.HashPassword(ctx, newPassword) if err != nil { return fmt.Errorf("failed to hash new password: %w", err) } // Complete the password reset return s.repo.CompletePasswordReset(ctx, username, hashedPassword) } // PasswordResetServiceImpl implements the PasswordResetService interface type PasswordResetServiceImpl struct { repo UserRepository auth *userServiceImpl } // NewPasswordResetService creates a new password reset service func NewPasswordResetService(repo UserRepository, auth *userServiceImpl) *PasswordResetServiceImpl { return &PasswordResetServiceImpl{ repo: repo, auth: auth, } } // RequestPasswordReset requests a password reset for a user func (s *PasswordResetServiceImpl) RequestPasswordReset(ctx context.Context, username string) error { // Check if user exists exists, err := s.repo.UserExists(ctx, username) if err != nil { return fmt.Errorf("failed to check if user exists: %w", err) } if !exists { return fmt.Errorf("user not found: %s", username) } // Allow password reset return s.repo.AllowPasswordReset(ctx, username) } // CompletePasswordReset completes the password reset process func (s *PasswordResetServiceImpl) CompletePasswordReset(ctx context.Context, username, newPassword string) error { // Hash the new password hashedPassword, err := s.auth.HashPassword(ctx, newPassword) if err != nil { return fmt.Errorf("failed to hash new password: %w", err) } // Complete the password reset return s.repo.CompletePasswordReset(ctx, username, hashedPassword) }