monorepo/cloud/maplefile-backend/internal/service/user/complete_deletion.go

348 lines
14 KiB
Go

// 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
}