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,75 @@
package site
import (
"context"
"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"
)
// AuthenticateAPIKeyUseCase handles API key authentication
type AuthenticateAPIKeyUseCase struct {
repo domainsite.Repository
apiKeyHasher apikey.Hasher
logger *zap.Logger
}
// ProvideAuthenticateAPIKeyUseCase creates a new AuthenticateAPIKeyUseCase
func ProvideAuthenticateAPIKeyUseCase(
repo domainsite.Repository,
apiKeyHasher apikey.Hasher,
logger *zap.Logger,
) *AuthenticateAPIKeyUseCase {
return &AuthenticateAPIKeyUseCase{
repo: repo,
apiKeyHasher: apiKeyHasher,
logger: logger,
}
}
// AuthenticateAPIKeyInput is the input for authenticating an API key
type AuthenticateAPIKeyInput struct {
APIKey string
}
// AuthenticateAPIKeyOutput is the output after authenticating an API key
type AuthenticateAPIKeyOutput struct {
Site *domainsite.Site
}
// Execute authenticates an API key and returns the associated site
func (uc *AuthenticateAPIKeyUseCase) Execute(ctx context.Context, input *AuthenticateAPIKeyInput) (*AuthenticateAPIKeyOutput, error) {
// Hash the API key
apiKeyHash := uc.apiKeyHasher.Hash(input.APIKey)
// Lookup site by API key hash (from sites_by_apikey table)
site, err := uc.repo.GetByAPIKeyHash(ctx, apiKeyHash)
if err != nil {
uc.logger.Debug("API key authentication failed", zap.Error(err))
return nil, domainsite.ErrInvalidAPIKey
}
// Verify API key using constant-time comparison
if !uc.apiKeyHasher.Verify(input.APIKey, site.APIKeyHash) {
uc.logger.Warn("API key hash mismatch",
zap.String("site_id", site.ID.String()))
return nil, domainsite.ErrInvalidAPIKey
}
// Check if site can access API (allows pending sites for initial setup)
if !site.CanAccessAPI() {
uc.logger.Warn("site cannot access API",
zap.String("site_id", site.ID.String()),
zap.String("status", site.Status),
zap.Bool("verified", site.IsVerified))
return nil, domainsite.ErrSiteNotActive
}
uc.logger.Debug("API key authenticated successfully",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain))
return &AuthenticateAPIKeyOutput{Site: site}, nil
}

View file

@ -0,0 +1,155 @@
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
}

View file

@ -0,0 +1,67 @@
package site
import (
"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/ipcrypt"
)
// CreateSiteEntityUseCase creates a site domain entity
type CreateSiteEntityUseCase struct {
ipEncryptor *ipcrypt.IPEncryptor
logger *zap.Logger
}
// ProvideCreateSiteEntityUseCase creates a new CreateSiteEntityUseCase
func ProvideCreateSiteEntityUseCase(
ipEncryptor *ipcrypt.IPEncryptor,
logger *zap.Logger,
) *CreateSiteEntityUseCase {
return &CreateSiteEntityUseCase{
ipEncryptor: ipEncryptor,
logger: logger,
}
}
// CreateSiteEntityInput contains the data needed to create a site entity
type CreateSiteEntityInput struct {
TenantID gocql.UUID
Domain string
SiteURL string
APIKeyHash string
APIKeyPrefix string
APIKeyLastFour string
VerificationToken string
IPAddress string // Plain IP address (will be encrypted before storage)
}
// Execute creates a new site domain entity
func (uc *CreateSiteEntityUseCase) Execute(input *CreateSiteEntityInput) (*domainsite.Site, error) {
// 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, err
}
site := domainsite.NewSite(
input.TenantID,
input.Domain,
input.SiteURL,
input.APIKeyHash,
input.APIKeyPrefix,
input.APIKeyLastFour,
encryptedIP,
)
site.VerificationToken = input.VerificationToken
uc.logger.Info("site entity created",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain))
return site, nil
}

View file

@ -0,0 +1,60 @@
package site
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// DeleteSiteUseCase handles site deletion
// DEPRECATED: This usecase is too simple but doesn't follow the refactored pattern.
// Use the service layer (service/site/delete.go) which orchestrates
// focused usecases: ValidateSiteForDeletionUseCase, DeleteSiteFromRepoUseCase.
// This will be removed after migration is complete.
type DeleteSiteUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideDeleteSiteUseCase creates a new DeleteSiteUseCase
func ProvideDeleteSiteUseCase(repo domainsite.Repository, logger *zap.Logger) *DeleteSiteUseCase {
return &DeleteSiteUseCase{
repo: repo,
logger: logger,
}
}
// DeleteSiteInput is the input for deleting a site
type DeleteSiteInput struct {
SiteID string
}
// DeleteSiteOutput is the output after deleting a site
type DeleteSiteOutput struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// Execute deletes a site
func (uc *DeleteSiteUseCase) Execute(ctx context.Context, tenantID gocql.UUID, input *DeleteSiteInput) (*DeleteSiteOutput, error) {
siteID, err := gocql.ParseUUID(input.SiteID)
if err != nil {
return nil, err
}
// Delete from repository (removes from all 4 tables)
if err := uc.repo.Delete(ctx, tenantID, siteID); err != nil {
uc.logger.Error("failed to delete site", zap.Error(err))
return nil, err
}
uc.logger.Info("site deleted successfully", zap.String("site_id", siteID.String()))
return &DeleteSiteOutput{
Success: true,
Message: "Site deleted successfully",
}, nil
}

View file

@ -0,0 +1,44 @@
package site
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// DeleteSiteFromRepoUseCase deletes a site from the repository
type DeleteSiteFromRepoUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideDeleteSiteFromRepoUseCase creates a new DeleteSiteFromRepoUseCase
func ProvideDeleteSiteFromRepoUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *DeleteSiteFromRepoUseCase {
return &DeleteSiteFromRepoUseCase{
repo: repo,
logger: logger,
}
}
// Execute deletes a site from all repository tables
func (uc *DeleteSiteFromRepoUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID) error {
// Delete from repository (removes from all 4 Cassandra tables)
if err := uc.repo.Delete(ctx, tenantID, siteID); err != nil {
uc.logger.Error("failed to delete site from repository",
zap.String("site_id", siteID.String()),
zap.Error(err))
return fmt.Errorf("failed to delete site: %w", err)
}
uc.logger.Info("site deleted from repository",
zap.String("site_id", siteID.String()))
return nil
}

View file

@ -0,0 +1,70 @@
package site
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/apikey"
)
// GenerateAPIKeyUseCase generates and hashes an API key
type GenerateAPIKeyUseCase struct {
apiKeyGen apikey.Generator
apiKeyHasher apikey.Hasher
logger *zap.Logger
}
// ProvideGenerateAPIKeyUseCase creates a new GenerateAPIKeyUseCase
func ProvideGenerateAPIKeyUseCase(
apiKeyGen apikey.Generator,
apiKeyHasher apikey.Hasher,
logger *zap.Logger,
) *GenerateAPIKeyUseCase {
return &GenerateAPIKeyUseCase{
apiKeyGen: apiKeyGen,
apiKeyHasher: apiKeyHasher,
logger: logger,
}
}
// APIKeyResult contains the generated API key details
type APIKeyResult struct {
PlaintextKey string
HashedKey string
Prefix string
LastFour string
}
// Execute generates an API key (test or live) and returns its details
func (uc *GenerateAPIKeyUseCase) Execute(testMode bool) (*APIKeyResult, error) {
// Generate API key (test or live based on test_mode)
var apiKey string
var err error
if 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)
uc.logger.Info("API key generated",
zap.String("prefix", apiKeyPrefix),
zap.String("last_four", apiKeyLastFour))
return &APIKeyResult{
PlaintextKey: apiKey,
HashedKey: apiKeyHash,
Prefix: apiKeyPrefix,
LastFour: apiKeyLastFour,
}, nil
}

View file

@ -0,0 +1,37 @@
package site
import (
"crypto/rand"
"encoding/base64"
"go.uber.org/zap"
)
// GenerateVerificationTokenUseCase generates a verification token for domain verification
type GenerateVerificationTokenUseCase struct {
logger *zap.Logger
}
// ProvideGenerateVerificationTokenUseCase creates a new GenerateVerificationTokenUseCase
func ProvideGenerateVerificationTokenUseCase(
logger *zap.Logger,
) *GenerateVerificationTokenUseCase {
return &GenerateVerificationTokenUseCase{
logger: logger,
}
}
// Execute generates a cryptographically secure verification token
func (uc *GenerateVerificationTokenUseCase) Execute() (string, error) {
b := make([]byte, 16) // 16 bytes = 128 bits
if _, err := rand.Read(b); err != nil {
uc.logger.Error("failed to generate random bytes", zap.Error(err))
return "", err
}
token := base64.RawURLEncoding.EncodeToString(b)
verificationToken := "mvp_" + token // mvp = maplepress verify
uc.logger.Info("verification token generated")
return verificationToken, nil
}

View file

@ -0,0 +1,50 @@
package site
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// GetSiteUseCase handles getting a site by ID
type GetSiteUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideGetSiteUseCase creates a new GetSiteUseCase
func ProvideGetSiteUseCase(repo domainsite.Repository, logger *zap.Logger) *GetSiteUseCase {
return &GetSiteUseCase{
repo: repo,
logger: logger,
}
}
// GetSiteInput is the input for getting a site
type GetSiteInput struct {
ID string
}
// GetSiteOutput is the output after getting a site
type GetSiteOutput struct {
Site *domainsite.Site
}
// Execute gets a site by ID
func (uc *GetSiteUseCase) Execute(ctx context.Context, tenantID gocql.UUID, input *GetSiteInput) (*GetSiteOutput, error) {
siteID, err := gocql.ParseUUID(input.ID)
if err != nil {
return nil, err
}
site, err := uc.repo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, err
}
return &GetSiteOutput{Site: site}, nil
}

View file

@ -0,0 +1,55 @@
package site
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ListSitesUseCase handles listing sites for a tenant
type ListSitesUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideListSitesUseCase creates a new ListSitesUseCase
func ProvideListSitesUseCase(repo domainsite.Repository, logger *zap.Logger) *ListSitesUseCase {
return &ListSitesUseCase{
repo: repo,
logger: logger,
}
}
// ListSitesInput is the input for listing sites
type ListSitesInput struct {
PageSize int
PageState []byte
}
// ListSitesOutput is the output after listing sites
type ListSitesOutput struct {
Sites []*domainsite.Site
PageState []byte
}
// Execute lists all sites for a tenant
func (uc *ListSitesUseCase) Execute(ctx context.Context, tenantID gocql.UUID, input *ListSitesInput) (*ListSitesOutput, error) {
pageSize := input.PageSize
if pageSize == 0 {
pageSize = 20 // Default page size
}
sites, nextPageState, err := uc.repo.ListByTenant(ctx, tenantID, pageSize, input.PageState)
if err != nil {
uc.logger.Error("failed to list sites", zap.Error(err))
return nil, err
}
return &ListSitesOutput{
Sites: sites,
PageState: nextPageState,
}, nil
}

View file

@ -0,0 +1,127 @@
package site
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ResetMonthlyUsageUseCase handles resetting monthly usage counters for all sites (for billing cycles)
type ResetMonthlyUsageUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideResetMonthlyUsageUseCase creates a new ResetMonthlyUsageUseCase
func ProvideResetMonthlyUsageUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *ResetMonthlyUsageUseCase {
return &ResetMonthlyUsageUseCase{
siteRepo: siteRepo,
logger: logger.Named("reset-monthly-usage-usecase"),
}
}
// ResetUsageOutput is the output after resetting usage counters
type ResetUsageOutput struct {
ProcessedSites int `json:"processed_sites"`
ResetCount int `json:"reset_count"`
FailedCount int `json:"failed_count"`
ProcessedAt time.Time `json:"processed_at"`
}
// Execute resets monthly usage counters for all sites (for billing cycles)
func (uc *ResetMonthlyUsageUseCase) Execute(ctx context.Context) (*ResetUsageOutput, error) {
uc.logger.Info("starting monthly usage counter reset for all sites")
startTime := time.Now()
processedSites := 0
resetCount := 0
failedCount := 0
// Pagination settings
const pageSize = 100
var pageState []byte
// Iterate through all sites using pagination
for {
// Get a batch of sites
sites, nextPageState, err := uc.siteRepo.GetAllSitesForUsageReset(ctx, pageSize, pageState)
if err != nil {
uc.logger.Error("failed to get sites for usage reset", zap.Error(err))
return nil, fmt.Errorf("failed to get sites: %w", err)
}
// Process each site in the batch
for _, site := range sites {
processedSites++
// Check if usage needs to be reset (monthly billing cycle)
now := time.Now()
needsReset := false
// Check if it's been a month since last reset
if site.LastResetAt.AddDate(0, 1, 0).Before(now) {
needsReset = true
}
if !needsReset {
uc.logger.Debug("site usage not due for reset",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain),
zap.Time("last_reset_at", site.LastResetAt))
continue
}
// Reset the usage counters
site.ResetMonthlyUsage()
// Update the site in database
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
uc.logger.Error("failed to reset usage for site",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain),
zap.Error(err))
failedCount++
continue
}
resetCount++
uc.logger.Debug("reset usage for site",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain),
zap.Time("last_reset_at", site.LastResetAt))
}
// Check if there are more pages
if len(nextPageState) == 0 {
break
}
pageState = nextPageState
uc.logger.Info("processed batch of sites",
zap.Int("batch_size", len(sites)),
zap.Int("total_processed", processedSites),
zap.Int("reset_count", resetCount),
zap.Int("failed_count", failedCount))
}
uc.logger.Info("monthly usage counter reset completed",
zap.Int("processed_sites", processedSites),
zap.Int("reset_count", resetCount),
zap.Int("failed_count", failedCount),
zap.Duration("duration", time.Since(startTime)))
return &ResetUsageOutput{
ProcessedSites: processedSites,
ResetCount: resetCount,
FailedCount: failedCount,
ProcessedAt: time.Now(),
}, nil
}

View file

@ -0,0 +1,106 @@
package site
import (
"context"
"fmt"
"time"
"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"
)
// RotateAPIKeyUseCase handles API key rotation
// DEPRECATED: This usecase is too fat and violates Clean Architecture.
// Use the service layer (service/site/rotate_apikey.go) which orchestrates
// focused usecases: GetSiteUseCase, GenerateAPIKeyUseCase, UpdateSiteAPIKeyUseCase, UpdateSiteToRepoUseCase.
// This will be removed after migration is complete.
type RotateAPIKeyUseCase struct {
repo domainsite.Repository
apiKeyGen apikey.Generator
apiKeyHasher apikey.Hasher
logger *zap.Logger
}
// ProvideRotateAPIKeyUseCase creates a new RotateAPIKeyUseCase
func ProvideRotateAPIKeyUseCase(
repo domainsite.Repository,
apiKeyGen apikey.Generator,
apiKeyHasher apikey.Hasher,
logger *zap.Logger,
) *RotateAPIKeyUseCase {
return &RotateAPIKeyUseCase{
repo: repo,
apiKeyGen: apiKeyGen,
apiKeyHasher: apiKeyHasher,
logger: logger,
}
}
// RotateAPIKeyInput is the input for rotating an API key
type RotateAPIKeyInput struct {
SiteID string
}
// RotateAPIKeyOutput is the output after rotating an API key
type RotateAPIKeyOutput struct {
NewAPIKey string `json:"new_api_key"`
OldKeyLastFour string `json:"old_key_last_four"`
RotatedAt time.Time `json:"rotated_at"`
}
// Execute rotates a site's API key
func (uc *RotateAPIKeyUseCase) Execute(ctx context.Context, tenantID gocql.UUID, input *RotateAPIKeyInput) (*RotateAPIKeyOutput, error) {
siteID, err := gocql.ParseUUID(input.SiteID)
if err != nil {
return nil, err
}
// Get current site
site, err := uc.repo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, err
}
// Store old key info
oldKeyLastFour := site.APIKeyLastFour
// Generate new API key
newAPIKey, err := uc.apiKeyGen.Generate()
if err != nil {
uc.logger.Error("failed to generate new API key", zap.Error(err))
return nil, fmt.Errorf("failed to generate API key: %w", err)
}
// Hash new key
newKeyHash := uc.apiKeyHasher.Hash(newAPIKey)
newKeyPrefix := apikey.ExtractPrefix(newAPIKey)
newKeyLastFour := apikey.ExtractLastFour(newAPIKey)
// Update site with new key
site.APIKeyHash = newKeyHash
site.APIKeyPrefix = newKeyPrefix
site.APIKeyLastFour = newKeyLastFour
site.UpdatedAt = time.Now()
// Update in repository (all 4 tables)
if err := uc.repo.Update(ctx, site); err != nil {
uc.logger.Error("failed to update site with new API key", zap.Error(err))
return nil, err
}
rotatedAt := time.Now()
uc.logger.Info("API key rotated successfully",
zap.String("site_id", siteID.String()),
zap.String("old_key_last_four", oldKeyLastFour))
return &RotateAPIKeyOutput{
NewAPIKey: newAPIKey, // PLAINTEXT - only shown once!
OldKeyLastFour: oldKeyLastFour,
RotatedAt: rotatedAt,
}, nil
}

View file

@ -0,0 +1,43 @@
package site
import (
"context"
"fmt"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// SaveSiteToRepoUseCase saves a site to the repository
type SaveSiteToRepoUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideSaveSiteToRepoUseCase creates a new SaveSiteToRepoUseCase
func ProvideSaveSiteToRepoUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *SaveSiteToRepoUseCase {
return &SaveSiteToRepoUseCase{
repo: repo,
logger: logger,
}
}
// Execute saves a site to the repository (writes to all 4 Cassandra tables)
func (uc *SaveSiteToRepoUseCase) Execute(ctx context.Context, site *domainsite.Site) error {
if err := uc.repo.Create(ctx, site); err != nil {
uc.logger.Error("failed to create site in repository",
zap.String("site_id", site.ID.String()),
zap.Error(err))
return fmt.Errorf("failed to create site: %w", err)
}
uc.logger.Info("site saved to repository",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain))
return nil
}

View file

@ -0,0 +1,42 @@
package site
import (
"time"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// UpdateSiteAPIKeyUseCase updates a site entity with new API key details
type UpdateSiteAPIKeyUseCase struct {
logger *zap.Logger
}
// ProvideUpdateSiteAPIKeyUseCase creates a new UpdateSiteAPIKeyUseCase
func ProvideUpdateSiteAPIKeyUseCase(logger *zap.Logger) *UpdateSiteAPIKeyUseCase {
return &UpdateSiteAPIKeyUseCase{
logger: logger,
}
}
// UpdateSiteAPIKeyInput contains the new API key details
type UpdateSiteAPIKeyInput struct {
Site *domainsite.Site
NewAPIKeyHash string
NewKeyPrefix string
NewKeyLastFour string
}
// Execute updates the site entity with new API key details
func (uc *UpdateSiteAPIKeyUseCase) Execute(input *UpdateSiteAPIKeyInput) {
input.Site.APIKeyHash = input.NewAPIKeyHash
input.Site.APIKeyPrefix = input.NewKeyPrefix
input.Site.APIKeyLastFour = input.NewKeyLastFour
input.Site.UpdatedAt = time.Now()
uc.logger.Debug("site entity updated with new API key",
zap.String("site_id", input.Site.ID.String()),
zap.String("new_prefix", input.NewKeyPrefix),
zap.String("new_last_four", input.NewKeyLastFour))
}

View file

@ -0,0 +1,62 @@
package site
import (
"context"
"fmt"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// UpdateSiteAPIKeyToRepoInput defines the input for updating a site's API key in the repository
type UpdateSiteAPIKeyToRepoInput struct {
Site *domainsite.Site
OldAPIKeyHash string
}
// UpdateSiteAPIKeyToRepoUseCase updates a site's API key in the repository (all tables)
// This use case properly handles the sites_by_apikey table by deleting the old entry
// and inserting a new one, since api_key_hash is part of the primary key
type UpdateSiteAPIKeyToRepoUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// NewUpdateSiteAPIKeyToRepoUseCase creates a new UpdateSiteAPIKeyToRepoUseCase
func NewUpdateSiteAPIKeyToRepoUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *UpdateSiteAPIKeyToRepoUseCase {
return &UpdateSiteAPIKeyToRepoUseCase{
repo: repo,
logger: logger.Named("update-site-apikey-to-repo-usecase"),
}
}
// ProvideUpdateSiteAPIKeyToRepoUseCase creates a new UpdateSiteAPIKeyToRepoUseCase for dependency injection
func ProvideUpdateSiteAPIKeyToRepoUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *UpdateSiteAPIKeyToRepoUseCase {
return NewUpdateSiteAPIKeyToRepoUseCase(repo, logger)
}
// Execute updates a site's API key in the repository (all tables)
func (uc *UpdateSiteAPIKeyToRepoUseCase) Execute(ctx context.Context, input *UpdateSiteAPIKeyToRepoInput) error {
if err := uc.repo.UpdateAPIKey(ctx, input.Site, input.OldAPIKeyHash); err != nil {
uc.logger.Error("failed to update site API key in repository",
zap.String("site_id", input.Site.ID.String()),
zap.String("old_key_hash", input.OldAPIKeyHash),
zap.Error(err))
return fmt.Errorf("failed to update site API key: %w", err)
}
uc.logger.Info("site API key updated in repository",
zap.String("site_id", input.Site.ID.String()),
zap.String("domain", input.Site.Domain),
zap.String("new_key_prefix", input.Site.APIKeyPrefix),
zap.String("new_key_last_four", input.Site.APIKeyLastFour))
return nil
}

View file

@ -0,0 +1,43 @@
package site
import (
"context"
"fmt"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// UpdateSiteToRepoUseCase updates a site in the repository
type UpdateSiteToRepoUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideUpdateSiteToRepoUseCase creates a new UpdateSiteToRepoUseCase
func ProvideUpdateSiteToRepoUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *UpdateSiteToRepoUseCase {
return &UpdateSiteToRepoUseCase{
repo: repo,
logger: logger,
}
}
// Execute updates a site in the repository (all tables)
func (uc *UpdateSiteToRepoUseCase) Execute(ctx context.Context, site *domainsite.Site) error {
if err := uc.repo.Update(ctx, site); err != nil {
uc.logger.Error("failed to update site in repository",
zap.String("site_id", site.ID.String()),
zap.Error(err))
return fmt.Errorf("failed to update site: %w", err)
}
uc.logger.Info("site updated in repository",
zap.String("site_id", site.ID.String()),
zap.String("domain", site.Domain))
return nil
}

View file

@ -0,0 +1,46 @@
package site
import (
"context"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ValidateDomainUseCase checks if a domain is available for registration
type ValidateDomainUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateDomainUseCase creates a new ValidateDomainUseCase
func ProvideValidateDomainUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *ValidateDomainUseCase {
return &ValidateDomainUseCase{
repo: repo,
logger: logger,
}
}
// Execute validates if a domain can be used for a new site
func (uc *ValidateDomainUseCase) Execute(ctx context.Context, domain string) error {
// Check if domain already exists
exists, err := uc.repo.DomainExists(ctx, domain)
if err != nil {
uc.logger.Error("failed to check domain existence",
zap.String("domain", domain),
zap.Error(err))
return err
}
if exists {
uc.logger.Warn("domain already exists", zap.String("domain", domain))
return domainsite.ErrDomainAlreadyExists
}
uc.logger.Info("domain is available", zap.String("domain", domain))
return nil
}

View file

@ -0,0 +1,44 @@
package site
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ValidateSiteForDeletionUseCase validates that a site exists and can be deleted
type ValidateSiteForDeletionUseCase struct {
repo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateSiteForDeletionUseCase creates a new ValidateSiteForDeletionUseCase
func ProvideValidateSiteForDeletionUseCase(
repo domainsite.Repository,
logger *zap.Logger,
) *ValidateSiteForDeletionUseCase {
return &ValidateSiteForDeletionUseCase{
repo: repo,
logger: logger,
}
}
// Execute validates that a site exists before deletion
func (uc *ValidateSiteForDeletionUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID) (*domainsite.Site, error) {
site, err := uc.repo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("site not found for deletion",
zap.String("site_id", siteID.String()),
zap.Error(err))
return nil, err
}
uc.logger.Debug("site validated for deletion",
zap.String("site_id", siteID.String()),
zap.String("domain", site.Domain))
return site, nil
}

View file

@ -0,0 +1,132 @@
package site
import (
"context"
"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/dns"
)
// VerifySiteUseCase handles site verification business logic
type VerifySiteUseCase struct {
repo domainsite.Repository
dnsVerifier *dns.Verifier
logger *zap.Logger
}
// ProvideVerifySiteUseCase creates a new VerifySiteUseCase
func ProvideVerifySiteUseCase(
repo domainsite.Repository,
dnsVerifier *dns.Verifier,
logger *zap.Logger,
) *VerifySiteUseCase {
return &VerifySiteUseCase{
repo: repo,
dnsVerifier: dnsVerifier,
logger: logger,
}
}
// VerifySiteInput is the input for verifying a site
// No input fields needed - verification is done via DNS TXT record lookup
type VerifySiteInput struct {
// Empty struct - DNS verification uses the token stored in the site entity
}
// VerifySiteOutput is the output after verifying a site
type VerifySiteOutput struct {
Success bool `json:"success"`
Status string `json:"status"`
Message string `json:"message"`
}
// Execute verifies a site using the verification token
func (uc *VerifySiteUseCase) Execute(
ctx context.Context,
tenantID gocql.UUID,
siteID gocql.UUID,
input *VerifySiteInput,
) (*VerifySiteOutput, error) {
uc.logger.Info("executing verify site use case via DNS",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()))
// Get site from repository
site, err := uc.repo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Check if site is already verified
if site.IsVerified {
uc.logger.Info("site already verified",
zap.String("site_id", siteID.String()))
return &VerifySiteOutput{
Success: true,
Status: site.Status,
Message: "Site is already verified",
}, nil
}
// Test mode sites don't need verification
if site.IsTestMode() {
uc.logger.Info("test mode site, skipping DNS verification",
zap.String("site_id", siteID.String()))
site.Verify()
if err := uc.repo.Update(ctx, site); err != nil {
uc.logger.Error("failed to update site", zap.Error(err))
return nil, fmt.Errorf("failed to update site: %w", err)
}
return &VerifySiteOutput{
Success: true,
Status: site.Status,
Message: "Test mode site verified successfully",
}, nil
}
// Perform DNS TXT record verification
uc.logger.Info("performing DNS verification",
zap.String("site_id", siteID.String()),
zap.String("domain", site.Domain),
zap.String("expected_token", site.VerificationToken))
verified, err := uc.dnsVerifier.VerifyDomainOwnership(ctx, site.Domain, site.VerificationToken)
if err != nil {
uc.logger.Error("DNS verification failed",
zap.String("site_id", siteID.String()),
zap.String("domain", site.Domain),
zap.Error(err))
return nil, fmt.Errorf("DNS verification failed: %w", err)
}
if !verified {
uc.logger.Warn("DNS verification record not found",
zap.String("site_id", siteID.String()),
zap.String("domain", site.Domain))
return nil, fmt.Errorf("DNS TXT record not found. Please add the verification record to your domain's DNS settings")
}
// DNS verification successful - mark site as verified
site.Verify()
// Update in repository
if err := uc.repo.Update(ctx, site); err != nil {
uc.logger.Error("failed to update site", zap.Error(err))
return nil, fmt.Errorf("failed to update site: %w", err)
}
uc.logger.Info("site verified successfully via DNS",
zap.String("site_id", siteID.String()),
zap.String("domain", site.Domain))
return &VerifySiteOutput{
Success: true,
Status: site.Status,
Message: "Domain ownership verified successfully via DNS TXT record",
}, nil
}