package services import ( "context" "errors" "fmt" "strings" "github.com/creativenoz/aurganize-v62/backend/internal/models" "github.com/creativenoz/aurganize-v62/backend/internal/repositories" "github.com/google/uuid" ) // Predefined errors for user operations. // These are package-level error variables that can be: // 1. Compared using errors.Is() for error handling // 2. Wrapped with context using fmt.Errorf() // 3. Tested reliably (same instance) // 4. Documented centrally // // Why define errors at package level? // - Consistency: Same error for same situation across codebase // - Testability: Can check for specific error types // - Documentation: Clear list of possible errors // - i18n ready: Can map errors to localized messages var ( // ErrUserNotFound indicates the requested user doesn't exist // Used when: Looking up user by ID/email and not found // HTTP code: 404 Not Found ErrUserNotFound = errors.New("user not found") // ErrInvalidCredentials indicates email or password is wrong // Used when: Login fails due to bad credentials // HTTP code: 401 Unauthorized // Security: Generic message prevents email enumeration ErrInvalidCredentials = errors.New("invalid credentials") // ErrEmailAlreadyExists indicates email is already registered // Used when: Registration with existing email // HTTP code: 400 Bad Request or 409 Conflict ErrEmailAlreadyExists = errors.New("email already exists") // ErrWeakPassword indicates password doesn't meet security requirements // Used when: Registration or password change with weak password // HTTP code: 400 Bad Request ErrWeakPassword = errors.New("password is too weak") // ErrInvalidEmail indicates email format is invalid // Used when: Registration with malformed email // HTTP code: 400 Bad Request ErrInvalidEmail = errors.New("invalid email format") ) // UserService handles all user-related business logic. // This service sits between HTTP handlers and data repositories. // // Responsibilities: // 1. User registration (with validation) // 2. User authentication (credential verification) // 3. User lookup (by ID or email) // 4. User updates (password, last login) // 5. Input validation (email format, password strength) // // Architecture: Service Layer Pattern // Benefits: // - Business logic separated from HTTP concerns // - Reusable across multiple handlers // - Testable without HTTP infrastructure // - Can coordinate multiple repositories // - Can add cross-cutting concerns (logging, metrics) // // Why this layer exists: // - Handlers should be thin (just HTTP translation) // - Repositories should be simple (just database operations) // - Business logic needs a home (validation, coordination) // // Security considerations: // - Email normalization (prevent duplicate accounts) // - Password strength validation (prevent weak passwords) // - Generic error messages (prevent information leakage) // - Input sanitization (prevent injection attacks) type UserService struct { userRepo *repositories.UserRepository // Database operations for users } // NewUserService creates a new UserService with injected dependencies. // This follows dependency injection pattern: // - Repository passed in (not created internally) // - Makes testing easier (can inject mock repository) // - Keeps service decoupled from repository implementation // // Parameters: // - userRepo: Repository for user database operations // // Returns: // - Fully initialized UserService func NewUserService(userRepo *repositories.UserRepository) *UserService { return &UserService{userRepo: userRepo} } // Register creates a new user account with validation. // This is the complete user registration flow. // // What happens: // 1. Validate input (email format, password strength, uniqueness) // 2. Normalize email (lowercase, trim whitespace) // 3. Create user in database (repository handles password hashing) // 4. Return created user object // // Validation performed: // - Email format validation (structure, length) // - Email uniqueness check (not already registered) // - Password strength validation (length, complexity, not common) // // Why validate in service layer? // - Business rules belong here // - Reusable validation (same rules everywhere) // - Clear error messages for different failure cases // - Can be tested independently // // Email normalization importance: // - Prevents duplicate accounts: user@example.com vs USER@Example.com // - Consistent storage format // - Easier searching and matching // - Standard practice for email handling // // Security considerations: // - Password never logged or exposed // - Email uniqueness check prevents enumeration (generic error) // - Strong password requirements enforced // - Input sanitization (trim, lowercase) // // After registration: // - User account created but may need email verification // - Caller should send verification email // - User might not be able to log in until verified (depends on status) // // Parameters: // - ctx: Context for database operations // - userInput: Registration data (email, password, names, etc.) // // Return values: // - (*User, nil): Successfully created user // - (nil, ErrInvalidEmail): Email format invalid // - (nil, ErrEmailAlreadyExists): Email already registered // - (nil, ErrWeakPassword): Password too weak // - (nil, error): Database error or other failure func (u *UserService) Register(ctx context.Context, userInput *models.CreateUserInput) (*models.User, error) { // Step 1: Validate registration input // This checks: // - Email format is valid // - Email not already registered // - Password meets strength requirements if err := u.ValidateRegistrationInput(ctx, userInput); err != nil { return nil, err // Return specific validation error } // Step 2: Normalize email for consistent storage // - TrimSpace: Remove leading/trailing whitespace // - ToLower: Convert to lowercase for case-insensitive matching // Why: "User@Example.COM " becomes "user@example.com" userInput.Email = strings.ToLower(strings.TrimSpace(userInput.Email)) // Step 3: Create user in database // Repository handles: // - Password hashing (bcrypt) // - Database insertion // - Generating user ID and timestamps user, err := u.userRepo.Create(ctx, userInput) if err != nil { // Wrap error with context for better debugging return nil, fmt.Errorf("failed to create user : %w", err) } // Step 4: Return created user // User object includes generated ID, timestamps, etc. return user, err } // AuthenticateUserByEmail verifies user credentials (email + password). // This is the core of the login process. // // What happens: // 1. Normalize email (lowercase, trim) // 2. Look up user by email // 3. Verify password against stored hash // 4. Return user if valid // // Security considerations: // - Generic error message (prevents email enumeration) // - Email normalization (consistent with registration) // - Password never logged or exposed // - Constant-time password comparison (via bcrypt) // // Why generic error? // - "Invalid credentials" for both wrong email AND wrong password // - Prevents attackers from discovering valid emails // - Standard security practice // - Trade-off: Slightly worse UX for better security // // Email enumeration attack explained: // - Attacker tries many emails // - Different errors for "email not found" vs "wrong password" // - Attacker can build list of valid emails // - Then focus on password cracking for valid emails // - Solution: Same error for both cases // // Password verification: // - Uses bcrypt.CompareHashAndPassword // - Constant-time comparison (prevents timing attacks) // - Automatically handles salt extraction // - Returns error if no match // // After successful authentication: // - Caller should check user.Status (active, suspended, etc.) // - Caller should generate auth tokens // - Caller should update last login timestamp // // Parameters: // - ctx: Context for database operations // - email: User's email address // - password: User's plaintext password // // Return values: // - (*User, nil): Authentication successful // - (nil, ErrInvalidCredentials): Wrong email or password // - (nil, error): Database error (wrapped) func (u *UserService) AuthenticateUserByEmail(ctx context.Context, email string, password string) (*models.User, error) { // Step 1: Normalize email // Must match normalization done during registration email = strings.ToLower(strings.TrimSpace(email)) // Step 2: Look up user by email user, err := u.userRepo.FindByEmail(ctx, email) if err != nil { // Wrap error with context // This is a database error, not "user not found" return nil, fmt.Errorf("repository error : %w", err) } // Step 3: Check if user exists if user == nil { // Email not found in database // Return generic error (don't reveal email doesn't exist) return nil, ErrInvalidCredentials } // Step 4: Verify password // Repository method uses bcrypt to compare // Returns false if password doesn't match if !u.userRepo.VerifyPassword(user, password) { // Password incorrect // Return generic error (don't reveal password was wrong) return nil, ErrInvalidCredentials } // Step 5: Authentication successful // Return user object for token generation return user, nil } // GetByID retrieves a user by their unique ID. // This is used for: // - Loading user after authentication // - Fetching user profile // - Validating user existence // // When to use this vs GetByEmail: // - Use GetByID: When you already have user ID (from token, etc.) // - Use GetByEmail: During login or user lookup // // Why this is simple: // - Just wraps repository call // - Adds consistent error wrapping // - Provides clear error when user not found // // Parameters: // - ctx: Context for database operations // - id: User's unique identifier (UUID) // // Return values: // - (*User, nil): User found // - (nil, ErrUserNotFound): User doesn't exist (or is deleted) // - (nil, error): Database error (wrapped) func (u *UserService) GetByID(ctx context.Context, id uuid.UUID) (*models.User, error) { // Look up user by ID user, err := u.userRepo.FindByID(ctx, id) if err != nil { // Database error return nil, fmt.Errorf("repository error : %w", err) } // Check if user was found if user == nil { // User doesn't exist (or is soft-deleted) return nil, ErrUserNotFound } return user, nil } // GetByEmail retrieves a user by their email address. // This is used for: // - User lookup in admin interfaces // - Checking if email is registered // - Loading user before certain operations // // When to use this vs GetByID: // - Use GetByEmail: When you have email (user lookup, admin search) // - Use GetByID: When you have ID (token validation, internal operations) // // Email should be normalized before calling (lowercase, trim). // // Parameters: // - ctx: Context for database operations // - email: User's email address // // Return values: // - (*User, nil): User found // - (nil, ErrUserNotFound): User doesn't exist // - (nil, error): Database error (wrapped) func (u *UserService) GetByEmail(ctx context.Context, email string) (*models.User, error) { // Look up user by email user, err := u.userRepo.FindByEmail(ctx, email) if err != nil { // Database error return nil, fmt.Errorf("repository error : %w", err) } // Check if user was found if user == nil { // User doesn't exist return nil, ErrUserNotFound } return user, nil } // UpdateLastLogin updates user's last login timestamp and IP address. // This is called after successful login for audit purposes. // // Why track this: // - Security monitoring // - User awareness ("last login") // - Account activity tracking // - Compliance requirements // // When to call: // - After successful login // - After token refresh (debatable) // - Before generating tokens // // This is a simple pass-through to repository. // Service layer included for consistency and future business logic. // // Parameters: // - ctx: Context for database operations // - id: User's ID // - ipAddress: IP address of request // // Returns: // - error: Database error if update fails func (u *UserService) UpdateLastLogin(ctx context.Context, id uuid.UUID, ipAddress *string) error { // Delegate to repository return u.userRepo.UpdateLastLogin(ctx, id, ipAddress) } // UpdatePassword changes a user's password. // This is used for: // - User-initiated password change // - Password reset flow // - Admin force password change // // Important: Before calling this: // 1. Validate user's identity (current password or reset token) // 2. Validate new password strength // 3. Verify user has permission (self or admin) // // After calling this: // 1. Revoke all user's sessions (force re-login) // 2. Send email notification to user // 3. Log event for audit trail // 4. Update password history (if tracking) // // This is a simple pass-through to repository. // Repository handles password hashing. // // Parameters: // - ctx: Context for database operations // - id: User's ID // - newPassword: New plaintext password (will be hashed) // // Returns: // - error: Hashing or database error func (u *UserService) UpdatePassword(ctx context.Context, id uuid.UUID, newPassword string) error { // Delegate to repository // Repository handles bcrypt hashing return u.userRepo.UpdatePassword(ctx, id, newPassword) } // ValidateRegistrationInput validates all user registration input. // This is called before creating a new user account. // // Validations performed: // 1. Email format validation (structure, length) // 2. Email uniqueness check (not already registered) // 3. Password strength validation (length, complexity) // // Why validate in service layer? // - Business rules belong here (not in handler or repository) // - Reusable validation (called from multiple places) // - Testable independently // - Clear separation of concerns // // Validation order matters: // 1. Format validation first (cheap, no database query) // 2. Uniqueness check second (database query, more expensive) // 3. Password validation last (computational cost) // // Why email uniqueness here vs database constraint? // - Both! Database constraint is backup // - Service check provides better error message // - Service check prevents unnecessary password hashing // - Database constraint prevents race conditions // // Parameters: // - ctx: Context for database operations // - input: Registration input to validate // // Return values: // - nil: All validation passed // - ErrInvalidEmail: Email format invalid // - ErrEmailAlreadyExists: Email already registered // - ErrWeakPassword: Password doesn't meet requirements // - error: Database error during uniqueness check func (u *UserService) ValidateRegistrationInput(ctx context.Context, input *models.CreateUserInput) error { // Step 1: Validate email format // Checks structure, length, basic format // This is cheap (no database query) if !isValidEmail(input.Email) { return ErrInvalidEmail } // Step 2: Check email uniqueness // Queries database to see if email already exists // Lowercase email for case-insensitive check exists, err := u.userRepo.EmailExists(ctx, strings.ToLower(input.Email)) if err != nil { // Database error during uniqueness check // Wrap with context for debugging return fmt.Errorf("failed to check email uniqueness : %w email id [%s]", err, input.Email) } if exists { // Email already registered return ErrEmailAlreadyExists } // Step 3: Validate password strength // Checks length, complexity, common passwords // Requires password, email (to prevent email in password), and first name (to prevent name in password) if !isStrongPassword(input.Password, input.Email, *input.FirstName) { return ErrWeakPassword } // All validation passed return nil } // isValidEmail checks if an email address has valid format. // This is a basic validation, not RFC 5322 compliant. // // Checks performed: // 1. Length: 3-254 characters (RFC 5321 limit) // 2. Contains @: Must have exactly one @ // 3. @ position: Not at start or end // 4. Local part: 1-64 characters (before @) // 5. Domain part: Contains at least one dot // // What this DOESN'T check: // - Special characters in local part // - International domain names // - Multiple @ symbols in quoted local part // - Full RFC 5322 compliance // // Why simple validation? // - Good enough for most cases // - Fast (no regex or complex parsing) // - Prevents obvious mistakes // - Final validation is sending verification email // // For production, consider: // - Using email validation library // - DNS MX record check (is domain valid?) // - Disposable email detection // - Email verification required // // Examples: // - Valid: "user@example.com", "john.doe@company.co.uk" // - Invalid: "user", "@example.com", "user@", "user@@example.com" // // Parameters: // - emailInput: Email string to validate // // Returns: // - true: Email format appears valid // - false: Email format is invalid func isValidEmail(emailInput string) bool { // Trim whitespace for validation email := strings.TrimSpace(emailInput) // Check length constraints // Min: "a@b.c" = 5 chars (but we use 3 to be permissive) // Max: 254 chars per RFC 5321 if len(email) < 3 || len(email) > 254 { return false } // Find position of last @ symbol // LastIndex returns -1 if not found atIndex := strings.LastIndex(email, "@") // Validate @ position // Must exist and not be at start (position 0) or end if atIndex < 1 || atIndex > len(email)-1 { return false } // Split email into local and domain parts localPart := email[:atIndex] // Before @ domainPart := email[atIndex+1:] // After @ // Validate local part length // RFC 5321: Maximum 64 characters before @ if len(localPart) < 1 || len(localPart) > 64 { return false } // Validate domain part has at least one dot // Required for valid domain (e.g., "example.com") // Note: This doesn't validate TLD or DNS if !strings.Contains(domainPart, ".") { return false } // Basic validation passed return true } // isStrongPassword validates password meets security requirements. // This enforces password policy to prevent weak passwords. // // Requirements: // 1. Minimum 8 characters (longer is better) // 2. At least one lowercase letter (a-z) // 3. At least one uppercase letter (A-Z) // 4. At least one number (0-9) // 5. At least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?/~) // 6. Must NOT contain user's email // 7. Must NOT contain user's first name // // Why these requirements? // - Length: Harder to brute force // - Lowercase: Increases character space // - Uppercase: Increases character space // - Number: Increases character space // - Special: Increases character space (most important) // - No email: Prevents easy guessing // - No name: Prevents easy guessing // // Character space importance: // - Lowercase only: 26^8 = 208 billion combinations // - + Uppercase: 52^8 = 53 trillion combinations // - + Numbers: 62^8 = 218 trillion combinations // - + Special chars: 90^8 = 4.3 quadrillion combinations // // What this DOESN'T check: // - Dictionary words (would need dictionary) // - Common passwords (would need list like "password123") // - Keyboard patterns (would need pattern matching) // - Previously breached passwords (would need Have I Been Pwned API) // // For production, consider: // - Password strength library (zxcvbn) // - Have I Been Pwned API integration // - Common password blacklist // - Personal information checking (birthdate, etc.) // // Parameters: // - passwordToCheck: Password to validate // - email: User's email (to prevent email in password) // - firstName: User's first name (to prevent name in password) // // Returns: // - true: Password meets all requirements // - false: Password fails one or more requirements func isStrongPassword(passwordToCheck string, email string, firstName string) bool { // Check minimum length // 8 characters minimum (NIST recommends at least 8) // Consider increasing to 12 or 16 for better security if len(passwordToCheck) < 8 { return false } // Initialize flags for each requirement hasLowerCase := false hasUpperCase := false hasSpecialCharacter := false hasNumber := false // Check each character in password // We iterate once through the string checking all requirements for _, char := range passwordToCheck { switch { // Check for uppercase letter (A-Z) case char >= 'A' && char <= 'Z': hasUpperCase = true // Check for lowercase letter (a-z) case char >= 'a' && char <= 'z': hasLowerCase = true // Check for digit (0-9) case char >= '0' && char <= '9': hasNumber = true // Check for special characters // Ranges cover common special characters on keyboard: // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / // :, ;, <, =, >, ?, @ // [, \, ], ^, _, ` // {, |, }, ~ case (char >= '!' && char <= '/') || // !"#$%&'()*+,-./ (char >= ':' && char <= '@') || // :;<=>?@ (char >= '[' && char <= '`') || // [\]^_` (char >= '{' && char <= '~'): // {|}~ hasSpecialCharacter = true } } // Check if all character type requirements are met if !hasLowerCase || !hasUpperCase || !hasSpecialCharacter || !hasNumber { return false // Missing at least one required character type } // Check if password contains user's email // Prevents passwords like "myemail@example.com123" // Case-insensitive check if strings.Contains(passwordToCheck, email) { return false } // Check if password contains user's first name // Prevents passwords like "JohnSmith123!" // Case-insensitive check if strings.Contains(passwordToCheck, firstName) { return false } // All requirements passed return true }