monorepo/cloud/maplefile-backend/internal/service/collection/remove_member.go

183 lines
5.8 KiB
Go

// monorepo/cloud/backend/internal/maplefile/service/collection/remove_member.go
package collection
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
"github.com/gocql/gocql"
)
type RemoveMemberRequestDTO struct {
CollectionID gocql.UUID `json:"collection_id"`
RecipientID gocql.UUID `json:"recipient_id"`
RemoveFromDescendants bool `json:"remove_from_descendants"`
}
type RemoveMemberResponseDTO struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type RemoveMemberService interface {
Execute(ctx context.Context, req *RemoveMemberRequestDTO) (*RemoveMemberResponseDTO, error)
}
type removeMemberServiceImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewRemoveMemberService(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveMemberService {
logger = logger.Named("RemoveMemberService")
return &removeMemberServiceImpl{
config: config,
logger: logger,
repo: repo,
}
}
func (svc *removeMemberServiceImpl) Execute(ctx context.Context, req *RemoveMemberRequestDTO) (*RemoveMemberResponseDTO, error) {
//
// STEP 1: Validation
//
if req == nil {
svc.logger.Warn("Failed validation with nil request")
return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "Remove member details are required")
}
e := make(map[string]string)
if req.CollectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if req.RecipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if len(e) != 0 {
svc.logger.Warn("Failed validation",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get user ID from context
//
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
if !ok {
svc.logger.Error("Failed getting user ID from context")
return nil, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error")
}
//
// STEP 3: Check if user has admin access to the collection
//
hasAccess, err := svc.repo.CheckAccess(ctx, req.CollectionID, userID, dom_collection.CollectionPermissionAdmin)
if err != nil {
svc.logger.Error("Failed to check access",
zap.Any("error", err),
zap.Any("collection_id", req.CollectionID),
zap.Any("user_id", userID))
return nil, err
}
// Collection owners and admin members can remove members
if !hasAccess {
isOwner, _ := svc.repo.IsCollectionOwner(ctx, req.CollectionID, userID)
if !isOwner {
svc.logger.Warn("Unauthorized member removal attempt",
zap.Any("user_id", userID),
zap.Any("collection_id", req.CollectionID))
return nil, httperror.NewForForbiddenWithSingleField("message", "You don't have permission to remove members from this collection")
}
}
//
// SAGA: Initialize distributed transaction manager
//
saga := transaction.NewSaga("remove-member", svc.logger)
//
// STEP 4: Retrieve the membership before removing (needed for compensation)
//
existingMembership, err := svc.repo.GetCollectionMembership(ctx, req.CollectionID, req.RecipientID)
if err != nil {
svc.logger.Error("Failed to get collection membership",
zap.Any("error", err),
zap.Any("collection_id", req.CollectionID),
zap.Any("recipient_id", req.RecipientID))
return nil, err
}
if existingMembership == nil {
svc.logger.Debug("Member not found in collection",
zap.Any("collection_id", req.CollectionID),
zap.Any("recipient_id", req.RecipientID))
return nil, httperror.NewForNotFoundWithSingleField("message", "Member not found in this collection")
}
//
// STEP 5: Remove the member
//
var err2 error
if req.RemoveFromDescendants {
err2 = svc.repo.RemoveMemberFromHierarchy(ctx, req.CollectionID, req.RecipientID)
} else {
err2 = svc.repo.RemoveMember(ctx, req.CollectionID, req.RecipientID)
}
if err2 != nil {
svc.logger.Error("Failed to remove member",
zap.Any("error", err2),
zap.Any("collection_id", req.CollectionID),
zap.Any("recipient_id", req.RecipientID),
zap.Bool("remove_from_descendants", req.RemoveFromDescendants))
saga.Rollback(ctx) // Rollback any previous operations
return nil, err2
}
//
// SAGA: Register compensation to re-add the member if needed
// IMPORTANT: Capture by value for closure
//
membershipCaptured := existingMembership
collectionIDCaptured := req.CollectionID
removeFromDescendantsCaptured := req.RemoveFromDescendants
saga.AddCompensation(func(ctx context.Context) error {
svc.logger.Warn("SAGA compensation: re-adding member to collection",
zap.String("collection_id", collectionIDCaptured.String()),
zap.String("recipient_id", membershipCaptured.RecipientID.String()),
zap.Bool("add_to_descendants", removeFromDescendantsCaptured))
if removeFromDescendantsCaptured {
// Re-add to hierarchy if it was removed from hierarchy
return svc.repo.AddMemberToHierarchy(ctx, collectionIDCaptured, membershipCaptured)
}
// Re-add to single collection if it was removed from single collection
return svc.repo.AddMember(ctx, collectionIDCaptured, membershipCaptured)
})
svc.logger.Info("Member removed successfully",
zap.Any("collection_id", req.CollectionID),
zap.Any("recipient_id", req.RecipientID),
zap.Bool("removed_from_descendants", req.RemoveFromDescendants))
return &RemoveMemberResponseDTO{
Success: true,
Message: "Member removed successfully",
}, nil
}