package services import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "strings" "time" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/repositories" ) // Deprecated: Legacy error constants - kept for reference during transition // Use apperrors package instead var ( // ErrInvalidCredentials = errors.New("invalid credentials") // ErrUsernameTaken = errors.New("username already taken") // ErrEmailTaken = errors.New("email already taken") // ErrUserInactive = errors.New("user account is inactive") // ErrInvalidCode = errors.New("invalid verification code") // ErrCodeExpired = errors.New("verification code expired") // ErrAlreadyVerified = errors.New("email already verified") // ErrRateLimitExceeded = errors.New("too many requests, please try again later") // ErrInvalidResetToken = errors.New("invalid or expired reset token") ErrAppleSignInFailed = errors.New("Apple Sign In failed") ErrGoogleSignInFailed = errors.New("Google Sign In failed") ) // AuthService handles authentication business logic type AuthService struct { userRepo *repositories.UserRepository notificationRepo *repositories.NotificationRepository cfg *config.Config } // NewAuthService creates a new auth service func NewAuthService(userRepo *repositories.UserRepository, cfg *config.Config) *AuthService { return &AuthService{ userRepo: userRepo, cfg: cfg, } } // SetNotificationRepository sets the notification repository for creating notification preferences func (s *AuthService) SetNotificationRepository(notificationRepo *repositories.NotificationRepository) { s.notificationRepo = notificationRepo } // Login authenticates a user and returns a token func (s *AuthService) Login(req *requests.LoginRequest) (*responses.LoginResponse, error) { // Find user by username or email identifier := req.Username if identifier == "" { identifier = req.Email } user, err := s.userRepo.FindByUsernameOrEmail(identifier) if err != nil { if errors.Is(err, repositories.ErrUserNotFound) { return nil, apperrors.Unauthorized("error.invalid_credentials") } return nil, apperrors.Internal(err) } // Check if user is active if !user.IsActive { return nil, apperrors.Unauthorized("error.account_inactive") } // Verify password if !user.CheckPassword(req.Password) { return nil, apperrors.Unauthorized("error.invalid_credentials") } // Get or create auth token token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, apperrors.Internal(err) } // Update last login if err := s.userRepo.UpdateLastLogin(user.ID); err != nil { // Log error but don't fail the login log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to update last login") } return &responses.LoginResponse{ Token: token.Key, User: responses.NewUserResponse(user), }, nil } // Register creates a new user account. // F-10: User creation, profile creation, notification preferences, and confirmation code // are wrapped in a transaction for atomicity. func (s *AuthService) Register(req *requests.RegisterRequest) (*responses.RegisterResponse, string, error) { // Check if username exists exists, err := s.userRepo.ExistsByUsername(req.Username) if err != nil { return nil, "", apperrors.Internal(err) } if exists { return nil, "", apperrors.Conflict("error.username_taken") } // Check if email exists exists, err = s.userRepo.ExistsByEmail(req.Email) if err != nil { return nil, "", apperrors.Internal(err) } if exists { return nil, "", apperrors.Conflict("error.email_taken") } // Create user user := &models.User{ Username: req.Username, Email: req.Email, FirstName: req.FirstName, LastName: req.LastName, IsActive: true, } // Hash password if err := user.SetPassword(req.Password); err != nil { return nil, "", apperrors.Internal(err) } // Generate confirmation code - use fixed code when DEBUG_FIXED_CODES is enabled for easier local testing var code string if s.cfg.Server.DebugFixedCodes { code = "123456" } else { code = generateSixDigitCode() } expiresAt := time.Now().UTC().Add(s.cfg.Security.ConfirmationExpiry) // Wrap user creation + profile + notification preferences + confirmation code in a transaction txErr := s.userRepo.Transaction(func(txRepo *repositories.UserRepository) error { // Save user if err := txRepo.Create(user); err != nil { return err } // Create user profile if _, err := txRepo.GetOrCreateProfile(user.ID); err != nil { log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to create user profile during registration") } // Create notification preferences with all options enabled if s.notificationRepo != nil { if _, err := s.notificationRepo.GetOrCreatePreferences(user.ID); err != nil { log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to create notification preferences during registration") } } // Create confirmation code if _, err := txRepo.CreateConfirmationCode(user.ID, code, expiresAt); err != nil { log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to create confirmation code during registration") } return nil }) if txErr != nil { return nil, "", apperrors.Internal(txErr) } // Create auth token (outside transaction since token generation is idempotent) token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, "", apperrors.Internal(err) } return &responses.RegisterResponse{ Token: token.Key, User: responses.NewUserResponse(user), Message: "Registration successful. Please check your email to verify your account.", }, code, nil } // Logout invalidates a user's token func (s *AuthService) Logout(token string) error { return s.userRepo.DeleteToken(token) } // GetCurrentUser returns the current authenticated user with profile func (s *AuthService) GetCurrentUser(userID uint) (*responses.CurrentUserResponse, error) { user, err := s.userRepo.FindByIDWithProfile(userID) if err != nil { return nil, err } authProvider, err := s.userRepo.FindAuthProvider(userID) if err != nil { // Log but don't fail - default to "email" log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider") authProvider = "email" } response := responses.NewCurrentUserResponse(user, authProvider) return &response, nil } // DeleteAccount deletes a user's account and all associated data. // For email auth users, password verification is required. // For social auth users, confirmation string "DELETE" is required. // Returns a list of file URLs that need to be deleted from disk. func (s *AuthService) DeleteAccount(userID uint, password, confirmation *string) ([]string, error) { // Fetch user user, err := s.userRepo.FindByID(userID) if err != nil { if errors.Is(err, repositories.ErrUserNotFound) { return nil, apperrors.NotFound("error.user_not_found") } return nil, apperrors.Internal(err) } // Determine auth provider authProvider, err := s.userRepo.FindAuthProvider(userID) if err != nil { return nil, apperrors.Internal(err) } // Validate credentials based on auth provider if authProvider == "email" { if password == nil || *password == "" { return nil, apperrors.BadRequest("error.password_required") } if !user.CheckPassword(*password) { return nil, apperrors.Unauthorized("error.invalid_credentials") } } else { // Social auth (apple or google) - require confirmation if confirmation == nil || *confirmation != "DELETE" { return nil, apperrors.BadRequest("error.confirmation_required") } } // Start transaction and cascade delete var fileURLs []string txErr := s.userRepo.Transaction(func(txRepo *repositories.UserRepository) error { urls, err := txRepo.DeleteUserCascade(userID) if err != nil { return err } fileURLs = urls return nil }) if txErr != nil { return nil, apperrors.Internal(txErr) } return fileURLs, nil } // UpdateProfile updates a user's profile func (s *AuthService) UpdateProfile(userID uint, req *requests.UpdateProfileRequest) (*responses.CurrentUserResponse, error) { user, err := s.userRepo.FindByID(userID) if err != nil { return nil, err } // Check if new email is taken (if email is being changed) if req.Email != nil && *req.Email != user.Email { exists, err := s.userRepo.ExistsByEmail(*req.Email) if err != nil { return nil, apperrors.Internal(err) } if exists { return nil, apperrors.Conflict("error.email_already_taken") } user.Email = *req.Email } if req.FirstName != nil { user.FirstName = *req.FirstName } if req.LastName != nil { user.LastName = *req.LastName } if err := s.userRepo.Update(user); err != nil { return nil, apperrors.Internal(err) } // Reload with profile user, err = s.userRepo.FindByIDWithProfile(userID) if err != nil { return nil, err } authProvider, err := s.userRepo.FindAuthProvider(userID) if err != nil { log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to determine auth provider") authProvider = "email" } response := responses.NewCurrentUserResponse(user, authProvider) return &response, nil } // VerifyEmail verifies a user's email with a confirmation code func (s *AuthService) VerifyEmail(userID uint, code string) error { // Get user profile profile, err := s.userRepo.GetOrCreateProfile(userID) if err != nil { return apperrors.Internal(err) } // Check if already verified if profile.Verified { return apperrors.BadRequest("error.email_already_verified") } // Check for test code when DEBUG_FIXED_CODES is enabled if s.cfg.Server.DebugFixedCodes && code == "123456" { if err := s.userRepo.SetProfileVerified(userID, true); err != nil { return apperrors.Internal(err) } return nil } // Find and validate confirmation code confirmCode, err := s.userRepo.FindConfirmationCode(userID, code) if err != nil { if errors.Is(err, repositories.ErrCodeNotFound) { return apperrors.BadRequest("error.invalid_verification_code") } if errors.Is(err, repositories.ErrCodeExpired) { return apperrors.BadRequest("error.verification_code_expired") } return apperrors.Internal(err) } // Mark code as used if err := s.userRepo.MarkConfirmationCodeUsed(confirmCode.ID); err != nil { return apperrors.Internal(err) } // Set profile as verified if err := s.userRepo.SetProfileVerified(userID, true); err != nil { return apperrors.Internal(err) } return nil } // ResendVerificationCode creates and returns a new verification code func (s *AuthService) ResendVerificationCode(userID uint) (string, error) { // Get user profile profile, err := s.userRepo.GetOrCreateProfile(userID) if err != nil { return "", apperrors.Internal(err) } // Check if already verified if profile.Verified { return "", apperrors.BadRequest("error.email_already_verified") } // Generate new code - use fixed code when DEBUG_FIXED_CODES is enabled for easier local testing var code string if s.cfg.Server.DebugFixedCodes { code = "123456" } else { code = generateSixDigitCode() } expiresAt := time.Now().UTC().Add(s.cfg.Security.ConfirmationExpiry) if _, err := s.userRepo.CreateConfirmationCode(userID, code, expiresAt); err != nil { return "", apperrors.Internal(err) } return code, nil } // ForgotPassword initiates the password reset process func (s *AuthService) ForgotPassword(email string) (string, *models.User, error) { // Find user by email user, err := s.userRepo.FindByEmail(email) if err != nil { if errors.Is(err, repositories.ErrUserNotFound) { // Don't reveal that the email doesn't exist return "", nil, nil } return "", nil, err } // Check rate limit count, err := s.userRepo.CountRecentPasswordResetRequests(user.ID) if err != nil { return "", nil, apperrors.Internal(err) } if count >= int64(s.cfg.Security.MaxPasswordResetRate) { return "", nil, apperrors.TooManyRequests("error.rate_limit_exceeded") } // Generate code and reset token - use fixed code when DEBUG_FIXED_CODES is enabled for easier local testing var code string if s.cfg.Server.DebugFixedCodes { code = "123456" } else { code = generateSixDigitCode() } resetToken := generateResetToken() expiresAt := time.Now().UTC().Add(s.cfg.Security.PasswordResetExpiry) // Hash the code before storing codeHash, err := bcrypt.GenerateFromPassword([]byte(code), bcrypt.DefaultCost) if err != nil { return "", nil, apperrors.Internal(err) } if _, err := s.userRepo.CreatePasswordResetCode(user.ID, string(codeHash), resetToken, expiresAt); err != nil { return "", nil, apperrors.Internal(err) } return code, user, nil } // VerifyResetCode verifies a password reset code and returns a reset token func (s *AuthService) VerifyResetCode(email, code string) (string, error) { // Find the reset code resetCode, user, err := s.userRepo.FindPasswordResetCodeByEmail(email) if err != nil { if errors.Is(err, repositories.ErrUserNotFound) || errors.Is(err, repositories.ErrCodeNotFound) { return "", apperrors.BadRequest("error.invalid_verification_code") } return "", apperrors.Internal(err) } // Check for test code when DEBUG_FIXED_CODES is enabled if s.cfg.Server.DebugFixedCodes && code == "123456" { return resetCode.ResetToken, nil } // Verify the code if !resetCode.CheckCode(code) { // Increment attempts s.userRepo.IncrementResetCodeAttempts(resetCode.ID) return "", apperrors.BadRequest("error.invalid_verification_code") } // Check if code is still valid if !resetCode.IsValid() { if resetCode.Used { return "", apperrors.BadRequest("error.invalid_verification_code") } if resetCode.Attempts >= resetCode.MaxAttempts { return "", apperrors.TooManyRequests("error.rate_limit_exceeded") } return "", apperrors.BadRequest("error.verification_code_expired") } _ = user // user available if needed return resetCode.ResetToken, nil } // ResetPassword resets the user's password using a reset token func (s *AuthService) ResetPassword(resetToken, newPassword string) error { // Find the reset code by token resetCode, err := s.userRepo.FindPasswordResetCodeByToken(resetToken) if err != nil { if errors.Is(err, repositories.ErrCodeNotFound) || errors.Is(err, repositories.ErrCodeExpired) { return apperrors.BadRequest("error.invalid_reset_token") } return apperrors.Internal(err) } // Get the user user, err := s.userRepo.FindByID(resetCode.UserID) if err != nil { return apperrors.Internal(err) } // Update password if err := user.SetPassword(newPassword); err != nil { return apperrors.Internal(err) } if err := s.userRepo.Update(user); err != nil { return apperrors.Internal(err) } // Mark reset code as used if err := s.userRepo.MarkPasswordResetCodeUsed(resetCode.ID); err != nil { // Log error but don't fail log.Warn().Err(err).Uint("reset_code_id", resetCode.ID).Msg("Failed to mark reset code as used") } // Invalidate all existing tokens for this user (security measure) if err := s.userRepo.DeleteTokenByUserID(user.ID); err != nil { // Log error but don't fail log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to delete user tokens after password reset") } return nil } // AppleSignIn handles Sign in with Apple authentication func (s *AuthService) AppleSignIn(ctx context.Context, appleAuth *AppleAuthService, req *requests.AppleSignInRequest) (*responses.AppleSignInResponse, error) { // 1. Verify the Apple JWT token claims, err := appleAuth.VerifyIdentityToken(ctx, req.IDToken) if err != nil { return nil, apperrors.Unauthorized("error.invalid_credentials").Wrap(err) } // Use the subject from claims as the authoritative Apple ID appleID := claims.Subject if appleID == "" { appleID = req.UserID // Fallback to request UserID } // 2. Check if this Apple ID is already linked to an account existingAuth, err := s.userRepo.FindByAppleID(appleID) if err == nil && existingAuth != nil { // User already linked with this Apple ID - log them in user, err := s.userRepo.FindByIDWithProfile(existingAuth.UserID) if err != nil { return nil, apperrors.Internal(err) } if !user.IsActive { return nil, apperrors.Unauthorized("error.account_inactive") } // Get or create token token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, apperrors.Internal(err) } // Update last login _ = s.userRepo.UpdateLastLogin(user.ID) return &responses.AppleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(user), IsNewUser: false, }, nil } // 3. Check if email matches an existing user (for account linking) email := getEmailFromRequest(req.Email, claims.Email) if email != "" { existingUser, err := s.userRepo.FindByEmail(email) if err == nil && existingUser != nil { // S-06: Log auto-linking of social account to existing user log.Warn(). Str("email", email). Str("provider", "apple"). Uint("user_id", existingUser.ID). Msg("Auto-linking social account to existing user by email match") // Link Apple ID to existing account appleAuthRecord := &models.AppleSocialAuth{ UserID: existingUser.ID, AppleID: appleID, Email: email, IsPrivateEmail: isPrivateRelayEmail(email) || claims.IsPrivateRelayEmail(), } if err := s.userRepo.CreateAppleSocialAuth(appleAuthRecord); err != nil { return nil, apperrors.Internal(err) } // Mark as verified since Apple verified the email _ = s.userRepo.SetProfileVerified(existingUser.ID, true) // Get or create token token, err := s.userRepo.GetOrCreateToken(existingUser.ID) if err != nil { return nil, apperrors.Internal(err) } // Update last login _ = s.userRepo.UpdateLastLogin(existingUser.ID) // B-08: Check error from FindByIDWithProfile existingUser, err = s.userRepo.FindByIDWithProfile(existingUser.ID) if err != nil { return nil, apperrors.Internal(err) } return &responses.AppleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(existingUser), IsNewUser: false, }, nil } } // 4. Create new user username := generateUniqueUsername(email, req.FirstName) user := &models.User{ Username: username, Email: getEmailOrDefault(email), FirstName: getStringOrEmpty(req.FirstName), LastName: getStringOrEmpty(req.LastName), IsActive: true, } // Set a random password (user won't use it since they log in with Apple) randomPassword := generateResetToken() _ = user.SetPassword(randomPassword) if err := s.userRepo.Create(user); err != nil { return nil, apperrors.Internal(err) } // Create profile (already verified since Apple verified) profile, _ := s.userRepo.GetOrCreateProfile(user.ID) if profile != nil { _ = s.userRepo.SetProfileVerified(user.ID, true) } // Create notification preferences with all options enabled if s.notificationRepo != nil { if _, err := s.notificationRepo.GetOrCreatePreferences(user.ID); err != nil { log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to create notification preferences for Apple Sign In user") } } // Link Apple ID appleAuthRecord := &models.AppleSocialAuth{ UserID: user.ID, AppleID: appleID, Email: getEmailOrDefault(email), IsPrivateEmail: isPrivateRelayEmail(email) || claims.IsPrivateRelayEmail(), } if err := s.userRepo.CreateAppleSocialAuth(appleAuthRecord); err != nil { return nil, apperrors.Internal(err) } // Create token token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, apperrors.Internal(err) } // B-08: Check error from FindByIDWithProfile user, err = s.userRepo.FindByIDWithProfile(user.ID) if err != nil { return nil, apperrors.Internal(err) } return &responses.AppleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(user), IsNewUser: true, }, nil } // GoogleSignIn handles Google Sign In authentication func (s *AuthService) GoogleSignIn(ctx context.Context, googleAuth *GoogleAuthService, req *requests.GoogleSignInRequest) (*responses.GoogleSignInResponse, error) { // 1. Verify the Google ID token tokenInfo, err := googleAuth.VerifyIDToken(ctx, req.IDToken) if err != nil { return nil, apperrors.Unauthorized("error.invalid_credentials").Wrap(err) } googleID := tokenInfo.Sub if googleID == "" { return nil, apperrors.Unauthorized("error.invalid_credentials") } // 2. Check if this Google ID is already linked to an account existingAuth, err := s.userRepo.FindByGoogleID(googleID) if err == nil && existingAuth != nil { // User already linked with this Google ID - log them in user, err := s.userRepo.FindByIDWithProfile(existingAuth.UserID) if err != nil { return nil, apperrors.Internal(err) } if !user.IsActive { return nil, apperrors.Unauthorized("error.account_inactive") } // Get or create token token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, apperrors.Internal(err) } // Update last login _ = s.userRepo.UpdateLastLogin(user.ID) return &responses.GoogleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(user), IsNewUser: false, }, nil } // 3. Check if email matches an existing user (for account linking) email := tokenInfo.Email if email != "" { existingUser, err := s.userRepo.FindByEmail(email) if err == nil && existingUser != nil { // S-06: Log auto-linking of social account to existing user log.Warn(). Str("email", email). Str("provider", "google"). Uint("user_id", existingUser.ID). Msg("Auto-linking social account to existing user by email match") // Link Google ID to existing account googleAuthRecord := &models.GoogleSocialAuth{ UserID: existingUser.ID, GoogleID: googleID, Email: email, Name: tokenInfo.Name, Picture: tokenInfo.Picture, } if err := s.userRepo.CreateGoogleSocialAuth(googleAuthRecord); err != nil { return nil, apperrors.Internal(err) } // Mark as verified since Google verified the email if tokenInfo.IsEmailVerified() { _ = s.userRepo.SetProfileVerified(existingUser.ID, true) } // Get or create token token, err := s.userRepo.GetOrCreateToken(existingUser.ID) if err != nil { return nil, apperrors.Internal(err) } // Update last login _ = s.userRepo.UpdateLastLogin(existingUser.ID) // B-08: Check error from FindByIDWithProfile existingUser, err = s.userRepo.FindByIDWithProfile(existingUser.ID) if err != nil { return nil, apperrors.Internal(err) } return &responses.GoogleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(existingUser), IsNewUser: false, }, nil } } // 4. Create new user username := generateGoogleUsername(email, tokenInfo.GivenName) user := &models.User{ Username: username, Email: email, FirstName: tokenInfo.GivenName, LastName: tokenInfo.FamilyName, IsActive: true, } // Set a random password (user won't use it since they log in with Google) randomPassword := generateResetToken() _ = user.SetPassword(randomPassword) if err := s.userRepo.Create(user); err != nil { return nil, apperrors.Internal(err) } // Create profile (already verified if Google verified email) profile, _ := s.userRepo.GetOrCreateProfile(user.ID) if profile != nil && tokenInfo.IsEmailVerified() { _ = s.userRepo.SetProfileVerified(user.ID, true) } // Create notification preferences with all options enabled if s.notificationRepo != nil { if _, err := s.notificationRepo.GetOrCreatePreferences(user.ID); err != nil { log.Warn().Err(err).Uint("user_id", user.ID).Msg("Failed to create notification preferences for Google Sign In user") } } // Link Google ID googleAuthRecord := &models.GoogleSocialAuth{ UserID: user.ID, GoogleID: googleID, Email: email, Name: tokenInfo.Name, Picture: tokenInfo.Picture, } if err := s.userRepo.CreateGoogleSocialAuth(googleAuthRecord); err != nil { return nil, apperrors.Internal(err) } // Create token token, err := s.userRepo.GetOrCreateToken(user.ID) if err != nil { return nil, apperrors.Internal(err) } // B-08: Check error from FindByIDWithProfile user, err = s.userRepo.FindByIDWithProfile(user.ID) if err != nil { return nil, apperrors.Internal(err) } return &responses.GoogleSignInResponse{ Token: token.Key, User: responses.NewUserResponse(user), IsNewUser: true, }, nil } // Helper functions func generateSixDigitCode() string { b := make([]byte, 4) rand.Read(b) num := int(b[0])<<24 | int(b[1])<<16 | int(b[2])<<8 | int(b[3]) if num < 0 { num = -num } code := num % 1000000 return fmt.Sprintf("%06d", code) } func generateResetToken() string { b := make([]byte, 32) rand.Read(b) return hex.EncodeToString(b) } // Helper functions for Apple Sign In func getEmailFromRequest(reqEmail *string, claimsEmail string) string { if reqEmail != nil && *reqEmail != "" { return *reqEmail } return claimsEmail } func getEmailOrDefault(email string) string { if email == "" { // Generate a placeholder email for users without one return fmt.Sprintf("apple_%s@privaterelay.appleid.com", generateResetToken()[:16]) } return email } func getStringOrEmpty(s *string) string { if s == nil { return "" } return *s } func isPrivateRelayEmail(email string) bool { return strings.HasSuffix(strings.ToLower(email), "@privaterelay.appleid.com") } func generateUniqueUsername(email string, firstName *string) string { // Try using first part of email if email != "" && !isPrivateRelayEmail(email) { parts := strings.Split(email, "@") if len(parts) > 0 && parts[0] != "" { // Add random suffix to ensure uniqueness return parts[0] + "_" + generateResetToken()[:6] } } // Try using first name if firstName != nil && *firstName != "" { return strings.ToLower(*firstName) + "_" + generateResetToken()[:6] } // Fallback to random username return "user_" + generateResetToken()[:10] } func generateGoogleUsername(email string, firstName string) string { // Try using first part of email if email != "" { parts := strings.Split(email, "@") if len(parts) > 0 && parts[0] != "" { // Add random suffix to ensure uniqueness return parts[0] + "_" + generateResetToken()[:6] } } // Try using first name if firstName != "" { return strings.ToLower(firstName) + "_" + generateResetToken()[:6] } // Fallback to random username return "google_" + generateResetToken()[:10] }