Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
62
cloud/maplepress-backend/internal/repository/tenant/get.go
Normal file
62
cloud/maplepress-backend/internal/repository/tenant/get.go
Normal 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
|
||||
}
|
||||
21
cloud/maplepress-backend/internal/repository/tenant/impl.go
Normal file
21
cloud/maplepress-backend/internal/repository/tenant/impl.go
Normal 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,
|
||||
}
|
||||
}
|
||||
37
cloud/maplepress-backend/internal/repository/tenant/list.go
Normal file
37
cloud/maplepress-backend/internal/repository/tenant/list.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
119
cloud/maplepress-backend/internal/repository/user/create.go
Normal file
119
cloud/maplepress-backend/internal/repository/user/create.go
Normal 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
|
||||
}
|
||||
47
cloud/maplepress-backend/internal/repository/user/delete.go
Normal file
47
cloud/maplepress-backend/internal/repository/user/delete.go
Normal 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
|
||||
}
|
||||
230
cloud/maplepress-backend/internal/repository/user/get.go
Normal file
230
cloud/maplepress-backend/internal/repository/user/get.go
Normal 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
|
||||
}
|
||||
22
cloud/maplepress-backend/internal/repository/user/impl.go
Normal file
22
cloud/maplepress-backend/internal/repository/user/impl.go
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
53
cloud/maplepress-backend/internal/repository/user/update.go
Normal file
53
cloud/maplepress-backend/internal/repository/user/update.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue