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,196 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
pagedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/page"
|
||||
pageservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/page"
|
||||
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
|
||||
)
|
||||
|
||||
// DeletePagesHandler handles page deletion from WordPress plugin
|
||||
type DeletePagesHandler struct {
|
||||
deleteService pageservice.DeletePagesService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideDeletePagesHandler creates a new DeletePagesHandler
|
||||
func ProvideDeletePagesHandler(
|
||||
deleteService pageservice.DeletePagesService,
|
||||
logger *zap.Logger,
|
||||
) *DeletePagesHandler {
|
||||
return &DeletePagesHandler{
|
||||
deleteService: deleteService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for deleting pages
|
||||
// This endpoint is protected by API key middleware
|
||||
func (h *DeletePagesHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("delete pages request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// CWE-436: Validate Content-Type before parsing
|
||||
if err := httpvalidation.ValidateJSONContentType(r); err != nil {
|
||||
httperror.ProblemBadRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req pagedto.DeleteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.ProblemBadRequest(w, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if len(req.PageIDs) == 0 {
|
||||
httperror.ProblemBadRequest(w, "page_ids array is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert DTO to use case input
|
||||
input := &pageusecase.DeletePagesInput{
|
||||
PageIDs: req.PageIDs,
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.deleteService.DeletePages(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete pages",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "site not found" {
|
||||
httperror.ProblemNotFound(w, "site not found")
|
||||
return
|
||||
}
|
||||
if err.Error() == "site is not verified" {
|
||||
httperror.ProblemForbidden(w, "site is not verified")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to delete pages")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.DeleteResponse{
|
||||
DeletedCount: output.DeletedCount,
|
||||
DeindexedCount: output.DeindexedCount,
|
||||
FailedPages: output.FailedPages,
|
||||
Message: output.Message,
|
||||
}
|
||||
|
||||
h.logger.Info("pages deleted successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.Int("deleted_count", output.DeletedCount))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
||||
// HandleDeleteAll handles the HTTP request for deleting all pages
|
||||
func (h *DeletePagesHandler) HandleDeleteAll(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("delete all pages request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.deleteService.DeleteAllPages(r.Context(), tenantID, siteID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete all pages",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "site not found" {
|
||||
httperror.ProblemNotFound(w, "site not found")
|
||||
return
|
||||
}
|
||||
if err.Error() == "site is not verified" {
|
||||
httperror.ProblemForbidden(w, "site is not verified")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to delete all pages")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.DeleteResponse{
|
||||
DeletedCount: output.DeletedCount,
|
||||
DeindexedCount: output.DeindexedCount,
|
||||
Message: output.Message,
|
||||
}
|
||||
|
||||
h.logger.Info("all pages deleted successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.Int("deleted_count", output.DeletedCount))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
pagedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/page"
|
||||
pageservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/page"
|
||||
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
|
||||
)
|
||||
|
||||
// SearchPagesHandler handles page search from WordPress plugin
|
||||
type SearchPagesHandler struct {
|
||||
searchService pageservice.SearchPagesService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideSearchPagesHandler creates a new SearchPagesHandler
|
||||
func ProvideSearchPagesHandler(
|
||||
searchService pageservice.SearchPagesService,
|
||||
logger *zap.Logger,
|
||||
) *SearchPagesHandler {
|
||||
return &SearchPagesHandler{
|
||||
searchService: searchService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for searching pages
|
||||
// This endpoint is protected by API key middleware
|
||||
func (h *SearchPagesHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("search pages request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// CWE-436: Validate Content-Type before parsing
|
||||
if err := httpvalidation.ValidateJSONContentType(r); err != nil {
|
||||
httperror.ProblemBadRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req pagedto.SearchRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.ProblemBadRequest(w, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if req.Query == "" {
|
||||
httperror.ProblemBadRequest(w, "query is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert DTO to use case input
|
||||
input := &pageusecase.SearchPagesInput{
|
||||
Query: req.Query,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
Filter: req.Filter,
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.searchService.SearchPages(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to search pages",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("query", req.Query))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "site not found" {
|
||||
httperror.ProblemNotFound(w, "site not found")
|
||||
return
|
||||
}
|
||||
if err.Error() == "site is not verified" {
|
||||
httperror.ProblemForbidden(w, "site is not verified")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to search pages")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.SearchResponse{
|
||||
Hits: output.Hits.([]map[string]interface{}),
|
||||
Query: output.Query,
|
||||
ProcessingTimeMs: output.ProcessingTimeMs,
|
||||
TotalHits: output.TotalHits,
|
||||
Limit: output.Limit,
|
||||
Offset: output.Offset,
|
||||
}
|
||||
|
||||
h.logger.Info("pages searched successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("query", req.Query),
|
||||
zap.Int64("total_hits", output.TotalHits))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
domainsite "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/site"
|
||||
siteservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/site"
|
||||
siteusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
)
|
||||
|
||||
// StatusHandler handles WordPress plugin status/verification requests
|
||||
type StatusHandler struct {
|
||||
getSiteService siteservice.GetSiteService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideStatusHandler creates a new StatusHandler
|
||||
func ProvideStatusHandler(
|
||||
getSiteService siteservice.GetSiteService,
|
||||
logger *zap.Logger,
|
||||
) *StatusHandler {
|
||||
return &StatusHandler{
|
||||
getSiteService: getSiteService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// StatusResponse represents the response for plugin status endpoint
|
||||
type StatusResponse struct {
|
||||
// Core Identity
|
||||
SiteID string `json:"site_id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Domain string `json:"domain"`
|
||||
SiteURL string `json:"site_url"`
|
||||
|
||||
// Status & Verification
|
||||
Status string `json:"status"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
VerificationStatus string `json:"verification_status"` // "pending" or "verified"
|
||||
VerificationToken string `json:"verification_token,omitempty"` // Only if pending
|
||||
VerificationInstructions string `json:"verification_instructions,omitempty"` // Only if pending
|
||||
|
||||
// Storage (usage tracking only - no quotas)
|
||||
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||||
|
||||
// Usage tracking (monthly, resets for billing)
|
||||
SearchRequestsCount int64 `json:"search_requests_count"`
|
||||
MonthlyPagesIndexed int64 `json:"monthly_pages_indexed"`
|
||||
TotalPagesIndexed int64 `json:"total_pages_indexed"` // All-time stat
|
||||
|
||||
// Search
|
||||
SearchIndexName string `json:"search_index_name"`
|
||||
|
||||
// Additional Info
|
||||
APIKeyPrefix string `json:"api_key_prefix"`
|
||||
APIKeyLastFour string `json:"api_key_last_four"`
|
||||
PluginVersion string `json:"plugin_version,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for plugin status verification
|
||||
// This endpoint is protected by API key middleware, so if we reach here, the API key is valid
|
||||
func (h *StatusHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("plugin status check",
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse UUIDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch full site details from database
|
||||
siteOutput, err := h.getSiteService.GetSite(r.Context(), tenantID, &siteusecase.GetSiteInput{
|
||||
ID: siteIDStr,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get site details", zap.Error(err))
|
||||
httperror.ProblemInternalServerError(w, "failed to retrieve site details")
|
||||
return
|
||||
}
|
||||
|
||||
site := siteOutput.Site
|
||||
|
||||
// Build response with full site details
|
||||
response := StatusResponse{
|
||||
SiteID: site.ID.String(),
|
||||
TenantID: site.TenantID.String(),
|
||||
Domain: site.Domain,
|
||||
SiteURL: site.SiteURL,
|
||||
|
||||
Status: site.Status,
|
||||
IsVerified: site.IsVerified,
|
||||
VerificationStatus: getVerificationStatus(site),
|
||||
|
||||
StorageUsedBytes: site.StorageUsedBytes,
|
||||
SearchRequestsCount: site.SearchRequestsCount,
|
||||
MonthlyPagesIndexed: site.MonthlyPagesIndexed,
|
||||
TotalPagesIndexed: site.TotalPagesIndexed,
|
||||
|
||||
SearchIndexName: site.SearchIndexName,
|
||||
|
||||
APIKeyPrefix: site.APIKeyPrefix,
|
||||
APIKeyLastFour: site.APIKeyLastFour,
|
||||
PluginVersion: site.PluginVersion,
|
||||
Language: site.Language,
|
||||
Timezone: site.Timezone,
|
||||
|
||||
Message: "API key is valid",
|
||||
}
|
||||
|
||||
// If site is not verified and requires verification, include instructions
|
||||
if site.RequiresVerification() && !site.IsVerified {
|
||||
response.VerificationToken = site.VerificationToken
|
||||
response.VerificationInstructions = generateVerificationInstructions(site)
|
||||
}
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
||||
// getVerificationStatus returns the verification status string
|
||||
func getVerificationStatus(site *domainsite.Site) string {
|
||||
if site.IsVerified {
|
||||
return "verified"
|
||||
}
|
||||
return "pending"
|
||||
}
|
||||
|
||||
// generateVerificationInstructions generates DNS verification instructions
|
||||
func generateVerificationInstructions(site *domainsite.Site) string {
|
||||
return fmt.Sprintf(
|
||||
"To verify ownership of %s, add this DNS TXT record:\n\n"+
|
||||
"Host/Name: %s\n"+
|
||||
"Type: TXT\n"+
|
||||
"Value: maplepress-verify=%s\n\n"+
|
||||
"Instructions:\n"+
|
||||
"1. Log in to your domain registrar (GoDaddy, Namecheap, Cloudflare, etc.)\n"+
|
||||
"2. Find DNS settings for your domain\n"+
|
||||
"3. Add a new TXT record with the values above\n"+
|
||||
"4. Wait 5-10 minutes for DNS propagation\n"+
|
||||
"5. Click 'Verify Domain' in your WordPress plugin settings",
|
||||
site.Domain,
|
||||
site.Domain,
|
||||
site.VerificationToken,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
pagedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/page"
|
||||
pageservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/page"
|
||||
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpvalidation"
|
||||
)
|
||||
|
||||
// SyncPagesHandler handles page synchronization from WordPress plugin
|
||||
type SyncPagesHandler struct {
|
||||
syncService pageservice.SyncPagesService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideSyncPagesHandler creates a new SyncPagesHandler
|
||||
func ProvideSyncPagesHandler(
|
||||
syncService pageservice.SyncPagesService,
|
||||
logger *zap.Logger,
|
||||
) *SyncPagesHandler {
|
||||
return &SyncPagesHandler{
|
||||
syncService: syncService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for syncing pages
|
||||
// This endpoint is protected by API key middleware
|
||||
func (h *SyncPagesHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("sync pages request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// CWE-436: Validate Content-Type before parsing
|
||||
if err := httpvalidation.ValidateJSONContentType(r); err != nil {
|
||||
httperror.ProblemBadRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req pagedto.SyncRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.ProblemBadRequest(w, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// CWE-20: Comprehensive input validation
|
||||
if err := req.Validate(); err != nil {
|
||||
h.logger.Warn("sync pages request validation failed", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Convert DTO to use case input
|
||||
pages := make([]pageusecase.SyncPageInput, len(req.Pages))
|
||||
for i, p := range req.Pages {
|
||||
pages[i] = pageusecase.SyncPageInput{
|
||||
PageID: p.PageID,
|
||||
Title: p.Title,
|
||||
Content: p.Content,
|
||||
Excerpt: p.Excerpt,
|
||||
URL: p.URL,
|
||||
Status: p.Status,
|
||||
PostType: p.PostType,
|
||||
Author: p.Author,
|
||||
PublishedAt: p.PublishedAt,
|
||||
ModifiedAt: p.ModifiedAt,
|
||||
}
|
||||
}
|
||||
|
||||
input := &pageusecase.SyncPagesInput{
|
||||
Pages: pages,
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.syncService.SyncPages(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to sync pages",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "site not found" {
|
||||
httperror.ProblemNotFound(w, "site not found")
|
||||
return
|
||||
}
|
||||
if err.Error() == "site is not verified" {
|
||||
httperror.ProblemForbidden(w, "site is not verified")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to sync pages")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.SyncResponse{
|
||||
SyncedCount: output.SyncedCount,
|
||||
IndexedCount: output.IndexedCount,
|
||||
FailedPages: output.FailedPages,
|
||||
Message: output.Message,
|
||||
}
|
||||
|
||||
h.logger.Info("pages synced successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.Int("synced_count", output.SyncedCount),
|
||||
zap.Int("indexed_count", output.IndexedCount))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
pagedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/page"
|
||||
pageservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/page"
|
||||
pageusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/page"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
)
|
||||
|
||||
// SyncStatusHandler handles sync status requests from WordPress plugin
|
||||
type SyncStatusHandler struct {
|
||||
statusService pageservice.SyncStatusService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideSyncStatusHandler creates a new SyncStatusHandler
|
||||
func ProvideSyncStatusHandler(
|
||||
statusService pageservice.SyncStatusService,
|
||||
logger *zap.Logger,
|
||||
) *SyncStatusHandler {
|
||||
return &SyncStatusHandler{
|
||||
statusService: statusService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for getting sync status
|
||||
// This endpoint is protected by API key middleware
|
||||
func (h *SyncStatusHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
h.logger.Info("sync status request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.statusService.GetSyncStatus(r.Context(), tenantID, siteID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get sync status",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "site not found" {
|
||||
httperror.ProblemNotFound(w, "site not found")
|
||||
return
|
||||
}
|
||||
if err.Error() == "site is not verified" {
|
||||
httperror.ProblemForbidden(w, "site is not verified")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to get sync status")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.StatusResponse{
|
||||
SiteID: output.SiteID,
|
||||
TotalPages: output.TotalPages,
|
||||
PublishedPages: output.PublishedPages,
|
||||
DraftPages: output.DraftPages,
|
||||
LastSyncedAt: output.LastSyncedAt,
|
||||
PagesIndexedMonth: output.PagesIndexedMonth,
|
||||
SearchRequestsMonth: output.SearchRequestsMonth,
|
||||
LastResetAt: output.LastResetAt,
|
||||
SearchIndexStatus: output.SearchIndexStatus,
|
||||
SearchIndexDocCount: output.SearchIndexDocCount,
|
||||
}
|
||||
|
||||
h.logger.Info("sync status retrieved successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.Int64("total_pages", output.TotalPages))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
||||
// HandleGetPageDetails handles the HTTP request for getting page details
|
||||
func (h *SyncStatusHandler) HandleGetPageDetails(w http.ResponseWriter, r *http.Request) {
|
||||
// Get site information from context (populated by API key middleware)
|
||||
isAuthenticated, ok := r.Context().Value(constants.SiteIsAuthenticated).(bool)
|
||||
if !ok || !isAuthenticated {
|
||||
h.logger.Error("site not authenticated in context")
|
||||
httperror.ProblemUnauthorized(w, "Invalid API key")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract site ID and tenant ID from context
|
||||
siteIDStr, _ := r.Context().Value(constants.SiteID).(string)
|
||||
tenantIDStr, _ := r.Context().Value(constants.SiteTenantID).(string)
|
||||
|
||||
// Get page ID from URL path parameter
|
||||
pageID := r.PathValue("page_id")
|
||||
|
||||
h.logger.Info("get page details request",
|
||||
zap.String("tenant_id", tenantIDStr),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("page_id", pageID))
|
||||
|
||||
// Parse IDs
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate page ID
|
||||
if pageID == "" {
|
||||
httperror.ProblemBadRequest(w, "page_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
input := &pageusecase.GetPageDetailsInput{
|
||||
PageID: pageID,
|
||||
}
|
||||
|
||||
output, err := h.statusService.GetPageDetails(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get page details",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("page_id", pageID))
|
||||
|
||||
// Check for specific errors
|
||||
if err.Error() == "page not found" {
|
||||
httperror.ProblemNotFound(w, "page not found")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "failed to get page details")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := pagedto.PageDetailsResponse{
|
||||
PageID: output.PageID,
|
||||
Title: output.Title,
|
||||
Excerpt: output.Excerpt,
|
||||
URL: output.URL,
|
||||
Status: output.Status,
|
||||
PostType: output.PostType,
|
||||
Author: output.Author,
|
||||
PublishedAt: output.PublishedAt,
|
||||
ModifiedAt: output.ModifiedAt,
|
||||
IndexedAt: output.IndexedAt,
|
||||
MeilisearchDocID: output.MeilisearchDocID,
|
||||
IsIndexed: output.IsIndexed,
|
||||
}
|
||||
|
||||
h.logger.Info("page details retrieved successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("page_id", pageID))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
siteservice "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service/site"
|
||||
siteusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/site"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
)
|
||||
|
||||
// PluginVerifyHandler handles domain verification from WordPress plugin
|
||||
type PluginVerifyHandler struct {
|
||||
service siteservice.VerifySiteService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvidePluginVerifyHandler creates a new PluginVerifyHandler
|
||||
func ProvidePluginVerifyHandler(service siteservice.VerifySiteService, logger *zap.Logger) *PluginVerifyHandler {
|
||||
return &PluginVerifyHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyResponse represents the verification response
|
||||
type VerifyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for verifying a site via plugin API
|
||||
// Uses API key authentication (site context from middleware)
|
||||
func (h *PluginVerifyHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID and site ID from API key middleware context
|
||||
tenantIDStr, ok := r.Context().Value(constants.SiteTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
siteIDStr, ok := r.Context().Value(constants.SiteID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("site ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Site context required")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID")
|
||||
return
|
||||
}
|
||||
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid site ID", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid site ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("plugin verify request",
|
||||
zap.String("tenant_id", tenantID.String()),
|
||||
zap.String("site_id", siteID.String()))
|
||||
|
||||
// Call verification service (reuses existing DNS verification logic)
|
||||
input := &siteusecase.VerifySiteInput{}
|
||||
output, err := h.service.VerifySite(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("verification failed",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteID.String()))
|
||||
|
||||
// Provide user-friendly error messages
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "DNS TXT record not found") {
|
||||
httperror.ProblemBadRequest(w, "DNS TXT record not found. Please ensure you've added the verification record to your domain's DNS settings and wait 5-10 minutes for propagation.")
|
||||
return
|
||||
}
|
||||
if strings.Contains(errMsg, "DNS lookup timed out") || strings.Contains(errMsg, "timeout") {
|
||||
httperror.ProblemBadRequest(w, "DNS lookup timed out. Please check that your domain's DNS is properly configured.")
|
||||
return
|
||||
}
|
||||
if strings.Contains(errMsg, "domain not found") {
|
||||
httperror.ProblemBadRequest(w, "Domain not found. Please check that your domain is properly registered and DNS is active.")
|
||||
return
|
||||
}
|
||||
if strings.Contains(errMsg, "DNS verification failed") {
|
||||
httperror.ProblemBadRequest(w, "DNS verification failed. Please check your DNS settings and try again.")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "Failed to verify site. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
// Success response
|
||||
response := VerifyResponse{
|
||||
Success: output.Success,
|
||||
Status: output.Status,
|
||||
Message: output.Message,
|
||||
}
|
||||
|
||||
h.logger.Info("site verified successfully via plugin",
|
||||
zap.String("site_id", siteID.String()))
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/httpresponse"
|
||||
)
|
||||
|
||||
// VersionHandler handles version requests from WordPress plugin
|
||||
type VersionHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideVersionHandler creates a new VersionHandler
|
||||
func ProvideVersionHandler(logger *zap.Logger) *VersionHandler {
|
||||
return &VersionHandler{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// VersionResponse represents the response for the version endpoint
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
APIVersion string `json:"api_version"`
|
||||
Environment string `json:"environment"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// Handle processes GET /api/v1/plugin/version requests
|
||||
func (h *VersionHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Version endpoint called",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
response := VersionResponse{
|
||||
Version: "1.0.0",
|
||||
APIVersion: "v1",
|
||||
Environment: "production", // Could be made configurable via environment variable
|
||||
Status: "operational",
|
||||
}
|
||||
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue