146 lines
4.6 KiB
Go
146 lines
4.6 KiB
Go
// 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
|
|
}
|