package gateway import ( "context" "fmt" "time" "github.com/google/uuid" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service" gatewayuc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/gateway" tenantuc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/tenant" userusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/user" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/distributedmutex" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/jwt" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/transaction" ) const ( // Role constants for the three-tier role system (numeric values) RoleExecutive int = 1 // Can access ANY tenant ANYTIME (root/SaaS owner) RoleManager int = 2 // User who registered and created tenant (can create users) RoleStaff int = 3 // User created by manager (cannot create users/tenants) // Role names for display/API responses RoleExecutiveName = "executive" RoleManagerName = "manager" RoleStaffName = "staff" // AccessTokenDuration is the lifetime of an access token AccessTokenDuration = 15 * time.Minute // RefreshTokenDuration is the lifetime of a refresh token RefreshTokenDuration = 7 * 24 * time.Hour // 7 days ) // RegisterService handles user registration operations type RegisterService interface { Register(ctx context.Context, input *RegisterInput) (*RegisterResponse, error) } // RegisterInput represents the input for user registration // This is an alias to the usecase layer type for backward compatibility type RegisterInput = gatewayuc.RegisterInput // RegisterResponse represents the response after successful registration type RegisterResponse struct { // User details UserID string `json:"user_id"` UserEmail string `json:"user_email"` UserName string `json:"user_name"` UserRole string `json:"user_role"` // Tenant details TenantID string `json:"tenant_id"` TenantName string `json:"tenant_name"` TenantSlug string `json:"tenant_slug"` // Session and tokens SessionID string `json:"session_id"` AccessToken string `json:"access_token"` AccessExpiry time.Time `json:"access_expiry"` RefreshToken string `json:"refresh_token"` RefreshExpiry time.Time `json:"refresh_expiry"` CreatedAt time.Time `json:"created_at"` } type registerService struct { // Focused usecases for validation and creation validateInputUC *gatewayuc.ValidateRegistrationInputUseCase checkTenantSlugUC *gatewayuc.CheckTenantSlugAvailabilityUseCase checkPasswordBreachUC *gatewayuc.CheckPasswordBreachUseCase // CWE-521: Password breach checking hashPasswordUC *gatewayuc.HashPasswordUseCase // Tenant creation - focused usecases following Clean Architecture validateTenantSlugUC *tenantuc.ValidateTenantSlugUniqueUseCase createTenantEntityUC *tenantuc.CreateTenantEntityUseCase saveTenantToRepoUC *tenantuc.SaveTenantToRepoUseCase // User creation - focused usecases following Clean Architecture validateUserEmailUC *userusecase.ValidateUserEmailUniqueUseCase createUserEntityUC *userusecase.CreateUserEntityUseCase saveUserToRepoUC *userusecase.SaveUserToRepoUseCase // Deletion usecases for compensation (SAGA pattern) deleteTenantUC *tenantuc.DeleteTenantUseCase deleteUserUC *userusecase.DeleteUserUseCase // Distributed mutex for preventing race conditions (CWE-664) distributedMutex distributedmutex.Adapter // Session and token management sessionService service.SessionService jwtProvider jwt.Provider logger *zap.Logger } // NewRegisterService creates a new register service func NewRegisterService( validateInputUC *gatewayuc.ValidateRegistrationInputUseCase, checkTenantSlugUC *gatewayuc.CheckTenantSlugAvailabilityUseCase, checkPasswordBreachUC *gatewayuc.CheckPasswordBreachUseCase, hashPasswordUC *gatewayuc.HashPasswordUseCase, validateTenantSlugUC *tenantuc.ValidateTenantSlugUniqueUseCase, createTenantEntityUC *tenantuc.CreateTenantEntityUseCase, saveTenantToRepoUC *tenantuc.SaveTenantToRepoUseCase, validateUserEmailUC *userusecase.ValidateUserEmailUniqueUseCase, createUserEntityUC *userusecase.CreateUserEntityUseCase, saveUserToRepoUC *userusecase.SaveUserToRepoUseCase, deleteTenantUC *tenantuc.DeleteTenantUseCase, deleteUserUC *userusecase.DeleteUserUseCase, distributedMutex distributedmutex.Adapter, sessionService service.SessionService, jwtProvider jwt.Provider, logger *zap.Logger, ) RegisterService { return ®isterService{ validateInputUC: validateInputUC, checkTenantSlugUC: checkTenantSlugUC, checkPasswordBreachUC: checkPasswordBreachUC, hashPasswordUC: hashPasswordUC, validateTenantSlugUC: validateTenantSlugUC, createTenantEntityUC: createTenantEntityUC, saveTenantToRepoUC: saveTenantToRepoUC, validateUserEmailUC: validateUserEmailUC, createUserEntityUC: createUserEntityUC, saveUserToRepoUC: saveUserToRepoUC, deleteTenantUC: deleteTenantUC, deleteUserUC: deleteUserUC, distributedMutex: distributedMutex, sessionService: sessionService, jwtProvider: jwtProvider, logger: logger.Named("register-service"), } } // Register handles the complete registration flow with SAGA pattern // Orchestrates: validation → tenant creation → user creation → session → tokens // Uses SAGA for automatic rollback if any database operation fails func (s *registerService) Register(ctx context.Context, input *RegisterInput) (*RegisterResponse, error) { // CWE-532: Log with redacted sensitive information s.logger.Info("registering new user", logger.EmailHash(input.Email), logger.TenantSlugHash(input.TenantSlug)) // Create SAGA for this registration workflow saga := transaction.NewSaga("user-registration", s.logger) // Step 1: Validate input (no DB writes, no compensation needed) validateInput := &gatewayuc.RegisterInput{ Email: input.Email, Password: input.Password, FirstName: input.FirstName, LastName: input.LastName, TenantName: input.TenantName, TenantSlug: input.TenantSlug, Timezone: input.Timezone, // Consent fields AgreeTermsOfService: input.AgreeTermsOfService, AgreePromotions: input.AgreePromotions, AgreeToTrackingAcrossThirdPartyAppsAndServices: input.AgreeToTrackingAcrossThirdPartyAppsAndServices, // IP address for audit trail CreatedFromIPAddress: input.CreatedFromIPAddress, } if err := s.validateInputUC.Execute(validateInput); err != nil { s.logger.Error("input validation failed", zap.Error(err)) return nil, err } // Step 2: Acquire distributed lock on tenant slug to prevent race conditions (CWE-664, CWE-755) // This prevents multiple concurrent registrations from creating duplicate tenants // with the same slug during the window between slug check and tenant creation lockKey := fmt.Sprintf("registration:tenant-slug:%s", input.TenantSlug) s.logger.Debug("acquiring distributed lock for tenant slug", zap.String("lock_key", lockKey)) // CWE-755: Proper error handling - fail registration if lock cannot be obtained if err := s.distributedMutex.Acquire(ctx, lockKey); err != nil { s.logger.Error("failed to acquire registration lock", zap.Error(err), zap.String("tenant_slug", input.TenantSlug), zap.String("lock_key", lockKey)) return nil, fmt.Errorf("registration temporarily unavailable, please try again later: %w", err) } defer func() { // Always release the lock when we're done, even if registration fails s.logger.Debug("releasing distributed lock for tenant slug", zap.String("lock_key", lockKey)) if err := s.distributedMutex.Release(ctx, lockKey); err != nil { // Log error but don't fail registration if already completed s.logger.Error("failed to release lock after registration", zap.Error(err), zap.String("lock_key", lockKey)) } }() s.logger.Debug("distributed lock acquired successfully", zap.String("lock_key", lockKey)) // Step 3: Check if tenant slug is available (now protected by lock) // Even if another request checked at the same time, only one can proceed if err := s.checkTenantSlugUC.Execute(ctx, input.TenantSlug); err != nil { s.logger.Error("tenant slug check failed", zap.Error(err)) return nil, err } // Step 4: Check if password has been breached (CWE-521: Password Breach Checking) // This prevents users from using passwords found in known data breaches if err := s.checkPasswordBreachUC.Execute(ctx, input.Password); err != nil { s.logger.Error("password breach check failed", zap.Error(err)) return nil, err } // Step 5: Validate and hash password (no DB writes, no compensation needed) passwordHash, err := s.hashPasswordUC.Execute(input.Password) if err != nil { s.logger.Error("password hashing failed", zap.Error(err)) return nil, err } // Step 6: Create tenant (FIRST DB WRITE - compensation required from here on) // Using focused use cases following Clean Architecture pattern // Step 6a: Validate tenant slug uniqueness if err := s.validateTenantSlugUC.Execute(ctx, input.TenantSlug); err != nil { s.logger.Error("tenant slug validation failed", zap.Error(err)) return nil, err } // Step 6b: Create tenant entity with IP address tenant, err := s.createTenantEntityUC.Execute(&tenantuc.CreateTenantInput{ Name: input.TenantName, Slug: input.TenantSlug, CreatedFromIPAddress: input.CreatedFromIPAddress, }) if err != nil { s.logger.Error("tenant entity creation failed", zap.Error(err)) return nil, err } // Step 6c: Save tenant to repository if err := s.saveTenantToRepoUC.Execute(ctx, tenant); err != nil { s.logger.Error("failed to save tenant", zap.Error(err)) return nil, err } s.logger.Info("tenant created successfully", zap.String("tenant_id", tenant.ID), zap.String("tenant_slug", tenant.Slug)) // Register compensation: if user creation fails, delete this tenant saga.AddCompensation(func(ctx context.Context) error { s.logger.Warn("compensating: deleting tenant due to user creation failure", zap.String("tenant_id", tenant.ID)) return s.deleteTenantUC.Execute(ctx, tenant.ID) }) // Step 7: Create user with hashed password (SECOND DB WRITE) // Using focused use cases following Clean Architecture pattern // Step 7a: Validate email uniqueness if err := s.validateUserEmailUC.Execute(ctx, tenant.ID, input.Email); err != nil { s.logger.Error("user email validation failed - executing compensating transactions", zap.String("tenant_id", tenant.ID), zap.Error(err)) saga.Rollback(ctx) return nil, err } // Step 7b: Create user entity user, err := s.createUserEntityUC.Execute(tenant.ID, &userusecase.CreateUserInput{ Email: input.Email, FirstName: input.FirstName, LastName: input.LastName, PasswordHash: passwordHash, PasswordHashAlgorithm: "argon2id", // Set the algorithm used Role: RoleManager, Timezone: input.Timezone, // Consent fields AgreeTermsOfService: input.AgreeTermsOfService, AgreePromotions: input.AgreePromotions, AgreeToTrackingAcrossThirdPartyAppsAndServices: input.AgreeToTrackingAcrossThirdPartyAppsAndServices, // IP address for audit trail CreatedFromIPAddress: input.CreatedFromIPAddress, }) if err != nil { s.logger.Error("user entity creation failed - executing compensating transactions", zap.String("tenant_id", tenant.ID), zap.Error(err)) saga.Rollback(ctx) return nil, err } // Step 7c: Save user to repository if err := s.saveUserToRepoUC.Execute(ctx, tenant.ID, user); err != nil { s.logger.Error("failed to save user - executing compensating transactions", zap.String("tenant_id", tenant.ID), zap.String("user_id", user.ID), zap.Error(err)) saga.Rollback(ctx) return nil, err } s.logger.Info("user created successfully", zap.String("user_id", user.ID), zap.String("tenant_id", tenant.ID)) // Step 8: Parse UUIDs for session creation tenantUUID, err := uuid.Parse(tenant.ID) if err != nil { s.logger.Error("failed to parse tenant ID", zap.Error(err)) // Rollback tenant and user saga.Rollback(ctx) return nil, err } userUUID, err := uuid.Parse(user.ID) if err != nil { s.logger.Error("failed to parse user ID", zap.Error(err)) // Rollback tenant and user saga.Rollback(ctx) return nil, err } // Step 9: Create session in two-tier cache // Note: Session.UserID expects uint64, but we're using UUIDs // We'll use 0 for now and rely on UserUUID session, err := s.sessionService.CreateSession( ctx, 0, // UserID as uint64 - not used in our UUID-based system userUUID, user.Email, user.FullName(), RoleManagerName, // Pass string name for session tenantUUID, ) if err != nil { s.logger.Error("failed to create session", zap.Error(err)) // Rollback tenant and user saga.Rollback(ctx) return nil, err } s.logger.Info("session created", zap.String("session_id", session.ID)) // Step 10: Generate JWT access and refresh tokens accessToken, accessExpiry, refreshToken, refreshExpiry, err := s.jwtProvider.GenerateTokenPair( session.ID, AccessTokenDuration, RefreshTokenDuration, ) if err != nil { s.logger.Error("failed to generate tokens", zap.Error(err)) // Clean up session _ = s.sessionService.DeleteSession(ctx, session.ID) // Rollback tenant and user saga.Rollback(ctx) return nil, err } // Success! Registration completed, distributed lock will be released by defer s.logger.Info("registration completed successfully", zap.String("user_id", user.ID), zap.String("tenant_id", tenant.ID), zap.String("session_id", session.ID)) return &RegisterResponse{ UserID: user.ID, UserEmail: user.Email, UserName: user.FullName(), UserRole: RoleManagerName, // Return string name for API response TenantID: tenant.ID, TenantName: tenant.Name, TenantSlug: tenant.Slug, SessionID: session.ID, AccessToken: accessToken, AccessExpiry: accessExpiry, RefreshToken: refreshToken, RefreshExpiry: refreshExpiry, CreatedAt: user.CreatedAt, }, nil }