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,183 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue