389 lines
14 KiB
Go
389 lines
14 KiB
Go
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
|
|
}
|