monorepo/cloud/maplepress-backend/internal/usecase/site/create.go

155 lines
4.7 KiB
Go

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
}