// Package service provides user service business logic. package service import ( "context" "crypto/rand" "encoding/base64" "fmt" "time" "git.dcentral.systems/toolz/goplt/internal/ent" "git.dcentral.systems/toolz/goplt/internal/ent/user" "git.dcentral.systems/toolz/goplt/pkg/logger" passwordpkg "git.dcentral.systems/toolz/goplt/services/identity/internal/password" "go.uber.org/zap" ) // UserService provides user management functionality. type UserService struct { client *ent.Client logger logger.Logger } // NewUserService creates a new user service. func NewUserService(client *ent.Client, log logger.Logger) *UserService { return &UserService{ client: client, logger: log, } } // generateToken generates a random token for email verification or password reset. func generateToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("failed to generate token: %w", err) } return base64.URLEncoding.EncodeToString(b), nil } // CreateUser creates a new user. func (s *UserService) CreateUser(ctx context.Context, email, username, password, firstName, lastName string) (*ent.User, error) { // Check if user with email already exists exists, err := s.client.User.Query(). Where(user.Email(email)). Exist(ctx) if err != nil { return nil, fmt.Errorf("failed to check email existence: %w", err) } if exists { return nil, fmt.Errorf("user with email %s already exists", email) } // Hash password passwordHash, err := passwordpkg.Hash(password) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } // Generate email verification token verificationToken, err := generateToken() if err != nil { return nil, fmt.Errorf("failed to generate verification token: %w", err) } // Create user create := s.client.User.Create(). SetID(fmt.Sprintf("%d", time.Now().UnixNano())). SetEmail(email). SetPasswordHash(passwordHash). SetVerified(false). SetEmailVerificationToken(verificationToken) if username != "" { create = create.SetUsername(username) } if firstName != "" { create = create.SetFirstName(firstName) } if lastName != "" { create = create.SetLastName(lastName) } u, err := create.Save(ctx) if err != nil { s.logger.Error("Failed to create user", zap.Error(err), zap.String("email", email), ) return nil, fmt.Errorf("failed to create user: %w", err) } s.logger.Info("User created", zap.String("user_id", u.ID), zap.String("email", email), ) return u, nil } // GetUser retrieves a user by ID. func (s *UserService) GetUser(ctx context.Context, id string) (*ent.User, error) { u, err := s.client.User.Get(ctx, id) if err != nil { if ent.IsNotFound(err) { return nil, fmt.Errorf("user not found: %w", err) } return nil, fmt.Errorf("failed to get user: %w", err) } return u, nil } // GetUserByEmail retrieves a user by email. func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*ent.User, error) { u, err := s.client.User.Query(). Where(user.Email(email)). Only(ctx) if err != nil { if ent.IsNotFound(err) { return nil, fmt.Errorf("user not found: %w", err) } return nil, fmt.Errorf("failed to get user by email: %w", err) } return u, nil } // UpdateUser updates a user's profile. func (s *UserService) UpdateUser(ctx context.Context, id string, email, username, firstName, lastName *string) (*ent.User, error) { update := s.client.User.UpdateOneID(id) if email != nil { // Check if email is already taken by another user exists, err := s.client.User.Query(). Where(user.Email(*email), user.IDNEQ(id)). Exist(ctx) if err != nil { return nil, fmt.Errorf("failed to check email existence: %w", err) } if exists { return nil, fmt.Errorf("email %s is already taken", *email) } update = update.SetEmail(*email) } if username != nil { update = update.SetUsername(*username) } if firstName != nil { update = update.SetFirstName(*firstName) } if lastName != nil { update = update.SetLastName(*lastName) } u, err := update.Save(ctx) if err != nil { if ent.IsNotFound(err) { return nil, fmt.Errorf("user not found: %w", err) } return nil, fmt.Errorf("failed to update user: %w", err) } s.logger.Info("User updated", zap.String("user_id", id), ) return u, nil } // DeleteUser deletes a user (soft delete by setting verified to false, or hard delete). func (s *UserService) DeleteUser(ctx context.Context, id string) error { err := s.client.User.DeleteOneID(id).Exec(ctx) if err != nil { if ent.IsNotFound(err) { return fmt.Errorf("user not found: %w", err) } return fmt.Errorf("failed to delete user: %w", err) } s.logger.Info("User deleted", zap.String("user_id", id), ) return nil } // VerifyEmail verifies a user's email address using a verification token. func (s *UserService) VerifyEmail(ctx context.Context, token string) error { u, err := s.client.User.Query(). Where(user.EmailVerificationToken(token)). Only(ctx) if err != nil { if ent.IsNotFound(err) { return fmt.Errorf("invalid verification token") } return fmt.Errorf("failed to find user: %w", err) } // Update user to verified and clear token _, err = s.client.User.UpdateOneID(u.ID). SetVerified(true). ClearEmailVerificationToken(). Save(ctx) if err != nil { return fmt.Errorf("failed to verify email: %w", err) } s.logger.Info("Email verified", zap.String("user_id", u.ID), zap.String("email", u.Email), ) return nil } // RequestPasswordReset requests a password reset token. func (s *UserService) RequestPasswordReset(ctx context.Context, email string) (string, error) { u, err := s.GetUserByEmail(ctx, email) if err != nil { // Don't reveal if user exists or not (security best practice) return "", nil } // Generate reset token resetToken, err := generateToken() if err != nil { return "", fmt.Errorf("failed to generate reset token: %w", err) } // Set reset token with expiration (24 hours) expiresAt := time.Now().Add(24 * time.Hour) _, err = s.client.User.UpdateOneID(u.ID). SetPasswordResetToken(resetToken). SetPasswordResetExpiresAt(expiresAt). Save(ctx) if err != nil { return "", fmt.Errorf("failed to set reset token: %w", err) } s.logger.Info("Password reset requested", zap.String("user_id", u.ID), zap.String("email", email), ) return resetToken, nil } // ResetPassword resets a user's password using a reset token. func (s *UserService) ResetPassword(ctx context.Context, token, newPassword string) error { u, err := s.client.User.Query(). Where(user.PasswordResetToken(token)). Only(ctx) if err != nil { if ent.IsNotFound(err) { return fmt.Errorf("invalid reset token") } return fmt.Errorf("failed to find user: %w", err) } // Check if token is expired if !u.PasswordResetExpiresAt.IsZero() && u.PasswordResetExpiresAt.Before(time.Now()) { return fmt.Errorf("reset token has expired") } // Hash new password passwordHash, err := passwordpkg.Hash(newPassword) if err != nil { return fmt.Errorf("failed to hash password: %w", err) } // Update password and clear reset token _, err = s.client.User.UpdateOneID(u.ID). SetPasswordHash(passwordHash). ClearPasswordResetToken(). ClearPasswordResetExpiresAt(). Save(ctx) if err != nil { return fmt.Errorf("failed to reset password: %w", err) } s.logger.Info("Password reset", zap.String("user_id", u.ID), ) return nil } // ChangePassword changes a user's password with old password verification. func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error { u, err := s.GetUser(ctx, userID) if err != nil { return err } // Verify old password valid, err := passwordpkg.Verify(oldPassword, u.PasswordHash) if err != nil { return fmt.Errorf("failed to verify password: %w", err) } if !valid { return fmt.Errorf("invalid old password") } // Hash new password passwordHash, err := passwordpkg.Hash(newPassword) if err != nil { return fmt.Errorf("failed to hash password: %w", err) } // Update password _, err = s.client.User.UpdateOneID(userID). SetPasswordHash(passwordHash). Save(ctx) if err != nil { return fmt.Errorf("failed to change password: %w", err) } s.logger.Info("Password changed", zap.String("user_id", userID), ) return nil } // VerifyPassword verifies a password against a user's password hash. func (s *UserService) VerifyPassword(ctx context.Context, email, password string) (*ent.User, error) { u, err := s.GetUserByEmail(ctx, email) if err != nil { return nil, err } valid, err := passwordpkg.Verify(password, u.PasswordHash) if err != nil { return nil, fmt.Errorf("failed to verify password: %w", err) } if !valid { return nil, fmt.Errorf("invalid password") } return u, nil }