// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/me/delete.go package me import ( "context" "errors" "go.uber.org/zap" "github.com/gocql/gocql" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants" dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user" svc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/user" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror" sstring "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/securestring" ) type DeleteMeRequestDTO struct { Password string `json:"password"` } type DeleteMeService interface { Execute(sessCtx context.Context, req *DeleteMeRequestDTO) error } type deleteMeServiceImpl struct { config *config.Configuration logger *zap.Logger completeUserDeletionService svc_user.CompleteUserDeletionService } func NewDeleteMeService( config *config.Configuration, logger *zap.Logger, completeUserDeletionService svc_user.CompleteUserDeletionService, ) DeleteMeService { logger = logger.Named("DeleteMeService") return &deleteMeServiceImpl{ config: config, logger: logger, completeUserDeletionService: completeUserDeletionService, } } func (svc *deleteMeServiceImpl) Execute(sessCtx context.Context, req *DeleteMeRequestDTO) error { // // STEP 1: Validation // if req == nil { svc.logger.Warn("Failed validation with nil request") return httperror.NewForBadRequestWithSingleField("non_field_error", "Password is required") } e := make(map[string]string) if req.Password == "" { e["password"] = "Password is required" } if len(e) != 0 { svc.logger.Warn("Failed validation", zap.Any("error", e)) return httperror.NewForBadRequest(&e) } // // STEP 2: Get required from context. // sessionUserID, ok := sessCtx.Value(constants.SessionUserID).(gocql.UUID) if !ok { svc.logger.Error("Failed getting local user id", zap.Any("error", "Not found in context: user_id")) return errors.New("user id not found in context") } // Defend against admin deleting themselves sessionUserRole, _ := sessCtx.Value(constants.SessionUserRole).(int8) if sessionUserRole == dom_user.UserRoleRoot { svc.logger.Warn("admin is not allowed to delete themselves", zap.Any("error", "")) return httperror.NewForForbiddenWithSingleField("message", "admins do not have permission to delete themselves") } // // STEP 3: Verify password (intent confirmation). // securePassword, err := sstring.NewSecureString(req.Password) if err != nil { svc.logger.Error("Failed to create secure string", zap.Any("error", err)) return err } defer securePassword.Wipe() // NOTE: In this E2EE architecture, the server does not store password hashes. // Password verification happens client-side during key derivation. // The frontend must verify the password locally before calling this endpoint // by successfully deriving the KEK and decrypting the master key. // If the password is wrong, the client-side decryption will fail. // // The password field in the request serves as a confirmation that the user // intentionally wants to delete their account (not cryptographic verification). _ = securePassword // Password used for user intent confirmation // // STEP 4: Execute GDPR right-to-be-forgotten complete deletion // svc.logger.Info("Starting GDPR right-to-be-forgotten complete user deletion", zap.String("user_id", sessionUserID.String())) deletionReq := &svc_user.CompleteUserDeletionRequest{ UserID: sessionUserID, Password: req.Password, } result, err := svc.completeUserDeletionService.Execute(sessCtx, deletionReq) if err != nil { svc.logger.Error("Failed to complete user deletion", zap.Error(err), zap.String("user_id", sessionUserID.String())) return err } // // SUCCESS: User account and all data permanently deleted (GDPR compliant) // svc.logger.Info("User account successfully deleted (GDPR right-to-be-forgotten)", zap.String("user_id", sessionUserID.String()), 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 nil }