// monorepo/cloud/backend/internal/maplefile/interface/http/collection/share_collection.go package collection import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "go.uber.org/zap" "github.com/gocql/gocql" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware" svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation" ) type ShareCollectionHTTPHandler struct { config *config.Configuration logger *zap.Logger service svc_collection.ShareCollectionService middleware middleware.Middleware } func NewShareCollectionHTTPHandler( config *config.Configuration, logger *zap.Logger, service svc_collection.ShareCollectionService, middleware middleware.Middleware, ) *ShareCollectionHTTPHandler { logger = logger.Named("ShareCollectionHTTPHandler") return &ShareCollectionHTTPHandler{ config: config, logger: logger, service: service, middleware: middleware, } } func (*ShareCollectionHTTPHandler) Pattern() string { return "POST /api/v1/collections/{id}/share" } func (h *ShareCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Apply middleware before handling the request h.middleware.Attach(h.Execute)(w, req) } func (h *ShareCollectionHTTPHandler) unmarshalRequest( ctx context.Context, r *http.Request, collectionID gocql.UUID, ) (*svc_collection.ShareCollectionRequestDTO, error) { // Initialize our structure which will store the parsed request data var requestData svc_collection.ShareCollectionRequestDTO defer r.Body.Close() var rawJSON bytes.Buffer teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it // Read the JSON string and convert it into our golang struct err := json.NewDecoder(teeReader).Decode(&requestData) if err != nil { h.logger.Error("JSON decoding error", zap.Any("err", err), zap.String("raw_json", rawJSON.String()), ) return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong") } // Log the decoded request for debugging (PII masked for security) h.logger.Debug("decoded share collection request", zap.String("collection_id_from_url", collectionID.String()), zap.String("collection_id_from_body", requestData.CollectionID.String()), zap.String("recipient_id", requestData.RecipientID.String()), zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail)), zap.String("permission_level", requestData.PermissionLevel), zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey)), zap.Bool("share_with_descendants", requestData.ShareWithDescendants)) // CRITICAL: Check if encrypted collection key is present in the request if len(requestData.EncryptedCollectionKey) == 0 { h.logger.Error("FRONTEND BUG: encrypted_collection_key is missing from request", zap.String("collection_id", collectionID.String()), zap.String("recipient_id", requestData.RecipientID.String()), zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail))) // Log raw JSON at debug level only to avoid PII exposure in production logs h.logger.Debug("raw request body for debugging", zap.String("collection_id", collectionID.String()), zap.String("raw_json", rawJSON.String())) } else { h.logger.Debug("encrypted_collection_key found in request", zap.String("collection_id", collectionID.String()), zap.String("recipient_id", requestData.RecipientID.String()), zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey))) } // Set the collection ID from the URL parameter requestData.CollectionID = collectionID return &requestData, nil } func (h *ShareCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) { // Set response content type w.Header().Set("Content-Type", "application/json") ctx := r.Context() // Extract collection ID from URL parameters collectionIDStr := r.PathValue("id") if collectionIDStr == "" { httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required")) return } // Convert string ID to ObjectID collectionID, err := gocql.ParseUUID(collectionIDStr) if err != nil { h.logger.Error("invalid collection ID format", zap.String("collection_id", collectionIDStr), zap.Error(err)) httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format")) return } h.logger.Info("processing share collection request", zap.String("collection_id", collectionID.String()), zap.String("method", r.Method), zap.String("content_type", r.Header.Get("Content-Type"))) req, err := h.unmarshalRequest(ctx, r, collectionID) if err != nil { httperror.RespondWithError(w, r, err) return } // Call service resp, err := h.service.Execute(ctx, req) if err != nil { h.logger.Error("share collection service failed", zap.String("collection_id", collectionID.String()), zap.String("recipient_id", req.RecipientID.String()), zap.Error(err)) httperror.RespondWithError(w, r, err) return } // Encode response if resp != nil { if err := json.NewEncoder(w).Encode(resp); err != nil { h.logger.Error("failed to encode response", zap.Any("error", err)) httperror.RespondWithError(w, r, err) return } } else { err := errors.New("no result") httperror.RespondWithError(w, r, err) return } }