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,157 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
sitedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/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/dns"
|
||||
"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"
|
||||
)
|
||||
|
||||
// CreateHandler handles site creation HTTP requests
|
||||
type CreateHandler struct {
|
||||
service siteservice.CreateSiteService
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideCreateHandler creates a new CreateHandler
|
||||
func ProvideCreateHandler(service siteservice.CreateSiteService, cfg *config.Config, logger *zap.Logger) *CreateHandler {
|
||||
return &CreateHandler{
|
||||
service: service,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for creating a site
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *CreateHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context (populated by TenantMiddleware)
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "tenant context required")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 sitedto.CreateRequest
|
||||
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("site creation request validation failed", zap.Error(err))
|
||||
|
||||
// Check if it's a structured validation error (RFC 9457 format)
|
||||
if validationErr, ok := err.(*sitedto.ValidationErrors); ok {
|
||||
httperror.ValidationError(w, validationErr.Errors, "One or more validation errors occurred")
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback for non-structured errors
|
||||
httperror.ProblemBadRequest(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Extract domain from site URL
|
||||
parsedURL, err := url.Parse(req.SiteURL)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to parse site URL", zap.Error(err), zap.String("site_url", req.SiteURL))
|
||||
httperror.ValidationError(w, map[string][]string{
|
||||
"site_url": {"Invalid URL format. Please provide a valid URL (e.g., https://example.com)."},
|
||||
}, "One or more validation errors occurred")
|
||||
return
|
||||
}
|
||||
|
||||
domain := parsedURL.Hostname()
|
||||
if domain == "" {
|
||||
h.logger.Warn("could not extract domain from site URL", zap.String("site_url", req.SiteURL))
|
||||
httperror.ValidationError(w, map[string][]string{
|
||||
"site_url": {"Could not extract domain from URL. Please provide a valid URL with a hostname."},
|
||||
}, "One or more validation errors occurred")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine test mode based on environment
|
||||
testMode := h.config.App.IsTestMode()
|
||||
|
||||
h.logger.Info("creating site",
|
||||
zap.String("domain", domain),
|
||||
zap.String("site_url", req.SiteURL),
|
||||
zap.String("environment", h.config.App.Environment),
|
||||
zap.Bool("test_mode", testMode))
|
||||
|
||||
// Map DTO to use case input
|
||||
input := &siteusecase.CreateSiteInput{
|
||||
Domain: domain,
|
||||
SiteURL: req.SiteURL,
|
||||
TestMode: testMode,
|
||||
}
|
||||
|
||||
// Call service
|
||||
output, err := h.service.CreateSite(r.Context(), tenantID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create site",
|
||||
zap.Error(err),
|
||||
zap.String("domain", domain),
|
||||
zap.String("site_url", req.SiteURL),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Check for domain already exists error
|
||||
if err.Error() == "domain already exists" {
|
||||
httperror.ProblemConflict(w, "This domain is already registered. Each domain can only be registered once.")
|
||||
return
|
||||
}
|
||||
|
||||
httperror.ProblemInternalServerError(w, "Failed to create site. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := sitedto.CreateResponse{
|
||||
ID: output.ID,
|
||||
Domain: output.Domain,
|
||||
SiteURL: output.SiteURL,
|
||||
APIKey: output.APIKey, // Only shown once!
|
||||
Status: output.Status,
|
||||
VerificationToken: output.VerificationToken,
|
||||
SearchIndexName: output.SearchIndexName,
|
||||
VerificationInstructions: dns.GetVerificationInstructions(output.Domain, output.VerificationToken),
|
||||
}
|
||||
|
||||
h.logger.Info("site created successfully",
|
||||
zap.String("site_id", output.ID),
|
||||
zap.String("domain", output.Domain),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Write response with pretty JSON
|
||||
httpresponse.Created(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// DeleteHandler handles site deletion HTTP requests
|
||||
type DeleteHandler struct {
|
||||
service siteservice.DeleteSiteService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideDeleteHandler creates a new DeleteHandler
|
||||
func ProvideDeleteHandler(service siteservice.DeleteSiteService, logger *zap.Logger) *DeleteHandler {
|
||||
return &DeleteHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for deleting a site
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *DeleteHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Tenant context is required to access this resource.")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID format. Please ensure you have a valid session.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get site ID from path parameter
|
||||
siteIDStr := r.PathValue("id")
|
||||
if siteIDStr == "" {
|
||||
httperror.ProblemBadRequest(w, "Site ID is required in the request path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
if _, err := gocql.ParseUUID(siteIDStr); err != nil {
|
||||
httperror.ProblemBadRequest(w, "Invalid site ID format. Please provide a valid site ID.")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
input := &siteusecase.DeleteSiteInput{SiteID: siteIDStr}
|
||||
_, err = h.service.DeleteSite(r.Context(), tenantID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete site",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
httperror.ProblemNotFound(w, "The requested site could not be found. It may have been deleted or you may not have access to it.")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("site deleted successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Write response
|
||||
httpresponse.OK(w, map[string]string{
|
||||
"message": "site deleted successfully",
|
||||
"site_id": siteIDStr,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
sitedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/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"
|
||||
)
|
||||
|
||||
// GetHandler handles getting a site by ID
|
||||
type GetHandler struct {
|
||||
service siteservice.GetSiteService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideGetHandler creates a new GetHandler
|
||||
func ProvideGetHandler(service siteservice.GetSiteService, logger *zap.Logger) *GetHandler {
|
||||
return &GetHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for getting a site by ID
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *GetHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Tenant context is required to access this resource.")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID format. Please ensure you have a valid session.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get site ID from path parameter
|
||||
siteIDStr := r.PathValue("id")
|
||||
if siteIDStr == "" {
|
||||
httperror.ProblemBadRequest(w, "Site ID is required in the request path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
if _, err := gocql.ParseUUID(siteIDStr); err != nil {
|
||||
httperror.ProblemBadRequest(w, "Invalid site ID format. Please provide a valid site ID.")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
input := &siteusecase.GetSiteInput{ID: siteIDStr}
|
||||
output, err := h.service.GetSite(r.Context(), tenantID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get site",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
httperror.ProblemNotFound(w, "The requested site could not be found. It may have been deleted or you may not have access to it.")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := sitedto.GetResponse{
|
||||
ID: output.Site.ID.String(),
|
||||
TenantID: output.Site.TenantID.String(),
|
||||
Domain: output.Site.Domain,
|
||||
SiteURL: output.Site.SiteURL,
|
||||
APIKeyPrefix: output.Site.APIKeyPrefix,
|
||||
APIKeyLastFour: output.Site.APIKeyLastFour,
|
||||
Status: output.Site.Status,
|
||||
IsVerified: output.Site.IsVerified,
|
||||
SearchIndexName: output.Site.SearchIndexName,
|
||||
TotalPagesIndexed: output.Site.TotalPagesIndexed,
|
||||
LastIndexedAt: output.Site.LastIndexedAt,
|
||||
PluginVersion: output.Site.PluginVersion,
|
||||
StorageUsedBytes: output.Site.StorageUsedBytes,
|
||||
SearchRequestsCount: output.Site.SearchRequestsCount,
|
||||
MonthlyPagesIndexed: output.Site.MonthlyPagesIndexed,
|
||||
LastResetAt: output.Site.LastResetAt,
|
||||
Language: output.Site.Language,
|
||||
Timezone: output.Site.Timezone,
|
||||
Notes: output.Site.Notes,
|
||||
CreatedAt: output.Site.CreatedAt,
|
||||
UpdatedAt: output.Site.UpdatedAt,
|
||||
}
|
||||
|
||||
// Write response with pretty JSON
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
sitedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/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"
|
||||
)
|
||||
|
||||
// ListHandler handles listing sites for a tenant
|
||||
type ListHandler struct {
|
||||
service siteservice.ListSitesService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideListHandler creates a new ListHandler
|
||||
func ProvideListHandler(service siteservice.ListSitesService, logger *zap.Logger) *ListHandler {
|
||||
return &ListHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for listing sites
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *ListHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Tenant context is required to access this resource.")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID format. Please ensure you have a valid session.")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
input := &siteusecase.ListSitesInput{}
|
||||
output, err := h.service.ListSites(r.Context(), tenantID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to list sites",
|
||||
zap.Error(err),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
httperror.ProblemInternalServerError(w, "Failed to retrieve your sites. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
items := make([]sitedto.SiteListItem, len(output.Sites))
|
||||
for i, s := range output.Sites {
|
||||
items[i] = sitedto.SiteListItem{
|
||||
ID: s.ID.String(),
|
||||
Domain: s.Domain,
|
||||
Status: s.Status,
|
||||
IsVerified: s.IsVerified,
|
||||
TotalPagesIndexed: s.TotalPagesIndexed,
|
||||
CreatedAt: s.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := sitedto.ListResponse{
|
||||
Sites: items,
|
||||
Total: len(items),
|
||||
}
|
||||
|
||||
// Write response with pretty JSON
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config/constants"
|
||||
sitedto "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/interface/http/dto/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"
|
||||
)
|
||||
|
||||
// RotateAPIKeyHandler handles API key rotation HTTP requests
|
||||
type RotateAPIKeyHandler struct {
|
||||
service siteservice.RotateAPIKeyService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideRotateAPIKeyHandler creates a new RotateAPIKeyHandler
|
||||
func ProvideRotateAPIKeyHandler(service siteservice.RotateAPIKeyService, logger *zap.Logger) *RotateAPIKeyHandler {
|
||||
return &RotateAPIKeyHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for rotating a site's API key
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *RotateAPIKeyHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Tenant context is required to access this resource.")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID format. Please ensure you have a valid session.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get site ID from path parameter
|
||||
siteIDStr := r.PathValue("id")
|
||||
if siteIDStr == "" {
|
||||
httperror.ProblemBadRequest(w, "Site ID is required in the request path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
if _, err := gocql.ParseUUID(siteIDStr); err != nil {
|
||||
httperror.ProblemBadRequest(w, "Invalid site ID format. Please provide a valid site ID.")
|
||||
return
|
||||
}
|
||||
|
||||
// Call service
|
||||
input := &siteusecase.RotateAPIKeyInput{SiteID: siteIDStr}
|
||||
output, err := h.service.RotateAPIKey(r.Context(), tenantID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to rotate API key",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
httperror.ProblemNotFound(w, "The requested site could not be found. It may have been deleted or you may not have access to it.")
|
||||
return
|
||||
}
|
||||
|
||||
// Map to response DTO
|
||||
response := sitedto.RotateAPIKeyResponse{
|
||||
NewAPIKey: output.NewAPIKey, // Only shown once!
|
||||
OldKeyLastFour: output.OldKeyLastFour,
|
||||
RotatedAt: output.RotatedAt,
|
||||
}
|
||||
|
||||
h.logger.Info("API key rotated successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Write response
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package site
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// VerifySiteHandler handles site verification HTTP requests
|
||||
type VerifySiteHandler struct {
|
||||
service siteservice.VerifySiteService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ProvideVerifySiteHandler creates a new VerifySiteHandler
|
||||
func ProvideVerifySiteHandler(service siteservice.VerifySiteService, logger *zap.Logger) *VerifySiteHandler {
|
||||
return &VerifySiteHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyResponse represents the verification response
|
||||
// No request body needed - verification is done via DNS TXT record
|
||||
type VerifyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// contains checks if a string contains a substring (helper for error checking)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
|
||||
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
||||
findSubstring(s, substr)))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle handles the HTTP request for verifying a site
|
||||
// Requires JWT authentication and tenant context
|
||||
func (h *VerifySiteHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
// Get tenant ID from context
|
||||
tenantIDStr, ok := r.Context().Value(constants.ContextKeyTenantID).(string)
|
||||
if !ok {
|
||||
h.logger.Error("tenant ID not found in context")
|
||||
httperror.ProblemUnauthorized(w, "Tenant context is required to access this resource.")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := gocql.ParseUUID(tenantIDStr)
|
||||
if err != nil {
|
||||
h.logger.Error("invalid tenant ID format", zap.Error(err))
|
||||
httperror.ProblemBadRequest(w, "Invalid tenant ID format. Please ensure you have a valid session.")
|
||||
return
|
||||
}
|
||||
|
||||
// Get site ID from path parameter
|
||||
siteIDStr := r.PathValue("id")
|
||||
if siteIDStr == "" {
|
||||
httperror.ProblemBadRequest(w, "Site ID is required in the request path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
siteID, err := gocql.ParseUUID(siteIDStr)
|
||||
if err != nil {
|
||||
httperror.ProblemBadRequest(w, "Invalid site ID format. Please provide a valid site ID.")
|
||||
return
|
||||
}
|
||||
|
||||
// No request body needed - DNS verification uses the token stored in the site entity
|
||||
// Call service with empty input
|
||||
input := &siteusecase.VerifySiteInput{}
|
||||
output, err := h.service.VerifySite(r.Context(), tenantID, siteID, input)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to verify site",
|
||||
zap.Error(err),
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Check for specific error types
|
||||
errMsg := err.Error()
|
||||
|
||||
if errMsg == "site not found" {
|
||||
httperror.ProblemNotFound(w, "The requested site could not be found. It may have been deleted or you may not have access to it.")
|
||||
return
|
||||
}
|
||||
|
||||
// DNS-related errors
|
||||
if contains(errMsg, "DNS TXT record not found") {
|
||||
httperror.ProblemBadRequest(w, "DNS TXT record not found. Please add the verification record to your domain's DNS settings and wait 5-10 minutes for propagation.")
|
||||
return
|
||||
}
|
||||
if contains(errMsg, "DNS lookup timed out") {
|
||||
httperror.ProblemBadRequest(w, "DNS lookup timed out. Please check that your domain's DNS is properly configured.")
|
||||
return
|
||||
}
|
||||
if 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 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
|
||||
}
|
||||
|
||||
// Map to response
|
||||
response := VerifyResponse{
|
||||
Success: output.Success,
|
||||
Status: output.Status,
|
||||
Message: output.Message,
|
||||
}
|
||||
|
||||
h.logger.Info("site verified successfully",
|
||||
zap.String("site_id", siteIDStr),
|
||||
zap.String("tenant_id", tenantID.String()))
|
||||
|
||||
// Write response
|
||||
httpresponse.OK(w, response)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue