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
146
cloud/maplefile-backend/internal/service/me/delete.go
Normal file
146
cloud/maplefile-backend/internal/service/me/delete.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue