package user import ( "context" "github.com/gocql/gocql" "go.uber.org/zap" domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/user/models" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger" ) // GetByID retrieves a user by ID from the users_by_id table func (r *repository) GetByID(ctx context.Context, tenantID string, id string) (*domainuser.User, error) { r.logger.Debug("getting user by ID", zap.String("tenant_id", tenantID), zap.String("id", id)) // EXPLICIT: We're querying the users_by_id table with tenant isolation var userByID models.UserByID query := `SELECT tenant_id, id, email, first_name, last_name, name, lexical_name, timezone, role, status, phone, country, region, city, postal_code, address_line1, address_line2, has_shipping_address, shipping_name, shipping_phone, shipping_country, shipping_region, shipping_city, shipping_postal_code, shipping_address_line1, shipping_address_line2, profile_timezone, agree_terms_of_service, agree_promotions, agree_to_tracking_across_third_party_apps_and_services, password_hash_algorithm, password_hash, was_email_verified, code, code_type, code_expiry, otp_enabled, otp_verified, otp_validated, otp_secret, otp_auth_url, otp_backup_code_hash, otp_backup_code_hash_algorithm, created_from_ip_address, created_from_ip_timestamp, created_by_user_id, created_by_name, modified_from_ip_address, modified_from_ip_timestamp, modified_by_user_id, modified_at, modified_by_name, last_login_at, created_at, updated_at FROM users_by_id WHERE tenant_id = ? AND id = ?` err := r.session.Query(query, tenantID, id). Consistency(gocql.Quorum). Scan(&userByID.TenantID, &userByID.ID, &userByID.Email, &userByID.FirstName, &userByID.LastName, &userByID.Name, &userByID.LexicalName, &userByID.Timezone, &userByID.Role, &userByID.Status, &userByID.Phone, &userByID.Country, &userByID.Region, &userByID.City, &userByID.PostalCode, &userByID.AddressLine1, &userByID.AddressLine2, &userByID.HasShippingAddress, &userByID.ShippingName, &userByID.ShippingPhone, &userByID.ShippingCountry, &userByID.ShippingRegion, &userByID.ShippingCity, &userByID.ShippingPostalCode, &userByID.ShippingAddressLine1, &userByID.ShippingAddressLine2, &userByID.ProfileTimezone, &userByID.AgreeTermsOfService, &userByID.AgreePromotions, &userByID.AgreeToTrackingAcrossThirdPartyAppsAndServices, &userByID.PasswordHashAlgorithm, &userByID.PasswordHash, &userByID.WasEmailVerified, &userByID.Code, &userByID.CodeType, &userByID.CodeExpiry, &userByID.OTPEnabled, &userByID.OTPVerified, &userByID.OTPValidated, &userByID.OTPSecret, &userByID.OTPAuthURL, &userByID.OTPBackupCodeHash, &userByID.OTPBackupCodeHashAlgorithm, &userByID.CreatedFromIPAddress, &userByID.CreatedFromIPTimestamp, &userByID.CreatedByUserID, &userByID.CreatedByName, &userByID.ModifiedFromIPAddress, &userByID.ModifiedFromIPTimestamp, &userByID.ModifiedByUserID, &userByID.ModifiedAt, &userByID.ModifiedByName, &userByID.LastLoginAt, &userByID.CreatedAt, &userByID.UpdatedAt) if err != nil { if err == gocql.ErrNotFound { return nil, domainuser.ErrUserNotFound } r.logger.Error("failed to get user by ID", zap.Error(err)) return nil, err } // Convert table model to domain entity return userByID.ToUser(), nil } // GetByEmail retrieves a user by email from the users_by_email table func (r *repository) GetByEmail(ctx context.Context, tenantID string, email string) (*domainuser.User, error) { // CWE-532: Use redacted email for logging r.logger.Debug("getting user by email", zap.String("tenant_id", tenantID), logger.EmailHash(email), logger.SafeEmail("email_redacted", email)) // EXPLICIT: We're querying the users_by_email table with tenant isolation var userByEmail models.UserByEmail query := `SELECT tenant_id, email, id, first_name, last_name, name, lexical_name, timezone, role, status, phone, country, region, city, postal_code, address_line1, address_line2, has_shipping_address, shipping_name, shipping_phone, shipping_country, shipping_region, shipping_city, shipping_postal_code, shipping_address_line1, shipping_address_line2, profile_timezone, agree_terms_of_service, agree_promotions, agree_to_tracking_across_third_party_apps_and_services, password_hash_algorithm, password_hash, was_email_verified, code, code_type, code_expiry, otp_enabled, otp_verified, otp_validated, otp_secret, otp_auth_url, otp_backup_code_hash, otp_backup_code_hash_algorithm, created_from_ip_address, created_from_ip_timestamp, created_by_user_id, created_by_name, modified_from_ip_address, modified_from_ip_timestamp, modified_by_user_id, modified_at, modified_by_name, last_login_at, created_at, updated_at FROM users_by_email WHERE tenant_id = ? AND email = ?` err := r.session.Query(query, tenantID, email). Consistency(gocql.Quorum). Scan(&userByEmail.TenantID, &userByEmail.Email, &userByEmail.ID, &userByEmail.FirstName, &userByEmail.LastName, &userByEmail.Name, &userByEmail.LexicalName, &userByEmail.Timezone, &userByEmail.Role, &userByEmail.Status, &userByEmail.Phone, &userByEmail.Country, &userByEmail.Region, &userByEmail.City, &userByEmail.PostalCode, &userByEmail.AddressLine1, &userByEmail.AddressLine2, &userByEmail.HasShippingAddress, &userByEmail.ShippingName, &userByEmail.ShippingPhone, &userByEmail.ShippingCountry, &userByEmail.ShippingRegion, &userByEmail.ShippingCity, &userByEmail.ShippingPostalCode, &userByEmail.ShippingAddressLine1, &userByEmail.ShippingAddressLine2, &userByEmail.ProfileTimezone, &userByEmail.AgreeTermsOfService, &userByEmail.AgreePromotions, &userByEmail.AgreeToTrackingAcrossThirdPartyAppsAndServices, &userByEmail.PasswordHashAlgorithm, &userByEmail.PasswordHash, &userByEmail.WasEmailVerified, &userByEmail.Code, &userByEmail.CodeType, &userByEmail.CodeExpiry, &userByEmail.OTPEnabled, &userByEmail.OTPVerified, &userByEmail.OTPValidated, &userByEmail.OTPSecret, &userByEmail.OTPAuthURL, &userByEmail.OTPBackupCodeHash, &userByEmail.OTPBackupCodeHashAlgorithm, &userByEmail.CreatedFromIPAddress, &userByEmail.CreatedFromIPTimestamp, &userByEmail.CreatedByUserID, &userByEmail.CreatedByName, &userByEmail.ModifiedFromIPAddress, &userByEmail.ModifiedFromIPTimestamp, &userByEmail.ModifiedByUserID, &userByEmail.ModifiedAt, &userByEmail.ModifiedByName, &userByEmail.LastLoginAt, &userByEmail.CreatedAt, &userByEmail.UpdatedAt) if err != nil { if err == gocql.ErrNotFound { return nil, domainuser.ErrUserNotFound } r.logger.Error("failed to get user by email", zap.Error(err)) return nil, err } // Convert table model to domain entity return userByEmail.ToUser(), nil } // GetByEmailGlobal retrieves a user by email across all tenants (for login) // WARNING: This bypasses tenant isolation and should ONLY be used for authentication func (r *repository) GetByEmailGlobal(ctx context.Context, email string) (*domainuser.User, error) { // CWE-532: Use redacted email for logging r.logger.Debug("getting user by email globally (no tenant filter)", logger.EmailHash(email), logger.SafeEmail("email_redacted", email)) // EXPLICIT: Querying users_by_email WITHOUT tenant_id filter // This allows login with just email/password, finding the user's tenant automatically var userByEmail models.UserByEmail query := `SELECT tenant_id, email, id, first_name, last_name, name, lexical_name, timezone, role, status, phone, country, region, city, postal_code, address_line1, address_line2, has_shipping_address, shipping_name, shipping_phone, shipping_country, shipping_region, shipping_city, shipping_postal_code, shipping_address_line1, shipping_address_line2, profile_timezone, agree_terms_of_service, agree_promotions, agree_to_tracking_across_third_party_apps_and_services, password_hash_algorithm, password_hash, was_email_verified, code, code_type, code_expiry, otp_enabled, otp_verified, otp_validated, otp_secret, otp_auth_url, otp_backup_code_hash, otp_backup_code_hash_algorithm, created_from_ip_address, created_from_ip_timestamp, created_by_user_id, created_by_name, modified_from_ip_address, modified_from_ip_timestamp, modified_by_user_id, modified_at, modified_by_name, last_login_at, created_at, updated_at FROM users_by_email WHERE email = ? LIMIT 1 ALLOW FILTERING` err := r.session.Query(query, email). Consistency(gocql.Quorum). Scan(&userByEmail.TenantID, &userByEmail.Email, &userByEmail.ID, &userByEmail.FirstName, &userByEmail.LastName, &userByEmail.Name, &userByEmail.LexicalName, &userByEmail.Timezone, &userByEmail.Role, &userByEmail.Status, &userByEmail.Phone, &userByEmail.Country, &userByEmail.Region, &userByEmail.City, &userByEmail.PostalCode, &userByEmail.AddressLine1, &userByEmail.AddressLine2, &userByEmail.HasShippingAddress, &userByEmail.ShippingName, &userByEmail.ShippingPhone, &userByEmail.ShippingCountry, &userByEmail.ShippingRegion, &userByEmail.ShippingCity, &userByEmail.ShippingPostalCode, &userByEmail.ShippingAddressLine1, &userByEmail.ShippingAddressLine2, &userByEmail.ProfileTimezone, &userByEmail.AgreeTermsOfService, &userByEmail.AgreePromotions, &userByEmail.AgreeToTrackingAcrossThirdPartyAppsAndServices, &userByEmail.PasswordHashAlgorithm, &userByEmail.PasswordHash, &userByEmail.WasEmailVerified, &userByEmail.Code, &userByEmail.CodeType, &userByEmail.CodeExpiry, &userByEmail.OTPEnabled, &userByEmail.OTPVerified, &userByEmail.OTPValidated, &userByEmail.OTPSecret, &userByEmail.OTPAuthURL, &userByEmail.OTPBackupCodeHash, &userByEmail.OTPBackupCodeHashAlgorithm, &userByEmail.CreatedFromIPAddress, &userByEmail.CreatedFromIPTimestamp, &userByEmail.CreatedByUserID, &userByEmail.CreatedByName, &userByEmail.ModifiedFromIPAddress, &userByEmail.ModifiedFromIPTimestamp, &userByEmail.ModifiedByUserID, &userByEmail.ModifiedAt, &userByEmail.ModifiedByName, &userByEmail.LastLoginAt, &userByEmail.CreatedAt, &userByEmail.UpdatedAt) if err != nil { if err == gocql.ErrNotFound { return nil, domainuser.ErrUserNotFound } r.logger.Error("failed to get user by email globally", zap.Error(err)) return nil, err } // CWE-532: Use redacted email for logging r.logger.Info("found user by email globally", logger.EmailHash(email), logger.SafeEmail("email_redacted", email), zap.String("tenant_id", userByEmail.TenantID)) // Convert table model to domain entity return userByEmail.ToUser(), nil } // ListByDate lists users created within a date range from the users_by_date table func (r *repository) ListByDate(ctx context.Context, tenantID string, startDate, endDate string, limit int) ([]*domainuser.User, error) { r.logger.Debug("listing users by date range", zap.String("tenant_id", tenantID), zap.String("start_date", startDate), zap.String("end_date", endDate), zap.Int("limit", limit)) // EXPLICIT: We're querying the users_by_date table query := `SELECT tenant_id, created_date, id, email, first_name, last_name, name, lexical_name, timezone, role, status, phone, country, region, city, postal_code, address_line1, address_line2, has_shipping_address, shipping_name, shipping_phone, shipping_country, shipping_region, shipping_city, shipping_postal_code, shipping_address_line1, shipping_address_line2, profile_timezone, agree_terms_of_service, agree_promotions, agree_to_tracking_across_third_party_apps_and_services, password_hash_algorithm, password_hash, was_email_verified, code, code_type, code_expiry, otp_enabled, otp_verified, otp_validated, otp_secret, otp_auth_url, otp_backup_code_hash, otp_backup_code_hash_algorithm, created_from_ip_address, created_from_ip_timestamp, created_by_user_id, created_by_name, modified_from_ip_address, modified_from_ip_timestamp, modified_by_user_id, modified_at, modified_by_name, last_login_at, created_at, updated_at FROM users_by_date WHERE tenant_id = ? AND created_date >= ? AND created_date <= ? LIMIT ?` iter := r.session.Query(query, tenantID, startDate, endDate, limit). Consistency(gocql.Quorum). Iter() var users []*domainuser.User var userByDate models.UserByDate for iter.Scan(&userByDate.TenantID, &userByDate.CreatedDate, &userByDate.ID, &userByDate.Email, &userByDate.FirstName, &userByDate.LastName, &userByDate.Name, &userByDate.LexicalName, &userByDate.Timezone, &userByDate.Role, &userByDate.Status, &userByDate.Phone, &userByDate.Country, &userByDate.Region, &userByDate.City, &userByDate.PostalCode, &userByDate.AddressLine1, &userByDate.AddressLine2, &userByDate.HasShippingAddress, &userByDate.ShippingName, &userByDate.ShippingPhone, &userByDate.ShippingCountry, &userByDate.ShippingRegion, &userByDate.ShippingCity, &userByDate.ShippingPostalCode, &userByDate.ShippingAddressLine1, &userByDate.ShippingAddressLine2, &userByDate.ProfileTimezone, &userByDate.AgreeTermsOfService, &userByDate.AgreePromotions, &userByDate.AgreeToTrackingAcrossThirdPartyAppsAndServices, &userByDate.PasswordHashAlgorithm, &userByDate.PasswordHash, &userByDate.WasEmailVerified, &userByDate.Code, &userByDate.CodeType, &userByDate.CodeExpiry, &userByDate.OTPEnabled, &userByDate.OTPVerified, &userByDate.OTPValidated, &userByDate.OTPSecret, &userByDate.OTPAuthURL, &userByDate.OTPBackupCodeHash, &userByDate.OTPBackupCodeHashAlgorithm, &userByDate.CreatedFromIPAddress, &userByDate.CreatedFromIPTimestamp, &userByDate.CreatedByUserID, &userByDate.CreatedByName, &userByDate.ModifiedFromIPAddress, &userByDate.ModifiedFromIPTimestamp, &userByDate.ModifiedByUserID, &userByDate.ModifiedAt, &userByDate.ModifiedByName, &userByDate.LastLoginAt, &userByDate.CreatedAt, &userByDate.UpdatedAt) { users = append(users, userByDate.ToUser()) } if err := iter.Close(); err != nil { r.logger.Error("failed to list users by date", zap.Error(err)) return nil, err } return users, nil }