package site import ( "context" "crypto/rand" "encoding/base64" "fmt" "github.com/gocql/gocql" "go.uber.org/zap" domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/apikey" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcrypt" ) // CreateSiteUseCase handles site creation business logic type CreateSiteUseCase struct { repo domainsite.Repository apiKeyGen apikey.Generator apiKeyHasher apikey.Hasher ipEncryptor *ipcrypt.IPEncryptor logger *zap.Logger } // ProvideCreateSiteUseCase creates a new CreateSiteUseCase func ProvideCreateSiteUseCase( repo domainsite.Repository, apiKeyGen apikey.Generator, apiKeyHasher apikey.Hasher, ipEncryptor *ipcrypt.IPEncryptor, logger *zap.Logger, ) *CreateSiteUseCase { return &CreateSiteUseCase{ repo: repo, apiKeyGen: apiKeyGen, apiKeyHasher: apiKeyHasher, ipEncryptor: ipEncryptor, logger: logger, } } // CreateSiteInput is the input for creating a site type CreateSiteInput struct { Domain string SiteURL string TestMode bool // true = generate test_sk_ key (skips verification) IPAddress string // Plain IP address (will be encrypted before storage) } // CreateSiteOutput is the output after creating a site type CreateSiteOutput struct { ID string `json:"id"` Domain string `json:"domain"` SiteURL string `json:"site_url"` APIKey string `json:"api_key"` // ONLY shown once! VerificationToken string `json:"verification_token"` Status string `json:"status"` SearchIndexName string `json:"search_index_name"` } // Execute creates a new site func (uc *CreateSiteUseCase) Execute(ctx context.Context, tenantID gocql.UUID, input *CreateSiteInput) (*CreateSiteOutput, error) { uc.logger.Info("executing create site use case", zap.String("tenant_id", tenantID.String()), zap.String("domain", input.Domain)) // Generate API key (test or live based on test_mode) var apiKey string var err error if input.TestMode { apiKey, err = uc.apiKeyGen.GenerateTest() // test_sk_... uc.logger.Info("generating test API key for development") } else { apiKey, err = uc.apiKeyGen.Generate() // live_sk_... } if err != nil { uc.logger.Error("failed to generate API key", zap.Error(err)) return nil, fmt.Errorf("failed to generate API key: %w", err) } // Hash API key apiKeyHash := uc.apiKeyHasher.Hash(apiKey) apiKeyPrefix := apikey.ExtractPrefix(apiKey) apiKeyLastFour := apikey.ExtractLastFour(apiKey) // Generate verification token verificationToken, err := generateVerificationToken() if err != nil { uc.logger.Error("failed to generate verification token", zap.Error(err)) return nil, fmt.Errorf("failed to generate verification token: %w", err) } // Encrypt IP address (CWE-359: GDPR compliance) encryptedIP, err := uc.ipEncryptor.Encrypt(input.IPAddress) if err != nil { uc.logger.Error("failed to encrypt IP address", zap.String("domain", input.Domain), zap.Error(err)) return nil, fmt.Errorf("failed to encrypt IP address: %w", err) } // Create site entity (no plan tier - usage-based billing) site := domainsite.NewSite( tenantID, input.Domain, input.SiteURL, apiKeyHash, apiKeyPrefix, apiKeyLastFour, encryptedIP, ) site.VerificationToken = verificationToken // Check if domain already exists exists, err := uc.repo.DomainExists(ctx, input.Domain) if err != nil { uc.logger.Error("failed to check domain existence", zap.Error(err)) return nil, fmt.Errorf("failed to check domain: %w", err) } if exists { uc.logger.Warn("domain already exists", zap.String("domain", input.Domain)) return nil, domainsite.ErrDomainAlreadyExists } // Create in repository (writes to all 4 Cassandra tables) if err := uc.repo.Create(ctx, site); err != nil { uc.logger.Error("failed to create site", zap.Error(err)) return nil, err } uc.logger.Info("site created successfully", zap.String("site_id", site.ID.String()), zap.String("domain", site.Domain)) return &CreateSiteOutput{ ID: site.ID.String(), Domain: site.Domain, SiteURL: site.SiteURL, APIKey: apiKey, // PLAINTEXT - only shown once! VerificationToken: verificationToken, Status: site.Status, SearchIndexName: site.SearchIndexName, }, nil } // generateVerificationToken generates a cryptographically secure verification token func generateVerificationToken() (string, error) { b := make([]byte, 16) // 16 bytes = 128 bits if _, err := rand.Read(b); err != nil { return "", err } token := base64.RawURLEncoding.EncodeToString(b) return "mvp_" + token, nil // mvp = maplepress verify }