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