package repositories import ( "context" "database/sql" "fmt" "github.com/creativenoz/aurganize-v62/backend/internal/models" "github.com/google/uuid" "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" ) // UserRepository handles all database operations related to users. // This is the data access layer for user management. // // What this repository does: // - Creates new users (registration) // - Finds users by email or ID (login, profile lookup) // - Updates user information (password, last login) // - Verifies passwords (authentication) // - Checks email uniqueness (prevent duplicates) // // Architecture pattern: Repository Pattern // Benefits: // - Separates database logic from business logic // - Makes testing easier (can mock repository) // - Provides clean interface for data access // - Centralizes SQL queries // - Makes it easy to change database later // // Security considerations: // - Passwords are NEVER stored in plaintext // - Always use bcrypt for password hashing // - Soft deletes (deleted_at) preserve data integrity // - Email normalization prevents duplicate accounts type UserRepository struct { db *sqlx.DB // sqlx provides enhanced database operations (named params, struct scanning) } // NewUserRepository creates a new instance of UserRepository. // This follows dependency injection pattern: // - Database connection passed in (not created internally) // - Makes testing easier (can inject test database) // - Keeps repository decoupled from connection setup // - Follows SOLID principles (Dependency Inversion) // // Parameter: // - db: The database connection pool for all operations // // Returns: // - Initialized UserRepository ready for use func NewUserRepository(db *sqlx.DB) *UserRepository { return &UserRepository{db: db} } // Create creates a new user in the database. // This is called during user registration. // // What happens: // 1. Hash password using bcrypt (NEVER store plaintext!) // 2. Insert user record with hashed password // 3. Database generates ID, timestamps, and computed fields (full_name) // 4. Return complete user object // // Why bcrypt for passwords? // - Specifically designed for password hashing // - Slow by design (resists brute-force attacks) // - Includes salt automatically (prevents rainbow table attacks) // - Adaptive (can increase cost factor as computers get faster) // - Industry standard for password storage // // bcrypt workflow: // 1. Generates random salt // 2. Combines password + salt // 3. Hashes multiple times (cost factor determines iterations) // 4. Result: "$2a$10$salt+hash" format (self-contained, includes cost and salt) // // Why DefaultCost? // - Balance between security and performance // - DefaultCost = 10 (2^10 = 1024 iterations) // - Takes ~100ms to hash (acceptable for login, but slows brute-force) // - Can increase for higher security (cost 12 = 4x slower, cost 14 = 16x slower) // // Database features used: // - RETURNING clause: Get back created record without separate SELECT // - Auto-generated fields: id (UUID), timestamps, full_name (computed) // // Error handling: // - Returns error if password hashing fails (very rare) // - Returns error if insert fails (constraint violations, etc.) // // Flow: // 1. Hash password with bcrypt // 2. Execute INSERT with RETURNING // 3. Scan returned row into user struct // 4. Return populated user object func (r *UserRepository) Create(ctx context.Context, input *models.CreateUserInput) (*models.User, error) { // Step 1: Hash the password using bcrypt // bcrypt.GenerateFromPassword: // - Takes password as []byte // - Takes cost factor (DefaultCost = 10) // - Returns hash as []byte (e.g., "$2a$10$...") // - Returns error if hashing fails (very rare, maybe out of memory) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { // Wrap error with context for better debugging return nil, fmt.Errorf("failed to hash password : %w", err) } // Prepare user struct to receive database response user := &models.User{} // SQL INSERT query with RETURNING clause // PostgreSQL returns the inserted row, avoiding a separate SELECT // This is atomic and more efficient query := ` INSERT INTO users ( tenant_id, -- Multi-tenancy: which organization user belongs to email, -- User's email (unique identifier for login) password_hash, -- Bcrypt hash of password (NEVER plaintext!) first_name, -- User's first name last_name, -- User's last name role, -- User's role (admin, user, manager, etc.) status -- Account status (active, pending, suspended, deleted) ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id,tenant_id, email, password_hash, first_name, last_name, full_name, avatar_url, phone, role, status, email_verified, email_verified_at, is_onboarded, last_login_at, last_login_ip, created_at, updated_at, deleted_at ` // Execute query and scan result into user struct // GetContext: // - Supports context (cancellation, timeout) // - Expects exactly one row // - Maps columns to struct fields by db tags err = r.db.GetContext( ctx, user, // Destination struct query, // Parameters matching $1-$7 in query input.TenantID, input.Email, string(hashedPassword), // Convert []byte to string for database input.FirstName, input.LastName, input.Role, input.Status, ) return user, err } // FindByEmail finds a user by their email address. // This is used during login and email existence checks. // // Why search by email? // - Email is the unique identifier for login // - Users remember emails better than IDs // - Standard practice for web applications // // Security considerations: // - Email comparison is case-sensitive in database // - Service layer should normalize email (lowercase, trim) // - Prevents duplicate accounts with different casing // // Soft delete handling: // - Only returns users where deleted_at IS NULL // - Deleted users are hidden but data preserved // - Allows for account recovery // - Maintains referential integrity // // Return values: // - (*User, nil): User found // - (nil, nil): User not found (not an error, just doesn't exist) // - (nil, error): Database error occurred // // Why return nil instead of error when not found? // - "Not found" is a valid state, not an error // - Allows caller to distinguish between "doesn't exist" and "database error" // - Follows repository pattern best practices func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) { user := &models.User{} // SQL SELECT query // Note: Should add LIMIT 1 for optimization (early exit) query := ` SELECT id,tenant_id, email, password_hash, first_name, last_name, full_name, avatar_url, phone, role, status, email_verified, email_verified_at, is_onboarded, last_login_at, last_login_ip, created_at, updated_at, deleted_at FROM users WHERE email = $1 -- Exact email match AND deleted_at IS NULL -- Only non-deleted users ` // Execute query err := r.db.GetContext( ctx, user, query, email, // $1 - Email to search for ) // Handle "not found" case specially // sql.ErrNoRows means query executed but returned no rows // This is expected when user doesn't exist if err == sql.ErrNoRows { return nil, nil // User not found (not an error) } if err != nil { return nil, err // Real database error } return user, err } // FindByID finds a user by their unique ID. // This is used for: // - Loading user after authentication // - Fetching user profile // - Validating user existence // - Retrieving user for operations // // Why search by ID vs email? // - ID lookup is faster (primary key index) // - ID never changes (email might change) // - Used internally after user is identified // // When to use ID vs email: // - Use email: Login, registration checks // - Use ID: After authentication, internal operations // // Soft delete handling: // - Only returns non-deleted users (deleted_at IS NULL) // - Prevents access to deleted accounts // - Maintains data for audit trail // // Return values: // - (*User, nil): User found // - (nil, nil): User not found or deleted // - (nil, error): Database error occurred func (r *UserRepository) FindByID(ctx context.Context, id uuid.UUID) (*models.User, error) { user := &models.User{} // SQL SELECT query by ID query := ` SELECT id,tenant_id, email, password_hash, first_name, last_name, full_name, avatar_url, phone, role, status, email_verified, email_verified_at, is_onboarded, last_login_at, last_login_ip, created_at, updated_at, deleted_at FROM users WHERE id = $1 -- Exact ID match (UUID) AND deleted_at IS NULL -- Only non-deleted users ` // Execute query err := r.db.GetContext( ctx, user, query, id, // $1 - User ID (UUID) ) // Handle "not found" case if err == sql.ErrNoRows { return nil, nil // User not found (not an error) } if err != nil { return nil, err // Database error } return user, nil } // EmailExists checks if an email is already registered in the system. // This is used during registration to prevent duplicate accounts. // // Why check email existence? // 1. Prevent duplicate accounts (UX issue) // 2. Provide clear error messages ("Email already registered") // 3. Enforce uniqueness at application level (in addition to database constraint) // 4. Allow custom error handling (e.g., suggest login instead) // // Implementation using EXISTS: // - EXISTS is efficient (stops at first match) // - Returns boolean directly // - Doesn't load full user data (faster than COUNT or SELECT) // - Uses index on email column // // Soft delete consideration: // - Only checks non-deleted users (deleted_at IS NULL) // - Allows email reuse after deletion (debatable design choice) // - Alternative: Never allow email reuse (more strict) // // Return values: // - (true, nil): Email exists (already registered) // - (false, nil): Email available (can register) // - (false, error): Database error occurred func (r *UserRepository) EmailExists(ctx context.Context, email string) (bool, error) { var email_already_exists bool // SQL EXISTS query // EXISTS(...) returns true/false based on whether subquery returns rows // More efficient than COUNT(*) or SELECT * for existence checks query := ` SELECT EXISTS ( SELECT FROM users -- SELECT doesn't need columns for EXISTS WHERE email = $1 -- Check for email match AND deleted_at IS NULL -- Only check non-deleted users ) ` // Execute query and scan boolean result err := r.db.GetContext( ctx, &email_already_exists, // Boolean result query, email, // $1 - Email to check ) return email_already_exists, err } // UpdateLastLogin updates the user's last login timestamp and IP address. // This is called after successful login for audit and security purposes. // // Why track last login? // 1. Security: Detect unusual login patterns (new location, time) // 2. User awareness: Show "Last login: 2 hours ago from New York" // 3. Audit trail: Compliance requirements (who accessed when) // 4. Account activity: Identify inactive accounts // 5. Support: Help users verify their own activity // // What gets updated: // - last_login_at: Current timestamp (when login occurred) // - last_login_ip: IP address of login (for location/security analysis) // - updated_at: Record last modification time // // IP address considerations: // - Can be IPv4 or IPv6 // - Might be proxy/load balancer IP (need X-Forwarded-For) // - Privacy concern: May need to anonymize after time period (GDPR) // - Useful for: Geographic analysis, fraud detection // // Performance note: // - This is a quick UPDATE (indexed by id) // - Usually fast enough to include in login flow // - Alternative: Update asynchronously if performance critical // // Error handling: // - Returns error if update fails // - Caller might ignore (login succeeds even if this fails) // - Non-critical operation (login more important than tracking) func (r *UserRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID, ip *string) error { // SQL UPDATE query // Uses NOW() for consistent database timestamp query := ` UPDATE users SET last_login_at = NOW(), -- Current database time last_login_ip = $2, -- IP address from request updated_at = NOW() -- Track this modification WHERE id = $1 -- Only update this user ` // Execute update // ExecContext for queries that don't return rows (UPDATE, DELETE) _, err := r.db.ExecContext( ctx, query, id, // $1 - User ID ip, // $2 - IP address (pointer allows NULL) ) return err } // UpdatePassword updates a user's password. // This is used for: // - Password change (user-initiated) // - Password reset (forgot password flow) // - Force password change (admin action) // // Security process: // 1. Hash new password with bcrypt // 2. Update password_hash in database // 3. Update updated_at timestamp // // What happens after password change: // - Caller should revoke all sessions (force re-login on all devices) // - User receives email notification (security alert) // - Audit log entry created // // Why hash before updating: // - NEVER store plaintext passwords in database // - bcrypt provides strong one-way hashing // - Even database admins can't see actual passwords // - Protects users even if database is breached // // Important considerations: // 1. Validate new password strength before calling this // 2. Verify user's identity (current password or reset token) // 3. Rate limit password changes (prevent abuse) // 4. Send notification to user's email // 5. Consider revoking all sessions // // Error handling: // - Returns error if hashing fails (rare, memory issues) // - Returns error if update fails (user not found, database error) func (r *UserRepository) UpdatePassword(ctx context.Context, id uuid.UUID, password string) error { // Step 1: Hash the new password // Always use bcrypt for password hashing (never plaintext!) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { // Wrap error with context return fmt.Errorf("failed to hash password: %w", err) } // SQL UPDATE query query := ` UPDATE users SET password_hash = $2, -- Update to new hashed password updated_at = NOW() -- Track modification time WHERE id = $1 -- Only update this user ` // Execute update _, err = r.db.ExecContext( ctx, query, id, // $1 - User ID string(hashedPassword), // $2 - New password hash ) return err } // VerifyPassword checks if a provided password matches the user's stored password hash. // This is the core of password-based authentication. // // How it works: // 1. Extract password_hash from user (from database) // 2. Use bcrypt.CompareHashAndPassword to verify // 3. Return true if match, false if not // // bcrypt verification process: // 1. Hash format: "$2a$10$salthashedpassword" // 2. bcrypt extracts salt from stored hash // 3. Hashes provided password with same salt // 4. Compares result with stored hash // 5. Returns nil error if match, error if mismatch // // Why bcrypt is good for this: // - Timing-safe comparison (prevents timing attacks) // - Salt is stored in hash (no separate storage needed) // - Slow by design (prevents brute force) // - Industry standard // // Security considerations: // - Never log or display passwords // - Don't reveal if email or password was wrong (prevents enumeration) // - Rate limit login attempts (prevent brute force) // - Consider account lockout after failed attempts // // Nil check importance: // - If password_hash is nil, dereferencing causes panic // - This might happen if: // - User row is corrupted // - Migration error // - Direct database manipulation // // - Better to return false than crash // // Return values: // - true: Password matches (authentication successful) // - false: Password doesn't match OR hash is nil (authentication failed) func (r *UserRepository) VerifyPassword(user *models.User, providedPassword string) bool { // Safety check: Prevent panic if password_hash is nil // This shouldn't happen in normal operation, but better safe than crashed if user.PasswordHash == nil { return false // No hash = can't verify = authentication fails } // Use bcrypt to compare provided password with stored hash // CompareHashAndPassword: // - Takes stored hash as []byte // - Takes provided password as []byte // - Returns nil if match, error if mismatch // - Handles salt extraction and timing-safe comparison err := bcrypt.CompareHashAndPassword([]byte(*user.PasswordHash), []byte(providedPassword)) // Return true if err is nil (passwords match) // Return false if err is not nil (passwords don't match) return err == nil }