Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
155
cloud/maplepress-backend/internal/usecase/site/create.go
Normal file
155
cloud/maplepress-backend/internal/usecase/site/create.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
60
cloud/maplepress-backend/internal/usecase/site/delete.go
Normal file
60
cloud/maplepress-backend/internal/usecase/site/delete.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
50
cloud/maplepress-backend/internal/usecase/site/get.go
Normal file
50
cloud/maplepress-backend/internal/usecase/site/get.go
Normal 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
|
||||
}
|
||||
55
cloud/maplepress-backend/internal/usecase/site/list.go
Normal file
55
cloud/maplepress-backend/internal/usecase/site/list.go
Normal 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
|
||||
}
|
||||
127
cloud/maplepress-backend/internal/usecase/site/reset_usage.go
Normal file
127
cloud/maplepress-backend/internal/usecase/site/reset_usage.go
Normal 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
|
||||
}
|
||||
106
cloud/maplepress-backend/internal/usecase/site/rotate_apikey.go
Normal file
106
cloud/maplepress-backend/internal/usecase/site/rotate_apikey.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
132
cloud/maplepress-backend/internal/usecase/site/verify.go
Normal file
132
cloud/maplepress-backend/internal/usecase/site/verify.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue