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
|
|
@ -0,0 +1,348 @@
|
|||
// 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
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// NOTE: Unit tests for CompleteUserDeletionService would require mocks.
|
||||
// For now, this service will be tested via integration tests.
|
||||
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
|
||||
|
||||
func TestCompleteUserDeletionService_Constructor(t *testing.T) {
|
||||
// Test that constructor creates service successfully
|
||||
cfg := &config.Configuration{}
|
||||
logger := zap.NewNop()
|
||||
|
||||
service := NewCompleteUserDeletionService(
|
||||
cfg,
|
||||
logger,
|
||||
nil, // getUserUseCase
|
||||
nil, // deleteUserByIDUseCase
|
||||
nil, // listFilesByOwnerIDService
|
||||
nil, // softDeleteFileService
|
||||
nil, // listCollectionsByUserUseCase
|
||||
nil, // softDeleteCollectionService
|
||||
nil, // removeUserFromAllCollectionsUseCase
|
||||
nil, // deleteStorageDailyUsageUseCase
|
||||
nil, // deleteStorageUsageEventUseCase
|
||||
nil, // anonymizeUserIPsImmediatelyUseCase
|
||||
nil, // clearUserCacheUseCase
|
||||
nil, // anonymizeFileUserReferencesUseCase
|
||||
nil, // anonymizeCollectionUserReferencesUseCase
|
||||
)
|
||||
|
||||
if service == nil {
|
||||
t.Error("Expected service to be created, got nil")
|
||||
}
|
||||
}
|
||||
61
cloud/maplefile-backend/internal/service/user/provider.go
Normal file
61
cloud/maplefile-backend/internal/service/user/provider.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/user/provider.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// ProvideUserPublicLookupService provides the user public lookup service
|
||||
func ProvideUserPublicLookupService(
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
userGetByEmailUC uc_user.UserGetByEmailUseCase,
|
||||
) UserPublicLookupService {
|
||||
return NewUserPublicLookupService(config, logger, userGetByEmailUC)
|
||||
}
|
||||
|
||||
// ProvideCompleteUserDeletionService provides the complete GDPR user deletion service
|
||||
func ProvideCompleteUserDeletionService(
|
||||
cfg *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 {
|
||||
return NewCompleteUserDeletionService(
|
||||
cfg,
|
||||
logger,
|
||||
getUserUseCase,
|
||||
deleteUserByIDUseCase,
|
||||
listFilesByOwnerIDService,
|
||||
softDeleteFileService,
|
||||
listCollectionsByUserUseCase,
|
||||
softDeleteCollectionService,
|
||||
removeUserFromAllCollectionsUseCase,
|
||||
deleteStorageDailyUsageUseCase,
|
||||
deleteStorageUsageEventUseCase,
|
||||
anonymizeUserIPsImmediatelyUseCase,
|
||||
clearUserCacheUseCase,
|
||||
anonymizeFileUserReferencesUseCase,
|
||||
anonymizeCollectionUserReferencesUseCase,
|
||||
)
|
||||
}
|
||||
109
cloud/maplefile-backend/internal/service/user/publiclookup.go
Normal file
109
cloud/maplefile-backend/internal/service/user/publiclookup.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/user/publiclookup.go
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
type UserPublicLookupRequestDTO struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type UserPublicLookupResponseDTO struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"` // Optional: for display
|
||||
PublicKeyInBase64 string `json:"public_key_in_base64"` // Base64 encoded
|
||||
VerificationID string `json:"verification_id"`
|
||||
}
|
||||
|
||||
type UserPublicLookupService interface {
|
||||
Execute(ctx context.Context, req *UserPublicLookupRequestDTO) (*UserPublicLookupResponseDTO, error)
|
||||
}
|
||||
|
||||
type userPublicLookupServiceImpl struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
userGetByEmailUC uc_user.UserGetByEmailUseCase
|
||||
}
|
||||
|
||||
func NewUserPublicLookupService(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
userGetByEmailUC uc_user.UserGetByEmailUseCase,
|
||||
) UserPublicLookupService {
|
||||
logger = logger.Named("UserPublicLookupService")
|
||||
return &userPublicLookupServiceImpl{cfg, logger, userGetByEmailUC}
|
||||
}
|
||||
|
||||
func (svc *userPublicLookupServiceImpl) Execute(ctx context.Context, req *UserPublicLookupRequestDTO) (*UserPublicLookupResponseDTO, error) {
|
||||
//
|
||||
// STEP 1: Sanitization of the input.
|
||||
//
|
||||
|
||||
// Defensive Code: For security purposes we need to perform some sanitization on the inputs.
|
||||
req.Email = strings.ToLower(req.Email)
|
||||
req.Email = strings.ReplaceAll(req.Email, " ", "")
|
||||
req.Email = strings.ReplaceAll(req.Email, "\t", "")
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
|
||||
svc.logger.Debug("sanitized email",
|
||||
zap.String("email", validation.MaskEmail(req.Email)))
|
||||
|
||||
//
|
||||
// STEP 2: Validation of input.
|
||||
//
|
||||
|
||||
e := make(map[string]string)
|
||||
if req.Email == "" {
|
||||
e["email"] = "Email is required"
|
||||
}
|
||||
if len(req.Email) > 255 {
|
||||
e["email"] = "Email is too long"
|
||||
}
|
||||
|
||||
if len(e) != 0 {
|
||||
svc.logger.Warn("failed validating",
|
||||
zap.Any("e", e))
|
||||
return nil, httperror.NewForBadRequest(&e)
|
||||
}
|
||||
|
||||
//
|
||||
// STEP 3: Lookup user by email
|
||||
//
|
||||
|
||||
// Lookup the user in our database, else return a `400 Bad Request` error.
|
||||
// Note: We return a generic error message to prevent user enumeration attacks.
|
||||
u, err := svc.userGetByEmailUC.Execute(ctx, req.Email)
|
||||
if err != nil {
|
||||
svc.logger.Error("failed getting user by email from database",
|
||||
zap.Any("error", err))
|
||||
return nil, httperror.NewForBadRequestWithSingleField("email", "Unable to complete lookup")
|
||||
}
|
||||
if u == nil {
|
||||
svc.logger.Warn("user lookup attempted for non-existent email",
|
||||
zap.String("email", validation.MaskEmail(req.Email)))
|
||||
// Return same error message as above to prevent user enumeration
|
||||
return nil, httperror.NewForBadRequestWithSingleField("email", "Unable to complete lookup")
|
||||
}
|
||||
|
||||
// STEP 4: Build response DTO
|
||||
dto := &UserPublicLookupResponseDTO{
|
||||
UserID: u.ID.String(),
|
||||
Email: u.Email,
|
||||
Name: u.Name,
|
||||
PublicKeyInBase64: base64.StdEncoding.EncodeToString(u.SecurityData.PublicKey.Key),
|
||||
VerificationID: u.SecurityData.VerificationID,
|
||||
}
|
||||
|
||||
return dto, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue