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,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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
47
cloud/maplefile-backend/internal/repo/user/check.go
Normal file
47
cloud/maplefile-backend/internal/repo/user/check.go
Normal 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
|
||||
}
|
||||
115
cloud/maplefile-backend/internal/repo/user/create.go
Normal file
115
cloud/maplefile-backend/internal/repo/user/create.go
Normal 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
|
||||
}
|
||||
68
cloud/maplefile-backend/internal/repo/user/delete.go
Normal file
68
cloud/maplefile-backend/internal/repo/user/delete.go
Normal 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)
|
||||
}
|
||||
199
cloud/maplefile-backend/internal/repo/user/get.go
Normal file
199
cloud/maplefile-backend/internal/repo/user/get.go
Normal 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
|
||||
}
|
||||
114
cloud/maplefile-backend/internal/repo/user/helpers.go
Normal file
114
cloud/maplefile-backend/internal/repo/user/helpers.go
Normal 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
|
||||
}
|
||||
29
cloud/maplefile-backend/internal/repo/user/impl.go
Normal file
29
cloud/maplefile-backend/internal/repo/user/impl.go
Normal 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
|
||||
}
|
||||
14
cloud/maplefile-backend/internal/repo/user/provider.go
Normal file
14
cloud/maplefile-backend/internal/repo/user/provider.go
Normal 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)
|
||||
}
|
||||
145
cloud/maplefile-backend/internal/repo/user/update.go
Normal file
145
cloud/maplefile-backend/internal/repo/user/update.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue