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,165 @@
package gateway
import (
"context"
"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"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/jwt"
)
// LoginService handles user login operations
type LoginService interface {
Login(ctx context.Context, input *LoginInput) (*LoginResponse, error)
}
// LoginInput represents the input for user login
type LoginInput struct {
Email string
Password string
}
// LoginResponse represents the response after successful login
type LoginResponse 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"`
// 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"`
LoginAt time.Time `json:"login_at"`
}
type loginService struct {
loginUC *gatewayuc.LoginUseCase
sessionService service.SessionService
jwtProvider jwt.Provider
logger *zap.Logger
}
// NewLoginService creates a new login service
func NewLoginService(
loginUC *gatewayuc.LoginUseCase,
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) LoginService {
return &loginService{
loginUC: loginUC,
sessionService: sessionService,
jwtProvider: jwtProvider,
logger: logger.Named("login-service"),
}
}
// Login handles the complete login flow
func (s *loginService) Login(ctx context.Context, input *LoginInput) (*LoginResponse, error) {
// CWE-532: Use hashed email to prevent PII in logs
s.logger.Info("processing login request",
logger.EmailHash(input.Email))
// Execute login use case (validates credentials)
loginOutput, err := s.loginUC.Execute(ctx, &gatewayuc.LoginInput{
Email: input.Email,
Password: input.Password,
})
if err != nil {
s.logger.Error("login failed", zap.Error(err))
return nil, err
}
// CWE-532: Use hashed email to prevent PII in logs
s.logger.Info("credentials validated successfully",
zap.String("user_id", loginOutput.UserID),
logger.EmailHash(loginOutput.UserEmail),
zap.String("tenant_id", loginOutput.TenantID))
// Parse tenant ID to UUID
tenantUUID, err := uuid.Parse(loginOutput.TenantID)
if err != nil {
s.logger.Error("failed to parse tenant ID", zap.Error(err))
return nil, err
}
// Parse user ID to UUID
userUUID, err := uuid.Parse(loginOutput.UserID)
if err != nil {
s.logger.Error("failed to parse user ID", zap.Error(err))
return nil, err
}
// CWE-384: Invalidate all existing sessions before creating new one (Session Fixation Prevention)
// This ensures that any session IDs an attacker may have obtained are invalidated
s.logger.Info("invalidating existing sessions for security",
zap.String("user_uuid", userUUID.String()))
if err := s.sessionService.InvalidateUserSessions(ctx, userUUID); err != nil {
// Log warning but don't fail login - this is best effort cleanup
s.logger.Warn("failed to invalidate existing sessions (non-fatal)",
zap.String("user_uuid", userUUID.String()),
zap.Error(err))
}
// Create new session in two-tier cache
session, err := s.sessionService.CreateSession(
ctx,
0, // UserID as uint64 - not used in our UUID-based system
userUUID,
loginOutput.UserEmail,
loginOutput.UserName,
loginOutput.UserRole,
tenantUUID,
)
if err != nil {
s.logger.Error("failed to create session", zap.Error(err))
return nil, err
}
s.logger.Info("session created", zap.String("session_id", session.ID))
// 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)
return nil, err
}
s.logger.Info("login completed successfully",
zap.String("user_id", loginOutput.UserID),
zap.String("tenant_id", loginOutput.TenantID),
zap.String("session_id", session.ID))
return &LoginResponse{
UserID: loginOutput.UserID,
UserEmail: loginOutput.UserEmail,
UserName: loginOutput.UserName,
UserRole: loginOutput.UserRole,
TenantID: loginOutput.TenantID,
SessionID: session.ID,
AccessToken: accessToken,
AccessExpiry: accessExpiry,
RefreshToken: refreshToken,
RefreshExpiry: refreshExpiry,
LoginAt: time.Now().UTC(),
}, nil
}

View file

@ -0,0 +1,70 @@
package gateway
import (
"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/security/jwt"
)
// ProvideRegisterService creates a new RegisterService for dependency injection
func ProvideRegisterService(
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 NewRegisterService(
validateInputUC,
checkTenantSlugUC,
checkPasswordBreachUC,
hashPasswordUC,
validateTenantSlugUC,
createTenantEntityUC,
saveTenantToRepoUC,
validateUserEmailUC,
createUserEntityUC,
saveUserToRepoUC,
deleteTenantUC,
deleteUserUC,
distributedMutex,
sessionService,
jwtProvider,
logger,
)
}
// ProvideLoginService creates a new LoginService for dependency injection
func ProvideLoginService(
loginUC *gatewayuc.LoginUseCase,
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) LoginService {
return NewLoginService(loginUC, sessionService, jwtProvider, logger)
}
// ProvideRefreshTokenService creates a new RefreshTokenService for dependency injection
func ProvideRefreshTokenService(
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) RefreshTokenService {
return NewRefreshTokenService(sessionService, jwtProvider, logger)
}

View file

@ -0,0 +1,123 @@
package gateway
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/jwt"
)
// RefreshTokenService handles token refresh operations
type RefreshTokenService interface {
RefreshToken(ctx context.Context, input *RefreshTokenInput) (*RefreshTokenResponse, error)
}
// RefreshTokenInput represents the input for token refresh
type RefreshTokenInput struct {
RefreshToken string
}
// RefreshTokenResponse represents the response after successful token refresh
type RefreshTokenResponse 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"`
// Session and new 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"`
RefreshedAt time.Time `json:"refreshed_at"`
}
type refreshTokenService struct {
sessionService service.SessionService
jwtProvider jwt.Provider
logger *zap.Logger
}
// NewRefreshTokenService creates a new refresh token service
func NewRefreshTokenService(
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) RefreshTokenService {
return &refreshTokenService{
sessionService: sessionService,
jwtProvider: jwtProvider,
logger: logger.Named("refresh-token-service"),
}
}
// RefreshToken validates the refresh token and generates new access/refresh tokens
// CWE-613: Validates session still exists before issuing new tokens
func (s *refreshTokenService) RefreshToken(ctx context.Context, input *RefreshTokenInput) (*RefreshTokenResponse, error) {
s.logger.Info("processing token refresh request")
// Validate the refresh token and extract session ID
sessionID, err := s.jwtProvider.ValidateToken(input.RefreshToken)
if err != nil {
s.logger.Warn("invalid refresh token", zap.Error(err))
return nil, fmt.Errorf("invalid or expired refresh token")
}
s.logger.Debug("refresh token validated", zap.String("session_id", sessionID))
// Retrieve the session to ensure it still exists
// CWE-613: This prevents using a refresh token after logout/session deletion
session, err := s.sessionService.GetSession(ctx, sessionID)
if err != nil {
s.logger.Warn("session not found or expired",
zap.String("session_id", sessionID),
zap.Error(err))
return nil, fmt.Errorf("session not found or expired")
}
s.logger.Info("session retrieved for token refresh",
zap.String("session_id", sessionID),
zap.String("user_id", session.UserUUID.String()),
zap.String("tenant_id", session.TenantID.String()))
// Generate new JWT access and refresh tokens
// Both tokens are regenerated to maintain rotation best practices
accessToken, accessExpiry, refreshToken, refreshExpiry, err := s.jwtProvider.GenerateTokenPair(
session.ID,
AccessTokenDuration,
RefreshTokenDuration,
)
if err != nil {
s.logger.Error("failed to generate new token pair", zap.Error(err))
return nil, fmt.Errorf("failed to generate new tokens")
}
s.logger.Info("token refresh completed successfully",
zap.String("user_id", session.UserUUID.String()),
zap.String("tenant_id", session.TenantID.String()),
zap.String("session_id", session.ID))
return &RefreshTokenResponse{
UserID: session.UserUUID.String(),
UserEmail: session.UserEmail,
UserName: session.UserName,
UserRole: session.UserRole,
TenantID: session.TenantID.String(),
SessionID: session.ID,
AccessToken: accessToken,
AccessExpiry: accessExpiry,
RefreshToken: refreshToken,
RefreshExpiry: refreshExpiry,
RefreshedAt: time.Now().UTC(),
}, nil
}

View file

@ -0,0 +1,389 @@
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 &registerService{
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
}