// monorepo/cloud/backend/internal/service/user/complete_deletion.go package user import ( "context" "fmt" "time" "go.uber.org/zap" "github.com/gocql/gocql" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection" svc_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/file" uc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection" uc_filemetadata "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata" uc_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storagedailyusage" uc_storageusageevent "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/storageusageevent" uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror" ) // CompleteUserDeletionRequest represents a GDPR right-to-be-forgotten deletion request type CompleteUserDeletionRequest struct { UserID gocql.UUID `json:"user_id"` Password string `json:"password"` // For authentication } // DeletionResult contains comprehensive information about the deletion operation type DeletionResult struct { UserID gocql.UUID `json:"user_id"` FilesDeleted int `json:"files_deleted"` CollectionsDeleted int `json:"collections_deleted"` S3ObjectsDeleted int `json:"s3_objects_deleted"` TotalDataSizeBytes int64 `json:"total_data_size_bytes"` MembershipsRemoved int `json:"memberships_removed"` DeletedAt time.Time `json:"deleted_at"` Success bool `json:"success"` Errors []string `json:"errors,omitempty"` // Non-fatal errors } // CompleteUserDeletionService orchestrates complete GDPR-compliant user deletion type CompleteUserDeletionService interface { Execute(ctx context.Context, req *CompleteUserDeletionRequest) (*DeletionResult, error) } type completeUserDeletionServiceImpl struct { config *config.Configuration logger *zap.Logger getUserUseCase uc_user.UserGetByIDUseCase deleteUserByIDUseCase uc_user.UserDeleteByIDUseCase listFilesByOwnerIDService svc_file.ListFilesByOwnerIDService softDeleteFileService svc_file.SoftDeleteFileService listCollectionsByUserUseCase uc_collection.ListCollectionsByUserUseCase softDeleteCollectionService svc_collection.SoftDeleteCollectionService removeUserFromAllCollectionsUseCase uc_collection.RemoveUserFromAllCollectionsUseCase deleteStorageDailyUsageUseCase uc_storagedailyusage.DeleteByUserUseCase deleteStorageUsageEventUseCase uc_storageusageevent.DeleteByUserUseCase anonymizeUserIPsImmediatelyUseCase uc_user.AnonymizeUserIPsImmediatelyUseCase clearUserCacheUseCase uc_user.ClearUserCacheUseCase anonymizeFileUserReferencesUseCase uc_filemetadata.AnonymizeUserReferencesUseCase anonymizeCollectionUserReferencesUseCase uc_collection.AnonymizeUserReferencesUseCase } func NewCompleteUserDeletionService( config *config.Configuration, logger *zap.Logger, getUserUseCase uc_user.UserGetByIDUseCase, deleteUserByIDUseCase uc_user.UserDeleteByIDUseCase, listFilesByOwnerIDService svc_file.ListFilesByOwnerIDService, softDeleteFileService svc_file.SoftDeleteFileService, listCollectionsByUserUseCase uc_collection.ListCollectionsByUserUseCase, softDeleteCollectionService svc_collection.SoftDeleteCollectionService, removeUserFromAllCollectionsUseCase uc_collection.RemoveUserFromAllCollectionsUseCase, deleteStorageDailyUsageUseCase uc_storagedailyusage.DeleteByUserUseCase, deleteStorageUsageEventUseCase uc_storageusageevent.DeleteByUserUseCase, anonymizeUserIPsImmediatelyUseCase uc_user.AnonymizeUserIPsImmediatelyUseCase, clearUserCacheUseCase uc_user.ClearUserCacheUseCase, anonymizeFileUserReferencesUseCase uc_filemetadata.AnonymizeUserReferencesUseCase, anonymizeCollectionUserReferencesUseCase uc_collection.AnonymizeUserReferencesUseCase, ) CompleteUserDeletionService { logger = logger.Named("CompleteUserDeletionService") return &completeUserDeletionServiceImpl{ config: config, logger: logger, getUserUseCase: getUserUseCase, deleteUserByIDUseCase: deleteUserByIDUseCase, listFilesByOwnerIDService: listFilesByOwnerIDService, softDeleteFileService: softDeleteFileService, listCollectionsByUserUseCase: listCollectionsByUserUseCase, softDeleteCollectionService: softDeleteCollectionService, removeUserFromAllCollectionsUseCase: removeUserFromAllCollectionsUseCase, deleteStorageDailyUsageUseCase: deleteStorageDailyUsageUseCase, deleteStorageUsageEventUseCase: deleteStorageUsageEventUseCase, anonymizeUserIPsImmediatelyUseCase: anonymizeUserIPsImmediatelyUseCase, clearUserCacheUseCase: clearUserCacheUseCase, anonymizeFileUserReferencesUseCase: anonymizeFileUserReferencesUseCase, anonymizeCollectionUserReferencesUseCase: anonymizeCollectionUserReferencesUseCase, } } func (svc *completeUserDeletionServiceImpl) Execute(ctx context.Context, req *CompleteUserDeletionRequest) (*DeletionResult, error) { // // STEP 0: Validation // if req == nil { svc.logger.Warn("Failed validation with nil request") return nil, httperror.NewForBadRequestWithSingleField("non_field_error", "Request is required") } e := make(map[string]string) if req.UserID.String() == "" { e["user_id"] = "User ID is required" } if len(e) != 0 { svc.logger.Warn("Failed validating complete user deletion", zap.Any("error", e)) return nil, httperror.NewForBadRequest(&e) } result := &DeletionResult{ UserID: req.UserID, DeletedAt: time.Now(), Errors: []string{}, } svc.logger.Info("🚨 Starting GDPR right-to-be-forgotten complete user deletion", zap.String("user_id", req.UserID.String())) // // STEP 1: Verify user exists // user, err := svc.getUserUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("User not found for deletion", zap.String("user_id", req.UserID.String()), zap.Error(err)) return nil, err } svc.logger.Info("User verified for deletion", zap.String("user_id", req.UserID.String()), zap.String("email", user.Email)) // // STEP 2: List and hard delete all user files // svc.logger.Info("Step 2/11: Deleting user files...") listFilesReq := &svc_file.ListFilesByOwnerIDRequestDTO{OwnerID: req.UserID} filesResp, err := svc.listFilesByOwnerIDService.Execute(ctx, listFilesReq) if err != nil { svc.logger.Error("Failed to list user files", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("List files: %v", err)) } else { result.FilesDeleted = len(filesResp.Files) svc.logger.Info("Found files to delete", zap.Int("file_count", result.FilesDeleted)) // Hard delete each file (no tombstone - GDPR mode) for _, file := range filesResp.Files { deleteFileReq := &svc_file.SoftDeleteFileRequestDTO{ FileID: file.ID, ForceHardDelete: true, // GDPR mode - immediate permanent deletion } deleteResp, err := svc.softDeleteFileService.Execute(ctx, deleteFileReq) if err != nil { svc.logger.Error("Failed to delete file", zap.String("file_id", file.ID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("File %s: %v", file.ID, err)) } else { result.S3ObjectsDeleted++ result.TotalDataSizeBytes += deleteResp.ReleasedBytes } } } // // STEP 3: List and hard delete all user collections // svc.logger.Info("Step 3/11: Deleting user collections...") collections, err := svc.listCollectionsByUserUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to list user collections", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("List collections: %v", err)) } else { result.CollectionsDeleted = len(collections) svc.logger.Info("Found collections to delete", zap.Int("collection_count", result.CollectionsDeleted)) // Hard delete each collection (no tombstone - GDPR mode) for _, collection := range collections { deleteColReq := &svc_collection.SoftDeleteCollectionRequestDTO{ ID: collection.ID, ForceHardDelete: true, // GDPR mode - immediate permanent deletion } _, err := svc.softDeleteCollectionService.Execute(ctx, deleteColReq) if err != nil { svc.logger.Error("Failed to delete collection", zap.String("collection_id", collection.ID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Collection %s: %v", collection.ID, err)) } } } // // STEP 4: Remove user from shared collections // svc.logger.Info("Step 4/11: Removing user from shared collections...") removedCount, err := svc.removeUserFromAllCollectionsUseCase.Execute(ctx, req.UserID, user.Email) if err != nil { svc.logger.Error("Failed to remove user from shared collections", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Membership cleanup: %v", err)) } else { result.MembershipsRemoved = removedCount svc.logger.Info("Removed user from shared collections", zap.Int("memberships_removed", removedCount)) } // // STEP 5: Delete storage daily usage data // svc.logger.Info("Step 5/11: Deleting storage daily usage data...") err = svc.deleteStorageDailyUsageUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to delete storage daily usage", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Storage daily usage: %v", err)) } // // STEP 6: Delete storage usage events // svc.logger.Info("Step 6/11: Deleting storage usage events...") err = svc.deleteStorageUsageEventUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to delete storage usage events", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Storage usage events: %v", err)) } // // STEP 7: Anonymize all IP addresses // svc.logger.Info("Step 7/11: Anonymizing IP addresses...") err = svc.anonymizeUserIPsImmediatelyUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to anonymize IP addresses", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("IP anonymization: %v", err)) } // // STEP 8: Anonymize user references in files (CreatedByUserID/ModifiedByUserID) // svc.logger.Info("Step 8/11: Anonymizing user references in files...") filesUpdated, err := svc.anonymizeFileUserReferencesUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to anonymize user references in files", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("File user references: %v", err)) } else { svc.logger.Info("Anonymized user references in files", zap.Int("files_updated", filesUpdated)) } // // STEP 9: Anonymize user references in collections (CreatedByUserID/ModifiedByUserID/GrantedByID) // svc.logger.Info("Step 9/11: Anonymizing user references in collections...") collectionsUpdated, err := svc.anonymizeCollectionUserReferencesUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("Failed to anonymize user references in collections", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Collection user references: %v", err)) } else { svc.logger.Info("Anonymized user references in collections", zap.Int("collections_updated", collectionsUpdated)) } // // STEP 10: Clear cache and session data // svc.logger.Info("Step 10/11: Clearing cache and session data...") err = svc.clearUserCacheUseCase.Execute(ctx, req.UserID, user.Email) if err != nil { svc.logger.Error("Failed to clear user cache", zap.String("user_id", req.UserID.String()), zap.Error(err)) result.Errors = append(result.Errors, fmt.Sprintf("Cache cleanup: %v", err)) } // // STEP 11: Delete user account (final step - point of no return) // svc.logger.Info("Step 11/11: Deleting user account (final step)...") err = svc.deleteUserByIDUseCase.Execute(ctx, req.UserID) if err != nil { svc.logger.Error("CRITICAL: User account deletion failed", zap.String("user_id", req.UserID.String()), zap.Error(err)) return nil, fmt.Errorf("CRITICAL: User account deletion failed: %w", err) } // // SUCCESS // result.Success = true svc.logger.Info("✅ GDPR right-to-be-forgotten complete user deletion SUCCEEDED", zap.String("user_id", req.UserID.String()), zap.String("email", user.Email), zap.Int("files_deleted", result.FilesDeleted), zap.Int("collections_deleted", result.CollectionsDeleted), zap.Int("s3_objects_deleted", result.S3ObjectsDeleted), zap.Int("memberships_removed", result.MembershipsRemoved), zap.Int64("data_size_bytes", result.TotalDataSizeBytes), zap.Int("non_fatal_errors", len(result.Errors))) if len(result.Errors) > 0 { svc.logger.Warn("Deletion completed with non-fatal errors", zap.Strings("errors", result.Errors)) } return result, nil }