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,57 @@
package page
import (
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcrypt"
)
// CreatePageEntityUseCase creates a domain page entity from input
type CreatePageEntityUseCase struct {
ipEncryptor *ipcrypt.IPEncryptor
logger *zap.Logger
}
// ProvideCreatePageEntityUseCase creates a new CreatePageEntityUseCase
func ProvideCreatePageEntityUseCase(
ipEncryptor *ipcrypt.IPEncryptor,
logger *zap.Logger,
) *CreatePageEntityUseCase {
return &CreatePageEntityUseCase{
ipEncryptor: ipEncryptor,
logger: logger,
}
}
// Execute converts SyncPageInput to a domain Page entity
func (uc *CreatePageEntityUseCase) Execute(
siteID, tenantID gocql.UUID,
input SyncPageInput,
) (*domainpage.Page, 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("page_id", input.PageID),
zap.Error(err))
return nil, err
}
return domainpage.NewPage(
siteID,
tenantID,
input.PageID,
input.Title,
input.Content,
input.Excerpt,
input.URL,
input.Status,
input.PostType,
input.Author,
input.PublishedAt,
input.ModifiedAt,
encryptedIP,
), nil
}

View file

@ -0,0 +1,190 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// DeletePagesUseCase handles page deletion
type DeletePagesUseCase struct {
pageRepo domainpage.Repository
siteRepo domainsite.Repository
searchClient *search.Client
logger *zap.Logger
}
// ProvideDeletePagesUseCase creates a new DeletePagesUseCase
func ProvideDeletePagesUseCase(
pageRepo domainpage.Repository,
siteRepo domainsite.Repository,
searchClient *search.Client,
logger *zap.Logger,
) *DeletePagesUseCase {
return &DeletePagesUseCase{
pageRepo: pageRepo,
siteRepo: siteRepo,
searchClient: searchClient,
logger: logger,
}
}
// DeletePagesInput is the input for deleting pages
type DeletePagesInput struct {
PageIDs []string `json:"page_ids"`
}
// DeletePagesOutput is the output after deleting pages
type DeletePagesOutput struct {
DeletedCount int `json:"deleted_count"`
DeindexedCount int `json:"deindexed_count"`
FailedPages []string `json:"failed_pages,omitempty"`
Message string `json:"message"`
}
// Execute deletes pages from both database and search index
func (uc *DeletePagesUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID, input *DeletePagesInput) (*DeletePagesOutput, error) {
uc.logger.Info("executing delete pages use case",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.Int("page_count", len(input.PageIDs)))
// Get site to validate
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
deletedCount := 0
deindexedCount := 0
var failedPages []string
// Delete pages from database
if len(input.PageIDs) > 1 {
// Use batch delete for multiple pages
if err := uc.pageRepo.DeleteMultiple(ctx, siteID, input.PageIDs); err != nil {
uc.logger.Error("failed to batch delete pages", zap.Error(err))
return nil, fmt.Errorf("failed to delete pages: %w", err)
}
deletedCount = len(input.PageIDs)
} else if len(input.PageIDs) == 1 {
// Single page delete
if err := uc.pageRepo.Delete(ctx, siteID, input.PageIDs[0]); err != nil {
uc.logger.Error("failed to delete page",
zap.String("page_id", input.PageIDs[0]),
zap.Error(err))
failedPages = append(failedPages, input.PageIDs[0])
} else {
deletedCount = 1
}
}
// Delete from search index
if deletedCount > 0 {
if len(input.PageIDs) > 1 {
// Batch delete from Meilisearch
_, err := uc.searchClient.DeleteDocuments(siteID.String(), input.PageIDs)
if err != nil {
uc.logger.Error("failed to delete documents from search index", zap.Error(err))
// Don't fail the whole operation since database delete succeeded
} else {
deindexedCount = len(input.PageIDs)
}
} else if len(input.PageIDs) == 1 && len(failedPages) == 0 {
// Single document delete
_, err := uc.searchClient.DeleteDocument(siteID.String(), input.PageIDs[0])
if err != nil {
uc.logger.Error("failed to delete document from search index",
zap.String("page_id", input.PageIDs[0]),
zap.Error(err))
// Don't fail the whole operation since database delete succeeded
} else {
deindexedCount = 1
}
}
}
uc.logger.Info("pages deleted successfully",
zap.String("site_id", siteID.String()),
zap.Int("deleted", deletedCount),
zap.Int("deindexed", deindexedCount),
zap.Int("failed", len(failedPages)))
message := fmt.Sprintf("Successfully deleted %d pages from database, removed %d from search index", deletedCount, deindexedCount)
if len(failedPages) > 0 {
message += fmt.Sprintf(", failed %d pages", len(failedPages))
}
return &DeletePagesOutput{
DeletedCount: deletedCount,
DeindexedCount: deindexedCount,
FailedPages: failedPages,
Message: message,
}, nil
}
// DeleteAllPagesInput is the input for deleting all pages for a site
type DeleteAllPagesInput struct{}
// ExecuteDeleteAll deletes all pages for a site
func (uc *DeletePagesUseCase) ExecuteDeleteAll(ctx context.Context, tenantID, siteID gocql.UUID) (*DeletePagesOutput, error) {
uc.logger.Info("executing delete all pages use case",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()))
// Get site to validate
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
// Count pages before deletion
count, err := uc.pageRepo.CountBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to count pages", zap.Error(err))
return nil, fmt.Errorf("failed to count pages: %w", err)
}
// Delete all pages from database
if err := uc.pageRepo.DeleteBySiteID(ctx, siteID); err != nil {
uc.logger.Error("failed to delete all pages", zap.Error(err))
return nil, fmt.Errorf("failed to delete pages: %w", err)
}
// Delete all documents from search index
_, err = uc.searchClient.DeleteAllDocuments(siteID.String())
if err != nil {
uc.logger.Error("failed to delete all documents from search index", zap.Error(err))
// Don't fail the whole operation since database delete succeeded
}
uc.logger.Info("all pages deleted successfully",
zap.String("site_id", siteID.String()),
zap.Int64("count", count))
return &DeletePagesOutput{
DeletedCount: int(count),
DeindexedCount: int(count),
Message: fmt.Sprintf("Successfully deleted all %d pages", count),
}, nil
}

View file

@ -0,0 +1,92 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
)
// DeletePagesFromRepoUseCase deletes pages from the database repository
type DeletePagesFromRepoUseCase struct {
pageRepo domainpage.Repository
logger *zap.Logger
}
// ProvideDeletePagesFromRepoUseCase creates a new DeletePagesFromRepoUseCase
func ProvideDeletePagesFromRepoUseCase(
pageRepo domainpage.Repository,
logger *zap.Logger,
) *DeletePagesFromRepoUseCase {
return &DeletePagesFromRepoUseCase{
pageRepo: pageRepo,
logger: logger,
}
}
// DeletePagesResult contains the result of page deletion
type DeletePagesResult struct {
DeletedCount int
FailedPages []string
}
// Execute deletes specific pages from the database
func (uc *DeletePagesFromRepoUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
pageIDs []string,
) (*DeletePagesResult, error) {
result := &DeletePagesResult{
DeletedCount: 0,
FailedPages: []string{},
}
if len(pageIDs) == 0 {
return result, nil
}
// Use batch delete for multiple pages
if len(pageIDs) > 1 {
if err := uc.pageRepo.DeleteMultiple(ctx, siteID, pageIDs); err != nil {
uc.logger.Error("failed to batch delete pages", zap.Error(err))
return nil, fmt.Errorf("failed to delete pages: %w", err)
}
result.DeletedCount = len(pageIDs)
} else {
// Single page delete
if err := uc.pageRepo.Delete(ctx, siteID, pageIDs[0]); err != nil {
uc.logger.Error("failed to delete page",
zap.String("page_id", pageIDs[0]),
zap.Error(err))
result.FailedPages = append(result.FailedPages, pageIDs[0])
} else {
result.DeletedCount = 1
}
}
return result, nil
}
// ExecuteDeleteAll deletes all pages for a site from the database
func (uc *DeletePagesFromRepoUseCase) ExecuteDeleteAll(
ctx context.Context,
siteID gocql.UUID,
) (int64, error) {
// Count pages before deletion
count, err := uc.pageRepo.CountBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to count pages", zap.Error(err))
return 0, fmt.Errorf("failed to count pages: %w", err)
}
// Delete all pages from database
if err := uc.pageRepo.DeleteBySiteID(ctx, siteID); err != nil {
uc.logger.Error("failed to delete all pages", zap.Error(err))
return 0, fmt.Errorf("failed to delete pages: %w", err)
}
return count, nil
}

View file

@ -0,0 +1,79 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// DeletePagesFromSearchUseCase deletes pages from the search index
type DeletePagesFromSearchUseCase struct {
searchClient *search.Client
logger *zap.Logger
}
// ProvideDeletePagesFromSearchUseCase creates a new DeletePagesFromSearchUseCase
func ProvideDeletePagesFromSearchUseCase(
searchClient *search.Client,
logger *zap.Logger,
) *DeletePagesFromSearchUseCase {
return &DeletePagesFromSearchUseCase{
searchClient: searchClient,
logger: logger,
}
}
// Execute deletes specific pages from the search index
func (uc *DeletePagesFromSearchUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
pageIDs []string,
) (int, error) {
if len(pageIDs) == 0 {
return 0, nil
}
deindexedCount := 0
// Batch delete from Meilisearch
if len(pageIDs) > 1 {
_, err := uc.searchClient.DeleteDocuments(siteID.String(), pageIDs)
if err != nil {
uc.logger.Error("failed to delete documents from search index", zap.Error(err))
// Don't fail the whole operation since database delete may have succeeded
return 0, nil
}
deindexedCount = len(pageIDs)
} else {
// Single document delete
_, err := uc.searchClient.DeleteDocument(siteID.String(), pageIDs[0])
if err != nil {
uc.logger.Error("failed to delete document from search index",
zap.String("page_id", pageIDs[0]),
zap.Error(err))
// Don't fail the whole operation since database delete may have succeeded
return 0, nil
}
deindexedCount = 1
}
return deindexedCount, nil
}
// ExecuteDeleteAll deletes all documents for a site from the search index
func (uc *DeletePagesFromSearchUseCase) ExecuteDeleteAll(
ctx context.Context,
siteID gocql.UUID,
) error {
_, err := uc.searchClient.DeleteAllDocuments(siteID.String())
if err != nil {
uc.logger.Error("failed to delete all documents from search index", zap.Error(err))
// Don't fail the whole operation since database delete may have succeeded
return nil
}
return nil
}

View file

@ -0,0 +1,47 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// EnsureSearchIndexUseCase ensures search index exists for a site
type EnsureSearchIndexUseCase struct {
searchClient *search.Client
logger *zap.Logger
}
// ProvideEnsureSearchIndexUseCase creates a new EnsureSearchIndexUseCase
func ProvideEnsureSearchIndexUseCase(
searchClient *search.Client,
logger *zap.Logger,
) *EnsureSearchIndexUseCase {
return &EnsureSearchIndexUseCase{
searchClient: searchClient,
logger: logger,
}
}
// Execute ensures the search index exists, creating it if necessary
func (uc *EnsureSearchIndexUseCase) Execute(ctx context.Context, siteID gocql.UUID) error {
indexExists, err := uc.searchClient.IndexExists(siteID.String())
if err != nil {
uc.logger.Error("failed to check index existence", zap.Error(err))
return fmt.Errorf("failed to check search index: %w", err)
}
if !indexExists {
uc.logger.Info("creating search index", zap.String("site_id", siteID.String()))
if err := uc.searchClient.CreateIndex(siteID.String()); err != nil {
uc.logger.Error("failed to create index", zap.Error(err))
return fmt.Errorf("failed to create search index: %w", err)
}
}
return nil
}

View file

@ -0,0 +1,74 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// ExecuteSearchQueryUseCase performs the actual search query
type ExecuteSearchQueryUseCase struct {
searchClient *search.Client
logger *zap.Logger
}
// ProvideExecuteSearchQueryUseCase creates a new ExecuteSearchQueryUseCase
func ProvideExecuteSearchQueryUseCase(
searchClient *search.Client,
logger *zap.Logger,
) *ExecuteSearchQueryUseCase {
return &ExecuteSearchQueryUseCase{
searchClient: searchClient,
logger: logger,
}
}
// Execute performs the search query against Meilisearch
func (uc *ExecuteSearchQueryUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
query string,
limit, offset int64,
filter string,
) (*search.SearchResult, error) {
// Set default limits if not provided
if limit <= 0 || limit > 100 {
limit = 20 // Default to 20 results
}
if offset < 0 {
offset = 0
}
// Build search request
searchReq := search.SearchRequest{
Query: query,
Limit: limit,
Offset: offset,
Filter: filter,
}
// If no filter provided, default to only published pages
if searchReq.Filter == "" {
searchReq.Filter = "status = publish"
}
// Perform search
result, err := uc.searchClient.Search(siteID.String(), searchReq)
if err != nil {
uc.logger.Error("failed to search pages", zap.Error(err))
return nil, fmt.Errorf("failed to search pages: %w", err)
}
uc.logger.Info("search completed",
zap.String("site_id", siteID.String()),
zap.String("query", query),
zap.Int64("total_hits", result.TotalHits),
zap.Int64("processing_time_ms", result.ProcessingTimeMs))
return result, nil
}

View file

@ -0,0 +1,50 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
)
// GetPageByIDUseCase retrieves a specific page by ID
type GetPageByIDUseCase struct {
pageRepo domainpage.Repository
logger *zap.Logger
}
// ProvideGetPageByIDUseCase creates a new GetPageByIDUseCase
func ProvideGetPageByIDUseCase(
pageRepo domainpage.Repository,
logger *zap.Logger,
) *GetPageByIDUseCase {
return &GetPageByIDUseCase{
pageRepo: pageRepo,
logger: logger,
}
}
// Execute retrieves a page by its ID
func (uc *GetPageByIDUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
pageID string,
) (*domainpage.Page, error) {
// Get page from database
page, err := uc.pageRepo.GetByID(ctx, siteID, pageID)
if err != nil {
uc.logger.Error("failed to get page",
zap.String("page_id", pageID),
zap.Error(err))
return nil, fmt.Errorf("page not found")
}
uc.logger.Info("page retrieved",
zap.String("site_id", siteID.String()),
zap.String("page_id", pageID))
return page, nil
}

View file

@ -0,0 +1,77 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
)
// GetPageStatisticsUseCase retrieves page count statistics
type GetPageStatisticsUseCase struct {
pageRepo domainpage.Repository
logger *zap.Logger
}
// ProvideGetPageStatisticsUseCase creates a new GetPageStatisticsUseCase
func ProvideGetPageStatisticsUseCase(
pageRepo domainpage.Repository,
logger *zap.Logger,
) *GetPageStatisticsUseCase {
return &GetPageStatisticsUseCase{
pageRepo: pageRepo,
logger: logger,
}
}
// PageStatistics contains page count statistics
type PageStatistics struct {
TotalPages int64
PublishedPages int64
DraftPages int64
}
// Execute retrieves page statistics for a site
func (uc *GetPageStatisticsUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
) (*PageStatistics, error) {
// Count total pages in database
totalPages, err := uc.pageRepo.CountBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to count pages", zap.Error(err))
return nil, fmt.Errorf("failed to count pages: %w", err)
}
// Get all pages to count by status (this could be optimized with a dedicated query)
pages, err := uc.pageRepo.GetBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to get pages", zap.Error(err))
return nil, fmt.Errorf("failed to get pages: %w", err)
}
// Count pages by status
var publishedPages, draftPages int64
for _, page := range pages {
if page.Status == "publish" {
publishedPages++
} else if page.Status == "draft" {
draftPages++
}
}
uc.logger.Info("page statistics retrieved",
zap.String("site_id", siteID.String()),
zap.Int64("total", totalPages),
zap.Int64("published", publishedPages),
zap.Int64("draft", draftPages))
return &PageStatistics{
TotalPages: totalPages,
PublishedPages: publishedPages,
DraftPages: draftPages,
}, nil
}

View file

@ -0,0 +1,75 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// GetSearchIndexStatusUseCase retrieves search index status information
type GetSearchIndexStatusUseCase struct {
searchClient *search.Client
logger *zap.Logger
}
// ProvideGetSearchIndexStatusUseCase creates a new GetSearchIndexStatusUseCase
func ProvideGetSearchIndexStatusUseCase(
searchClient *search.Client,
logger *zap.Logger,
) *GetSearchIndexStatusUseCase {
return &GetSearchIndexStatusUseCase{
searchClient: searchClient,
logger: logger,
}
}
// SearchIndexStatus contains search index status information
type SearchIndexStatus struct {
Status string // "not_created", "active", "error"
DocumentCount int64
}
// Execute retrieves search index status for a site
func (uc *GetSearchIndexStatusUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
) (*SearchIndexStatus, error) {
status := &SearchIndexStatus{
Status: "not_created",
DocumentCount: 0,
}
// Check if index exists
indexExists, err := uc.searchClient.IndexExists(siteID.String())
if err != nil {
uc.logger.Error("failed to check index existence", zap.Error(err))
status.Status = "error"
return status, nil
}
if !indexExists {
return status, nil
}
// Index exists, mark as active
status.Status = "active"
// Get index stats
stats, err := uc.searchClient.GetStats(siteID.String())
if err != nil {
uc.logger.Error("failed to get index stats", zap.Error(err))
// Don't change status to error, index is still active
} else {
status.DocumentCount = stats.NumberOfDocuments
}
uc.logger.Info("search index status retrieved",
zap.String("site_id", siteID.String()),
zap.String("status", status.Status),
zap.Int64("doc_count", status.DocumentCount))
return status, nil
}

View file

@ -0,0 +1,52 @@
package page
import (
"context"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// IncrementSearchCountUseCase increments the search request counter for a site
type IncrementSearchCountUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideIncrementSearchCountUseCase creates a new IncrementSearchCountUseCase
func ProvideIncrementSearchCountUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *IncrementSearchCountUseCase {
return &IncrementSearchCountUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute increments the search count and updates the site usage tracking
func (uc *IncrementSearchCountUseCase) Execute(
ctx context.Context,
site *domainsite.Site,
) error {
// Increment search request count
site.IncrementSearchCount()
uc.logger.Info("incremented search count",
zap.String("site_id", site.ID.String()),
zap.Int64("new_count", site.SearchRequestsCount))
// Update usage tracking in database
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
uc.logger.Error("failed to update search usage", zap.Error(err))
// Don't fail the search, just log the error and return
return err
}
uc.logger.Info("search usage updated successfully",
zap.String("site_id", site.ID.String()),
zap.Int64("search_count", site.SearchRequestsCount))
return nil
}

View file

@ -0,0 +1,78 @@
package page
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// IndexPageToSearchUseCase indexes pages to the search engine
type IndexPageToSearchUseCase struct {
searchClient *search.Client
logger *zap.Logger
}
// ProvideIndexPageToSearchUseCase creates a new IndexPageToSearchUseCase
func ProvideIndexPageToSearchUseCase(
searchClient *search.Client,
logger *zap.Logger,
) *IndexPageToSearchUseCase {
return &IndexPageToSearchUseCase{
searchClient: searchClient,
logger: logger,
}
}
// Execute indexes a batch of pages to Meilisearch
func (uc *IndexPageToSearchUseCase) Execute(
ctx context.Context,
siteID gocql.UUID,
pages []*domainpage.Page,
) (int, error) {
if len(pages) == 0 {
return 0, nil
}
// Convert pages to search documents
documents := make([]search.PageDocument, 0, len(pages))
for _, page := range pages {
if page.ShouldIndex() {
page.MarkIndexed()
doc := search.PageDocument{
ID: page.PageID,
SiteID: page.SiteID.String(),
TenantID: page.TenantID.String(),
Title: page.Title,
Content: page.Content,
Excerpt: page.Excerpt,
URL: page.URL,
Status: page.Status,
PostType: page.PostType,
Author: page.Author,
PublishedAt: page.PublishedAt.Unix(),
ModifiedAt: page.ModifiedAt.Unix(),
}
documents = append(documents, doc)
}
}
if len(documents) == 0 {
return 0, nil
}
// Bulk index to Meilisearch
_, err := uc.searchClient.AddDocuments(siteID.String(), documents)
if err != nil {
uc.logger.Error("failed to index documents", zap.Error(err))
return 0, fmt.Errorf("failed to index documents: %w", err)
}
return len(documents), nil
}

View file

@ -0,0 +1,134 @@
package page
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/search"
)
// SearchPagesUseCase handles page search functionality
type SearchPagesUseCase struct {
siteRepo domainsite.Repository
searchClient *search.Client
logger *zap.Logger
}
// ProvideSearchPagesUseCase creates a new SearchPagesUseCase
func ProvideSearchPagesUseCase(
siteRepo domainsite.Repository,
searchClient *search.Client,
logger *zap.Logger,
) *SearchPagesUseCase {
return &SearchPagesUseCase{
siteRepo: siteRepo,
searchClient: searchClient,
logger: logger,
}
}
// SearchPagesInput is the input for searching pages
type SearchPagesInput struct {
Query string `json:"query"`
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Filter string `json:"filter,omitempty"` // e.g., "status = publish AND post_type = post"
}
// SearchPagesOutput is the output after searching pages
type SearchPagesOutput struct {
Hits interface{} `json:"hits"` // meilisearch.Hits
Query string `json:"query"`
ProcessingTimeMs int64 `json:"processing_time_ms"`
TotalHits int64 `json:"total_hits"`
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
}
// Execute performs a search on the site's indexed pages
func (uc *SearchPagesUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID, input *SearchPagesInput) (*SearchPagesOutput, error) {
uc.logger.Info("executing search pages use case",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.String("query", input.Query))
// Get site to validate and check quotas
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
// No quota checking - usage-based billing (anti-abuse via rate limiting only)
// Set default limits if not provided
limit := input.Limit
if limit <= 0 || limit > 100 {
limit = 20 // Default to 20 results
}
offset := input.Offset
if offset < 0 {
offset = 0
}
// Build search request
searchReq := search.SearchRequest{
Query: input.Query,
Limit: limit,
Offset: offset,
Filter: input.Filter,
}
// If no filter provided, default to only published pages
if searchReq.Filter == "" {
searchReq.Filter = "status = publish"
}
// Perform search
result, err := uc.searchClient.Search(siteID.String(), searchReq)
if err != nil {
uc.logger.Error("failed to search pages", zap.Error(err))
return nil, fmt.Errorf("failed to search pages: %w", err)
}
// Increment search request count (for usage tracking/billing)
site.IncrementSearchCount()
uc.logger.Info("incremented search count",
zap.String("site_id", siteID.String()),
zap.Int64("new_count", site.SearchRequestsCount))
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
uc.logger.Error("failed to update search usage", zap.Error(err))
// Don't fail the search, just log the error
} else {
uc.logger.Info("search usage updated successfully",
zap.String("site_id", siteID.String()),
zap.Int64("search_count", site.SearchRequestsCount))
}
uc.logger.Info("search completed successfully",
zap.String("site_id", siteID.String()),
zap.String("query", input.Query),
zap.Int64("total_hits", result.TotalHits),
zap.Int64("processing_time_ms", result.ProcessingTimeMs))
return &SearchPagesOutput{
Hits: result.Hits,
Query: result.Query,
ProcessingTimeMs: result.ProcessingTimeMs,
TotalHits: result.TotalHits,
Limit: result.Limit,
Offset: result.Offset,
}, nil
}

View file

@ -0,0 +1,199 @@
package page
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
)
// GetSyncStatusUseCase handles retrieving synchronization status
type GetSyncStatusUseCase struct {
pageRepo domainpage.Repository
siteRepo domainsite.Repository
searchClient *search.Client
logger *zap.Logger
}
// ProvideGetSyncStatusUseCase creates a new GetSyncStatusUseCase
func ProvideGetSyncStatusUseCase(
pageRepo domainpage.Repository,
siteRepo domainsite.Repository,
searchClient *search.Client,
logger *zap.Logger,
) *GetSyncStatusUseCase {
return &GetSyncStatusUseCase{
pageRepo: pageRepo,
siteRepo: siteRepo,
searchClient: searchClient,
logger: logger,
}
}
// SyncStatusOutput provides synchronization status information
type SyncStatusOutput struct {
SiteID string `json:"site_id"`
TotalPages int64 `json:"total_pages"`
PublishedPages int64 `json:"published_pages"`
DraftPages int64 `json:"draft_pages"`
LastSyncedAt time.Time `json:"last_synced_at"`
PagesIndexedMonth int64 `json:"pages_indexed_month"` // Usage tracking
SearchRequestsMonth int64 `json:"search_requests_month"` // Usage tracking
LastResetAt time.Time `json:"last_reset_at"` // Monthly billing cycle
SearchIndexStatus string `json:"search_index_status"`
SearchIndexDocCount int64 `json:"search_index_doc_count"`
}
// Execute retrieves the current sync status for a site
func (uc *GetSyncStatusUseCase) Execute(ctx context.Context, tenantID, siteID gocql.UUID) (*SyncStatusOutput, error) {
uc.logger.Info("executing get sync status use case",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()))
// Get site to validate and get quota information
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
// Count total pages in database
totalPages, err := uc.pageRepo.CountBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to count pages", zap.Error(err))
return nil, fmt.Errorf("failed to count pages: %w", err)
}
// Get all pages to count by status (this could be optimized with a dedicated query)
pages, err := uc.pageRepo.GetBySiteID(ctx, siteID)
if err != nil {
uc.logger.Error("failed to get pages", zap.Error(err))
return nil, fmt.Errorf("failed to get pages: %w", err)
}
// Count pages by status
var publishedPages, draftPages int64
for _, page := range pages {
if page.Status == "publish" {
publishedPages++
} else if page.Status == "draft" {
draftPages++
}
}
// Check search index status
indexStatus := "not_created"
var indexDocCount int64 = 0
indexExists, err := uc.searchClient.IndexExists(siteID.String())
if err != nil {
uc.logger.Error("failed to check index existence", zap.Error(err))
indexStatus = "error"
} else if indexExists {
indexStatus = "active"
// Get index stats
stats, err := uc.searchClient.GetStats(siteID.String())
if err != nil {
uc.logger.Error("failed to get index stats", zap.Error(err))
} else {
indexDocCount = stats.NumberOfDocuments
}
}
uc.logger.Info("sync status retrieved successfully",
zap.String("site_id", siteID.String()),
zap.Int64("total_pages", totalPages),
zap.Int64("published", publishedPages),
zap.Int64("draft", draftPages))
return &SyncStatusOutput{
SiteID: siteID.String(),
TotalPages: totalPages,
PublishedPages: publishedPages,
DraftPages: draftPages,
LastSyncedAt: site.LastIndexedAt,
PagesIndexedMonth: site.MonthlyPagesIndexed,
SearchRequestsMonth: site.SearchRequestsCount,
LastResetAt: site.LastResetAt,
SearchIndexStatus: indexStatus,
SearchIndexDocCount: indexDocCount,
}, nil
}
// GetPageDetailsInput is the input for getting page details
type GetPageDetailsInput struct {
PageID string `json:"page_id"`
}
// PageDetailsOutput provides detailed information about a specific page
type PageDetailsOutput struct {
PageID string `json:"page_id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
URL string `json:"url"`
Status string `json:"status"`
PostType string `json:"post_type"`
Author string `json:"author"`
PublishedAt time.Time `json:"published_at"`
ModifiedAt time.Time `json:"modified_at"`
IndexedAt time.Time `json:"indexed_at"`
MeilisearchDocID string `json:"meilisearch_doc_id"`
IsIndexed bool `json:"is_indexed"`
}
// ExecuteGetPageDetails retrieves detailed information about a specific page
func (uc *GetSyncStatusUseCase) ExecuteGetPageDetails(ctx context.Context, tenantID, siteID gocql.UUID, input *GetPageDetailsInput) (*PageDetailsOutput, error) {
uc.logger.Info("executing get page details use case",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.String("page_id", input.PageID))
// Get site to validate
_, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Get page from database
page, err := uc.pageRepo.GetByID(ctx, siteID, input.PageID)
if err != nil {
uc.logger.Error("failed to get page", zap.Error(err))
return nil, fmt.Errorf("page not found")
}
// Check if page is indexed in Meilisearch
isIndexed := !page.IndexedAt.IsZero()
uc.logger.Info("page details retrieved successfully",
zap.String("site_id", siteID.String()),
zap.String("page_id", input.PageID))
return &PageDetailsOutput{
PageID: page.PageID,
Title: page.Title,
Excerpt: page.Excerpt,
URL: page.URL,
Status: page.Status,
PostType: page.PostType,
Author: page.Author,
PublishedAt: page.PublishedAt,
ModifiedAt: page.ModifiedAt,
IndexedAt: page.IndexedAt,
MeilisearchDocID: page.MeilisearchDocID,
IsIndexed: isIndexed,
}, nil
}

View file

@ -0,0 +1,205 @@
package page
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/search"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/ipcrypt"
)
// SyncPagesUseCase handles page synchronization from WordPress
type SyncPagesUseCase struct {
pageRepo domainpage.Repository
siteRepo domainsite.Repository
searchClient *search.Client
ipEncryptor *ipcrypt.IPEncryptor
logger *zap.Logger
}
// ProvideSyncPagesUseCase creates a new SyncPagesUseCase
func ProvideSyncPagesUseCase(
pageRepo domainpage.Repository,
siteRepo domainsite.Repository,
searchClient *search.Client,
ipEncryptor *ipcrypt.IPEncryptor,
logger *zap.Logger,
) *SyncPagesUseCase {
return &SyncPagesUseCase{
pageRepo: pageRepo,
siteRepo: siteRepo,
searchClient: searchClient,
ipEncryptor: ipEncryptor,
logger: logger,
}
}
// SyncPageInput represents a single page to sync
type SyncPageInput struct {
PageID string `json:"page_id"`
Title string `json:"title"`
Content string `json:"content"`
Excerpt string `json:"excerpt"`
URL string `json:"url"`
Status string `json:"status"` // publish, draft, trash
PostType string `json:"post_type"` // page, post
Author string `json:"author"`
PublishedAt time.Time `json:"published_at"`
ModifiedAt time.Time `json:"modified_at"`
IPAddress string `json:"-"` // Plain IP address (will be encrypted before storage), never exposed in JSON
}
// SyncPagesInput is the input for syncing pages
type SyncPagesInput struct {
Pages []SyncPageInput `json:"pages"`
}
// SyncPagesOutput is the output after syncing pages
type SyncPagesOutput struct {
SyncedCount int `json:"synced_count"`
IndexedCount int `json:"indexed_count"`
FailedPages []string `json:"failed_pages,omitempty"`
Message string `json:"message"`
}
// SyncPages syncs a batch of pages for a site
func (uc *SyncPagesUseCase) SyncPages(ctx context.Context, tenantID, siteID gocql.UUID, input *SyncPagesInput) (*SyncPagesOutput, error) {
uc.logger.Info("syncing pages",
zap.String("tenant_id", tenantID.String()),
zap.String("site_id", siteID.String()),
zap.Int("page_count", len(input.Pages)))
// Get site to validate and check quotas
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
// No quota limits - usage-based billing (anti-abuse via rate limiting only)
// Ensure search index exists
indexExists, err := uc.searchClient.IndexExists(siteID.String())
if err != nil {
uc.logger.Error("failed to check index existence", zap.Error(err))
return nil, fmt.Errorf("failed to check search index: %w", err)
}
if !indexExists {
uc.logger.Info("creating search index", zap.String("site_id", siteID.String()))
if err := uc.searchClient.CreateIndex(siteID.String()); err != nil {
uc.logger.Error("failed to create index", zap.Error(err))
return nil, fmt.Errorf("failed to create search index: %w", err)
}
}
// Process each page
syncedCount := 0
indexedCount := 0
var failedPages []string
var documentsToIndex []search.PageDocument
for _, pageInput := range input.Pages {
// Encrypt IP address (CWE-359: GDPR compliance)
encryptedIP, err := uc.ipEncryptor.Encrypt(pageInput.IPAddress)
if err != nil {
uc.logger.Error("failed to encrypt IP address",
zap.String("page_id", pageInput.PageID),
zap.Error(err))
failedPages = append(failedPages, pageInput.PageID)
continue
}
// Create page entity
page := domainpage.NewPage(
siteID,
site.TenantID,
pageInput.PageID,
pageInput.Title,
pageInput.Content,
pageInput.Excerpt,
pageInput.URL,
pageInput.Status,
pageInput.PostType,
pageInput.Author,
pageInput.PublishedAt,
pageInput.ModifiedAt,
encryptedIP,
)
// Upsert page to database
if err := uc.pageRepo.Upsert(ctx, page); err != nil {
uc.logger.Error("failed to upsert page",
zap.String("page_id", pageInput.PageID),
zap.Error(err))
failedPages = append(failedPages, pageInput.PageID)
continue
}
syncedCount++
// Only index published pages
if page.ShouldIndex() {
page.MarkIndexed()
// Prepare document for Meilisearch
doc := search.PageDocument{
ID: page.PageID,
SiteID: page.SiteID.String(),
TenantID: page.TenantID.String(),
Title: page.Title,
Content: page.Content,
Excerpt: page.Excerpt,
URL: page.URL,
Status: page.Status,
PostType: page.PostType,
Author: page.Author,
PublishedAt: page.PublishedAt.Unix(),
ModifiedAt: page.ModifiedAt.Unix(),
}
documentsToIndex = append(documentsToIndex, doc)
}
}
// Index documents in Meilisearch if any
if len(documentsToIndex) > 0 {
_, err := uc.searchClient.AddDocuments(siteID.String(), documentsToIndex)
if err != nil {
uc.logger.Error("failed to index documents", zap.Error(err))
return nil, fmt.Errorf("failed to index documents: %w", err)
}
indexedCount = len(documentsToIndex)
// Note: Usage tracking is handled by the service layer via UpdateSiteUsageUseCase
}
uc.logger.Info("pages synced successfully",
zap.String("site_id", siteID.String()),
zap.Int("synced", syncedCount),
zap.Int("indexed", indexedCount),
zap.Int("failed", len(failedPages)))
message := fmt.Sprintf("Successfully synced %d pages, indexed %d pages", syncedCount, indexedCount)
if len(failedPages) > 0 {
message += fmt.Sprintf(", failed %d pages", len(failedPages))
}
return &SyncPagesOutput{
SyncedCount: syncedCount,
IndexedCount: indexedCount,
FailedPages: failedPages,
Message: message,
}, nil
}

View file

@ -0,0 +1,47 @@
package page
import (
"context"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// UpdateSiteUsageUseCase updates site usage counters after indexing
type UpdateSiteUsageUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideUpdateSiteUsageUseCase creates a new UpdateSiteUsageUseCase
func ProvideUpdateSiteUsageUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *UpdateSiteUsageUseCase {
return &UpdateSiteUsageUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute updates the site's monthly page indexed count (for billing tracking)
func (uc *UpdateSiteUsageUseCase) Execute(
ctx context.Context,
site *domainsite.Site,
indexedCount int,
) error {
if indexedCount <= 0 {
return nil
}
site.IncrementMonthlyPageCount(int64(indexedCount))
if err := uc.siteRepo.UpdateUsage(ctx, site); err != nil {
uc.logger.Error("failed to update usage", zap.Error(err))
// Don't fail the whole operation, just log the error
return err
}
return nil
}

View file

@ -0,0 +1,38 @@
package page
import (
"context"
"go.uber.org/zap"
domainpage "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page"
)
// UpsertPageUseCase saves or updates a page in the repository
type UpsertPageUseCase struct {
pageRepo domainpage.Repository
logger *zap.Logger
}
// ProvideUpsertPageUseCase creates a new UpsertPageUseCase
func ProvideUpsertPageUseCase(
pageRepo domainpage.Repository,
logger *zap.Logger,
) *UpsertPageUseCase {
return &UpsertPageUseCase{
pageRepo: pageRepo,
logger: logger,
}
}
// Execute saves or updates a page in the database
func (uc *UpsertPageUseCase) Execute(ctx context.Context, page *domainpage.Page) error {
if err := uc.pageRepo.Upsert(ctx, page); err != nil {
uc.logger.Error("failed to upsert page",
zap.String("page_id", page.PageID),
zap.Error(err))
return err
}
return nil
}

View file

@ -0,0 +1,48 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ValidateSiteUseCase validates site status and verification
type ValidateSiteUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateSiteUseCase creates a new ValidateSiteUseCase
func ProvideValidateSiteUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *ValidateSiteUseCase {
return &ValidateSiteUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute validates the site and returns it if valid
func (uc *ValidateSiteUseCase) Execute(
ctx context.Context,
tenantID, siteID gocql.UUID,
) (*domainsite.Site, error) {
// Get site from repository
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
return site, nil
}

View file

@ -0,0 +1,48 @@
package page
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 is authorized for deletion
type ValidateSiteForDeletionUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateSiteForDeletionUseCase creates a new ValidateSiteForDeletionUseCase
func ProvideValidateSiteForDeletionUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *ValidateSiteForDeletionUseCase {
return &ValidateSiteForDeletionUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute validates the site for deletion operations
func (uc *ValidateSiteForDeletionUseCase) Execute(
ctx context.Context,
tenantID, siteID gocql.UUID,
) (*domainsite.Site, error) {
// Get site from repository
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
return site, nil
}

View file

@ -0,0 +1,48 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ValidateSiteForSearchUseCase validates that a site exists and is authorized for search
type ValidateSiteForSearchUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateSiteForSearchUseCase creates a new ValidateSiteForSearchUseCase
func ProvideValidateSiteForSearchUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *ValidateSiteForSearchUseCase {
return &ValidateSiteForSearchUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute validates the site for search operations
func (uc *ValidateSiteForSearchUseCase) Execute(
ctx context.Context,
tenantID, siteID gocql.UUID,
) (*domainsite.Site, error) {
// Get site from repository
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
return site, nil
}

View file

@ -0,0 +1,48 @@
package page
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
)
// ValidateSiteForStatusUseCase validates that a site exists and is authorized for status queries
type ValidateSiteForStatusUseCase struct {
siteRepo domainsite.Repository
logger *zap.Logger
}
// ProvideValidateSiteForStatusUseCase creates a new ValidateSiteForStatusUseCase
func ProvideValidateSiteForStatusUseCase(
siteRepo domainsite.Repository,
logger *zap.Logger,
) *ValidateSiteForStatusUseCase {
return &ValidateSiteForStatusUseCase{
siteRepo: siteRepo,
logger: logger,
}
}
// Execute validates the site for status operations
func (uc *ValidateSiteForStatusUseCase) Execute(
ctx context.Context,
tenantID, siteID gocql.UUID,
) (*domainsite.Site, error) {
// Get site from repository
site, err := uc.siteRepo.GetByID(ctx, tenantID, siteID)
if err != nil {
uc.logger.Error("failed to get site", zap.Error(err))
return nil, domainsite.ErrSiteNotFound
}
// Verify site is verified (skip for test mode)
if site.RequiresVerification() && !site.IsVerified {
uc.logger.Warn("site not verified", zap.String("site_id", siteID.String()))
return nil, domainsite.ErrSiteNotVerified
}
return site, nil
}