155 lines
4.7 KiB
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
|
|
}
|