Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,119 @@
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"
)
// Create creates a new user in all tables using batched writes
func (r *repository) Create(ctx context.Context, tenantID string, u *domainuser.User) error {
// CWE-532: Use redacted email for logging
r.logger.Info("creating user",
zap.String("tenant_id", tenantID),
logger.EmailHash(u.Email),
logger.SafeEmail("email_redacted", u.Email))
// Convert domain entity to ALL table models
userByID := models.FromUser(tenantID, u)
userByEmail := models.FromUserByEmail(tenantID, u)
userByDate := models.FromUserByDate(tenantID, u)
// Use batched writes to maintain consistency across all tables
batch := r.session.NewBatch(gocql.LoggedBatch)
// Insert into users_by_id table
batch.Query(`INSERT INTO users_by_id (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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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)
// Insert into users_by_email table
batch.Query(`INSERT INTO users_by_email (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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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)
// Insert into users_by_date table
batch.Query(`INSERT INTO users_by_date (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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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)
// Execute batch atomically
if err := r.session.ExecuteBatch(batch); err != nil {
r.logger.Error("failed to create user", zap.Error(err))
return err
}
r.logger.Info("user created successfully", zap.String("user_id", u.ID))
return nil
}

View file

@ -0,0 +1,47 @@
package user
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
)
// Delete deletes a user from all tables using batched writes
func (r *repository) Delete(ctx context.Context, tenantID string, id string) error {
r.logger.Info("deleting user",
zap.String("tenant_id", tenantID),
zap.String("id", id))
// First, get the user to retrieve email and created_date for deleting from other tables
user, err := r.GetByID(ctx, tenantID, id)
if err != nil {
return err
}
createdDate := user.CreatedAt.Format("2006-01-02")
// Use batched writes to maintain consistency across all tables
batch := r.session.NewBatch(gocql.LoggedBatch)
// Delete from users_by_id table
batch.Query(`DELETE FROM users_by_id WHERE tenant_id = ? AND id = ?`,
tenantID, id)
// Delete from users_by_email table
batch.Query(`DELETE FROM users_by_email WHERE tenant_id = ? AND email = ?`,
tenantID, user.Email)
// Delete from users_by_date table
batch.Query(`DELETE FROM users_by_date WHERE tenant_id = ? AND created_date = ? AND id = ?`,
tenantID, createdDate, id)
// Execute batch atomically
if err := r.session.ExecuteBatch(batch); err != nil {
r.logger.Error("failed to delete user", zap.Error(err))
return err
}
r.logger.Info("user deleted successfully", zap.String("user_id", id))
return nil
}

View file

@ -0,0 +1,230 @@
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
}

View file

@ -0,0 +1,22 @@
package user
import (
"github.com/gocql/gocql"
"go.uber.org/zap"
domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
)
// repository implements the user.Repository interface
type repository struct {
session *gocql.Session
logger *zap.Logger
}
// ProvideRepository creates a new user repository
func ProvideRepository(session *gocql.Session, logger *zap.Logger) domainuser.Repository {
return &repository{
session: session,
logger: logger,
}
}

View file

@ -0,0 +1,225 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
)
// UserByDate represents the users_by_date table
// Query pattern: List users sorted by creation date
// Primary key: ((tenant_id, created_date), id) - composite partition key + clustering
type UserByDate struct {
TenantID string `db:"tenant_id"` // Multi-tenant isolation (partition key part 1)
CreatedDate string `db:"created_date"` // Format: YYYY-MM-DD (partition key part 2)
ID string `db:"id"` // Clustering column
Email string `db:"email"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Name string `db:"name"`
LexicalName string `db:"lexical_name"`
Timezone string `db:"timezone"`
Role int `db:"role"`
Status int `db:"status"`
// Profile data fields (flattened)
Phone string `db:"phone"`
Country string `db:"country"`
Region string `db:"region"`
City string `db:"city"`
PostalCode string `db:"postal_code"`
AddressLine1 string `db:"address_line1"`
AddressLine2 string `db:"address_line2"`
HasShippingAddress bool `db:"has_shipping_address"`
ShippingName string `db:"shipping_name"`
ShippingPhone string `db:"shipping_phone"`
ShippingCountry string `db:"shipping_country"`
ShippingRegion string `db:"shipping_region"`
ShippingCity string `db:"shipping_city"`
ShippingPostalCode string `db:"shipping_postal_code"`
ShippingAddressLine1 string `db:"shipping_address_line1"`
ShippingAddressLine2 string `db:"shipping_address_line2"`
ProfileTimezone string `db:"profile_timezone"`
AgreeTermsOfService bool `db:"agree_terms_of_service"`
AgreePromotions bool `db:"agree_promotions"`
AgreeToTrackingAcrossThirdPartyAppsAndServices bool `db:"agree_to_tracking_across_third_party_apps_and_services"`
// Security data fields (flattened)
PasswordHashAlgorithm string `db:"password_hash_algorithm"`
PasswordHash string `db:"password_hash"`
WasEmailVerified bool `db:"was_email_verified"`
Code string `db:"code"`
CodeType string `db:"code_type"`
CodeExpiry time.Time `db:"code_expiry"`
OTPEnabled bool `db:"otp_enabled"`
OTPVerified bool `db:"otp_verified"`
OTPValidated bool `db:"otp_validated"`
OTPSecret string `db:"otp_secret"`
OTPAuthURL string `db:"otp_auth_url"`
OTPBackupCodeHash string `db:"otp_backup_code_hash"`
OTPBackupCodeHashAlgorithm string `db:"otp_backup_code_hash_algorithm"`
// Metadata fields (flattened)
// CWE-359: Encrypted IP addresses for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"` // Encrypted with go-ipcrypt
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"` // For 90-day expiration tracking
CreatedByUserID string `db:"created_by_user_id"`
CreatedByName string `db:"created_by_name"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"` // Encrypted with go-ipcrypt
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"` // For 90-day expiration tracking
ModifiedByUserID string `db:"modified_by_user_id"`
ModifiedAt time.Time `db:"modified_at"`
ModifiedByName string `db:"modified_by_name"`
LastLoginAt time.Time `db:"last_login_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// ToUser converts table model to domain entity
func (u *UserByDate) ToUser() *user.User {
return &user.User{
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
ProfileData: &user.UserProfileData{
Phone: u.Phone,
Country: u.Country,
Region: u.Region,
City: u.City,
PostalCode: u.PostalCode,
AddressLine1: u.AddressLine1,
AddressLine2: u.AddressLine2,
HasShippingAddress: u.HasShippingAddress,
ShippingName: u.ShippingName,
ShippingPhone: u.ShippingPhone,
ShippingCountry: u.ShippingCountry,
ShippingRegion: u.ShippingRegion,
ShippingCity: u.ShippingCity,
ShippingPostalCode: u.ShippingPostalCode,
ShippingAddressLine1: u.ShippingAddressLine1,
ShippingAddressLine2: u.ShippingAddressLine2,
Timezone: u.ProfileTimezone,
AgreeTermsOfService: u.AgreeTermsOfService,
AgreePromotions: u.AgreePromotions,
AgreeToTrackingAcrossThirdPartyAppsAndServices: u.AgreeToTrackingAcrossThirdPartyAppsAndServices,
},
SecurityData: &user.UserSecurityData{
PasswordHashAlgorithm: u.PasswordHashAlgorithm,
PasswordHash: u.PasswordHash,
WasEmailVerified: u.WasEmailVerified,
Code: u.Code,
CodeType: u.CodeType,
CodeExpiry: u.CodeExpiry,
OTPEnabled: u.OTPEnabled,
OTPVerified: u.OTPVerified,
OTPValidated: u.OTPValidated,
OTPSecret: u.OTPSecret,
OTPAuthURL: u.OTPAuthURL,
OTPBackupCodeHash: u.OTPBackupCodeHash,
OTPBackupCodeHashAlgorithm: u.OTPBackupCodeHashAlgorithm,
},
Metadata: &user.UserMetadata{
CreatedFromIPAddress: u.CreatedFromIPAddress,
CreatedFromIPTimestamp: u.CreatedFromIPTimestamp,
CreatedByUserID: u.CreatedByUserID,
CreatedAt: u.CreatedAt,
CreatedByName: u.CreatedByName,
ModifiedFromIPAddress: u.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: u.ModifiedFromIPTimestamp,
ModifiedByUserID: u.ModifiedByUserID,
ModifiedAt: u.ModifiedAt,
ModifiedByName: u.ModifiedByName,
LastLoginAt: u.LastLoginAt,
},
TenantID: u.TenantID,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// FromUserByDate converts domain entity to table model
func FromUserByDate(tenantID string, u *user.User) *UserByDate {
userByDate := &UserByDate{
TenantID: tenantID,
CreatedDate: u.CreatedAt.Format("2006-01-02"),
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
// Map ProfileData if present
if u.ProfileData != nil {
userByDate.Phone = u.ProfileData.Phone
userByDate.Country = u.ProfileData.Country
userByDate.Region = u.ProfileData.Region
userByDate.City = u.ProfileData.City
userByDate.PostalCode = u.ProfileData.PostalCode
userByDate.AddressLine1 = u.ProfileData.AddressLine1
userByDate.AddressLine2 = u.ProfileData.AddressLine2
userByDate.HasShippingAddress = u.ProfileData.HasShippingAddress
userByDate.ShippingName = u.ProfileData.ShippingName
userByDate.ShippingPhone = u.ProfileData.ShippingPhone
userByDate.ShippingCountry = u.ProfileData.ShippingCountry
userByDate.ShippingRegion = u.ProfileData.ShippingRegion
userByDate.ShippingCity = u.ProfileData.ShippingCity
userByDate.ShippingPostalCode = u.ProfileData.ShippingPostalCode
userByDate.ShippingAddressLine1 = u.ProfileData.ShippingAddressLine1
userByDate.ShippingAddressLine2 = u.ProfileData.ShippingAddressLine2
userByDate.ProfileTimezone = u.ProfileData.Timezone
userByDate.AgreeTermsOfService = u.ProfileData.AgreeTermsOfService
userByDate.AgreePromotions = u.ProfileData.AgreePromotions
userByDate.AgreeToTrackingAcrossThirdPartyAppsAndServices = u.ProfileData.AgreeToTrackingAcrossThirdPartyAppsAndServices
}
// Map SecurityData if present
if u.SecurityData != nil {
userByDate.PasswordHashAlgorithm = u.SecurityData.PasswordHashAlgorithm
userByDate.PasswordHash = u.SecurityData.PasswordHash
userByDate.WasEmailVerified = u.SecurityData.WasEmailVerified
userByDate.Code = u.SecurityData.Code
userByDate.CodeType = u.SecurityData.CodeType
userByDate.CodeExpiry = u.SecurityData.CodeExpiry
userByDate.OTPEnabled = u.SecurityData.OTPEnabled
userByDate.OTPVerified = u.SecurityData.OTPVerified
userByDate.OTPValidated = u.SecurityData.OTPValidated
userByDate.OTPSecret = u.SecurityData.OTPSecret
userByDate.OTPAuthURL = u.SecurityData.OTPAuthURL
userByDate.OTPBackupCodeHash = u.SecurityData.OTPBackupCodeHash
userByDate.OTPBackupCodeHashAlgorithm = u.SecurityData.OTPBackupCodeHashAlgorithm
}
// Map Metadata if present
if u.Metadata != nil {
userByDate.CreatedFromIPAddress = u.Metadata.CreatedFromIPAddress
userByDate.CreatedFromIPTimestamp = u.Metadata.CreatedFromIPTimestamp
userByDate.CreatedByUserID = u.Metadata.CreatedByUserID
userByDate.CreatedByName = u.Metadata.CreatedByName
userByDate.ModifiedFromIPAddress = u.Metadata.ModifiedFromIPAddress
userByDate.ModifiedFromIPTimestamp = u.Metadata.ModifiedFromIPTimestamp
userByDate.ModifiedByUserID = u.Metadata.ModifiedByUserID
userByDate.ModifiedAt = u.Metadata.ModifiedAt
userByDate.ModifiedByName = u.Metadata.ModifiedByName
userByDate.LastLoginAt = u.Metadata.LastLoginAt
}
return userByDate
}

View file

@ -0,0 +1,223 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
)
// UserByEmail represents the users_by_email table
// Query pattern: Get user by email (for login, uniqueness checks)
// Primary key: (tenant_id, email) - composite partition key for multi-tenancy
type UserByEmail struct {
TenantID string `db:"tenant_id"` // Multi-tenant isolation
Email string `db:"email"`
ID string `db:"id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Name string `db:"name"`
LexicalName string `db:"lexical_name"`
Timezone string `db:"timezone"`
Role int `db:"role"`
Status int `db:"status"`
// Profile data fields (flattened)
Phone string `db:"phone"`
Country string `db:"country"`
Region string `db:"region"`
City string `db:"city"`
PostalCode string `db:"postal_code"`
AddressLine1 string `db:"address_line1"`
AddressLine2 string `db:"address_line2"`
HasShippingAddress bool `db:"has_shipping_address"`
ShippingName string `db:"shipping_name"`
ShippingPhone string `db:"shipping_phone"`
ShippingCountry string `db:"shipping_country"`
ShippingRegion string `db:"shipping_region"`
ShippingCity string `db:"shipping_city"`
ShippingPostalCode string `db:"shipping_postal_code"`
ShippingAddressLine1 string `db:"shipping_address_line1"`
ShippingAddressLine2 string `db:"shipping_address_line2"`
ProfileTimezone string `db:"profile_timezone"`
AgreeTermsOfService bool `db:"agree_terms_of_service"`
AgreePromotions bool `db:"agree_promotions"`
AgreeToTrackingAcrossThirdPartyAppsAndServices bool `db:"agree_to_tracking_across_third_party_apps_and_services"`
// Security data fields (flattened)
PasswordHashAlgorithm string `db:"password_hash_algorithm"`
PasswordHash string `db:"password_hash"`
WasEmailVerified bool `db:"was_email_verified"`
Code string `db:"code"`
CodeType string `db:"code_type"`
CodeExpiry time.Time `db:"code_expiry"`
OTPEnabled bool `db:"otp_enabled"`
OTPVerified bool `db:"otp_verified"`
OTPValidated bool `db:"otp_validated"`
OTPSecret string `db:"otp_secret"`
OTPAuthURL string `db:"otp_auth_url"`
OTPBackupCodeHash string `db:"otp_backup_code_hash"`
OTPBackupCodeHashAlgorithm string `db:"otp_backup_code_hash_algorithm"`
// Metadata fields (flattened)
// CWE-359: Encrypted IP addresses for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"` // Encrypted with go-ipcrypt
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"` // For 90-day expiration tracking
CreatedByUserID string `db:"created_by_user_id"`
CreatedByName string `db:"created_by_name"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"` // Encrypted with go-ipcrypt
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"` // For 90-day expiration tracking
ModifiedByUserID string `db:"modified_by_user_id"`
ModifiedAt time.Time `db:"modified_at"`
ModifiedByName string `db:"modified_by_name"`
LastLoginAt time.Time `db:"last_login_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// ToUser converts table model to domain entity
func (u *UserByEmail) ToUser() *user.User {
return &user.User{
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
ProfileData: &user.UserProfileData{
Phone: u.Phone,
Country: u.Country,
Region: u.Region,
City: u.City,
PostalCode: u.PostalCode,
AddressLine1: u.AddressLine1,
AddressLine2: u.AddressLine2,
HasShippingAddress: u.HasShippingAddress,
ShippingName: u.ShippingName,
ShippingPhone: u.ShippingPhone,
ShippingCountry: u.ShippingCountry,
ShippingRegion: u.ShippingRegion,
ShippingCity: u.ShippingCity,
ShippingPostalCode: u.ShippingPostalCode,
ShippingAddressLine1: u.ShippingAddressLine1,
ShippingAddressLine2: u.ShippingAddressLine2,
Timezone: u.ProfileTimezone,
AgreeTermsOfService: u.AgreeTermsOfService,
AgreePromotions: u.AgreePromotions,
AgreeToTrackingAcrossThirdPartyAppsAndServices: u.AgreeToTrackingAcrossThirdPartyAppsAndServices,
},
SecurityData: &user.UserSecurityData{
PasswordHashAlgorithm: u.PasswordHashAlgorithm,
PasswordHash: u.PasswordHash,
WasEmailVerified: u.WasEmailVerified,
Code: u.Code,
CodeType: u.CodeType,
CodeExpiry: u.CodeExpiry,
OTPEnabled: u.OTPEnabled,
OTPVerified: u.OTPVerified,
OTPValidated: u.OTPValidated,
OTPSecret: u.OTPSecret,
OTPAuthURL: u.OTPAuthURL,
OTPBackupCodeHash: u.OTPBackupCodeHash,
OTPBackupCodeHashAlgorithm: u.OTPBackupCodeHashAlgorithm,
},
Metadata: &user.UserMetadata{
CreatedFromIPAddress: u.CreatedFromIPAddress,
CreatedFromIPTimestamp: u.CreatedFromIPTimestamp,
CreatedByUserID: u.CreatedByUserID,
CreatedAt: u.CreatedAt,
CreatedByName: u.CreatedByName,
ModifiedFromIPAddress: u.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: u.ModifiedFromIPTimestamp,
ModifiedByUserID: u.ModifiedByUserID,
ModifiedAt: u.ModifiedAt,
ModifiedByName: u.ModifiedByName,
LastLoginAt: u.LastLoginAt,
},
TenantID: u.TenantID,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// FromUserByEmail converts domain entity to table model
func FromUserByEmail(tenantID string, u *user.User) *UserByEmail {
userByEmail := &UserByEmail{
TenantID: tenantID,
Email: u.Email,
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
// Map ProfileData if present
if u.ProfileData != nil {
userByEmail.Phone = u.ProfileData.Phone
userByEmail.Country = u.ProfileData.Country
userByEmail.Region = u.ProfileData.Region
userByEmail.City = u.ProfileData.City
userByEmail.PostalCode = u.ProfileData.PostalCode
userByEmail.AddressLine1 = u.ProfileData.AddressLine1
userByEmail.AddressLine2 = u.ProfileData.AddressLine2
userByEmail.HasShippingAddress = u.ProfileData.HasShippingAddress
userByEmail.ShippingName = u.ProfileData.ShippingName
userByEmail.ShippingPhone = u.ProfileData.ShippingPhone
userByEmail.ShippingCountry = u.ProfileData.ShippingCountry
userByEmail.ShippingRegion = u.ProfileData.ShippingRegion
userByEmail.ShippingCity = u.ProfileData.ShippingCity
userByEmail.ShippingPostalCode = u.ProfileData.ShippingPostalCode
userByEmail.ShippingAddressLine1 = u.ProfileData.ShippingAddressLine1
userByEmail.ShippingAddressLine2 = u.ProfileData.ShippingAddressLine2
userByEmail.ProfileTimezone = u.ProfileData.Timezone
userByEmail.AgreeTermsOfService = u.ProfileData.AgreeTermsOfService
userByEmail.AgreePromotions = u.ProfileData.AgreePromotions
userByEmail.AgreeToTrackingAcrossThirdPartyAppsAndServices = u.ProfileData.AgreeToTrackingAcrossThirdPartyAppsAndServices
}
// Map SecurityData if present
if u.SecurityData != nil {
userByEmail.PasswordHashAlgorithm = u.SecurityData.PasswordHashAlgorithm
userByEmail.PasswordHash = u.SecurityData.PasswordHash
userByEmail.WasEmailVerified = u.SecurityData.WasEmailVerified
userByEmail.Code = u.SecurityData.Code
userByEmail.CodeType = u.SecurityData.CodeType
userByEmail.CodeExpiry = u.SecurityData.CodeExpiry
userByEmail.OTPEnabled = u.SecurityData.OTPEnabled
userByEmail.OTPVerified = u.SecurityData.OTPVerified
userByEmail.OTPValidated = u.SecurityData.OTPValidated
userByEmail.OTPSecret = u.SecurityData.OTPSecret
userByEmail.OTPAuthURL = u.SecurityData.OTPAuthURL
userByEmail.OTPBackupCodeHash = u.SecurityData.OTPBackupCodeHash
userByEmail.OTPBackupCodeHashAlgorithm = u.SecurityData.OTPBackupCodeHashAlgorithm
}
// Map Metadata if present
if u.Metadata != nil {
userByEmail.CreatedFromIPAddress = u.Metadata.CreatedFromIPAddress
userByEmail.CreatedFromIPTimestamp = u.Metadata.CreatedFromIPTimestamp
userByEmail.CreatedByUserID = u.Metadata.CreatedByUserID
userByEmail.CreatedByName = u.Metadata.CreatedByName
userByEmail.ModifiedFromIPAddress = u.Metadata.ModifiedFromIPAddress
userByEmail.ModifiedFromIPTimestamp = u.Metadata.ModifiedFromIPTimestamp
userByEmail.ModifiedByUserID = u.Metadata.ModifiedByUserID
userByEmail.ModifiedAt = u.Metadata.ModifiedAt
userByEmail.ModifiedByName = u.Metadata.ModifiedByName
userByEmail.LastLoginAt = u.Metadata.LastLoginAt
}
return userByEmail
}

View file

@ -0,0 +1,223 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
)
// UserByID represents the users_by_id table
// Query pattern: Get user by ID
// Primary key: (tenant_id, id) - composite partition key for multi-tenancy
type UserByID struct {
TenantID string `db:"tenant_id"` // Multi-tenant isolation
ID string `db:"id"`
Email string `db:"email"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Name string `db:"name"`
LexicalName string `db:"lexical_name"`
Timezone string `db:"timezone"`
Role int `db:"role"`
Status int `db:"status"`
// Profile data fields (flattened)
Phone string `db:"phone"`
Country string `db:"country"`
Region string `db:"region"`
City string `db:"city"`
PostalCode string `db:"postal_code"`
AddressLine1 string `db:"address_line1"`
AddressLine2 string `db:"address_line2"`
HasShippingAddress bool `db:"has_shipping_address"`
ShippingName string `db:"shipping_name"`
ShippingPhone string `db:"shipping_phone"`
ShippingCountry string `db:"shipping_country"`
ShippingRegion string `db:"shipping_region"`
ShippingCity string `db:"shipping_city"`
ShippingPostalCode string `db:"shipping_postal_code"`
ShippingAddressLine1 string `db:"shipping_address_line1"`
ShippingAddressLine2 string `db:"shipping_address_line2"`
ProfileTimezone string `db:"profile_timezone"`
AgreeTermsOfService bool `db:"agree_terms_of_service"`
AgreePromotions bool `db:"agree_promotions"`
AgreeToTrackingAcrossThirdPartyAppsAndServices bool `db:"agree_to_tracking_across_third_party_apps_and_services"`
// Security data fields (flattened)
PasswordHashAlgorithm string `db:"password_hash_algorithm"`
PasswordHash string `db:"password_hash"`
WasEmailVerified bool `db:"was_email_verified"`
Code string `db:"code"`
CodeType string `db:"code_type"`
CodeExpiry time.Time `db:"code_expiry"`
OTPEnabled bool `db:"otp_enabled"`
OTPVerified bool `db:"otp_verified"`
OTPValidated bool `db:"otp_validated"`
OTPSecret string `db:"otp_secret"`
OTPAuthURL string `db:"otp_auth_url"`
OTPBackupCodeHash string `db:"otp_backup_code_hash"`
OTPBackupCodeHashAlgorithm string `db:"otp_backup_code_hash_algorithm"`
// Metadata fields (flattened)
// CWE-359: Encrypted IP addresses for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"` // Encrypted with go-ipcrypt
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"` // For 90-day expiration tracking
CreatedByUserID string `db:"created_by_user_id"`
CreatedByName string `db:"created_by_name"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"` // Encrypted with go-ipcrypt
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"` // For 90-day expiration tracking
ModifiedByUserID string `db:"modified_by_user_id"`
ModifiedAt time.Time `db:"modified_at"`
ModifiedByName string `db:"modified_by_name"`
LastLoginAt time.Time `db:"last_login_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// ToUser converts table model to domain entity
func (u *UserByID) ToUser() *user.User {
return &user.User{
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
ProfileData: &user.UserProfileData{
Phone: u.Phone,
Country: u.Country,
Region: u.Region,
City: u.City,
PostalCode: u.PostalCode,
AddressLine1: u.AddressLine1,
AddressLine2: u.AddressLine2,
HasShippingAddress: u.HasShippingAddress,
ShippingName: u.ShippingName,
ShippingPhone: u.ShippingPhone,
ShippingCountry: u.ShippingCountry,
ShippingRegion: u.ShippingRegion,
ShippingCity: u.ShippingCity,
ShippingPostalCode: u.ShippingPostalCode,
ShippingAddressLine1: u.ShippingAddressLine1,
ShippingAddressLine2: u.ShippingAddressLine2,
Timezone: u.ProfileTimezone,
AgreeTermsOfService: u.AgreeTermsOfService,
AgreePromotions: u.AgreePromotions,
AgreeToTrackingAcrossThirdPartyAppsAndServices: u.AgreeToTrackingAcrossThirdPartyAppsAndServices,
},
SecurityData: &user.UserSecurityData{
PasswordHashAlgorithm: u.PasswordHashAlgorithm,
PasswordHash: u.PasswordHash,
WasEmailVerified: u.WasEmailVerified,
Code: u.Code,
CodeType: u.CodeType,
CodeExpiry: u.CodeExpiry,
OTPEnabled: u.OTPEnabled,
OTPVerified: u.OTPVerified,
OTPValidated: u.OTPValidated,
OTPSecret: u.OTPSecret,
OTPAuthURL: u.OTPAuthURL,
OTPBackupCodeHash: u.OTPBackupCodeHash,
OTPBackupCodeHashAlgorithm: u.OTPBackupCodeHashAlgorithm,
},
Metadata: &user.UserMetadata{
CreatedFromIPAddress: u.CreatedFromIPAddress,
CreatedFromIPTimestamp: u.CreatedFromIPTimestamp,
CreatedByUserID: u.CreatedByUserID,
CreatedAt: u.CreatedAt,
CreatedByName: u.CreatedByName,
ModifiedFromIPAddress: u.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: u.ModifiedFromIPTimestamp,
ModifiedByUserID: u.ModifiedByUserID,
ModifiedAt: u.ModifiedAt,
ModifiedByName: u.ModifiedByName,
LastLoginAt: u.LastLoginAt,
},
TenantID: u.TenantID,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// FromUser converts domain entity to table model
func FromUser(tenantID string, u *user.User) *UserByID {
userByID := &UserByID{
TenantID: tenantID,
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
Name: u.Name,
LexicalName: u.LexicalName,
Timezone: u.Timezone,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
// Map ProfileData if present
if u.ProfileData != nil {
userByID.Phone = u.ProfileData.Phone
userByID.Country = u.ProfileData.Country
userByID.Region = u.ProfileData.Region
userByID.City = u.ProfileData.City
userByID.PostalCode = u.ProfileData.PostalCode
userByID.AddressLine1 = u.ProfileData.AddressLine1
userByID.AddressLine2 = u.ProfileData.AddressLine2
userByID.HasShippingAddress = u.ProfileData.HasShippingAddress
userByID.ShippingName = u.ProfileData.ShippingName
userByID.ShippingPhone = u.ProfileData.ShippingPhone
userByID.ShippingCountry = u.ProfileData.ShippingCountry
userByID.ShippingRegion = u.ProfileData.ShippingRegion
userByID.ShippingCity = u.ProfileData.ShippingCity
userByID.ShippingPostalCode = u.ProfileData.ShippingPostalCode
userByID.ShippingAddressLine1 = u.ProfileData.ShippingAddressLine1
userByID.ShippingAddressLine2 = u.ProfileData.ShippingAddressLine2
userByID.ProfileTimezone = u.ProfileData.Timezone
userByID.AgreeTermsOfService = u.ProfileData.AgreeTermsOfService
userByID.AgreePromotions = u.ProfileData.AgreePromotions
userByID.AgreeToTrackingAcrossThirdPartyAppsAndServices = u.ProfileData.AgreeToTrackingAcrossThirdPartyAppsAndServices
}
// Map SecurityData if present
if u.SecurityData != nil {
userByID.PasswordHashAlgorithm = u.SecurityData.PasswordHashAlgorithm
userByID.PasswordHash = u.SecurityData.PasswordHash
userByID.WasEmailVerified = u.SecurityData.WasEmailVerified
userByID.Code = u.SecurityData.Code
userByID.CodeType = u.SecurityData.CodeType
userByID.CodeExpiry = u.SecurityData.CodeExpiry
userByID.OTPEnabled = u.SecurityData.OTPEnabled
userByID.OTPVerified = u.SecurityData.OTPVerified
userByID.OTPValidated = u.SecurityData.OTPValidated
userByID.OTPSecret = u.SecurityData.OTPSecret
userByID.OTPAuthURL = u.SecurityData.OTPAuthURL
userByID.OTPBackupCodeHash = u.SecurityData.OTPBackupCodeHash
userByID.OTPBackupCodeHashAlgorithm = u.SecurityData.OTPBackupCodeHashAlgorithm
}
// Map Metadata if present
if u.Metadata != nil {
userByID.CreatedFromIPAddress = u.Metadata.CreatedFromIPAddress
userByID.CreatedFromIPTimestamp = u.Metadata.CreatedFromIPTimestamp
userByID.CreatedByUserID = u.Metadata.CreatedByUserID
userByID.CreatedByName = u.Metadata.CreatedByName
userByID.ModifiedFromIPAddress = u.Metadata.ModifiedFromIPAddress
userByID.ModifiedFromIPTimestamp = u.Metadata.ModifiedFromIPTimestamp
userByID.ModifiedByUserID = u.Metadata.ModifiedByUserID
userByID.ModifiedAt = u.Metadata.ModifiedAt
userByID.ModifiedByName = u.Metadata.ModifiedByName
userByID.LastLoginAt = u.Metadata.LastLoginAt
}
return userByID
}

View file

@ -0,0 +1,53 @@
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"
)
// Update updates an existing user in all tables using batched writes
func (r *repository) Update(ctx context.Context, tenantID string, u *domainuser.User) error {
r.logger.Info("updating user",
zap.String("tenant_id", tenantID),
zap.String("id", u.ID))
// Convert domain entity to table models
userByID := models.FromUser(tenantID, u)
userByEmail := models.FromUserByEmail(tenantID, u)
userByDate := models.FromUserByDate(tenantID, u)
// Use batched writes to maintain consistency across all tables
batch := r.session.NewBatch(gocql.LoggedBatch)
// Update users_by_id table
batch.Query(`UPDATE users_by_id
SET name = ?, updated_at = ?
WHERE tenant_id = ? AND id = ?`,
userByID.Name, userByID.UpdatedAt, userByID.TenantID, userByID.ID)
// Update users_by_email table
batch.Query(`UPDATE users_by_email
SET name = ?, updated_at = ?
WHERE tenant_id = ? AND email = ?`,
userByEmail.Name, userByEmail.UpdatedAt, userByEmail.TenantID, userByEmail.Email)
// Update users_by_date table
batch.Query(`UPDATE users_by_date
SET name = ?, updated_at = ?
WHERE tenant_id = ? AND created_date = ? AND id = ?`,
userByDate.Name, userByDate.UpdatedAt, userByDate.TenantID, userByDate.CreatedDate, userByDate.ID)
// Execute batch atomically
if err := r.session.ExecuteBatch(batch); err != nil {
r.logger.Error("failed to update user", zap.Error(err))
return err
}
r.logger.Info("user updated successfully", zap.String("user_id", u.ID))
return nil
}