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,76 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/anonymize_old_ips.go
package user
import (
"context"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
)
// AnonymizeOldIPs anonymizes IP addresses in user tables older than the cutoff date
func (impl *userStorerImpl) AnonymizeOldIPs(ctx context.Context, cutoffDate time.Time) (int, error) {
totalAnonymized := 0
// Anonymize users_by_id table
count, err := impl.anonymizeUsersById(ctx, cutoffDate)
if err != nil {
impl.logger.Error("Failed to anonymize users_by_id",
zap.Error(err),
zap.Time("cutoff_date", cutoffDate))
return totalAnonymized, err
}
totalAnonymized += count
impl.logger.Info("IP anonymization completed for user tables",
zap.Int("total_anonymized", totalAnonymized),
zap.Time("cutoff_date", cutoffDate))
return totalAnonymized, nil
}
// anonymizeUsersById processes the users_by_id table
func (impl *userStorerImpl) anonymizeUsersById(ctx context.Context, cutoffDate time.Time) (int, error) {
count := 0
// Query all users (efficient primary key scan, no ALLOW FILTERING)
query := `SELECT id, created_at, ip_anonymized_at FROM maplefile.users_by_id`
iter := impl.session.Query(query).WithContext(ctx).Iter()
var id gocql.UUID
var createdAt time.Time
var ipAnonymizedAt *time.Time
for iter.Scan(&id, &createdAt, &ipAnonymizedAt) {
// Filter in application code: older than cutoff AND not yet anonymized
if createdAt.Before(cutoffDate) && ipAnonymizedAt == nil {
// Update the record to anonymize IPs
updateQuery := `
UPDATE maplefile.users_by_id
SET created_from_ip_address = '',
modified_from_ip_address = '',
ip_anonymized_at = ?
WHERE id = ?
`
if err := impl.session.Query(updateQuery, time.Now(), id).WithContext(ctx).Exec(); err != nil {
impl.logger.Error("Failed to anonymize user record",
zap.String("user_id", id.String()),
zap.Error(err))
continue
}
count++
}
}
if err := iter.Close(); err != nil {
impl.logger.Error("Error during users_by_id iteration", zap.Error(err))
return count, err
}
impl.logger.Debug("Anonymized users_by_id table",
zap.Int("count", count),
zap.Time("cutoff_date", cutoffDate))
return count, nil
}

View file

@ -0,0 +1,38 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/anonymize_user_ips.go
package user
import (
"context"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
)
// AnonymizeUserIPs immediately anonymizes all IP addresses for a specific user
// Used for GDPR right-to-be-forgotten implementation
func (impl *userStorerImpl) AnonymizeUserIPs(ctx context.Context, userID gocql.UUID) error {
impl.logger.Info("Anonymizing IPs for specific user (GDPR mode)",
zap.String("user_id", userID.String()))
// Update the user record to anonymize all IP addresses
query := `
UPDATE maplefile.users_by_id
SET created_from_ip_address = '0.0.0.0',
modified_from_ip_address = '0.0.0.0',
ip_anonymized_at = ?
WHERE id = ?
`
if err := impl.session.Query(query, time.Now(), userID).WithContext(ctx).Exec(); err != nil {
impl.logger.Error("Failed to anonymize user IPs",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
impl.logger.Info("✅ Successfully anonymized user IPs",
zap.String("user_id", userID.String()))
return nil
}

View file

@ -0,0 +1,47 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/check.go
package user
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
func (r *userStorerImpl) CheckIfExistsByID(ctx context.Context, id gocql.UUID) (bool, error) {
query := `SELECT id FROM users_by_id WHERE id = ? LIMIT 1`
err := r.session.Query(query, id).WithContext(ctx).Scan(&id)
if err == gocql.ErrNotFound {
return false, nil
}
if err != nil {
r.logger.Error("Failed to check if user exists by id",
zap.String("id", id.String()),
zap.Error(err))
return false, err
}
return true, nil
}
func (r *userStorerImpl) CheckIfExistsByEmail(ctx context.Context, email string) (bool, error) {
var id gocql.UUID
query := `SELECT id FROM users_by_email WHERE email = ? LIMIT 1`
err := r.session.Query(query, email).WithContext(ctx).Scan(&id)
if err == gocql.ErrNotFound {
return false, nil
}
if err != nil {
r.logger.Error("Failed to check if user exists by email",
zap.String("email", validation.MaskEmail(email)),
zap.Error(err))
return false, err
}
return true, nil
}

View file

@ -0,0 +1,115 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/create.go
package user
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
func (impl userStorerImpl) Create(ctx context.Context, user *dom_user.User) error {
// Ensure we have a valid UUID
if user.ID == (gocql.UUID{}) {
user.ID = gocql.TimeUUID()
}
// Set timestamps if not set
now := time.Now()
if user.CreatedAt.IsZero() {
user.CreatedAt = now
}
if user.ModifiedAt.IsZero() {
user.ModifiedAt = now
}
// Serialize complex data to JSON
profileDataJSON, err := impl.serializeProfileData(user.ProfileData)
if err != nil {
return fmt.Errorf("failed to serialize profile data: %w", err)
}
securityDataJSON, err := impl.serializeSecurityData(user.SecurityData)
if err != nil {
return fmt.Errorf("failed to serialize security data: %w", err)
}
metadataJSON, err := impl.serializeMetadata(user.Metadata)
if err != nil {
return fmt.Errorf("failed to serialize metadata: %w", err)
}
// Use a batch for atomic writes across multiple tables
batch := impl.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
// 1. Insert into users_by_id (primary table)
batch.Query(`
INSERT INTO users_by_id (
id, email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
user.ID, user.Email, user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.Timezone, user.CreatedAt, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
)
// 2. Insert into users_by_email
batch.Query(`
INSERT INTO users_by_email (
email, id, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
user.Email, user.ID, user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.Timezone, user.CreatedAt, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
)
// 3. Insert into users_by_verification_code if verification code exists
if user.SecurityData != nil && user.SecurityData.Code != "" {
batch.Query(`
INSERT INTO users_by_verification_code (
verification_code, id, email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
user.SecurityData.Code, user.ID, user.Email, user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.Timezone, user.CreatedAt, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
)
}
// 4. Insert into users_by_status_and_date for listing
// Skip
// 5. If status is active, also insert into active users table
if user.Status == dom_user.UserStatusActive {
// Skip
}
// 6. Add to search index (simplified - you might want to use external search)
if user.Name != "" || user.Email != "" {
// Skip
}
// Execute the batch
if err := impl.session.ExecuteBatch(batch); err != nil {
impl.logger.Error("Failed to create user",
zap.String("user_id", user.ID.String()),
zap.String("email", validation.MaskEmail(user.Email)),
zap.Error(err))
return fmt.Errorf("failed to create user: %w", err)
}
impl.logger.Info("User created successfully",
zap.String("user_id", user.ID.String()),
zap.String("email", validation.MaskEmail(user.Email)))
return nil
}

View file

@ -0,0 +1,68 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/delete.go
package user
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
func (impl userStorerImpl) DeleteByID(ctx context.Context, id gocql.UUID) error {
// First, get the user to know all the data we need to delete
user, err := impl.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to get user for deletion: %w", err)
}
if user == nil {
return nil // User doesn't exist, nothing to delete
}
batch := impl.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
// Delete from all user tables
batch.Query(`DELETE FROM users_by_id WHERE id = ?`, id)
batch.Query(`DELETE FROM users_by_email WHERE email = ?`, user.Email)
// Delete from verification code table if user has verification code
// Note: We delete by scanning since verification_code is the partition key
// This is acceptable for GDPR deletion (rare operation, thorough cleanup)
if user.SecurityData != nil && user.SecurityData.Code != "" {
batch.Query(`DELETE FROM users_by_verification_code WHERE verification_code = ?`, user.SecurityData.Code)
}
// Delete all user sessions
// Note: sessions_by_user_id is partitioned by user_id, so this is efficient
batch.Query(`DELETE FROM sessions_by_user_id WHERE user_id = ?`, id)
// Execute the batch
if err := impl.session.ExecuteBatch(batch); err != nil {
impl.logger.Error("Failed to delete user",
zap.String("user_id", id.String()),
zap.Error(err))
return fmt.Errorf("failed to delete user: %w", err)
}
impl.logger.Info("User deleted successfully",
zap.String("user_id", id.String()),
zap.String("email", validation.MaskEmail(user.Email)))
return nil
}
func (impl userStorerImpl) DeleteByEmail(ctx context.Context, email string) error {
// First get the user by email to get the ID
user, err := impl.GetByEmail(ctx, email)
if err != nil {
return fmt.Errorf("failed to get user by email for deletion: %w", err)
}
if user == nil {
return nil // User doesn't exist
}
// Delete by ID
return impl.DeleteByID(ctx, user.ID)
}

View file

@ -0,0 +1,199 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/get.go
package user
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
func (impl userStorerImpl) GetByID(ctx context.Context, id gocql.UUID) (*dom_user.User, error) {
var (
email, firstName, lastName, name, lexicalName string
role, status int8
timezone string
createdAt, modifiedAt time.Time
profileData, securityData, metadata string
)
query := `
SELECT email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
FROM users_by_id
WHERE id = ?`
err := impl.session.Query(query, id).WithContext(ctx).Scan(
&email, &firstName, &lastName, &name, &lexicalName,
&role, &status, &timezone, &createdAt, &modifiedAt,
&profileData, &securityData, &metadata,
)
if err == gocql.ErrNotFound {
return nil, nil
}
if err != nil {
impl.logger.Error("Failed to get user by ID",
zap.String("user_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to get user by ID: %w", err)
}
// Construct the user object
user := &dom_user.User{
ID: id,
Email: email,
FirstName: firstName,
LastName: lastName,
Name: name,
LexicalName: lexicalName,
Role: role,
Status: status,
Timezone: timezone,
CreatedAt: createdAt,
ModifiedAt: modifiedAt,
}
// Deserialize JSON fields
if err := impl.deserializeUserData(profileData, securityData, metadata, user); err != nil {
impl.logger.Error("Failed to deserialize user data",
zap.String("user_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to deserialize user data: %w", err)
}
return user, nil
}
func (impl userStorerImpl) GetByEmail(ctx context.Context, email string) (*dom_user.User, error) {
var (
id gocql.UUID
emailResult string
firstName, lastName, name, lexicalName string
role, status int8
timezone string
createdAt, modifiedAt time.Time
profileData, securityData, metadata string
)
query := `
SELECT id, email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
FROM users_by_email
WHERE email = ?`
err := impl.session.Query(query, email).WithContext(ctx).Scan(
&id, &emailResult, &firstName, &lastName, &name, &lexicalName, // 🔧 FIXED: Use emailResult variable
&role, &status, &timezone, &createdAt, &modifiedAt,
&profileData, &securityData, &metadata,
)
if err == gocql.ErrNotFound {
return nil, nil
}
if err != nil {
impl.logger.Error("Failed to get user by Email",
zap.String("user_email", validation.MaskEmail(email)),
zap.Error(err))
return nil, fmt.Errorf("failed to get user by email: %w", err)
}
// Construct the user object
user := &dom_user.User{
ID: id,
Email: emailResult,
FirstName: firstName,
LastName: lastName,
Name: name,
LexicalName: lexicalName,
Role: role,
Status: status,
Timezone: timezone,
CreatedAt: createdAt,
ModifiedAt: modifiedAt,
}
// Deserialize JSON fields
if err := impl.deserializeUserData(profileData, securityData, metadata, user); err != nil {
impl.logger.Error("Failed to deserialize user data",
zap.String("user_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to deserialize user data: %w", err)
}
return user, nil
}
func (impl userStorerImpl) GetByVerificationCode(ctx context.Context, verificationCode string) (*dom_user.User, error) {
var (
id gocql.UUID
email string
firstName, lastName, name, lexicalName string
role, status int8
timezone string
createdAt, modifiedAt time.Time
profileData, securityData, metadata string
)
// Query the users_by_verification_code table
query := `
SELECT id, email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
FROM users_by_verification_code
WHERE verification_code = ?`
err := impl.session.Query(query, verificationCode).WithContext(ctx).Scan(
&id, &email, &firstName, &lastName, &name, &lexicalName,
&role, &status, &timezone, &createdAt, &modifiedAt,
&profileData, &securityData, &metadata,
)
if err == gocql.ErrNotFound {
impl.logger.Debug("User not found by verification code",
zap.String("verification_code", verificationCode))
return nil, nil
}
if err != nil {
impl.logger.Error("Failed to get user by verification code",
zap.String("verification_code", verificationCode),
zap.Error(err))
return nil, fmt.Errorf("failed to get user by verification code: %w", err)
}
// Construct the user object
user := &dom_user.User{
ID: id,
Email: email,
FirstName: firstName,
LastName: lastName,
Name: name,
LexicalName: lexicalName,
Role: role,
Status: status,
Timezone: timezone,
CreatedAt: createdAt,
ModifiedAt: modifiedAt,
}
// Deserialize JSON fields
if err := impl.deserializeUserData(profileData, securityData, metadata, user); err != nil {
impl.logger.Error("Failed to deserialize user data",
zap.String("user_id", id.String()),
zap.Error(err))
return nil, fmt.Errorf("failed to deserialize user data: %w", err)
}
impl.logger.Debug("User found by verification code",
zap.String("user_id", id.String()),
zap.String("email", validation.MaskEmail(email)))
return user, nil
}

View file

@ -0,0 +1,114 @@
package user
import (
"encoding/json"
"fmt"
"hash/fnv"
"strings"
dom "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
)
// Serialization helpers
func (r *userStorerImpl) serializeProfileData(data *dom.UserProfileData) (string, error) {
if data == nil {
return "", nil
}
bytes, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(bytes), nil
}
func (r *userStorerImpl) serializeSecurityData(data *dom.UserSecurityData) (string, error) {
if data == nil {
return "", nil
}
bytes, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(bytes), nil
}
func (r *userStorerImpl) serializeMetadata(data *dom.UserMetadata) (string, error) {
if data == nil {
return "", nil
}
bytes, err := json.Marshal(data)
if err != nil {
return "", err
}
return string(bytes), nil
}
// Deserialization helper
func (r *userStorerImpl) deserializeUserData(profileJSON, securityJSON, metadataJSON string, user *dom.User) error {
// Deserialize profile data
if profileJSON != "" {
var profileData dom.UserProfileData
if err := json.Unmarshal([]byte(profileJSON), &profileData); err != nil {
return fmt.Errorf("failed to unmarshal profile data: %w", err)
}
user.ProfileData = &profileData
}
// Deserialize security data
if securityJSON != "" {
var securityData dom.UserSecurityData
if err := json.Unmarshal([]byte(securityJSON), &securityData); err != nil {
return fmt.Errorf("failed to unmarshal security data: %w", err)
}
user.SecurityData = &securityData
}
// Deserialize metadata
if metadataJSON != "" {
var metadata dom.UserMetadata
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
return fmt.Errorf("failed to unmarshal metadata: %w", err)
}
user.Metadata = &metadata
}
return nil
}
// Search helpers
func (r *userStorerImpl) generateSearchTerms(user *dom.User) []string {
terms := make([]string, 0)
// Add lowercase versions of searchable fields
if user.Email != "" {
terms = append(terms, strings.ToLower(user.Email))
// Also add email prefix for partial matching
parts := strings.Split(user.Email, "@")
if len(parts) > 0 {
terms = append(terms, strings.ToLower(parts[0]))
}
}
if user.Name != "" {
terms = append(terms, strings.ToLower(user.Name))
// Add individual words from name
words := strings.Fields(strings.ToLower(user.Name))
terms = append(terms, words...)
}
if user.FirstName != "" {
terms = append(terms, strings.ToLower(user.FirstName))
}
if user.LastName != "" {
terms = append(terms, strings.ToLower(user.LastName))
}
return terms
}
func (r *userStorerImpl) calculateSearchBucket(term string) int {
h := fnv.New32a()
h.Write([]byte(term))
return int(h.Sum32() % 100) // Distribute across 100 buckets
}

View file

@ -0,0 +1,29 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/impl.go
package user
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
)
type userStorerImpl struct {
session *gocql.Session
logger *zap.Logger
}
func NewRepository(session *gocql.Session, logger *zap.Logger) dom_user.Repository {
logger = logger.Named("MapleFileUserRepository")
return &userStorerImpl{
session: session,
logger: logger,
}
}
// ListAll retrieves all users from the database
func (impl userStorerImpl) ListAll(ctx context.Context) ([]*dom_user.User, error) {
return nil, nil
}

View file

@ -0,0 +1,14 @@
package user
import (
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
)
// ProvideRepository provides a user repository for Wire DI
func ProvideRepository(cfg *config.Config, session *gocql.Session, logger *zap.Logger) dom_user.Repository {
return NewRepository(session, logger)
}

View file

@ -0,0 +1,145 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/user/update.go
package user
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
func (impl userStorerImpl) UpdateByID(ctx context.Context, user *dom_user.User) error {
// First, get the existing user to check what changed
existingUser, err := impl.GetByID(ctx, user.ID)
if err != nil {
return fmt.Errorf("failed to get existing user: %w", err)
}
if existingUser == nil {
return fmt.Errorf("user not found: %s", user.ID)
}
// Update modified timestamp
user.ModifiedAt = time.Now()
// Serialize data
profileDataJSON, err := impl.serializeProfileData(user.ProfileData)
if err != nil {
return fmt.Errorf("failed to serialize profile data: %w", err)
}
securityDataJSON, err := impl.serializeSecurityData(user.SecurityData)
if err != nil {
return fmt.Errorf("failed to serialize security data: %w", err)
}
metadataJSON, err := impl.serializeMetadata(user.Metadata)
if err != nil {
return fmt.Errorf("failed to serialize metadata: %w", err)
}
batch := impl.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
// 1. Update main table
batch.Query(`
UPDATE users_by_id
SET email = ?, first_name = ?, last_name = ?, name = ?, lexical_name = ?,
role = ?, status = ?, modified_at = ?,
profile_data = ?, security_data = ?, metadata = ?
WHERE id = ?`,
user.Email, user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
user.ID,
)
// 2. Handle email change
if existingUser.Email != user.Email {
// Delete old email entry
batch.Query(`DELETE FROM users_by_email WHERE email = ?`, existingUser.Email)
// Insert new email entry
batch.Query(`
INSERT INTO users_by_email (
email, id, first_name, last_name, status, created_at
) VALUES (?, ?, ?, ?, ?, ?)`,
user.Email, user.ID, user.FirstName, user.LastName,
user.Status, user.CreatedAt,
)
} else {
// Just update the existing email entry
batch.Query(`
UPDATE users_by_email
SET first_name = ?, last_name = ?, name = ?, lexical_name = ?,
role = ?, status = ?, timezone = ?, modified_at = ?,
profile_data = ?, security_data = ?, metadata = ?
WHERE email = ?`,
user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.Timezone, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
user.Email,
)
}
// 3. Handle status change
if existingUser.Status != user.Status {
// Remove from old status table
// kip
// Add to new status table
// Skip
// Handle active users table
if existingUser.Status == dom_user.UserStatusActive {
// Skip
}
if user.Status == dom_user.UserStatusActive {
// Skip
} else {
// Just update the existing status entry
// Skip
if user.Status == dom_user.UserStatusActive {
// Skip
}
}
}
// 4. Handle verification code changes
// Delete old verification code entry if it exists
if existingUser.SecurityData != nil && existingUser.SecurityData.Code != "" {
batch.Query(`DELETE FROM users_by_verification_code WHERE verification_code = ?`, existingUser.SecurityData.Code)
}
// Insert new verification code entry if it exists
if user.SecurityData != nil && user.SecurityData.Code != "" {
batch.Query(`
INSERT INTO users_by_verification_code (
verification_code, id, email, first_name, last_name, name, lexical_name,
role, status, timezone, created_at, modified_at,
profile_data, security_data, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
user.SecurityData.Code, user.ID, user.Email, user.FirstName, user.LastName, user.Name, user.LexicalName,
user.Role, user.Status, user.Timezone, user.CreatedAt, user.ModifiedAt,
profileDataJSON, securityDataJSON, metadataJSON,
)
}
// Execute the batch
if err := impl.session.ExecuteBatch(batch); err != nil {
impl.logger.Error("Failed to update user",
zap.String("user_id", user.ID.String()),
zap.Error(err))
return fmt.Errorf("failed to update user: %w", err)
}
impl.logger.Info("User updated successfully",
zap.String("user_id", user.ID.String()),
zap.String("email", validation.MaskEmail(user.Email)))
return nil
}