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,56 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/tenant/models"
)
// Create creates a new tenant
// Uses batched writes to maintain consistency across denormalized tables
func (r *repository) Create(ctx context.Context, t *domaintenant.Tenant) error {
// Convert to table models
tenantByID := models.FromTenant(t)
tenantBySlug := models.FromTenantBySlug(t)
tenantByStatus := models.FromTenantByStatus(t)
// Create batch for atomic write
batch := r.session.NewBatch(gocql.LoggedBatch)
// Insert into tenants_by_id table
batch.Query(`INSERT INTO tenants_by_id (id, name, slug, status, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tenantByID.ID, tenantByID.Name, tenantByID.Slug, tenantByID.Status,
tenantByID.CreatedAt, tenantByID.UpdatedAt,
tenantByID.CreatedFromIPAddress, tenantByID.CreatedFromIPTimestamp,
tenantByID.ModifiedFromIPAddress, tenantByID.ModifiedFromIPTimestamp)
// Insert into tenants_by_slug table
batch.Query(`INSERT INTO tenants_by_slug (slug, id, name, status, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tenantBySlug.Slug, tenantBySlug.ID, tenantBySlug.Name, tenantBySlug.Status,
tenantBySlug.CreatedAt, tenantBySlug.UpdatedAt,
tenantBySlug.CreatedFromIPAddress, tenantBySlug.CreatedFromIPTimestamp,
tenantBySlug.ModifiedFromIPAddress, tenantBySlug.ModifiedFromIPTimestamp)
// Insert into tenants_by_status table
batch.Query(`INSERT INTO tenants_by_status (status, id, name, slug, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tenantByStatus.Status, tenantByStatus.ID, tenantByStatus.Name, tenantByStatus.Slug,
tenantByStatus.CreatedAt, tenantByStatus.UpdatedAt,
tenantByStatus.CreatedFromIPAddress, tenantByStatus.CreatedFromIPTimestamp,
tenantByStatus.ModifiedFromIPAddress, tenantByStatus.ModifiedFromIPTimestamp)
// Execute batch
if err := r.session.ExecuteBatch(batch); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,43 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
)
// Delete deletes a tenant from all tables
// Uses batched writes to maintain consistency across denormalized tables
// Note: Consider implementing soft delete (status = 'deleted') instead
func (r *repository) Delete(ctx context.Context, id string) error {
// First, get the tenant to retrieve the slug and status
// (needed to delete from tenants_by_slug and tenants_by_status tables)
tenant, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Create batch for atomic delete
batch := r.session.NewBatch(gocql.LoggedBatch)
// Delete from tenants_by_id table
batch.Query(`DELETE FROM tenants_by_id WHERE id = ?`, id)
// Delete from tenants_by_slug table
batch.Query(`DELETE FROM tenants_by_slug WHERE slug = ?`, tenant.Slug)
// Delete from tenants_by_status table
batch.Query(`DELETE FROM tenants_by_status WHERE status = ? AND id = ?`,
string(tenant.Status), id)
// Execute batch
if err := r.session.ExecuteBatch(batch); err != nil {
r.logger.Error("failed to delete tenant",
zap.String("tenant_id", id),
zap.Error(err))
return err
}
return nil
}

View file

@ -0,0 +1,62 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/tenant/models"
)
// GetByID retrieves a tenant by ID
func (r *repository) GetByID(ctx context.Context, id string) (*domaintenant.Tenant, error) {
var tenantByID models.TenantByID
query := `SELECT id, name, slug, status, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp
FROM tenants_by_id
WHERE id = ?`
err := r.session.Query(query, id).
Consistency(gocql.Quorum).
Scan(&tenantByID.ID, &tenantByID.Name, &tenantByID.Slug, &tenantByID.Status,
&tenantByID.CreatedAt, &tenantByID.UpdatedAt,
&tenantByID.CreatedFromIPAddress, &tenantByID.CreatedFromIPTimestamp,
&tenantByID.ModifiedFromIPAddress, &tenantByID.ModifiedFromIPTimestamp)
if err != nil {
if err == gocql.ErrNotFound {
return nil, domaintenant.ErrTenantNotFound
}
return nil, err
}
return tenantByID.ToTenant(), nil
}
// GetBySlug retrieves a tenant by slug
func (r *repository) GetBySlug(ctx context.Context, slug string) (*domaintenant.Tenant, error) {
var tenantBySlug models.TenantBySlug
query := `SELECT slug, id, name, status, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp
FROM tenants_by_slug
WHERE slug = ?`
err := r.session.Query(query, slug).
Consistency(gocql.Quorum).
Scan(&tenantBySlug.Slug, &tenantBySlug.ID, &tenantBySlug.Name, &tenantBySlug.Status,
&tenantBySlug.CreatedAt, &tenantBySlug.UpdatedAt,
&tenantBySlug.CreatedFromIPAddress, &tenantBySlug.CreatedFromIPTimestamp,
&tenantBySlug.ModifiedFromIPAddress, &tenantBySlug.ModifiedFromIPTimestamp)
if err != nil {
if err == gocql.ErrNotFound {
return nil, domaintenant.ErrTenantNotFound
}
return nil, err
}
return tenantBySlug.ToTenant(), nil
}

View file

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

View file

@ -0,0 +1,37 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/tenant/models"
)
// List retrieves all tenants (paginated)
// Note: This is a table scan and should be used sparingly in production
// Consider adding a tenants_by_status table for filtered queries
func (r *repository) List(ctx context.Context, limit int) ([]*domaintenant.Tenant, error) {
query := `SELECT id, name, slug, status, created_at, updated_at
FROM tenants_by_id
LIMIT ?`
iter := r.session.Query(query, limit).
Consistency(gocql.Quorum).
Iter()
var tenants []*domaintenant.Tenant
var tenantByID models.TenantByID
for iter.Scan(&tenantByID.ID, &tenantByID.Name, &tenantByID.Slug, &tenantByID.Status,
&tenantByID.CreatedAt, &tenantByID.UpdatedAt) {
tenants = append(tenants, tenantByID.ToTenant())
}
if err := iter.Close(); err != nil {
return nil, err
}
return tenants, nil
}

View file

@ -0,0 +1,40 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/tenant/models"
)
// ListByStatus retrieves all tenants with the specified status (paginated)
// Uses the tenants_by_status table for efficient filtering
func (r *repository) ListByStatus(ctx context.Context, status domaintenant.Status, limit int) ([]*domaintenant.Tenant, error) {
query := `SELECT status, id, name, slug, created_at, updated_at,
created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp
FROM tenants_by_status
WHERE status = ?
LIMIT ?`
iter := r.session.Query(query, string(status), limit).
Consistency(gocql.Quorum).
Iter()
var tenants []*domaintenant.Tenant
var tenantByStatus models.TenantByStatus
for iter.Scan(&tenantByStatus.Status, &tenantByStatus.ID, &tenantByStatus.Name, &tenantByStatus.Slug,
&tenantByStatus.CreatedAt, &tenantByStatus.UpdatedAt,
&tenantByStatus.CreatedFromIPAddress, &tenantByStatus.CreatedFromIPTimestamp,
&tenantByStatus.ModifiedFromIPAddress, &tenantByStatus.ModifiedFromIPTimestamp) {
tenants = append(tenants, tenantByStatus.ToTenant())
}
if err := iter.Close(); err != nil {
return nil, err
}
return tenants, nil
}

View file

@ -0,0 +1,61 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
)
// TenantByID represents the tenants_by_id table
// Query pattern: Get tenant by ID
// Primary key: id
type TenantByID struct {
ID string `db:"id"`
Name string `db:"name"`
Slug string `db:"slug"`
Status string `db:"status"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
// CWE-359: IP address tracking for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"`
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"`
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"`
}
// ToTenant converts table model to domain entity
func (t *TenantByID) ToTenant() *tenant.Tenant {
return &tenant.Tenant{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Status: tenant.Status(t.Status),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}
// FromTenant converts domain entity to table model
func FromTenant(t *tenant.Tenant) *TenantByID {
return &TenantByID{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Status: string(t.Status),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}

View file

@ -0,0 +1,61 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
)
// TenantBySlug represents the tenants_by_slug table
// Query pattern: Get tenant by slug (URL-friendly identifier)
// Primary key: slug
type TenantBySlug struct {
Slug string `db:"slug"`
ID string `db:"id"`
Name string `db:"name"`
Status string `db:"status"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
// CWE-359: IP address tracking for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"`
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"`
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"`
}
// ToTenant converts table model to domain entity
func (t *TenantBySlug) ToTenant() *tenant.Tenant {
return &tenant.Tenant{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Status: tenant.Status(t.Status),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}
// FromTenantBySlug converts domain entity to table model
func FromTenantBySlug(t *tenant.Tenant) *TenantBySlug {
return &TenantBySlug{
Slug: t.Slug,
ID: t.ID,
Name: t.Name,
Status: string(t.Status),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}

View file

@ -0,0 +1,61 @@
package models
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
)
// TenantByStatus represents the tenants_by_status table
// Query pattern: List tenants by status (e.g., active, inactive, suspended)
// Primary key: (status, id) - status is partition key, id is clustering key
type TenantByStatus struct {
Status string `db:"status"`
ID string `db:"id"`
Name string `db:"name"`
Slug string `db:"slug"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
// CWE-359: IP address tracking for GDPR compliance
CreatedFromIPAddress string `db:"created_from_ip_address"`
CreatedFromIPTimestamp time.Time `db:"created_from_ip_timestamp"`
ModifiedFromIPAddress string `db:"modified_from_ip_address"`
ModifiedFromIPTimestamp time.Time `db:"modified_from_ip_timestamp"`
}
// ToTenant converts table model to domain entity
func (t *TenantByStatus) ToTenant() *tenant.Tenant {
return &tenant.Tenant{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Status: tenant.Status(t.Status),
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}
// FromTenantByStatus converts domain entity to table model
func FromTenantByStatus(t *tenant.Tenant) *TenantByStatus {
return &TenantByStatus{
Status: string(t.Status),
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
// CWE-359: IP address tracking
CreatedFromIPAddress: t.CreatedFromIPAddress,
CreatedFromIPTimestamp: t.CreatedFromIPTimestamp,
ModifiedFromIPAddress: t.ModifiedFromIPAddress,
ModifiedFromIPTimestamp: t.ModifiedFromIPTimestamp,
}
}

View file

@ -0,0 +1,68 @@
package tenant
import (
"context"
"github.com/gocql/gocql"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/repository/tenant/models"
)
// Update updates an existing tenant
// Uses batched writes to maintain consistency across denormalized tables
func (r *repository) Update(ctx context.Context, t *domaintenant.Tenant) error {
// Get the old tenant to check if status changed
oldTenant, err := r.GetByID(ctx, t.ID)
if err != nil {
return err
}
// Convert to table models
tenantByID := models.FromTenant(t)
tenantBySlug := models.FromTenantBySlug(t)
tenantByStatus := models.FromTenantByStatus(t)
// Create batch for atomic write
batch := r.session.NewBatch(gocql.LoggedBatch)
// Update tenants_by_id table
batch.Query(`UPDATE tenants_by_id SET name = ?, slug = ?, status = ?, updated_at = ?
WHERE id = ?`,
tenantByID.Name, tenantByID.Slug, tenantByID.Status, tenantByID.UpdatedAt,
tenantByID.ID)
// Update tenants_by_slug table
// Note: If slug changed, we need to delete old slug entry and insert new one
// For simplicity, we'll update in place (slug changes require delete + create)
batch.Query(`UPDATE tenants_by_slug SET id = ?, name = ?, status = ?, updated_at = ?
WHERE slug = ?`,
tenantBySlug.ID, tenantBySlug.Name, tenantBySlug.Status, tenantBySlug.UpdatedAt,
tenantBySlug.Slug)
// Handle tenants_by_status table
// If status changed, delete from old partition and insert into new one
if oldTenant.Status != t.Status {
// Delete from old status partition
batch.Query(`DELETE FROM tenants_by_status WHERE status = ? AND id = ?`,
string(oldTenant.Status), t.ID)
// Insert into new status partition
batch.Query(`INSERT INTO tenants_by_status (status, id, name, slug, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`,
tenantByStatus.Status, tenantByStatus.ID, tenantByStatus.Name, tenantByStatus.Slug,
tenantByStatus.CreatedAt, tenantByStatus.UpdatedAt)
} else {
// Status didn't change, just update in place
batch.Query(`UPDATE tenants_by_status SET name = ?, slug = ?, updated_at = ?
WHERE status = ? AND id = ?`,
tenantByStatus.Name, tenantByStatus.Slug, tenantByStatus.UpdatedAt,
tenantByStatus.Status, tenantByStatus.ID)
}
// Execute batch
if err := r.session.ExecuteBatch(batch); err != nil {
return err
}
return nil
}

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
}