Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,54 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/check.go
package blockedemail
import (
"context"
"strings"
"go.uber.org/zap"
"github.com/gocql/gocql"
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
type CheckBlockedEmailUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, email string) (bool, error)
}
type checkBlockedEmailUseCaseImpl struct {
logger *zap.Logger
repo dom_blockedemail.BlockedEmailRepository
}
func NewCheckBlockedEmailUseCase(
logger *zap.Logger,
repo dom_blockedemail.BlockedEmailRepository,
) CheckBlockedEmailUseCase {
logger = logger.Named("CheckBlockedEmailUseCase")
return &checkBlockedEmailUseCaseImpl{
logger: logger,
repo: repo,
}
}
func (uc *checkBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) (bool, error) {
// Normalize email
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
isBlocked, err := uc.repo.IsBlocked(ctx, userID, normalizedEmail)
if err != nil {
uc.logger.Error("Failed to check if email is blocked",
zap.Any("error", err),
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return false, err
}
uc.logger.Debug("Checked blocked status",
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)),
zap.Bool("is_blocked", isBlocked))
return isBlocked, nil
}

View file

@ -0,0 +1,100 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/create.go
package blockedemail
import (
"context"
"strings"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
const MaxBlockedEmails = 100
type CreateBlockedEmailUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, email string, blockedUserID gocql.UUID, reason string) (*dom_blockedemail.BlockedEmail, error)
}
type createBlockedEmailUseCaseImpl struct {
logger *zap.Logger
repo dom_blockedemail.BlockedEmailRepository
}
func NewCreateBlockedEmailUseCase(
logger *zap.Logger,
repo dom_blockedemail.BlockedEmailRepository,
) CreateBlockedEmailUseCase {
logger = logger.Named("CreateBlockedEmailUseCase")
return &createBlockedEmailUseCaseImpl{
logger: logger,
repo: repo,
}
}
func (uc *createBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string, blockedUserID gocql.UUID, reason string) (*dom_blockedemail.BlockedEmail, error) {
// Normalize email
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
// Check if email is already blocked
existing, err := uc.repo.Get(ctx, userID, normalizedEmail)
if err != nil {
uc.logger.Error("Failed to check existing blocked email",
zap.Any("error", err),
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return nil, err
}
if existing != nil {
uc.logger.Debug("Email already blocked",
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return nil, httperror.NewConflictError("This email is already blocked")
}
// Check limit
count, err := uc.repo.Count(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count blocked emails",
zap.Any("error", err),
zap.Any("user_id", userID))
return nil, err
}
if count >= MaxBlockedEmails {
uc.logger.Warn("Blocked email limit reached",
zap.Any("user_id", userID),
zap.Int("count", count),
zap.Int("limit", MaxBlockedEmails))
return nil, httperror.NewBadRequestError("You have reached the maximum number of blocked emails")
}
// Create blocked email entry
blockedEmail := &dom_blockedemail.BlockedEmail{
UserID: userID,
BlockedEmail: normalizedEmail,
BlockedUserID: blockedUserID,
Reason: reason,
CreatedAt: time.Now().UTC(),
}
err = uc.repo.Create(ctx, blockedEmail)
if err != nil {
uc.logger.Error("Failed to create blocked email",
zap.Any("error", err),
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return nil, err
}
uc.logger.Info("Blocked email created",
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return blockedEmail, nil
}

View file

@ -0,0 +1,72 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/delete.go
package blockedemail
import (
"context"
"strings"
"go.uber.org/zap"
"github.com/gocql/gocql"
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
type DeleteBlockedEmailUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, email string) error
}
type deleteBlockedEmailUseCaseImpl struct {
logger *zap.Logger
repo dom_blockedemail.BlockedEmailRepository
}
func NewDeleteBlockedEmailUseCase(
logger *zap.Logger,
repo dom_blockedemail.BlockedEmailRepository,
) DeleteBlockedEmailUseCase {
logger = logger.Named("DeleteBlockedEmailUseCase")
return &deleteBlockedEmailUseCaseImpl{
logger: logger,
repo: repo,
}
}
func (uc *deleteBlockedEmailUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) error {
// Normalize email
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
// Check if email exists in blocked list
existing, err := uc.repo.Get(ctx, userID, normalizedEmail)
if err != nil {
uc.logger.Error("Failed to check existing blocked email",
zap.Any("error", err),
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return err
}
if existing == nil {
uc.logger.Debug("Blocked email not found",
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return httperror.NewNotFoundError("Email not found in blocked list")
}
// Delete blocked email
err = uc.repo.Delete(ctx, userID, normalizedEmail)
if err != nil {
uc.logger.Error("Failed to delete blocked email",
zap.Any("error", err),
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return err
}
uc.logger.Info("Blocked email deleted",
zap.Any("user_id", userID),
zap.String("email", validation.MaskEmail(normalizedEmail)))
return nil
}

View file

@ -0,0 +1,47 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/blockedemail/list.go
package blockedemail
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
dom_blockedemail "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail"
)
type ListBlockedEmailsUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_blockedemail.BlockedEmail, error)
}
type listBlockedEmailsUseCaseImpl struct {
logger *zap.Logger
repo dom_blockedemail.BlockedEmailRepository
}
func NewListBlockedEmailsUseCase(
logger *zap.Logger,
repo dom_blockedemail.BlockedEmailRepository,
) ListBlockedEmailsUseCase {
logger = logger.Named("ListBlockedEmailsUseCase")
return &listBlockedEmailsUseCaseImpl{
logger: logger,
repo: repo,
}
}
func (uc *listBlockedEmailsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_blockedemail.BlockedEmail, error) {
blockedEmails, err := uc.repo.List(ctx, userID)
if err != nil {
uc.logger.Error("Failed to list blocked emails",
zap.Any("error", err),
zap.Any("user_id", userID))
return nil, err
}
uc.logger.Debug("Listed blocked emails",
zap.Any("user_id", userID),
zap.Int("count", len(blockedEmails)))
return blockedEmails, nil
}

View file

@ -0,0 +1,82 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/add_member.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type AddCollectionMemberUseCase interface {
Execute(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error
}
type addCollectionMemberUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewAddCollectionMemberUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AddCollectionMemberUseCase {
logger = logger.Named("AddCollectionMemberUseCase")
return &addCollectionMemberUseCaseImpl{config, logger, repo}
}
func (uc *addCollectionMemberUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if membership == nil {
e["membership"] = "Membership details are required"
} else {
// Generate member ID if not provided
if membership.ID.String() == "" || membership.ID.String() == "00000000-0000-0000-0000-000000000000" {
membership.ID = gocql.TimeUUID()
}
if membership.RecipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if membership.RecipientEmail == "" {
e["recipient_email"] = "Recipient email is required"
}
if membership.GrantedByID.String() == "" {
e["granted_by_id"] = "Granted by ID is required"
}
if len(membership.EncryptedCollectionKey) == 0 {
e["encrypted_collection_key"] = "Encrypted collection key is required"
}
if membership.PermissionLevel == "" {
// Default permission level will be set in the repository
} else if membership.PermissionLevel != dom_collection.CollectionPermissionReadOnly &&
membership.PermissionLevel != dom_collection.CollectionPermissionReadWrite &&
membership.PermissionLevel != dom_collection.CollectionPermissionAdmin {
e["permission_level"] = "Invalid permission level"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating add collection member",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Add member to collection.
//
return uc.repo.AddMember(ctx, collectionID, membership)
}

View file

@ -0,0 +1,82 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/add_member_to_hierarchy.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type AddMemberToHierarchyUseCase interface {
Execute(ctx context.Context, rootID gocql.UUID, membership *dom_collection.CollectionMembership) error
}
type addMemberToHierarchyUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewAddMemberToHierarchyUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AddMemberToHierarchyUseCase {
logger = logger.Named("AddMemberToHierarchyUseCase")
return &addMemberToHierarchyUseCaseImpl{config, logger, repo}
}
func (uc *addMemberToHierarchyUseCaseImpl) Execute(ctx context.Context, rootID gocql.UUID, membership *dom_collection.CollectionMembership) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if rootID.String() == "" {
e["root_id"] = "Root collection ID is required"
}
if membership == nil {
e["membership"] = "Membership details are required"
} else {
// Generate member ID if not provided
if membership.ID.String() == "" || membership.ID.String() == "00000000-0000-0000-0000-000000000000" {
membership.ID = gocql.TimeUUID()
}
if membership.RecipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if membership.RecipientEmail == "" {
e["recipient_email"] = "Recipient email is required"
}
if membership.GrantedByID.String() == "" {
e["granted_by_id"] = "Granted by ID is required"
}
if len(membership.EncryptedCollectionKey) == 0 {
e["encrypted_collection_key"] = "Encrypted collection key is required"
}
if membership.PermissionLevel == "" {
// Default permission level will be set in the repository
} else if membership.PermissionLevel != dom_collection.CollectionPermissionReadOnly &&
membership.PermissionLevel != dom_collection.CollectionPermissionReadWrite &&
membership.PermissionLevel != dom_collection.CollectionPermissionAdmin {
e["permission_level"] = "Invalid permission level"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating add member to hierarchy",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Add member to collection hierarchy.
//
return uc.repo.AddMemberToHierarchy(ctx, rootID, membership)
}

View file

@ -0,0 +1,50 @@
// monorepo/cloud/backend/internal/usecase/collection/anonymize_old_ips.go
package collection
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
)
type AnonymizeOldIPsUseCase interface {
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
}
type anonymizeOldIPsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewAnonymizeOldIPsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AnonymizeOldIPsUseCase {
logger = logger.Named("CollectionAnonymizeOldIPsUseCase")
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
}
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
uc.logger.Debug("Anonymizing old IPs in collection tables",
zap.Time("cutoff_date", cutoffDate))
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
if err != nil {
uc.logger.Error("Failed to anonymize old IPs in collection tables",
zap.Error(err),
zap.Time("cutoff_date", cutoffDate))
return 0, err
}
uc.logger.Info("Successfully anonymized old IPs in collection tables",
zap.Int("count", count),
zap.Time("cutoff_date", cutoffDate))
return count, nil
}

View file

@ -0,0 +1,97 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/collection/anonymize_user_references.go
package collection
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
)
// AnonymizeUserReferencesUseCase handles anonymizing CreatedByUserID and ModifiedByUserID
// references when a user is deleted, replacing them with a special "deleted user" UUID.
type AnonymizeUserReferencesUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (int, error)
}
type anonymizeUserReferencesUseCaseImpl struct {
logger *zap.Logger
repo dom_collection.CollectionRepository
}
// NewAnonymizeUserReferencesUseCase creates a new use case for anonymizing user references in collections
func NewAnonymizeUserReferencesUseCase(
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AnonymizeUserReferencesUseCase {
return &anonymizeUserReferencesUseCaseImpl{
logger: logger,
repo: repo,
}
}
// DeletedUserUUID is a well-known UUID representing a deleted user
// UUID: 00000000-0000-0000-0000-000000000001 (DELETED_USER)
var DeletedUserUUID = gocql.UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
func (uc *anonymizeUserReferencesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (int, error) {
uc.logger.Info("Anonymizing user references in collection metadata",
zap.String("user_id", userID.String()))
// Get all collections owned by this user
collections, err := uc.repo.GetAllByUserID(ctx, userID)
if err != nil {
uc.logger.Error("Failed to get collections by owner",
zap.String("user_id", userID.String()),
zap.Error(err))
return 0, fmt.Errorf("failed to get collections by owner: %w", err)
}
updatedCount := 0
// Update each collection to replace user references with deleted user UUID
for _, collection := range collections {
needsUpdate := false
// Check if this collection has references to the deleted user
if collection.CreatedByUserID == userID {
collection.CreatedByUserID = DeletedUserUUID
needsUpdate = true
}
if collection.ModifiedByUserID == userID {
collection.ModifiedByUserID = DeletedUserUUID
needsUpdate = true
}
// Also anonymize GrantedByID in collection memberships
for i := range collection.Members {
if collection.Members[i].GrantedByID == userID {
collection.Members[i].GrantedByID = DeletedUserUUID
needsUpdate = true
}
}
if needsUpdate {
// Update the collection with anonymized references
if err := uc.repo.Update(ctx, collection); err != nil {
uc.logger.Error("Failed to anonymize user references in collection",
zap.String("collection_id", collection.ID.String()),
zap.String("user_id", userID.String()),
zap.Error(err))
// Continue with other collections even if one fails
continue
}
updatedCount++
}
}
uc.logger.Info("✅ Anonymized user references in collection metadata",
zap.String("user_id", userID.String()),
zap.Int("collections_updated", updatedCount))
return updatedCount, nil
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/archive.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ArchiveCollectionUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type archiveCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewArchiveCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ArchiveCollectionUseCase {
logger = logger.Named("ArchiveCollectionUseCase")
return &archiveCollectionUseCaseImpl{config, logger, repo}
}
func (uc *archiveCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection archival",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Archive collection using repository method.
//
return uc.repo.Archive(ctx, id)
}

View file

@ -0,0 +1,65 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/check_access.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CheckCollectionAccessUseCase interface {
Execute(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error)
}
type checkCollectionAccessUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewCheckCollectionAccessUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CheckCollectionAccessUseCase {
logger = logger.Named("CheckCollectionAccessUseCase")
return &checkCollectionAccessUseCaseImpl{config, logger, repo}
}
func (uc *checkCollectionAccessUseCaseImpl) Execute(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if requiredPermission == "" {
// Default to read-only if not specified
requiredPermission = dom_collection.CollectionPermissionReadOnly
} else if requiredPermission != dom_collection.CollectionPermissionReadOnly &&
requiredPermission != dom_collection.CollectionPermissionReadWrite &&
requiredPermission != dom_collection.CollectionPermissionAdmin {
e["required_permission"] = "Invalid permission level"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating check collection access",
zap.Any("error", e))
return false, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Check access.
//
return uc.repo.CheckAccess(ctx, collectionID, userID, requiredPermission)
}

View file

@ -0,0 +1,198 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/count_collections.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// CountCollectionsResponse contains the collection counts for a user
type CountCollectionsResponse struct {
OwnedCollections int `json:"owned_collections"`
SharedCollections int `json:"shared_collections"`
TotalCollections int `json:"total_collections"`
}
// CountFoldersResponse contains the folder counts for a user (folders only, not albums)
type CountFoldersResponse struct {
OwnedFolders int `json:"owned_folders"`
SharedFolders int `json:"shared_folders"`
TotalFolders int `json:"total_folders"`
}
type CountUserCollectionsUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (*CountCollectionsResponse, error)
}
// NEW: Use case specifically for counting folders only
type CountUserFoldersUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (*CountFoldersResponse, error)
}
type countUserCollectionsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
type countUserFoldersUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewCountUserCollectionsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CountUserCollectionsUseCase {
logger = logger.Named("CountUserCollectionsUseCase")
return &countUserCollectionsUseCaseImpl{config, logger, repo}
}
func NewCountUserFoldersUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CountUserFoldersUseCase {
logger = logger.Named("CountUserFoldersUseCase")
return &countUserFoldersUseCaseImpl{config, logger, repo}
}
func (uc *countUserCollectionsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountCollectionsResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating count user collections",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Count collections.
//
ownedCollections, err := uc.repo.CountOwnedCollections(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count owned collections",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
sharedCollections, err := uc.repo.CountSharedCollections(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count shared collections",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
response := &CountCollectionsResponse{
OwnedCollections: ownedCollections,
SharedCollections: sharedCollections,
TotalCollections: ownedCollections + sharedCollections,
}
uc.logger.Debug("Successfully counted user collections",
zap.String("user_id", userID.String()),
zap.Int("owned_collections", ownedCollections),
zap.Int("shared_collections", sharedCollections),
zap.Int("total_collections", response.TotalCollections))
return response, nil
}
func (uc *countUserFoldersUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountFoldersResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating count user folders",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: DEBUG - Check what's actually in the database
//
// ADD DEBUG LOGGING - Cast to concrete type to access debug method
if debugRepo, ok := uc.repo.(interface {
DebugCollectionRecords(context.Context, gocql.UUID) error
}); ok {
if debugErr := debugRepo.DebugCollectionRecords(ctx, userID); debugErr != nil {
uc.logger.Warn("Failed to debug collection records", zap.Error(debugErr))
}
}
//
// STEP 3: Count folders with separate owned/shared counts AND total unique count
//
ownedFolders, err := uc.repo.CountOwnedFolders(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count owned folders",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
sharedFolders, err := uc.repo.CountSharedFolders(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count shared folders",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
// NEW: Get the deduplicated total count
var totalUniqueFolders int
if uniqueRepo, ok := uc.repo.(interface {
CountTotalUniqueFolders(context.Context, gocql.UUID) (int, error)
}); ok {
totalUniqueFolders, err = uniqueRepo.CountTotalUniqueFolders(ctx, userID)
if err != nil {
uc.logger.Error("Failed to count unique total folders",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
} else {
// Fallback to simple addition if the method is not available
uc.logger.Warn("CountTotalUniqueFolders method not available, using simple addition")
totalUniqueFolders = ownedFolders + sharedFolders
}
response := &CountFoldersResponse{
OwnedFolders: ownedFolders,
SharedFolders: sharedFolders,
TotalFolders: totalUniqueFolders, // Use deduplicated count
}
uc.logger.Info("Successfully counted user folders with deduplication",
zap.String("user_id", userID.String()),
zap.Int("owned_folders", ownedFolders),
zap.Int("shared_folders", sharedFolders),
zap.Int("total_unique_folders", totalUniqueFolders),
zap.Int("would_be_simple_sum", ownedFolders+sharedFolders))
return response, nil
}

View file

@ -0,0 +1,78 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/create.go
package collection
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreateCollectionUseCase interface {
Execute(ctx context.Context, collection *dom_collection.Collection) error
}
type createCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewCreateCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CreateCollectionUseCase {
logger = logger.Named("CreateCollectionUseCase")
return &createCollectionUseCaseImpl{config, logger, repo}
}
func (uc *createCollectionUseCaseImpl) Execute(ctx context.Context, collection *dom_collection.Collection) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collection == nil {
e["collection"] = "Collection is required"
} else {
if collection.OwnerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if collection.EncryptedName == "" {
e["encrypted_name"] = "Collection name is required"
}
if collection.CollectionType == "" {
e["collection_type"] = "Collection type is required"
} else if collection.CollectionType != dom_collection.CollectionTypeFolder && collection.CollectionType != dom_collection.CollectionTypeAlbum {
e["collection_type"] = "Collection type must be either 'folder' or 'album'"
}
if collection.EncryptedCollectionKey.Ciphertext == nil || len(collection.EncryptedCollectionKey.Ciphertext) == 0 {
e["encrypted_collection_key"] = "Encrypted collection key is required"
}
if collection.State == "" {
e["state"] = "File state is required"
} else if collection.State != dom_collection.CollectionStateActive &&
collection.State != dom_collection.CollectionStateDeleted &&
collection.State != dom_collection.CollectionStateArchived {
e["state"] = "Invalid collection state"
}
if err := dom_collection.IsValidStateTransition(dom_collection.CollectionStateActive, collection.State); err != nil {
e["state"] = err.Error()
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection creation",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Insert into database.
//
return uc.repo.Create(ctx, collection)
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_by_parent.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FindCollectionsByParentUseCase interface {
Execute(ctx context.Context, parentID gocql.UUID) ([]*dom_collection.Collection, error)
}
type findCollectionsByParentUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewFindCollectionsByParentUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindCollectionsByParentUseCase {
logger = logger.Named("FindCollectionsByParentUseCase")
return &findCollectionsByParentUseCaseImpl{config, logger, repo}
}
func (uc *findCollectionsByParentUseCaseImpl) Execute(ctx context.Context, parentID gocql.UUID) ([]*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if parentID.String() == "" {
e["parent_id"] = "Parent ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating find collections by parent",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Find collections by parent.
//
return uc.repo.FindByParent(ctx, parentID)
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_descendants.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FindDescendantsUseCase interface {
Execute(ctx context.Context, collectionID gocql.UUID) ([]*dom_collection.Collection, error)
}
type findDescendantsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewFindDescendantsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindDescendantsUseCase {
logger = logger.Named("FindDescendantsUseCase")
return &findDescendantsUseCaseImpl{config, logger, repo}
}
func (uc *findDescendantsUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID) ([]*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating find descendants",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Find descendants.
//
return uc.repo.FindDescendants(ctx, collectionID)
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/find_root_collections.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FindRootCollectionsUseCase interface {
Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error)
}
type findRootCollectionsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewFindRootCollectionsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindRootCollectionsUseCase {
logger = logger.Named("FindRootCollectionsUseCase")
return &findRootCollectionsUseCaseImpl{config, logger, repo}
}
func (uc *findRootCollectionsUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ownerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating find root collections",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Find root collections.
//
return uc.repo.FindRootCollections(ctx, ownerID)
}

View file

@ -0,0 +1,65 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetCollectionUseCase interface {
Execute(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error)
}
type getCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewGetCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetCollectionUseCase {
logger = logger.Named("GetCollectionUseCase")
return &getCollectionUseCaseImpl{config, logger, repo}
}
func (uc *getCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) (*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection retrieval",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
collection, err := uc.repo.Get(ctx, id)
if err != nil {
return nil, err
}
if collection == nil {
uc.logger.Debug("Collection not found",
zap.Any("id", id))
return nil, httperror.NewForNotFoundWithSingleField("message", "Collection not found")
}
return collection, nil
}

View file

@ -0,0 +1,70 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get_filtered.go
package collection
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFilteredCollectionsUseCase interface {
Execute(ctx context.Context, options dom_collection.CollectionFilterOptions) (*dom_collection.CollectionFilterResult, error)
}
type getFilteredCollectionsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewGetFilteredCollectionsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetFilteredCollectionsUseCase {
logger = logger.Named("GetFilteredCollectionsUseCase")
return &getFilteredCollectionsUseCaseImpl{config, logger, repo}
}
func (uc *getFilteredCollectionsUseCaseImpl) Execute(ctx context.Context, options dom_collection.CollectionFilterOptions) (*dom_collection.CollectionFilterResult, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if options.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if !options.IsValid() {
e["filter_options"] = "At least one filter option (include_owned or include_shared) must be enabled"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get filtered collections",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get filtered collections from repository.
//
result, err := uc.repo.GetCollectionsWithFilter(ctx, options)
if err != nil {
uc.logger.Error("Failed to get filtered collections from repository",
zap.Any("error", err),
zap.Any("options", options))
return nil, err
}
uc.logger.Debug("Successfully retrieved filtered collections",
zap.Int("owned_count", len(result.OwnedCollections)),
zap.Int("shared_count", len(result.SharedCollections)),
zap.Int("total_count", result.TotalCount),
zap.Any("user_id", options.UserID))
return result, nil
}

View file

@ -0,0 +1,69 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/get_sync_data.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
)
type GetCollectionSyncDataUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64, accessType string) (*dom_collection.CollectionSyncResponse, error)
}
type getCollectionSyncDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewGetCollectionSyncDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetCollectionSyncDataUseCase {
logger = logger.Named("GetCollectionSyncDataUseCase")
return &getCollectionSyncDataUseCaseImpl{config, logger, repo}
}
func (uc *getCollectionSyncDataUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_collection.CollectionSyncCursor, limit int64, accessType string) (*dom_collection.CollectionSyncResponse, error) {
//
// STEP 1: Validation.
//
// (Skip)
//
// STEP 2: Get filtered collections from repository.
//
if accessType != dom_collection.CollectionAccessTypeMember && accessType != dom_collection.CollectionAccessTypeOwner {
result, err := uc.repo.GetCollectionSyncData(ctx, userID, cursor, limit)
if err != nil {
uc.logger.Error("Failed to get filtered collections from repository",
zap.Any("error", err),
zap.Any("userID", userID),
zap.Any("cursor", cursor),
zap.Int64("limit", limit))
return nil, err
}
return result, nil
}
result, err := uc.repo.GetCollectionSyncDataByAccessType(ctx, userID, cursor, limit, accessType)
if err != nil {
uc.logger.Error("Failed to get filtered collections from repository",
zap.Any("error", err),
zap.Any("userID", userID),
zap.Any("cursor", cursor),
zap.Int64("limit", limit),
zap.String("access_type", accessType))
return nil, err
}
return result, nil
}

View file

@ -0,0 +1,70 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/harddelete.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// HardDeleteCollectionUseCase permanently deletes a collection
// Used for GDPR right-to-be-forgotten implementation
type HardDeleteCollectionUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type hardDeleteCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewHardDeleteCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) HardDeleteCollectionUseCase {
logger = logger.Named("HardDeleteCollectionUseCase")
return &hardDeleteCollectionUseCaseImpl{config, logger, repo}
}
func (uc *hardDeleteCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection hard deletion",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Hard delete from database (no tombstone).
//
uc.logger.Info("Hard deleting collection (GDPR mode)",
zap.String("collection_id", id.String()))
err := uc.repo.HardDelete(ctx, id)
if err != nil {
uc.logger.Error("Failed to hard delete collection",
zap.String("collection_id", id.String()),
zap.Error(err))
return err
}
uc.logger.Info("✅ Collection hard deleted successfully",
zap.String("collection_id", id.String()))
return nil
}

View file

@ -0,0 +1,25 @@
package collection
import (
"testing"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
// NOTE: Unit tests for HardDeleteCollectionUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestHardDeleteCollectionUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
cfg := &config.Configuration{}
logger := zap.NewNop()
useCase := NewHardDeleteCollectionUseCase(cfg, logger, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/list_by_user.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListCollectionsByUserUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error)
}
type listCollectionsByUserUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewListCollectionsByUserUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ListCollectionsByUserUseCase {
logger = logger.Named("ListCollectionsByUserUseCase")
return &listCollectionsByUserUseCaseImpl{config, logger, repo}
}
func (uc *listCollectionsByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating list collections by user",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetAllByUserID(ctx, userID)
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/list_shared_with_user.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListCollectionsSharedWithUserUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error)
}
type listCollectionsSharedWithUserUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewListCollectionsSharedWithUserUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ListCollectionsSharedWithUserUseCase {
logger = logger.Named("ListCollectionsSharedWithUserUseCase")
return &listCollectionsSharedWithUserUseCaseImpl{config, logger, repo}
}
func (uc *listCollectionsSharedWithUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_collection.Collection, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating list shared collections",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetCollectionsSharedWithUser(ctx, userID)
}

View file

@ -0,0 +1,77 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/move_collection.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// MoveCollectionRequest contains data needed to move a collection
type MoveCollectionRequest struct {
CollectionID gocql.UUID `json:"collection_id"`
NewParentID gocql.UUID `json:"new_parent_id"`
UpdatedAncestors []gocql.UUID `json:"updated_ancestors"`
UpdatedPathSegments []string `json:"updated_path_segments"`
}
type MoveCollectionUseCase interface {
Execute(ctx context.Context, request MoveCollectionRequest) error
}
type moveCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewMoveCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) MoveCollectionUseCase {
logger = logger.Named("MoveCollectionUseCase")
return &moveCollectionUseCaseImpl{config, logger, repo}
}
func (uc *moveCollectionUseCaseImpl) Execute(ctx context.Context, request MoveCollectionRequest) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if request.CollectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if request.NewParentID.String() == "" {
e["new_parent_id"] = "New parent ID is required"
}
if len(request.UpdatedAncestors) == 0 {
e["updated_ancestors"] = "Updated ancestors are required"
}
if len(request.UpdatedPathSegments) == 0 {
e["updated_path_segments"] = "Updated path segments are required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating move collection",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Move collection.
//
return uc.repo.MoveCollection(
ctx,
request.CollectionID,
request.NewParentID,
request.UpdatedAncestors,
request.UpdatedPathSegments,
)
}

View file

@ -0,0 +1,216 @@
package collection
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
)
// Wire providers for collection use cases
func ProvideCreateCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CreateCollectionUseCase {
return NewCreateCollectionUseCase(cfg, logger, repo)
}
func ProvideUpdateCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) UpdateCollectionUseCase {
return NewUpdateCollectionUseCase(cfg, logger, repo)
}
func ProvideGetCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetCollectionUseCase {
return NewGetCollectionUseCase(cfg, logger, repo)
}
func ProvideSoftDeleteCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) SoftDeleteCollectionUseCase {
return NewSoftDeleteCollectionUseCase(cfg, logger, repo)
}
func ProvideArchiveCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ArchiveCollectionUseCase {
return NewArchiveCollectionUseCase(cfg, logger, repo)
}
func ProvideRestoreCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RestoreCollectionUseCase {
return NewRestoreCollectionUseCase(cfg, logger, repo)
}
func ProvideListCollectionsByUserUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ListCollectionsByUserUseCase {
return NewListCollectionsByUserUseCase(cfg, logger, repo)
}
func ProvideListCollectionsSharedWithUserUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) ListCollectionsSharedWithUserUseCase {
return NewListCollectionsSharedWithUserUseCase(cfg, logger, repo)
}
func ProvideFindRootCollectionsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindRootCollectionsUseCase {
return NewFindRootCollectionsUseCase(cfg, logger, repo)
}
func ProvideFindCollectionsByParentUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindCollectionsByParentUseCase {
return NewFindCollectionsByParentUseCase(cfg, logger, repo)
}
func ProvideGetCollectionSyncDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetCollectionSyncDataUseCase {
return NewGetCollectionSyncDataUseCase(cfg, logger, repo)
}
func ProvideCheckCollectionAccessUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CheckCollectionAccessUseCase {
return NewCheckCollectionAccessUseCase(cfg, logger, repo)
}
func ProvideCountUserCollectionsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CountUserCollectionsUseCase {
return NewCountUserCollectionsUseCase(cfg, logger, repo)
}
func ProvideCountUserFoldersUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) CountUserFoldersUseCase {
return NewCountUserFoldersUseCase(cfg, logger, repo)
}
func ProvideUpdateMemberPermissionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) UpdateMemberPermissionUseCase {
return NewUpdateMemberPermissionUseCase(cfg, logger, repo)
}
func ProvideMoveCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) MoveCollectionUseCase {
return NewMoveCollectionUseCase(cfg, logger, repo)
}
func ProvideGetFilteredCollectionsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) GetFilteredCollectionsUseCase {
return NewGetFilteredCollectionsUseCase(cfg, logger, repo)
}
func ProvideAddMemberToHierarchyUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AddMemberToHierarchyUseCase {
return NewAddMemberToHierarchyUseCase(cfg, logger, repo)
}
func ProvideRemoveMemberFromHierarchyUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveMemberFromHierarchyUseCase {
return NewRemoveMemberFromHierarchyUseCase(cfg, logger, repo)
}
func ProvideFindDescendantsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) FindDescendantsUseCase {
return NewFindDescendantsUseCase(cfg, logger, repo)
}
func ProvideAddCollectionMemberUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AddCollectionMemberUseCase {
return NewAddCollectionMemberUseCase(cfg, logger, repo)
}
func ProvideRemoveCollectionMemberUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveCollectionMemberUseCase {
return NewRemoveCollectionMemberUseCase(cfg, logger, repo)
}
func ProvideAnonymizeOldIPsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AnonymizeOldIPsUseCase {
return NewAnonymizeOldIPsUseCase(cfg, logger, repo)
}
func ProvideRemoveUserFromAllCollectionsUseCase(
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveUserFromAllCollectionsUseCase {
return NewRemoveUserFromAllCollectionsUseCase(logger, repo)
}
func ProvideHardDeleteCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) HardDeleteCollectionUseCase {
return NewHardDeleteCollectionUseCase(cfg, logger, repo)
}
func ProvideAnonymizeUserReferencesUseCase(
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) AnonymizeUserReferencesUseCase {
return NewAnonymizeUserReferencesUseCase(logger, repo)
}

View file

@ -0,0 +1,57 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/remove_member.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RemoveCollectionMemberUseCase interface {
Execute(ctx context.Context, collectionID, recipientID gocql.UUID) error
}
type removeCollectionMemberUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewRemoveCollectionMemberUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveCollectionMemberUseCase {
logger = logger.Named("RemoveCollectionMemberUseCase")
return &removeCollectionMemberUseCaseImpl{config, logger, repo}
}
func (uc *removeCollectionMemberUseCaseImpl) Execute(ctx context.Context, collectionID, recipientID gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if recipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating remove collection member",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Remove member from collection.
//
return uc.repo.RemoveMember(ctx, collectionID, recipientID)
}

View file

@ -0,0 +1,57 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/remove_member_from_hierarchy.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RemoveMemberFromHierarchyUseCase interface {
Execute(ctx context.Context, rootID, recipientID gocql.UUID) error
}
type removeMemberFromHierarchyUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewRemoveMemberFromHierarchyUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RemoveMemberFromHierarchyUseCase {
logger = logger.Named("RemoveMemberFromHierarchyUseCase")
return &removeMemberFromHierarchyUseCaseImpl{config, logger, repo}
}
func (uc *removeMemberFromHierarchyUseCaseImpl) Execute(ctx context.Context, rootID, recipientID gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if rootID.String() == "" {
e["root_id"] = "Root collection ID is required"
}
if recipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating remove member from hierarchy",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Remove member from collection hierarchy.
//
return uc.repo.RemoveMemberFromHierarchy(ctx, rootID, recipientID)
}

View file

@ -0,0 +1,53 @@
package collection
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
)
// RemoveUserFromAllCollectionsUseCase removes a user from all collections they are a member of
// Used for GDPR right-to-be-forgotten implementation
type RemoveUserFromAllCollectionsUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, userEmail string) (int, error)
}
type removeUserFromAllCollectionsUseCaseImpl struct {
logger *zap.Logger
repo collection.CollectionRepository
}
// NewRemoveUserFromAllCollectionsUseCase creates a new use case for removing user from all collections
func NewRemoveUserFromAllCollectionsUseCase(
logger *zap.Logger,
repo collection.CollectionRepository,
) RemoveUserFromAllCollectionsUseCase {
return &removeUserFromAllCollectionsUseCaseImpl{
logger: logger.Named("RemoveUserFromAllCollectionsUseCase"),
repo: repo,
}
}
func (uc *removeUserFromAllCollectionsUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, userEmail string) (int, error) {
uc.logger.Info("Removing user from all shared collections",
zap.String("user_id", userID.String()))
modifiedCollections, err := uc.repo.RemoveUserFromAllCollections(ctx, userID, userEmail)
if err != nil {
uc.logger.Error("Failed to remove user from all collections",
zap.String("user_id", userID.String()),
zap.Error(err))
return 0, err
}
count := len(modifiedCollections)
uc.logger.Info("✅ Successfully removed user from all shared collections",
zap.String("user_id", userID.String()),
zap.Int("collections_modified", count))
return count, nil
}

View file

@ -0,0 +1,22 @@
package collection
import (
"testing"
"go.uber.org/zap"
)
// NOTE: Unit tests for RemoveUserFromAllCollectionsUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestRemoveUserFromAllCollectionsUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
logger := zap.NewNop()
useCase := NewRemoveUserFromAllCollectionsUseCase(logger, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/collection/restore.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RestoreCollectionUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type restoreCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewRestoreCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) RestoreCollectionUseCase {
logger = logger.Named("RestoreCollectionUseCase")
return &restoreCollectionUseCaseImpl{config, logger, repo}
}
func (uc *restoreCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection restoration",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Restore collection using repository method.
//
return uc.repo.Restore(ctx, id)
}

View file

@ -0,0 +1,54 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/softdelete.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SoftDeleteCollectionUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type softDeleteCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewSoftDeleteCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) SoftDeleteCollectionUseCase {
logger = logger.Named("SoftDeleteCollectionUseCase")
return &softDeleteCollectionUseCaseImpl{config, logger, repo}
}
func (uc *softDeleteCollectionUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection deletion",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Delete from database.
//
return uc.repo.SoftDelete(ctx, id)
}

View file

@ -0,0 +1,57 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/update.go
package collection
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UpdateCollectionUseCase interface {
Execute(ctx context.Context, collection *dom_collection.Collection) error
}
type updateCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewUpdateCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) UpdateCollectionUseCase {
logger = logger.Named("UpdateCollectionUseCase")
return &updateCollectionUseCaseImpl{config, logger, repo}
}
func (uc *updateCollectionUseCaseImpl) Execute(ctx context.Context, collection *dom_collection.Collection) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collection == nil {
e["collection"] = "Collection is required"
} else {
if collection.ID.String() == "" {
e["id"] = "Collection ID is required"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating collection update",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Update in database.
//
return uc.repo.Update(ctx, collection)
}

View file

@ -0,0 +1,64 @@
// monorepo/cloud/backend/internal/maplefile/usecase/collection/update_member_permission.go
package collection
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UpdateMemberPermissionUseCase interface {
Execute(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error
}
type updateMemberPermissionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_collection.CollectionRepository
}
func NewUpdateMemberPermissionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_collection.CollectionRepository,
) UpdateMemberPermissionUseCase {
logger = logger.Named("UpdateMemberPermissionUseCase")
return &updateMemberPermissionUseCaseImpl{config, logger, repo}
}
func (uc *updateMemberPermissionUseCaseImpl) Execute(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if recipientID.String() == "" {
e["recipient_id"] = "Recipient ID is required"
}
if newPermission == "" {
// Default permission level will be set in the repository
} else if newPermission != dom_collection.CollectionPermissionReadOnly &&
newPermission != dom_collection.CollectionPermissionReadWrite &&
newPermission != dom_collection.CollectionPermissionAdmin {
e["permission_level"] = "Invalid permission level"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating update member permission",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Update member permission.
//
return uc.repo.UpdateMemberPermission(ctx, collectionID, recipientID, newPermission)
}

View file

@ -0,0 +1,61 @@
package emailer
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
domain "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/templatedemailer"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SendUserPasswordResetEmailUseCase interface {
Execute(ctx context.Context, user *domain.User) error
}
type sendUserPasswordResetEmailUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
emailer templatedemailer.TemplatedEmailer
}
func NewSendUserPasswordResetEmailUseCase(config *config.Configuration, logger *zap.Logger, emailer templatedemailer.TemplatedEmailer) SendUserPasswordResetEmailUseCase {
logger = logger.Named("SendUserPasswordResetEmailUseCase")
return &sendUserPasswordResetEmailUseCaseImpl{config, logger, emailer}
}
func (uc *sendUserPasswordResetEmailUseCaseImpl) Execute(ctx context.Context, user *domain.User) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if user == nil {
e["user"] = "User is missing value"
} else {
if user.FirstName == "" {
e["first_name"] = "First name is required"
}
if user.Email == "" {
e["email"] = "Email is required"
}
if user.SecurityData.Code == "" {
e["code"] = "Code is required for password reset verification "
}
if user.SecurityData.CodeType != domain.UserCodeTypePasswordReset {
e["code_type"] = "Code type is required for password reset verification "
}
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Send email
//
return uc.emailer.SendUserPasswordResetEmail(ctx, user.Email, user.SecurityData.Code, user.FirstName)
}

View file

@ -0,0 +1,61 @@
package emailer
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
domain "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/templatedemailer"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SendUserVerificationEmailUseCase interface {
Execute(ctx context.Context, user *domain.User) error
}
type sendUserVerificationEmailUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
emailer templatedemailer.TemplatedEmailer
}
func NewSendUserVerificationEmailUseCase(config *config.Configuration, logger *zap.Logger, emailer templatedemailer.TemplatedEmailer) SendUserVerificationEmailUseCase {
logger = logger.Named("SendUserVerificationEmailUseCase")
return &sendUserVerificationEmailUseCaseImpl{config, logger, emailer}
}
func (uc *sendUserVerificationEmailUseCaseImpl) Execute(ctx context.Context, user *domain.User) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if user == nil {
e["user"] = "User is missing value"
} else {
if user.FirstName == "" {
e["first_name"] = "First name is required"
}
if user.Email == "" {
e["email"] = "Email is required"
}
if user.SecurityData.Code == "" {
e["code"] = "Code is required for password reset verification "
}
if user.SecurityData.CodeType != domain.UserCodeTypePasswordReset {
e["code_type"] = "Code type is required for password reset verification "
}
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Send email
//
return uc.emailer.SendUserVerificationEmail(ctx, user.Email, user.SecurityData.Code, user.FirstName)
}

View file

@ -0,0 +1,50 @@
// monorepo/cloud/backend/internal/usecase/filemetadata/anonymize_old_ips.go
package filemetadata
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
)
type AnonymizeOldIPsUseCase interface {
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
}
type anonymizeOldIPsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewAnonymizeOldIPsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) AnonymizeOldIPsUseCase {
logger = logger.Named("FileMetadataAnonymizeOldIPsUseCase")
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
}
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
uc.logger.Debug("Anonymizing old IPs in file metadata tables",
zap.Time("cutoff_date", cutoffDate))
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
if err != nil {
uc.logger.Error("Failed to anonymize old IPs in file metadata tables",
zap.Error(err),
zap.Time("cutoff_date", cutoffDate))
return 0, err
}
uc.logger.Info("Successfully anonymized old IPs in file metadata tables",
zap.Int("count", count),
zap.Time("cutoff_date", cutoffDate))
return count, nil
}

View file

@ -0,0 +1,89 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/anonymize_user_references.go
package filemetadata
import (
"context"
"fmt"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
)
// AnonymizeUserReferencesUseCase handles anonymizing CreatedByUserID and ModifiedByUserID
// references when a user is deleted, replacing them with a special "deleted user" UUID.
type AnonymizeUserReferencesUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (int, error)
}
type anonymizeUserReferencesUseCaseImpl struct {
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
// NewAnonymizeUserReferencesUseCase creates a new use case for anonymizing user references in files
func NewAnonymizeUserReferencesUseCase(
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) AnonymizeUserReferencesUseCase {
return &anonymizeUserReferencesUseCaseImpl{
logger: logger,
repo: repo,
}
}
// DeletedUserUUID is a well-known UUID representing a deleted user
// UUID: 00000000-0000-0000-0000-000000000001 (DELETED_USER)
var DeletedUserUUID = gocql.UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
func (uc *anonymizeUserReferencesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (int, error) {
uc.logger.Info("Anonymizing user references in file metadata",
zap.String("user_id", userID.String()))
// Get all files created or modified by this user
files, err := uc.repo.GetByCreatedByUserID(userID)
if err != nil {
uc.logger.Error("Failed to get files by created_by_user_id",
zap.String("user_id", userID.String()),
zap.Error(err))
return 0, fmt.Errorf("failed to get files by creator: %w", err)
}
updatedCount := 0
// Update each file to replace user references with deleted user UUID
for _, file := range files {
needsUpdate := false
// Check if this file has references to the deleted user
if file.CreatedByUserID == userID {
file.CreatedByUserID = DeletedUserUUID
needsUpdate = true
}
if file.ModifiedByUserID == userID {
file.ModifiedByUserID = DeletedUserUUID
needsUpdate = true
}
if needsUpdate {
// Update the file with anonymized references
if err := uc.repo.Update(file); err != nil {
uc.logger.Error("Failed to anonymize user references in file",
zap.String("file_id", file.ID.String()),
zap.String("user_id", userID.String()),
zap.Error(err))
// Continue with other files even if one fails
continue
}
updatedCount++
}
}
uc.logger.Info("✅ Anonymized user references in file metadata",
zap.String("user_id", userID.String()),
zap.Int("files_updated", updatedCount))
return updatedCount, nil
}

View file

@ -0,0 +1,55 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/check_access.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CheckFileAccessUseCase interface {
Execute(fileID, userID gocql.UUID) (bool, error)
}
type checkFileAccessUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewCheckFileAccessUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CheckFileAccessUseCase {
logger = logger.Named("CheckFileAccessUseCase")
return &checkFileAccessUseCaseImpl{config, logger, repo}
}
func (uc *checkFileAccessUseCaseImpl) Execute(fileID, userID gocql.UUID) (bool, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if fileID.String() == "" {
e["file_id"] = "File ID is required"
}
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file access check",
zap.Any("error", e))
return false, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Check access in database.
//
return uc.repo.CheckIfUserHasAccess(fileID, userID)
}

View file

@ -0,0 +1,52 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/check_exists.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CheckFileExistsUseCase interface {
Execute(id gocql.UUID) (bool, error)
}
type checkFileExistsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewCheckFileExistsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CheckFileExistsUseCase {
logger = logger.Named("CheckFileExistsUseCase")
return &checkFileExistsUseCaseImpl{config, logger, repo}
}
func (uc *checkFileExistsUseCaseImpl) Execute(id gocql.UUID) (bool, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "File ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file existence check",
zap.Any("error", e))
return false, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Check existence in database.
//
return uc.repo.CheckIfExistsByID(id)
}

View file

@ -0,0 +1,112 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/count_files.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// CountFilesResponse contains the file count for a user
type CountFilesResponse struct {
TotalFiles int `json:"total_files"`
}
type CountUserFilesUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (*CountFilesResponse, error)
}
type countUserFilesUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
fileRepo dom_file.FileMetadataRepository
collectionRepo dom_collection.CollectionRepository
}
func NewCountUserFilesUseCase(
config *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) CountUserFilesUseCase {
logger = logger.Named("CountUserFilesUseCase")
return &countUserFilesUseCaseImpl{config, logger, fileRepo, collectionRepo}
}
func (uc *countUserFilesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*CountFilesResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating count user files",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get accessible collections for the user.
//
// Get collections using the efficient filtered query
filterOptions := dom_collection.CollectionFilterOptions{
UserID: userID,
IncludeOwned: true,
IncludeShared: true,
}
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
if err != nil {
uc.logger.Error("Failed to get accessible collections for file count",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
// Extract collection IDs
allCollections := collectionResult.GetAllCollections()
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
for _, collection := range allCollections {
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
}
uc.logger.Debug("Found accessible collections for file counting",
zap.String("user_id", userID.String()),
zap.Int("owned_collections", len(collectionResult.OwnedCollections)),
zap.Int("shared_collections", len(collectionResult.SharedCollections)),
zap.Int("total_accessible", len(accessibleCollectionIDs)))
//
// STEP 3: Count files in accessible collections.
//
fileCount, err := uc.fileRepo.CountFilesByUser(ctx, userID, accessibleCollectionIDs)
if err != nil {
uc.logger.Error("Failed to count files for user",
zap.String("user_id", userID.String()),
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
zap.Error(err))
return nil, err
}
response := &CountFilesResponse{
TotalFiles: fileCount,
}
uc.logger.Debug("Successfully counted user files",
zap.String("user_id", userID.String()),
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
zap.Int("total_files", fileCount))
return response, nil
}

View file

@ -0,0 +1,84 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/create.go
package filemetadata
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreateFileMetadataUseCase interface {
Execute(file *dom_file.File) error
}
type createFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewCreateFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CreateFileMetadataUseCase {
logger = logger.Named("CreateFileMetadataUseCase")
return &createFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *createFileMetadataUseCaseImpl) Execute(file *dom_file.File) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if file == nil {
e["file"] = "File is required"
} else {
if file.CollectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if file.OwnerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if file.EncryptedMetadata == "" {
e["encrypted_metadata"] = "Encrypted metadata is required"
}
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
e["encrypted_file_key"] = "Encrypted file key is required"
}
if file.EncryptionVersion == "" {
e["encryption_version"] = "Encryption version is required"
}
if file.EncryptedHash == "" {
e["encrypted_hash"] = "Encrypted hash is required"
}
if file.EncryptedFileObjectKey == "" {
e["encrypted_file_object_key"] = "Encrypted file object key is required"
}
if file.EncryptedFileSizeInBytes <= 0 {
e["encrypted_file_size_in_bytes"] = "Encrypted file size must be greater than 0"
}
if file.State == "" {
e["state"] = "File state is required"
} else if file.State != dom_file.FileStatePending &&
file.State != dom_file.FileStateActive &&
file.State != dom_file.FileStateDeleted &&
file.State != dom_file.FileStateArchived {
e["state"] = "Invalid file state"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata creation",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Insert into database.
//
return uc.repo.Create(file)
}

View file

@ -0,0 +1,84 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/create_many.go
package filemetadata
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreateManyFileMetadataUseCase interface {
Execute(files []*dom_file.File) error
}
type createManyFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewCreateManyFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CreateManyFileMetadataUseCase {
logger = logger.Named("CreateManyFileMetadataUseCase")
return &createManyFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *createManyFileMetadataUseCaseImpl) Execute(files []*dom_file.File) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if files == nil || len(files) == 0 {
e["files"] = "Files are required"
} else {
for i, file := range files {
if file == nil {
e[fmt.Sprintf("files[%d]", i)] = "File is required"
continue
}
if file.CollectionID.String() == "" {
e[fmt.Sprintf("files[%d].collection_id", i)] = "Collection ID is required"
}
if file.OwnerID.String() == "" {
e[fmt.Sprintf("files[%d].owner_id", i)] = "Owner ID is required"
}
if file.EncryptedMetadata == "" {
e[fmt.Sprintf("files[%d].encrypted_metadata", i)] = "Encrypted metadata is required"
}
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
e[fmt.Sprintf("files[%d].encrypted_file_key", i)] = "Encrypted file key is required"
}
if file.EncryptionVersion == "" {
e[fmt.Sprintf("files[%d].encryption_version", i)] = "Encryption version is required"
}
if file.EncryptedHash == "" {
e[fmt.Sprintf("files[%d].encrypted_hash", i)] = "Encrypted hash is required"
}
if file.EncryptedFileObjectKey == "" {
e[fmt.Sprintf("files[%d].encrypted_file_object_key", i)] = "Encrypted file object key is required"
}
if file.EncryptedFileSizeInBytes <= 0 {
e[fmt.Sprintf("files[%d].encrypted_file_size_in_bytes", i)] = "Encrypted file size must be greater than 0"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata batch creation",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Insert into database.
//
return uc.repo.CreateMany(files)
}

View file

@ -0,0 +1,60 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/delete_many.go
package filemetadata
import (
"fmt"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type DeleteManyFileMetadataUseCase interface {
Execute(ids []gocql.UUID) error
}
type deleteManyFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewDeleteManyFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) DeleteManyFileMetadataUseCase {
logger = logger.Named("DeleteManyFileMetadataUseCase")
return &deleteManyFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *deleteManyFileMetadataUseCaseImpl) Execute(ids []gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ids == nil || len(ids) == 0 {
e["ids"] = "File IDs are required"
} else {
for i, id := range ids {
if id.String() == "" {
e[fmt.Sprintf("ids[%d]", i)] = "File ID is required"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata batch deletion",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Delete from database.
//
return uc.repo.SoftDeleteMany(ids)
}

View file

@ -0,0 +1,63 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileMetadataUseCase interface {
Execute(id gocql.UUID) (*dom_file.File, error)
}
type getFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewGetFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataUseCase {
logger = logger.Named("GetFileMetadataUseCase")
return &getFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *getFileMetadataUseCaseImpl) Execute(id gocql.UUID) (*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "File ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata retrieval",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
file, err := uc.repo.Get(id)
if err != nil {
return nil, err
}
if file == nil {
uc.logger.Debug("File metadata not found",
zap.Any("id", id))
return nil, httperror.NewForNotFoundWithSingleField("message", "File not found")
}
return file, nil
}

View file

@ -0,0 +1,52 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_collection.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileMetadataByCollectionUseCase interface {
Execute(collectionID gocql.UUID) ([]*dom_file.File, error)
}
type getFileMetadataByCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewGetFileMetadataByCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByCollectionUseCase {
logger = logger.Named("GetFileMetadataByCollectionUseCase")
return &getFileMetadataByCollectionUseCaseImpl{config, logger, repo}
}
func (uc *getFileMetadataByCollectionUseCaseImpl) Execute(collectionID gocql.UUID) ([]*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata retrieval by collection",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetByCollection(collectionID)
}

View file

@ -0,0 +1,52 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_created_by_user_id.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileMetadataByCreatedByUserIDUseCase interface {
Execute(createdByUserID gocql.UUID) ([]*dom_file.File, error)
}
type getFileMetadataByCreatedByUserIDUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewGetFileMetadataByCreatedByUserIDUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByCreatedByUserIDUseCase {
logger = logger.Named("GetFileMetadataByCreatedByUserIDUseCase")
return &getFileMetadataByCreatedByUserIDUseCaseImpl{config, logger, repo}
}
func (uc *getFileMetadataByCreatedByUserIDUseCaseImpl) Execute(createdByUserID gocql.UUID) ([]*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if createdByUserID.String() == "" {
e["created_by_user_id"] = "Created by user ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata retrieval by created_by_user_id",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetByCreatedByUserID(createdByUserID)
}

View file

@ -0,0 +1,60 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_ids.go
package filemetadata
import (
"fmt"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileMetadataByIDsUseCase interface {
Execute(ids []gocql.UUID) ([]*dom_file.File, error)
}
type getFileMetadataByIDsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewGetFileMetadataByIDsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByIDsUseCase {
logger = logger.Named("GetFileMetadataByIDsUseCase")
return &getFileMetadataByIDsUseCaseImpl{config, logger, repo}
}
func (uc *getFileMetadataByIDsUseCaseImpl) Execute(ids []gocql.UUID) ([]*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ids == nil || len(ids) == 0 {
e["ids"] = "File IDs are required"
} else {
for i, id := range ids {
if id.String() == "" {
e[fmt.Sprintf("ids[%d]", i)] = "File ID is required"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata retrieval by IDs",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetByIDs(ids)
}

View file

@ -0,0 +1,52 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/get_by_owner_id.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFileMetadataByOwnerIDUseCase interface {
Execute(ownerID gocql.UUID) ([]*dom_file.File, error)
}
type getFileMetadataByOwnerIDUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewGetFileMetadataByOwnerIDUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByOwnerIDUseCase {
logger = logger.Named("GetFileMetadataByOwnerIDUseCase")
return &getFileMetadataByOwnerIDUseCaseImpl{config, logger, repo}
}
func (uc *getFileMetadataByOwnerIDUseCaseImpl) Execute(ownerID gocql.UUID) ([]*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ownerID.String() == "" {
e["owner_id"] = "Created by user ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata retrieval by owner_id",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetByOwnerID(ownerID)
}

View file

@ -0,0 +1,68 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/harddelete.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// HardDeleteFileMetadataUseCase permanently deletes file metadata
// Used for GDPR right-to-be-forgotten implementation
type HardDeleteFileMetadataUseCase interface {
Execute(id gocql.UUID) error
}
type hardDeleteFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewHardDeleteFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) HardDeleteFileMetadataUseCase {
logger = logger.Named("HardDeleteFileMetadataUseCase")
return &hardDeleteFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *hardDeleteFileMetadataUseCaseImpl) Execute(id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "File ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata hard deletion",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Hard delete from database (no tombstone).
//
uc.logger.Info("Hard deleting file metadata (GDPR mode)",
zap.String("file_id", id.String()))
err := uc.repo.HardDelete(id)
if err != nil {
uc.logger.Error("Failed to hard delete file metadata",
zap.String("file_id", id.String()),
zap.Error(err))
return err
}
uc.logger.Info("✅ File metadata hard deleted successfully",
zap.String("file_id", id.String()))
return nil
}

View file

@ -0,0 +1,25 @@
package filemetadata
import (
"testing"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
// NOTE: Unit tests for HardDeleteFileMetadataUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestHardDeleteFileMetadataUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
cfg := &config.Configuration{}
logger := zap.NewNop()
useCase := NewHardDeleteFileMetadataUseCase(cfg, logger, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,66 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/list_by_owner.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListFilesByOwnerIDUseCase interface {
Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_file.File, error)
}
type listFilesByOwnerIDUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewListFilesByOwnerIDUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) ListFilesByOwnerIDUseCase {
logger = logger.Named("ListFilesByOwnerIDUseCase")
return &listFilesByOwnerIDUseCaseImpl{config, logger, repo}
}
func (uc *listFilesByOwnerIDUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) ([]*dom_file.File, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ownerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating list files by owner",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
files, err := uc.repo.GetByOwnerID(ownerID)
if err != nil {
uc.logger.Error("Failed to get files by owner ID",
zap.String("owner_id", ownerID.String()),
zap.Error(err))
return nil, err
}
uc.logger.Debug("Files successfully retrieved by owner ID",
zap.String("owner_id", ownerID.String()),
zap.Int("count", len(files)))
return files, nil
}

View file

@ -0,0 +1,131 @@
// cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/list_recent_files.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListRecentFilesUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.RecentFilesCursor, limit int64) (*dom_file.RecentFilesResponse, error)
}
type listRecentFilesUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
fileRepo dom_file.FileMetadataRepository
collectionRepo dom_collection.CollectionRepository
}
func NewListRecentFilesUseCase(
config *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) ListRecentFilesUseCase {
logger = logger.Named("ListRecentFilesUseCase")
return &listRecentFilesUseCaseImpl{config, logger, fileRepo, collectionRepo}
}
func (uc *listRecentFilesUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.RecentFilesCursor, limit int64) (*dom_file.RecentFilesResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if limit <= 0 {
e["limit"] = "Limit must be greater than 0"
}
if limit > 100 {
e["limit"] = "Limit cannot exceed 100"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating list recent files",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get accessible collections for the user.
//
uc.logger.Debug("Getting accessible collections for recent files",
zap.String("user_id", userID.String()))
// Get collections using the efficient filtered query
filterOptions := dom_collection.CollectionFilterOptions{
UserID: userID,
IncludeOwned: true,
IncludeShared: true,
}
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
if err != nil {
uc.logger.Error("Failed to get accessible collections for recent files",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
// Extract collection IDs
allCollections := collectionResult.GetAllCollections()
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
for _, collection := range allCollections {
// Only include active collections
if collection.State == "active" {
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
}
}
uc.logger.Debug("Found accessible collections for recent files",
zap.String("user_id", userID.String()),
zap.Int("owned_collections", len(collectionResult.OwnedCollections)),
zap.Int("shared_collections", len(collectionResult.SharedCollections)),
zap.Int("total_accessible", len(accessibleCollectionIDs)))
// If no accessible collections, return empty response
if len(accessibleCollectionIDs) == 0 {
uc.logger.Info("User has no accessible collections for recent files",
zap.String("user_id", userID.String()))
return &dom_file.RecentFilesResponse{
Files: []dom_file.RecentFilesItem{},
NextCursor: nil,
HasMore: false,
}, nil
}
//
// STEP 3: List recent files for accessible collections.
//
recentFiles, err := uc.fileRepo.ListRecentFiles(ctx, userID, cursor, limit, accessibleCollectionIDs)
if err != nil {
uc.logger.Error("Failed to list recent files",
zap.Any("error", err),
zap.String("user_id", userID.String()))
return nil, err
}
if recentFiles == nil {
uc.logger.Debug("Recent files not found",
zap.String("user_id", userID.String()))
return nil, httperror.NewForNotFoundWithSingleField("message", "Recent files not found")
}
uc.logger.Debug("Recent files successfully retrieved",
zap.String("user_id", userID.String()),
zap.Any("next_cursor", recentFiles.NextCursor),
zap.Int("files_count", len(recentFiles.Files)))
return recentFiles, nil
}

View file

@ -0,0 +1,91 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/list_sync_data.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListFileMetadataSyncDataUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.FileSyncCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*dom_file.FileSyncResponse, error)
}
type listFileMetadataSyncDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewListFileMetadataSyncDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) ListFileMetadataSyncDataUseCase {
logger = logger.Named("ListFileMetadataSyncDataUseCase")
return &listFileMetadataSyncDataUseCaseImpl{config, logger, repo}
}
func (uc *listFileMetadataSyncDataUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, cursor *dom_file.FileSyncCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*dom_file.FileSyncResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(accessibleCollectionIDs) == 0 {
e["accessible_collections"] = "At least one accessible collection is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating list file sync data",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
uc.logger.Debug("Listing file sync data",
zap.String("user_id", userID.String()),
zap.Int("accessible_collections_count", len(accessibleCollectionIDs)),
zap.Any("cursor", cursor),
zap.Int64("limit", limit))
//
// STEP 2: List file sync data from repository for accessible collections.
//
result, err := uc.repo.ListSyncData(ctx, userID, cursor, limit, accessibleCollectionIDs)
if err != nil {
uc.logger.Error("Failed to list file sync data from repository",
zap.Any("error", err),
zap.String("user_id", userID.String()))
return nil, err
}
// Log the sync items for debugging
uc.logger.Debug("File sync data retrieved from repository",
zap.String("user_id", userID.String()),
zap.Int("files_count", len(result.Files)),
zap.Bool("has_more", result.HasMore))
// Log each sync item to verify all fields are populated
for i, item := range result.Files {
uc.logger.Debug("File sync item",
zap.Int("index", i),
zap.String("file_id", item.ID.String()),
zap.String("collection_id", item.CollectionID.String()),
zap.Uint64("version", item.Version),
zap.Time("modified_at", item.ModifiedAt),
zap.String("state", item.State),
zap.Uint64("tombstone_version", item.TombstoneVersion),
zap.Time("tombstone_expiry", item.TombstoneExpiry),
zap.Int64("encrypted_file_size_in_bytes", item.EncryptedFileSizeInBytes))
}
return result, nil
}

View file

@ -0,0 +1,197 @@
package filemetadata
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
)
// Wire providers for file metadata use cases
func ProvideCreateFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CreateFileMetadataUseCase {
return NewCreateFileMetadataUseCase(cfg, logger, repo)
}
func ProvideUpdateFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) UpdateFileMetadataUseCase {
return NewUpdateFileMetadataUseCase(cfg, logger, repo)
}
func ProvideGetFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataUseCase {
return NewGetFileMetadataUseCase(cfg, logger, repo)
}
func ProvideSoftDeleteFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) SoftDeleteFileMetadataUseCase {
return NewSoftDeleteFileMetadataUseCase(cfg, logger, repo)
}
func ProvideGetFileMetadataByCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByCollectionUseCase {
return NewGetFileMetadataByCollectionUseCase(cfg, logger, repo)
}
func ProvideGetFileMetadataByCreatedByUserIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByCreatedByUserIDUseCase {
return NewGetFileMetadataByCreatedByUserIDUseCase(cfg, logger, repo)
}
func ProvideDeleteManyFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) DeleteManyFileMetadataUseCase {
return NewDeleteManyFileMetadataUseCase(cfg, logger, repo)
}
func ProvideCountUserFilesUseCase(
cfg *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) CountUserFilesUseCase {
return NewCountUserFilesUseCase(cfg, logger, fileRepo, collectionRepo)
}
func ProvideCheckFileAccessUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CheckFileAccessUseCase {
return NewCheckFileAccessUseCase(cfg, logger, repo)
}
func ProvideGetStorageSizeByCollectionUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetStorageSizeByCollectionUseCase {
return NewGetStorageSizeByCollectionUseCase(cfg, logger, repo)
}
func ProvideGetFileMetadataByOwnerIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByOwnerIDUseCase {
return NewGetFileMetadataByOwnerIDUseCase(cfg, logger, repo)
}
func ProvideCheckFileExistsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CheckFileExistsUseCase {
return NewCheckFileExistsUseCase(cfg, logger, repo)
}
func ProvideGetStorageSizeByUserUseCase(
cfg *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) GetStorageSizeByUserUseCase {
return NewGetStorageSizeByUserUseCase(cfg, logger, fileRepo, collectionRepo)
}
func ProvideListRecentFilesUseCase(
cfg *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) ListRecentFilesUseCase {
return NewListRecentFilesUseCase(cfg, logger, fileRepo, collectionRepo)
}
func ProvideGetFileMetadataByIDsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetFileMetadataByIDsUseCase {
return NewGetFileMetadataByIDsUseCase(cfg, logger, repo)
}
func ProvideCreateManyFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) CreateManyFileMetadataUseCase {
return NewCreateManyFileMetadataUseCase(cfg, logger, repo)
}
func ProvideListFileMetadataSyncDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) ListFileMetadataSyncDataUseCase {
return NewListFileMetadataSyncDataUseCase(cfg, logger, repo)
}
func ProvideGetStorageSizeByOwnerUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) GetStorageSizeByOwnerUseCase {
return NewGetStorageSizeByOwnerUseCase(cfg, logger, repo)
}
func ProvideAnonymizeOldIPsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) AnonymizeOldIPsUseCase {
return NewAnonymizeOldIPsUseCase(cfg, logger, repo)
}
func ProvideListFilesByOwnerIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) ListFilesByOwnerIDUseCase {
return NewListFilesByOwnerIDUseCase(cfg, logger, repo)
}
func ProvideRestoreFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) RestoreFileMetadataUseCase {
return NewRestoreFileMetadataUseCase(cfg, logger, repo)
}
func ProvideHardDeleteFileMetadataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) HardDeleteFileMetadataUseCase {
return NewHardDeleteFileMetadataUseCase(cfg, logger, repo)
}
func ProvideAnonymizeUserReferencesUseCase(
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) AnonymizeUserReferencesUseCase {
return NewAnonymizeUserReferencesUseCase(logger, repo)
}

View file

@ -0,0 +1,65 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/filemetadata/restore.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RestoreFileMetadataUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type restoreFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewRestoreFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) RestoreFileMetadataUseCase {
logger = logger.Named("RestoreFileMetadataUseCase")
return &restoreFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *restoreFileMetadataUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "File ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata restoration",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Restore file metadata using repository method.
//
err := uc.repo.Restore(id)
if err != nil {
uc.logger.Error("Failed to restore file metadata",
zap.String("file_id", id.String()),
zap.Error(err))
return err
}
uc.logger.Info("File metadata successfully restored",
zap.String("file_id", id.String()))
return nil
}

View file

@ -0,0 +1,52 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/delete.go
package filemetadata
import (
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SoftDeleteFileMetadataUseCase interface {
Execute(id gocql.UUID) error
}
type softDeleteFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewSoftDeleteFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) SoftDeleteFileMetadataUseCase {
logger = logger.Named("SoftDeleteFileMetadataUseCase")
return &softDeleteFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *softDeleteFileMetadataUseCaseImpl) Execute(id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "File ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata deletion",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Soft-delete from database.
//
return uc.repo.SoftDelete(id)
}

View file

@ -0,0 +1,88 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// StorageSizeBreakdownResponse contains detailed storage breakdown
type StorageSizeBreakdownResponse struct {
OwnedSizeBytes int64 `json:"owned_size_bytes"`
SharedSizeBytes int64 `json:"shared_size_bytes"`
TotalSizeBytes int64 `json:"total_size_bytes"`
CollectionBreakdownBytes map[string]int64 `json:"collection_breakdown_bytes"`
OwnedCollectionsCount int `json:"owned_collections_count"`
SharedCollectionsCount int `json:"shared_collections_count"`
}
// Use case interfaces
type GetStorageSizeByCollectionUseCase interface {
Execute(ctx context.Context, collectionID gocql.UUID) (*StorageSizeResponse, error)
}
// Use case implementations
type getStorageSizeByCollectionUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
fileRepo dom_file.FileMetadataRepository
}
// Constructors
func NewGetStorageSizeByCollectionUseCase(
config *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
) GetStorageSizeByCollectionUseCase {
logger = logger.Named("GetStorageSizeByCollectionUseCase")
return &getStorageSizeByCollectionUseCaseImpl{config, logger, fileRepo}
}
// Use case implementations
func (uc *getStorageSizeByCollectionUseCaseImpl) Execute(ctx context.Context, collectionID gocql.UUID) (*StorageSizeResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if collectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage size by collection",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Calculate storage size.
//
totalSize, err := uc.fileRepo.GetTotalStorageSizeByCollection(ctx, collectionID)
if err != nil {
uc.logger.Error("Failed to get storage size by collection",
zap.String("collection_id", collectionID.String()),
zap.Error(err))
return nil, err
}
response := &StorageSizeResponse{
TotalSizeBytes: totalSize,
}
uc.logger.Debug("Successfully calculated storage size by collection",
zap.String("collection_id", collectionID.String()),
zap.Int64("total_size_bytes", totalSize))
return response, nil
}

View file

@ -0,0 +1,80 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// StorageSizeResponse contains storage size information
type StorageSizeResponse struct {
TotalSizeBytes int64 `json:"total_size_bytes"`
}
// Use case interfaces
type GetStorageSizeByOwnerUseCase interface {
Execute(ctx context.Context, ownerID gocql.UUID) (*StorageSizeResponse, error)
}
// Use case implementations
type getStorageSizeByOwnerUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
fileRepo dom_file.FileMetadataRepository
}
// Constructors
func NewGetStorageSizeByOwnerUseCase(
config *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
) GetStorageSizeByOwnerUseCase {
logger = logger.Named("GetStorageSizeByOwnerUseCase")
return &getStorageSizeByOwnerUseCaseImpl{config, logger, fileRepo}
}
// Use case implementations
func (uc *getStorageSizeByOwnerUseCaseImpl) Execute(ctx context.Context, ownerID gocql.UUID) (*StorageSizeResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ownerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage size by owner",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Calculate storage size.
//
totalSize, err := uc.fileRepo.GetTotalStorageSizeByOwner(ctx, ownerID)
if err != nil {
uc.logger.Error("Failed to get storage size by owner",
zap.String("owner_id", ownerID.String()),
zap.Error(err))
return nil, err
}
response := &StorageSizeResponse{
TotalSizeBytes: totalSize,
}
uc.logger.Debug("Successfully calculated storage size by owner",
zap.String("owner_id", ownerID.String()),
zap.Int64("total_size_bytes", totalSize))
return response, nil
}

View file

@ -0,0 +1,108 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/filemetadata/storage_size.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// Use case interfaces
type GetStorageSizeByUserUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) (*StorageSizeResponse, error)
}
// Use case implementations
type getStorageSizeByUserUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
fileRepo dom_file.FileMetadataRepository
collectionRepo dom_collection.CollectionRepository
}
// Constructors
func NewGetStorageSizeByUserUseCase(
config *config.Configuration,
logger *zap.Logger,
fileRepo dom_file.FileMetadataRepository,
collectionRepo dom_collection.CollectionRepository,
) GetStorageSizeByUserUseCase {
logger = logger.Named("GetStorageSizeByUserUseCase")
return &getStorageSizeByUserUseCaseImpl{config, logger, fileRepo, collectionRepo}
}
// Use case implementations
func (uc *getStorageSizeByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) (*StorageSizeResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage size by user",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get accessible collections for the user.
//
filterOptions := dom_collection.CollectionFilterOptions{
UserID: userID,
IncludeOwned: true,
IncludeShared: true,
}
collectionResult, err := uc.collectionRepo.GetCollectionsWithFilter(ctx, filterOptions)
if err != nil {
uc.logger.Error("Failed to get accessible collections for storage size calculation",
zap.String("user_id", userID.String()),
zap.Error(err))
return nil, err
}
// Extract collection IDs
allCollections := collectionResult.GetAllCollections()
accessibleCollectionIDs := make([]gocql.UUID, 0, len(allCollections))
for _, collection := range allCollections {
accessibleCollectionIDs = append(accessibleCollectionIDs, collection.ID)
}
//
// STEP 3: Calculate storage size.
//
totalSize, err := uc.fileRepo.GetTotalStorageSizeByUser(ctx, userID, accessibleCollectionIDs)
if err != nil {
uc.logger.Error("Failed to get storage size by user",
zap.String("user_id", userID.String()),
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
zap.Error(err))
return nil, err
}
response := &StorageSizeResponse{
TotalSizeBytes: totalSize,
}
uc.logger.Debug("Successfully calculated storage size by user",
zap.String("user_id", userID.String()),
zap.Int("accessible_collections", len(accessibleCollectionIDs)),
zap.Int64("total_size_bytes", totalSize))
return response, nil
}

View file

@ -0,0 +1,81 @@
// monorepo/cloud/backend/internal/maplefile/usecase/filemetadata/update.go
package filemetadata
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UpdateFileMetadataUseCase interface {
Execute(ctx context.Context, file *dom_file.File) error
}
type updateFileMetadataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileMetadataRepository
}
func NewUpdateFileMetadataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileMetadataRepository,
) UpdateFileMetadataUseCase {
logger = logger.Named("UpdateFileMetadataUseCase")
return &updateFileMetadataUseCaseImpl{config, logger, repo}
}
func (uc *updateFileMetadataUseCaseImpl) Execute(ctx context.Context, file *dom_file.File) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if file == nil {
e["file"] = "File is required"
} else {
if file.ID.String() == "" {
e["id"] = "File ID is required"
}
if file.CollectionID.String() == "" {
e["collection_id"] = "Collection ID is required"
}
if file.OwnerID.String() == "" {
e["owner_id"] = "Owner ID is required"
}
if file.EncryptedMetadata == "" {
e["encrypted_metadata"] = "Encrypted metadata is required"
}
if file.EncryptedFileKey.Ciphertext == nil || len(file.EncryptedFileKey.Ciphertext) == 0 {
e["encrypted_file_key"] = "Encrypted file key is required"
}
if file.EncryptionVersion == "" {
e["encryption_version"] = "Encryption version is required"
}
if file.EncryptedHash == "" {
e["encrypted_hash"] = "Encrypted hash is required"
}
if file.EncryptedFileObjectKey == "" {
e["encrypted_file_object_key"] = "Encrypted file object key is required"
}
if file.EncryptedFileSizeInBytes <= 0 {
e["encrypted_file_size_in_bytes"] = "Encrypted file size must be greater than 0"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating file metadata update",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Update in database.
//
return uc.repo.Update(file)
}

View file

@ -0,0 +1,62 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/delete_encrypted_data.go
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type DeleteEncryptedDataUseCase interface {
Execute(storagePath string) error
}
type deleteEncryptedDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewDeleteEncryptedDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) DeleteEncryptedDataUseCase {
logger = logger.Named("DeleteEncryptedDataUseCase")
return &deleteEncryptedDataUseCaseImpl{config, logger, repo}
}
func (uc *deleteEncryptedDataUseCaseImpl) Execute(storagePath string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating delete encrypted data",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Delete encrypted data.
//
err := uc.repo.DeleteEncryptedData(storagePath)
if err != nil {
uc.logger.Error("Failed to delete encrypted data",
zap.String("storage_path", storagePath),
zap.Error(err))
return err
}
uc.logger.Info("Successfully deleted encrypted data",
zap.String("storage_path", storagePath))
return nil
}

View file

@ -0,0 +1,93 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/delete_multiple_encrypted_data.go
package fileobjectstorage
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type DeleteMultipleEncryptedDataUseCase interface {
Execute(storagePaths []string) error
}
type deleteMultipleEncryptedDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewDeleteMultipleEncryptedDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) DeleteMultipleEncryptedDataUseCase {
logger = logger.Named("DeleteMultipleEncryptedDataUseCase")
return &deleteMultipleEncryptedDataUseCaseImpl{config, logger, repo}
}
func (uc *deleteMultipleEncryptedDataUseCaseImpl) Execute(storagePaths []string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePaths == nil || len(storagePaths) == 0 {
e["storage_paths"] = "Storage paths are required"
} else {
for i, path := range storagePaths {
if path == "" {
e[fmt.Sprintf("storage_paths[%d]", i)] = "Storage path is required"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating delete multiple encrypted data",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Delete encrypted data files.
//
var errors []error
successCount := 0
for _, storagePath := range storagePaths {
err := uc.repo.DeleteEncryptedData(storagePath)
if err != nil {
uc.logger.Error("Failed to delete encrypted data",
zap.String("storage_path", storagePath),
zap.Error(err))
errors = append(errors, fmt.Errorf("failed to delete %s: %w", storagePath, err))
} else {
successCount++
uc.logger.Debug("Successfully deleted encrypted data",
zap.String("storage_path", storagePath))
}
}
// Log summary
uc.logger.Info("Completed bulk delete operation",
zap.Int("total_requested", len(storagePaths)),
zap.Int("successful_deletions", successCount),
zap.Int("failed_deletions", len(errors)))
// If all operations failed, return the first error
if len(errors) == len(storagePaths) {
return errors[0]
}
// If some operations failed, log but don't return error (partial success)
if len(errors) > 0 {
uc.logger.Warn("Some delete operations failed",
zap.Int("failed_count", len(errors)))
}
return nil
}

View file

@ -0,0 +1,63 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/get_encrypted_data.go
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetEncryptedDataUseCase interface {
Execute(storagePath string) ([]byte, error)
}
type getEncryptedDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewGetEncryptedDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GetEncryptedDataUseCase {
logger = logger.Named("GetEncryptedDataUseCase")
return &getEncryptedDataUseCaseImpl{config, logger, repo}
}
func (uc *getEncryptedDataUseCaseImpl) Execute(storagePath string) ([]byte, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get encrypted data",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get encrypted data.
//
data, err := uc.repo.GetEncryptedData(storagePath)
if err != nil {
uc.logger.Error("Failed to get encrypted data",
zap.String("storage_path", storagePath),
zap.Error(err))
return nil, err
}
uc.logger.Debug("Successfully retrieved encrypted data",
zap.String("storage_path", storagePath),
zap.Int("data_size", len(data)))
return data, nil
}

View file

@ -0,0 +1,63 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/get_object_size.go
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetObjectSizeUseCase interface {
Execute(storagePath string) (int64, error)
}
type getObjectSizeUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewGetObjectSizeUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GetObjectSizeUseCase {
logger = logger.Named("GetObjectSizeUseCase")
return &getObjectSizeUseCaseImpl{config, logger, repo}
}
func (uc *getObjectSizeUseCaseImpl) Execute(storagePath string) (int64, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get object size",
zap.Any("error", e))
return 0, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get object size.
//
size, err := uc.repo.GetObjectSize(storagePath)
if err != nil {
uc.logger.Error("Failed to get object size",
zap.String("storage_path", storagePath),
zap.Error(err))
return 0, err
}
uc.logger.Debug("Retrieved object size",
zap.String("storage_path", storagePath),
zap.Int64("size", size))
return size, nil
}

View file

@ -0,0 +1,71 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/presigned_download_url.go
package fileobjectstorage
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GeneratePresignedDownloadURLUseCase interface {
Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error)
}
type generatePresignedDownloadURLUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewGeneratePresignedDownloadURLUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GeneratePresignedDownloadURLUseCase {
logger = logger.Named("GeneratePresignedDownloadURLUseCase")
return &generatePresignedDownloadURLUseCaseImpl{config, logger, repo}
}
func (uc *generatePresignedDownloadURLUseCaseImpl) Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if duration <= 0 {
e["duration"] = "Duration must be greater than 0"
}
// Set reasonable limits for presigned URL duration
maxDuration := 24 * time.Hour // 24 hours max
if duration > maxDuration {
e["duration"] = "Duration cannot exceed 24 hours"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating generate presigned download URL",
zap.Any("error", e))
return "", httperror.NewForBadRequest(&e)
}
//
// STEP 2: Generate and get presigned download URL.
//
url, err := uc.repo.GeneratePresignedDownloadURL(storagePath, duration)
if err != nil {
uc.logger.Error("Failed to generate presigned download URL",
zap.String("storage_path", storagePath),
zap.Duration("duration", duration),
zap.Error(err))
return "", err
}
return url, nil
}

View file

@ -0,0 +1,71 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/presigned_upload_url.go
package fileobjectstorage
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GeneratePresignedUploadURLUseCase interface {
Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error)
}
type generatePresignedUploadURLUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewGeneratePresignedUploadURLUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GeneratePresignedUploadURLUseCase {
logger = logger.Named("GeneratePresignedUploadURLUseCase")
return &generatePresignedUploadURLUseCaseImpl{config, logger, repo}
}
func (uc *generatePresignedUploadURLUseCaseImpl) Execute(ctx context.Context, storagePath string, duration time.Duration) (string, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if duration <= 0 {
e["duration"] = "Duration must be greater than 0"
}
// Set reasonable limits for presigned URL duration
maxDuration := 24 * time.Hour // 24 hours max
if duration > maxDuration {
e["duration"] = "Duration cannot exceed 24 hours"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating generate presigned upload URL",
zap.Any("error", e))
return "", httperror.NewForBadRequest(&e)
}
//
// STEP 2: Generate and get presigned upload URL.
//
url, err := uc.repo.GeneratePresignedUploadURL(storagePath, duration)
if err != nil {
uc.logger.Error("Failed to generate presigned upload URL",
zap.String("storage_path", storagePath),
zap.Duration("duration", duration),
zap.Error(err))
return "", err
}
return url, nil
}

View file

@ -0,0 +1,82 @@
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
)
// Wire providers for file object storage use cases
func ProvideStoreEncryptedDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) StoreEncryptedDataUseCase {
return NewStoreEncryptedDataUseCase(cfg, logger, repo)
}
func ProvideGetEncryptedDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GetEncryptedDataUseCase {
return NewGetEncryptedDataUseCase(cfg, logger, repo)
}
func ProvideDeleteEncryptedDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) DeleteEncryptedDataUseCase {
return NewDeleteEncryptedDataUseCase(cfg, logger, repo)
}
func ProvideStoreMultipleEncryptedDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) StoreMultipleEncryptedDataUseCase {
return NewStoreMultipleEncryptedDataUseCase(cfg, logger, repo)
}
func ProvideDeleteMultipleEncryptedDataUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) DeleteMultipleEncryptedDataUseCase {
return NewDeleteMultipleEncryptedDataUseCase(cfg, logger, repo)
}
func ProvideVerifyObjectExistsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) VerifyObjectExistsUseCase {
return NewVerifyObjectExistsUseCase(cfg, logger, repo)
}
func ProvideGeneratePresignedUploadURLUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GeneratePresignedUploadURLUseCase {
return NewGeneratePresignedUploadURLUseCase(cfg, logger, repo)
}
func ProvideGetObjectSizeUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GetObjectSizeUseCase {
return NewGetObjectSizeUseCase(cfg, logger, repo)
}
func ProvideGeneratePresignedDownloadURLUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) GeneratePresignedDownloadURLUseCase {
return NewGeneratePresignedDownloadURLUseCase(cfg, logger, repo)
}

View file

@ -0,0 +1,73 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/store_encrypted_data.go
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type StoreEncryptedDataUseCase interface {
Execute(ownerID string, fileID string, encryptedData []byte) (string, error)
}
type storeEncryptedDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewStoreEncryptedDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) StoreEncryptedDataUseCase {
logger = logger.Named("StoreEncryptedDataUseCase")
return &storeEncryptedDataUseCaseImpl{config, logger, repo}
}
func (uc *storeEncryptedDataUseCaseImpl) Execute(ownerID string, fileID string, encryptedData []byte) (string, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if ownerID == "" {
e["owner_id"] = "Owner ID is required"
}
if fileID == "" {
e["file_id"] = "File ID is required"
}
if encryptedData == nil || len(encryptedData) == 0 {
e["encrypted_data"] = "Encrypted data is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating store encrypted data",
zap.Any("error", e))
return "", httperror.NewForBadRequest(&e)
}
//
// STEP 2: Store encrypted data.
//
storagePath, err := uc.repo.StoreEncryptedData(ownerID, fileID, encryptedData)
if err != nil {
uc.logger.Error("Failed to store encrypted data",
zap.String("owner_id", ownerID),
zap.String("file_id", fileID),
zap.Int("data_size", len(encryptedData)),
zap.Error(err))
return "", err
}
uc.logger.Info("Successfully stored encrypted data",
zap.String("owner_id", ownerID),
zap.String("file_id", fileID),
zap.String("storage_path", storagePath),
zap.Int("data_size", len(encryptedData)))
return storagePath, nil
}

View file

@ -0,0 +1,113 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/store_multiple_encrypted_data.go
package fileobjectstorage
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// EncryptedDataItem represents a single item to be stored
type EncryptedDataItem struct {
OwnerID string `json:"owner_id"`
FileID string `json:"file_id"`
EncryptedData []byte `json:"encrypted_data"`
}
// StorageResult represents the result of storing a single item
type StorageResult struct {
FileID string `json:"file_id"`
StoragePath string `json:"storage_path,omitempty"`
Error error `json:"error,omitempty"`
}
type StoreMultipleEncryptedDataUseCase interface {
Execute(items []EncryptedDataItem) ([]StorageResult, error)
}
type storeMultipleEncryptedDataUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewStoreMultipleEncryptedDataUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) StoreMultipleEncryptedDataUseCase {
logger = logger.Named("StoreMultipleEncryptedDataUseCase")
return &storeMultipleEncryptedDataUseCaseImpl{config, logger, repo}
}
func (uc *storeMultipleEncryptedDataUseCaseImpl) Execute(items []EncryptedDataItem) ([]StorageResult, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if items == nil || len(items) == 0 {
e["items"] = "Items are required"
} else {
for i, item := range items {
if item.OwnerID == "" {
e[fmt.Sprintf("items[%d].owner_id", i)] = "Owner ID is required"
}
if item.FileID == "" {
e[fmt.Sprintf("items[%d].file_id", i)] = "File ID is required"
}
if item.EncryptedData == nil || len(item.EncryptedData) == 0 {
e[fmt.Sprintf("items[%d].encrypted_data", i)] = "Encrypted data is required"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating store multiple encrypted data",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Store encrypted data files.
//
results := make([]StorageResult, len(items))
successCount := 0
for i, item := range items {
storagePath, err := uc.repo.StoreEncryptedData(item.OwnerID, item.FileID, item.EncryptedData)
results[i] = StorageResult{
FileID: item.FileID,
StoragePath: storagePath,
Error: err,
}
if err != nil {
uc.logger.Error("Failed to store encrypted data",
zap.String("owner_id", item.OwnerID),
zap.String("file_id", item.FileID),
zap.Int("data_size", len(item.EncryptedData)),
zap.Error(err))
} else {
successCount++
uc.logger.Debug("Successfully stored encrypted data",
zap.String("owner_id", item.OwnerID),
zap.String("file_id", item.FileID),
zap.String("storage_path", storagePath),
zap.Int("data_size", len(item.EncryptedData)))
}
}
// Log summary
uc.logger.Info("Completed bulk store operation",
zap.Int("total_requested", len(items)),
zap.Int("successful_stores", successCount),
zap.Int("failed_stores", len(items)-successCount))
return results, nil
}

View file

@ -0,0 +1,63 @@
// monorepo/cloud/backend/internal/maplefile/usecase/fileobjectstorage/verify_object_exists.go
package fileobjectstorage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type VerifyObjectExistsUseCase interface {
Execute(storagePath string) (bool, error)
}
type verifyObjectExistsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_file.FileObjectStorageRepository
}
func NewVerifyObjectExistsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo dom_file.FileObjectStorageRepository,
) VerifyObjectExistsUseCase {
logger = logger.Named("VerifyObjectExistsUseCase")
return &verifyObjectExistsUseCaseImpl{config, logger, repo}
}
func (uc *verifyObjectExistsUseCaseImpl) Execute(storagePath string) (bool, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if storagePath == "" {
e["storage_path"] = "Storage path is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating verify if object exists",
zap.Any("error", e))
return false, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Verify if object exists.
//
exists, err := uc.repo.VerifyObjectExists(storagePath)
if err != nil {
uc.logger.Error("Failed to verify if object exists",
zap.String("storage_path", storagePath),
zap.Error(err))
return false, err
}
uc.logger.Debug("Object existence verified",
zap.String("storage_path", storagePath),
zap.Bool("exists", exists))
return exists, nil
}

View file

@ -0,0 +1,50 @@
package storagedailyusage
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
)
// DeleteByUserUseCase deletes all storage daily usage records for a user
// Used for GDPR right-to-be-forgotten implementation
type DeleteByUserUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) error
}
type deleteByUserUseCaseImpl struct {
logger *zap.Logger
repo storagedailyusage.StorageDailyUsageRepository
}
// NewDeleteByUserUseCase creates a new use case for deleting all storage daily usage by user ID
func NewDeleteByUserUseCase(
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) DeleteByUserUseCase {
return &deleteByUserUseCaseImpl{
logger: logger.Named("DeleteStorageDailyUsageByUserUseCase"),
repo: repo,
}
}
func (uc *deleteByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
uc.logger.Info("Deleting all storage daily usage for user",
zap.String("user_id", userID.String()))
err := uc.repo.DeleteByUserID(ctx, userID)
if err != nil {
uc.logger.Error("Failed to delete storage daily usage",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
uc.logger.Info("✅ Successfully deleted all storage daily usage for user",
zap.String("user_id", userID.String()))
return nil
}

View file

@ -0,0 +1,22 @@
package storagedailyusage
import (
"testing"
"go.uber.org/zap"
)
// NOTE: Unit tests for DeleteByUserUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestDeleteByUserUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
logger := zap.NewNop()
useCase := NewDeleteByUserUseCase(logger, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,120 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_trend.go
package storagedailyusage
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// GetStorageDailyUsageTrendRequest contains the trend parameters
type GetStorageDailyUsageTrendRequest struct {
UserID gocql.UUID `json:"user_id"`
TrendPeriod string `json:"trend_period"` // "7days", "monthly", "yearly"
Year *int `json:"year,omitempty"`
Month *time.Month `json:"month,omitempty"`
}
type GetStorageDailyUsageTrendUseCase interface {
Execute(ctx context.Context, req *GetStorageDailyUsageTrendRequest) (*storagedailyusage.StorageUsageTrend, error)
}
type getStorageDailyUsageTrendUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storagedailyusage.StorageDailyUsageRepository
}
func NewGetStorageDailyUsageTrendUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageDailyUsageTrendUseCase {
logger = logger.Named("GetStorageDailyUsageTrendUseCase")
return &getStorageDailyUsageTrendUseCaseImpl{config, logger, repo}
}
func (uc *getStorageDailyUsageTrendUseCaseImpl) Execute(ctx context.Context, req *GetStorageDailyUsageTrendRequest) (*storagedailyusage.StorageUsageTrend, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.TrendPeriod == "" {
e["trend_period"] = "Trend period is required"
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" {
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly"
}
// Validate period-specific parameters
switch req.TrendPeriod {
case "monthly":
if req.Year == nil {
e["year"] = "Year is required for monthly trend"
}
if req.Month == nil {
e["month"] = "Month is required for monthly trend"
}
case "yearly":
if req.Year == nil {
e["year"] = "Year is required for yearly trend"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage daily usage trend",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get trend based on period.
//
var trend *storagedailyusage.StorageUsageTrend
var err error
switch req.TrendPeriod {
case "7days":
trend, err = uc.repo.GetLast7DaysTrend(ctx, req.UserID)
case "monthly":
trend, err = uc.repo.GetMonthlyTrend(ctx, req.UserID, *req.Year, *req.Month)
case "yearly":
trend, err = uc.repo.GetYearlyTrend(ctx, req.UserID, *req.Year)
default:
return nil, httperror.NewForBadRequestWithSingleField("trend_period", "Invalid trend period")
}
if err != nil {
uc.logger.Error("Failed to get storage daily usage trend",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Error(err))
return nil, err
}
uc.logger.Debug("Successfully retrieved storage daily usage trend",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Int("daily_usages_count", len(trend.DailyUsages)),
zap.Int64("total_added", trend.TotalAdded),
zap.Int64("total_removed", trend.TotalRemoved),
zap.Int64("net_change", trend.NetChange))
return trend, nil
}

View file

@ -0,0 +1,185 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_usage_by_date_range.go
package storagedailyusage
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// GetStorageUsageByDateRangeRequest contains the date range parameters
type GetStorageUsageByDateRangeRequest struct {
UserID gocql.UUID `json:"user_id"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
}
// GetStorageUsageByDateRangeResponse contains the usage data for the date range
type GetStorageUsageByDateRangeResponse struct {
UserID gocql.UUID `json:"user_id"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
DailyUsages []*storagedailyusage.StorageDailyUsage `json:"daily_usages"`
Summary *DateRangeSummary `json:"summary"`
}
// DateRangeSummary contains aggregated statistics for the date range
type DateRangeSummary struct {
TotalDays int `json:"total_days"`
DaysWithData int `json:"days_with_data"`
TotalAdded int64 `json:"total_added"`
TotalRemoved int64 `json:"total_removed"`
NetChange int64 `json:"net_change"`
AverageDailyAdd float64 `json:"average_daily_add"`
PeakUsageDay *time.Time `json:"peak_usage_day,omitempty"`
PeakUsageBytes int64 `json:"peak_usage_bytes"`
LowestUsageDay *time.Time `json:"lowest_usage_day,omitempty"`
LowestUsageBytes int64 `json:"lowest_usage_bytes"`
}
type GetStorageUsageByDateRangeUseCase interface {
Execute(ctx context.Context, req *GetStorageUsageByDateRangeRequest) (*GetStorageUsageByDateRangeResponse, error)
}
type getStorageUsageByDateRangeUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storagedailyusage.StorageDailyUsageRepository
}
func NewGetStorageUsageByDateRangeUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageUsageByDateRangeUseCase {
logger = logger.Named("GetStorageUsageByDateRangeUseCase")
return &getStorageUsageByDateRangeUseCaseImpl{config, logger, repo}
}
func (uc *getStorageUsageByDateRangeUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageByDateRangeRequest) (*GetStorageUsageByDateRangeResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.StartDate.IsZero() {
e["start_date"] = "Start date is required"
}
if req.EndDate.IsZero() {
e["end_date"] = "End date is required"
}
if !req.StartDate.IsZero() && !req.EndDate.IsZero() && req.StartDate.After(req.EndDate) {
e["date_range"] = "Start date must be before or equal to end date"
}
// Check for reasonable date range (max 1 year)
if !req.StartDate.IsZero() && !req.EndDate.IsZero() {
daysDiff := int(req.EndDate.Sub(req.StartDate).Hours() / 24)
if daysDiff > 365 {
e["date_range"] = "Date range cannot exceed 365 days"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage usage by date range",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get usage data from repository.
//
// Truncate dates to ensure we're working with date-only values
startDate := req.StartDate.Truncate(24 * time.Hour)
endDate := req.EndDate.Truncate(24 * time.Hour)
dailyUsages, err := uc.repo.GetByUserDateRange(ctx, req.UserID, startDate, endDate)
if err != nil {
uc.logger.Error("Failed to get storage usage by date range",
zap.String("user_id", req.UserID.String()),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err))
return nil, err
}
//
// STEP 3: Generate summary statistics.
//
summary := uc.generateDateRangeSummary(startDate, endDate, dailyUsages)
response := &GetStorageUsageByDateRangeResponse{
UserID: req.UserID,
StartDate: startDate,
EndDate: endDate,
DailyUsages: dailyUsages,
Summary: summary,
}
uc.logger.Debug("Successfully retrieved storage usage by date range",
zap.String("user_id", req.UserID.String()),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Int("daily_usages_count", len(dailyUsages)),
zap.Int("days_with_data", summary.DaysWithData),
zap.Int64("net_change", summary.NetChange))
return response, nil
}
// generateDateRangeSummary creates summary statistics for the date range
func (uc *getStorageUsageByDateRangeUseCaseImpl) generateDateRangeSummary(startDate, endDate time.Time, dailyUsages []*storagedailyusage.StorageDailyUsage) *DateRangeSummary {
totalDays := int(endDate.Sub(startDate).Hours()/24) + 1
summary := &DateRangeSummary{
TotalDays: totalDays,
DaysWithData: len(dailyUsages),
LowestUsageBytes: int64(^uint64(0) >> 1), // Max int64 value as initial
}
if len(dailyUsages) == 0 {
summary.LowestUsageBytes = 0
return summary
}
for _, usage := range dailyUsages {
summary.TotalAdded += usage.TotalAddBytes
summary.TotalRemoved += usage.TotalRemoveBytes
// Track peak usage
if usage.TotalBytes > summary.PeakUsageBytes {
summary.PeakUsageBytes = usage.TotalBytes
peakDay := usage.UsageDay
summary.PeakUsageDay = &peakDay
}
// Track lowest usage
if usage.TotalBytes < summary.LowestUsageBytes {
summary.LowestUsageBytes = usage.TotalBytes
lowestDay := usage.UsageDay
summary.LowestUsageDay = &lowestDay
}
}
summary.NetChange = summary.TotalAdded - summary.TotalRemoved
// Calculate average daily add (only for days with data)
if summary.DaysWithData > 0 {
summary.AverageDailyAdd = float64(summary.TotalAdded) / float64(summary.DaysWithData)
}
return summary
}

View file

@ -0,0 +1,100 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/get_usage_summary.go
package storagedailyusage
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// GetStorageUsageSummaryRequest contains the summary parameters
type GetStorageUsageSummaryRequest struct {
UserID gocql.UUID `json:"user_id"`
SummaryType string `json:"summary_type"` // "current_month", "current_year"
}
type GetStorageUsageSummaryUseCase interface {
Execute(ctx context.Context, req *GetStorageUsageSummaryRequest) (*storagedailyusage.StorageUsageSummary, error)
}
type getStorageUsageSummaryUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storagedailyusage.StorageDailyUsageRepository
}
func NewGetStorageUsageSummaryUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageUsageSummaryUseCase {
logger = logger.Named("GetStorageUsageSummaryUseCase")
return &getStorageUsageSummaryUseCaseImpl{config, logger, repo}
}
func (uc *getStorageUsageSummaryUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageSummaryRequest) (*storagedailyusage.StorageUsageSummary, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.SummaryType == "" {
e["summary_type"] = "Summary type is required"
} else if req.SummaryType != "current_month" && req.SummaryType != "current_year" {
e["summary_type"] = "Summary type must be one of: current_month, current_year"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage usage summary",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get summary based on type.
//
var summary *storagedailyusage.StorageUsageSummary
var err error
switch req.SummaryType {
case "current_month":
summary, err = uc.repo.GetCurrentMonthUsage(ctx, req.UserID)
case "current_year":
summary, err = uc.repo.GetCurrentYearUsage(ctx, req.UserID)
default:
return nil, httperror.NewForBadRequestWithSingleField("summary_type", "Invalid summary type")
}
if err != nil {
uc.logger.Error("Failed to get storage usage summary",
zap.String("user_id", req.UserID.String()),
zap.String("summary_type", req.SummaryType),
zap.Error(err))
return nil, err
}
uc.logger.Debug("Successfully retrieved storage usage summary",
zap.String("user_id", req.UserID.String()),
zap.String("summary_type", req.SummaryType),
zap.Int64("current_usage", summary.CurrentUsage),
zap.Int64("total_added", summary.TotalAdded),
zap.Int64("total_removed", summary.TotalRemoved),
zap.Int64("net_change", summary.NetChange),
zap.Int("days_with_data", summary.DaysWithData))
return summary, nil
}

View file

@ -0,0 +1,49 @@
package storagedailyusage
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
)
// Wire providers for storage daily usage use cases
func ProvideGetStorageDailyUsageTrendUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageDailyUsageTrendUseCase {
return NewGetStorageDailyUsageTrendUseCase(cfg, logger, repo)
}
func ProvideGetStorageUsageSummaryUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageUsageSummaryUseCase {
return NewGetStorageUsageSummaryUseCase(cfg, logger, repo)
}
func ProvideGetStorageUsageByDateRangeUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) GetStorageUsageByDateRangeUseCase {
return NewGetStorageUsageByDateRangeUseCase(cfg, logger, repo)
}
func ProvideUpdateStorageUsageUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) UpdateStorageUsageUseCase {
return NewUpdateStorageUsageUseCase(cfg, logger, repo)
}
func ProvideDeleteByUserUseCase(
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) DeleteByUserUseCase {
return NewDeleteByUserUseCase(logger, repo)
}

View file

@ -0,0 +1,124 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storagedailyusage/update_usage.go
package storagedailyusage
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// UpdateStorageUsageRequest contains the update parameters
type UpdateStorageUsageRequest struct {
UserID gocql.UUID `json:"user_id"`
UsageDay *time.Time `json:"usage_day,omitempty"` // Optional, defaults to today
TotalBytes int64 `json:"total_bytes"`
AddBytes int64 `json:"add_bytes"`
RemoveBytes int64 `json:"remove_bytes"`
IsIncrement bool `json:"is_increment"` // If true, increment existing values; if false, set absolute values
}
type UpdateStorageUsageUseCase interface {
Execute(ctx context.Context, req *UpdateStorageUsageRequest) error
}
type updateStorageUsageUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storagedailyusage.StorageDailyUsageRepository
}
func NewUpdateStorageUsageUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storagedailyusage.StorageDailyUsageRepository,
) UpdateStorageUsageUseCase {
logger = logger.Named("UpdateStorageUsageUseCase")
return &updateStorageUsageUseCaseImpl{config, logger, repo}
}
func (uc *updateStorageUsageUseCaseImpl) Execute(ctx context.Context, req *UpdateStorageUsageRequest) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.AddBytes < 0 {
e["add_bytes"] = "Add bytes cannot be negative"
}
if req.RemoveBytes < 0 {
e["remove_bytes"] = "Remove bytes cannot be negative"
}
if !req.IsIncrement && req.TotalBytes < 0 {
e["total_bytes"] = "Total bytes cannot be negative when setting absolute values"
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating update storage usage",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Set usage day if not provided.
//
usageDay := time.Now().Truncate(24 * time.Hour)
if req.UsageDay != nil {
usageDay = req.UsageDay.Truncate(24 * time.Hour)
}
//
// STEP 3: Update or increment usage.
//
var err error
if req.IsIncrement {
// Increment existing values
err = uc.repo.IncrementUsage(ctx, req.UserID, usageDay, req.TotalBytes, req.AddBytes, req.RemoveBytes)
} else {
// Set absolute values
usage := &storagedailyusage.StorageDailyUsage{
UserID: req.UserID,
UsageDay: usageDay,
TotalBytes: req.TotalBytes,
TotalAddBytes: req.AddBytes,
TotalRemoveBytes: req.RemoveBytes,
}
err = uc.repo.UpdateOrCreate(ctx, usage)
}
if err != nil {
uc.logger.Error("Failed to update storage usage",
zap.String("user_id", req.UserID.String()),
zap.Time("usage_day", usageDay),
zap.Int64("total_bytes", req.TotalBytes),
zap.Int64("add_bytes", req.AddBytes),
zap.Int64("remove_bytes", req.RemoveBytes),
zap.Bool("is_increment", req.IsIncrement),
zap.Error(err))
return err
}
uc.logger.Debug("Successfully updated storage usage",
zap.String("user_id", req.UserID.String()),
zap.Time("usage_day", usageDay),
zap.Int64("total_bytes", req.TotalBytes),
zap.Int64("add_bytes", req.AddBytes),
zap.Int64("remove_bytes", req.RemoveBytes),
zap.Bool("is_increment", req.IsIncrement))
return nil
}

View file

@ -0,0 +1,87 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/create_event.go
package storageusageevent
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreateStorageUsageEventUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, fileSize int64, operation string) error
}
type createStorageUsageEventUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storageusageevent.StorageUsageEventRepository
}
func NewCreateStorageUsageEventUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) CreateStorageUsageEventUseCase {
logger = logger.Named("CreateStorageUsageEventUseCase")
return &createStorageUsageEventUseCaseImpl{config, logger, repo}
}
func (uc *createStorageUsageEventUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, fileSize int64, operation string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if fileSize <= 0 {
e["file_size"] = "File size must be greater than 0"
}
if operation == "" {
e["operation"] = "Operation is required"
} else if operation != "add" && operation != "remove" {
e["operation"] = "Operation must be 'add' or 'remove'"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating create storage usage event",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Create storage usage event.
//
now := time.Now()
event := &storageusageevent.StorageUsageEvent{
UserID: userID,
EventDay: now.Truncate(24 * time.Hour),
EventTime: now,
FileSize: fileSize,
Operation: operation,
}
err := uc.repo.Create(ctx, event)
if err != nil {
uc.logger.Error("Failed to create storage usage event",
zap.String("user_id", userID.String()),
zap.Int64("file_size", fileSize),
zap.String("operation", operation),
zap.Error(err))
return err
}
uc.logger.Debug("Successfully created storage usage event",
zap.String("user_id", userID.String()),
zap.Int64("file_size", fileSize),
zap.String("operation", operation))
return nil
}

View file

@ -0,0 +1,50 @@
package storageusageevent
import (
"context"
"github.com/gocql/gocql"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
)
// DeleteByUserUseCase deletes all storage usage events for a user
// Used for GDPR right-to-be-forgotten implementation
type DeleteByUserUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) error
}
type deleteByUserUseCaseImpl struct {
logger *zap.Logger
repo storageusageevent.StorageUsageEventRepository
}
// NewDeleteByUserUseCase creates a new use case for deleting all storage usage events by user ID
func NewDeleteByUserUseCase(
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) DeleteByUserUseCase {
return &deleteByUserUseCaseImpl{
logger: logger.Named("DeleteStorageUsageEventByUserUseCase"),
repo: repo,
}
}
func (uc *deleteByUserUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
uc.logger.Info("Deleting all storage usage events for user",
zap.String("user_id", userID.String()))
err := uc.repo.DeleteByUserID(ctx, userID)
if err != nil {
uc.logger.Error("Failed to delete storage usage events",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
uc.logger.Info("✅ Successfully deleted all storage usage events for user",
zap.String("user_id", userID.String()))
return nil
}

View file

@ -0,0 +1,22 @@
package storageusageevent
import (
"testing"
"go.uber.org/zap"
)
// NOTE: Unit tests for DeleteByUserUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestDeleteByUserUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
logger := zap.NewNop()
useCase := NewDeleteByUserUseCase(logger, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,159 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/get_events.go
package storageusageevent
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// GetStorageUsageEventsRequest contains the filtering parameters
type GetStorageUsageEventsRequest struct {
UserID gocql.UUID `json:"user_id"`
TrendPeriod string `json:"trend_period"` // "7days", "monthly", "yearly"
Year *int `json:"year,omitempty"`
Month *time.Month `json:"month,omitempty"`
Days *int `json:"days,omitempty"` // For custom day ranges
}
// GetStorageUsageEventsResponse contains the filtered events
type GetStorageUsageEventsResponse struct {
UserID gocql.UUID `json:"user_id"`
TrendPeriod string `json:"trend_period"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Events []*storageusageevent.StorageUsageEvent `json:"events"`
EventCount int `json:"event_count"`
}
type GetStorageUsageEventsUseCase interface {
Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*GetStorageUsageEventsResponse, error)
}
type getStorageUsageEventsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storageusageevent.StorageUsageEventRepository
}
func NewGetStorageUsageEventsUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) GetStorageUsageEventsUseCase {
logger = logger.Named("GetStorageUsageEventsUseCase")
return &getStorageUsageEventsUseCaseImpl{config, logger, repo}
}
func (uc *getStorageUsageEventsUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*GetStorageUsageEventsResponse, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.TrendPeriod == "" {
e["trend_period"] = "Trend period is required"
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" && req.TrendPeriod != "custom" {
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly, custom"
}
// Validate period-specific parameters
switch req.TrendPeriod {
case "monthly":
if req.Year == nil {
e["year"] = "Year is required for monthly trend"
}
if req.Month == nil {
e["month"] = "Month is required for monthly trend"
}
case "yearly":
if req.Year == nil {
e["year"] = "Year is required for yearly trend"
}
case "custom":
if req.Days == nil || *req.Days <= 0 {
e["days"] = "Days must be greater than 0 for custom trend"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage usage events",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get events based on trend period.
//
var events []*storageusageevent.StorageUsageEvent
var err error
var startDate, endDate time.Time
switch req.TrendPeriod {
case "7days":
events, err = uc.repo.GetLast7DaysEvents(ctx, req.UserID)
endDate = time.Now().Truncate(24 * time.Hour)
startDate = endDate.Add(-6 * 24 * time.Hour)
case "monthly":
events, err = uc.repo.GetMonthlyEvents(ctx, req.UserID, *req.Year, *req.Month)
startDate = time.Date(*req.Year, *req.Month, 1, 0, 0, 0, 0, time.UTC)
endDate = startDate.AddDate(0, 1, -1) // Last day of the month
case "yearly":
events, err = uc.repo.GetYearlyEvents(ctx, req.UserID, *req.Year)
startDate = time.Date(*req.Year, 1, 1, 0, 0, 0, 0, time.UTC)
endDate = time.Date(*req.Year, 12, 31, 0, 0, 0, 0, time.UTC)
case "custom":
events, err = uc.repo.GetLastNDaysEvents(ctx, req.UserID, *req.Days)
endDate = time.Now().Truncate(24 * time.Hour)
startDate = endDate.Add(-time.Duration(*req.Days-1) * 24 * time.Hour)
default:
return nil, httperror.NewForBadRequestWithSingleField("trend_period", "Invalid trend period")
}
if err != nil {
uc.logger.Error("Failed to get storage usage events",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Error(err))
return nil, err
}
//
// STEP 3: Build response.
//
response := &GetStorageUsageEventsResponse{
UserID: req.UserID,
TrendPeriod: req.TrendPeriod,
StartDate: startDate,
EndDate: endDate,
Events: events,
EventCount: len(events),
}
uc.logger.Debug("Successfully retrieved storage usage events",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Int("event_count", len(events)),
zap.Time("start_date", startDate),
zap.Time("end_date", endDate))
return response, nil
}

View file

@ -0,0 +1,238 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/usecase/storageusageevent/get_trend_analysis.go
package storageusageevent
import (
"context"
"time"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// StorageEventTrendAnalysis contains aggregated trend data
type StorageEventTrendAnalysis struct {
UserID gocql.UUID `json:"user_id"`
TrendPeriod string `json:"trend_period"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
TotalEvents int `json:"total_events"`
AddEvents int `json:"add_events"`
RemoveEvents int `json:"remove_events"`
TotalBytesAdded int64 `json:"total_bytes_added"`
TotalBytesRemoved int64 `json:"total_bytes_removed"`
NetBytesChange int64 `json:"net_bytes_change"`
AverageBytesPerAdd float64 `json:"average_bytes_per_add"`
AverageBytesPerRemove float64 `json:"average_bytes_per_remove"`
LargestAddEvent int64 `json:"largest_add_event"`
LargestRemoveEvent int64 `json:"largest_remove_event"`
DailyBreakdown []DailyStats `json:"daily_breakdown,omitempty"`
}
// DailyStats represents daily aggregated statistics
type DailyStats struct {
Date time.Time `json:"date"`
AddEvents int `json:"add_events"`
RemoveEvents int `json:"remove_events"`
BytesAdded int64 `json:"bytes_added"`
BytesRemoved int64 `json:"bytes_removed"`
NetChange int64 `json:"net_change"`
}
type GetStorageUsageEventsTrendAnalysisUseCase interface {
Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*StorageEventTrendAnalysis, error)
}
type getStorageUsageEventsTrendAnalysisUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo storageusageevent.StorageUsageEventRepository
}
func NewGetStorageUsageEventsTrendAnalysisUseCase(
config *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) GetStorageUsageEventsTrendAnalysisUseCase {
logger = logger.Named("GetStorageUsageEventsTrendAnalysisUseCase")
return &getStorageUsageEventsTrendAnalysisUseCaseImpl{config, logger, repo}
}
func (uc *getStorageUsageEventsTrendAnalysisUseCaseImpl) Execute(ctx context.Context, req *GetStorageUsageEventsRequest) (*StorageEventTrendAnalysis, error) {
//
// STEP 1: Validation (reuse from GetStorageUsageEventsUseCase).
//
e := make(map[string]string)
if req == nil {
e["request"] = "Request is required"
} else {
if req.UserID.String() == "" {
e["user_id"] = "User ID is required"
}
if req.TrendPeriod == "" {
e["trend_period"] = "Trend period is required"
} else if req.TrendPeriod != "7days" && req.TrendPeriod != "monthly" && req.TrendPeriod != "yearly" && req.TrendPeriod != "custom" {
e["trend_period"] = "Trend period must be one of: 7days, monthly, yearly, custom"
}
switch req.TrendPeriod {
case "monthly":
if req.Year == nil {
e["year"] = "Year is required for monthly trend"
}
if req.Month == nil {
e["month"] = "Month is required for monthly trend"
}
case "yearly":
if req.Year == nil {
e["year"] = "Year is required for yearly trend"
}
case "custom":
if req.Days == nil || *req.Days <= 0 {
e["days"] = "Days must be greater than 0 for custom trend"
}
}
}
if len(e) != 0 {
uc.logger.Warn("Failed validating get storage usage events trend analysis",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get events based on trend period.
//
var events []*storageusageevent.StorageUsageEvent
var err error
var startDate, endDate time.Time
switch req.TrendPeriod {
case "7days":
events, err = uc.repo.GetLast7DaysEvents(ctx, req.UserID)
endDate = time.Now().Truncate(24 * time.Hour)
startDate = endDate.Add(-6 * 24 * time.Hour)
case "monthly":
events, err = uc.repo.GetMonthlyEvents(ctx, req.UserID, *req.Year, *req.Month)
startDate = time.Date(*req.Year, *req.Month, 1, 0, 0, 0, 0, time.UTC)
endDate = startDate.AddDate(0, 1, -1)
case "yearly":
events, err = uc.repo.GetYearlyEvents(ctx, req.UserID, *req.Year)
startDate = time.Date(*req.Year, 1, 1, 0, 0, 0, 0, time.UTC)
endDate = time.Date(*req.Year, 12, 31, 0, 0, 0, 0, time.UTC)
case "custom":
events, err = uc.repo.GetLastNDaysEvents(ctx, req.UserID, *req.Days)
endDate = time.Now().Truncate(24 * time.Hour)
startDate = endDate.Add(-time.Duration(*req.Days-1) * 24 * time.Hour)
}
if err != nil {
uc.logger.Error("Failed to get storage usage events for trend analysis",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Error(err))
return nil, err
}
//
// STEP 3: Analyze events and build trend analysis.
//
analysis := uc.analyzeEvents(req.UserID, req.TrendPeriod, startDate, endDate, events)
uc.logger.Debug("Successfully analyzed storage usage events trend",
zap.String("user_id", req.UserID.String()),
zap.String("trend_period", req.TrendPeriod),
zap.Int("total_events", analysis.TotalEvents),
zap.Int64("net_bytes_change", analysis.NetBytesChange))
return analysis, nil
}
// analyzeEvents processes the events and generates trend analysis
func (uc *getStorageUsageEventsTrendAnalysisUseCaseImpl) analyzeEvents(userID gocql.UUID, trendPeriod string, startDate, endDate time.Time, events []*storageusageevent.StorageUsageEvent) *StorageEventTrendAnalysis {
analysis := &StorageEventTrendAnalysis{
UserID: userID,
TrendPeriod: trendPeriod,
StartDate: startDate,
EndDate: endDate,
}
if len(events) == 0 {
return analysis
}
// Daily breakdown map
dailyMap := make(map[string]*DailyStats)
// Process each event
for _, event := range events {
analysis.TotalEvents++
if event.Operation == "add" {
analysis.AddEvents++
analysis.TotalBytesAdded += event.FileSize
if event.FileSize > analysis.LargestAddEvent {
analysis.LargestAddEvent = event.FileSize
}
} else if event.Operation == "remove" {
analysis.RemoveEvents++
analysis.TotalBytesRemoved += event.FileSize
if event.FileSize > analysis.LargestRemoveEvent {
analysis.LargestRemoveEvent = event.FileSize
}
}
// Daily breakdown
dayKey := event.EventDay.Format("2006-01-02")
if dailyMap[dayKey] == nil {
dailyMap[dayKey] = &DailyStats{
Date: event.EventDay,
}
}
daily := dailyMap[dayKey]
if event.Operation == "add" {
daily.AddEvents++
daily.BytesAdded += event.FileSize
} else if event.Operation == "remove" {
daily.RemoveEvents++
daily.BytesRemoved += event.FileSize
}
daily.NetChange = daily.BytesAdded - daily.BytesRemoved
}
// Calculate derived metrics
analysis.NetBytesChange = analysis.TotalBytesAdded - analysis.TotalBytesRemoved
if analysis.AddEvents > 0 {
analysis.AverageBytesPerAdd = float64(analysis.TotalBytesAdded) / float64(analysis.AddEvents)
}
if analysis.RemoveEvents > 0 {
analysis.AverageBytesPerRemove = float64(analysis.TotalBytesRemoved) / float64(analysis.RemoveEvents)
}
// Convert daily map to slice and sort by date
for _, daily := range dailyMap {
analysis.DailyBreakdown = append(analysis.DailyBreakdown, *daily)
}
// Sort daily breakdown by date
for i := 0; i < len(analysis.DailyBreakdown)-1; i++ {
for j := i + 1; j < len(analysis.DailyBreakdown); j++ {
if analysis.DailyBreakdown[i].Date.After(analysis.DailyBreakdown[j].Date) {
analysis.DailyBreakdown[i], analysis.DailyBreakdown[j] = analysis.DailyBreakdown[j], analysis.DailyBreakdown[i]
}
}
}
return analysis
}

View file

@ -0,0 +1,41 @@
package storageusageevent
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storageusageevent"
)
// Wire providers for storage usage event use cases
func ProvideCreateStorageUsageEventUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) CreateStorageUsageEventUseCase {
return NewCreateStorageUsageEventUseCase(cfg, logger, repo)
}
func ProvideGetStorageUsageEventsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) GetStorageUsageEventsUseCase {
return NewGetStorageUsageEventsUseCase(cfg, logger, repo)
}
func ProvideGetStorageUsageEventsTrendAnalysisUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) GetStorageUsageEventsTrendAnalysisUseCase {
return NewGetStorageUsageEventsTrendAnalysisUseCase(cfg, logger, repo)
}
func ProvideDeleteByUserUseCase(
logger *zap.Logger,
repo storageusageevent.StorageUsageEventRepository,
) DeleteByUserUseCase {
return NewDeleteByUserUseCase(logger, repo)
}

View file

@ -0,0 +1,140 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/assigntag.go
package tag
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type AssignTagUseCase struct {
tagRepo dom_tag.Repository
collectionRepo dom_collection.CollectionRepository
fileRepo dom_file.FileMetadataRepository
}
func NewAssignTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) *AssignTagUseCase {
return &AssignTagUseCase{
tagRepo: tagRepo,
collectionRepo: collectionRepo,
fileRepo: fileRepo,
}
}
func (uc *AssignTagUseCase) Execute(ctx context.Context, userID, tagID, entityID gocql.UUID, entityType string) error {
// Validate entity type
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
return fmt.Errorf("invalid entity type: %s", entityType)
}
// Verify tag exists and belongs to user
tag, err := uc.tagRepo.GetByID(ctx, tagID)
if err != nil {
return fmt.Errorf("tag not found: %w", err)
}
if tag.UserID != userID {
return fmt.Errorf("unauthorized: tag does not belong to user")
}
// Handle collection or file assignment
switch entityType {
case dom_tag.EntityTypeCollection:
return uc.assignToCollection(ctx, userID, tag, entityID)
case dom_tag.EntityTypeFile:
return uc.assignToFile(ctx, userID, tag, entityID)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (uc *AssignTagUseCase) assignToCollection(ctx context.Context, userID gocql.UUID, tag *dom_tag.Tag, collectionID gocql.UUID) error {
// Get collection
collection, err := uc.collectionRepo.Get(ctx, collectionID)
if err != nil {
return fmt.Errorf("collection not found: %w", err)
}
// Check if tag is already assigned
for _, existingTag := range collection.Tags {
if existingTag.ID == tag.ID {
return nil // Already assigned, idempotent
}
}
// Add embedded tag to collection's tag list
embeddedTag := tag.ToEmbeddedTag()
collection.Tags = append(collection.Tags, *embeddedTag)
collection.ModifiedAt = time.Now()
// Update collection (this will trigger denormalized table maintenance in the repository)
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
return fmt.Errorf("failed to update collection: %w", err)
}
// Create lightweight assignment tracking
assignment := &dom_tag.TagAssignment{
ID: gocql.TimeUUID(),
UserID: userID,
TagID: tag.ID,
EntityID: collectionID,
EntityType: dom_tag.EntityTypeCollection,
CreatedAt: time.Now(),
}
if err := uc.tagRepo.AssignTag(ctx, assignment); err != nil {
return fmt.Errorf("failed to create tag assignment: %w", err)
}
return nil
}
func (uc *AssignTagUseCase) assignToFile(ctx context.Context, userID gocql.UUID, tag *dom_tag.Tag, fileID gocql.UUID) error {
// Get file metadata
file, err := uc.fileRepo.Get(fileID)
if err != nil {
return fmt.Errorf("file not found: %w", err)
}
// Check if tag is already assigned
for _, existingTag := range file.Tags {
if existingTag.ID == tag.ID {
return nil // Already assigned, idempotent
}
}
// Add embedded tag to file's tag list
embeddedTag := tag.ToEmbeddedTag()
file.Tags = append(file.Tags, *embeddedTag)
file.ModifiedAt = time.Now()
// Update file (this will trigger denormalized table maintenance in the repository)
if err := uc.fileRepo.Update(file); err != nil {
return fmt.Errorf("failed to update file: %w", err)
}
// Create lightweight assignment tracking
assignment := &dom_tag.TagAssignment{
ID: gocql.TimeUUID(),
UserID: userID,
TagID: tag.ID,
EntityID: fileID,
EntityType: dom_tag.EntityTypeFile,
CreatedAt: time.Now(),
}
if err := uc.tagRepo.AssignTag(ctx, assignment); err != nil {
return fmt.Errorf("failed to create tag assignment: %w", err)
}
return nil
}

View file

@ -0,0 +1,48 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/create.go
package tag
import (
"context"
"fmt"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type CreateTagUseCase struct {
tagRepo dom_tag.Repository
}
func NewCreateTagUseCase(tagRepo dom_tag.Repository) *CreateTagUseCase {
return &CreateTagUseCase{
tagRepo: tagRepo,
}
}
// Execute creates a new tag with encrypted data (E2EE)
// The client is responsible for:
// 1. Generating a random tag key
// 2. Encrypting name and color with the tag key
// 3. Encrypting the tag key with the user's master key
// 4. Sending all encrypted data to the backend
func (uc *CreateTagUseCase) Execute(ctx context.Context, tag *dom_tag.Tag) error {
// Validate encrypted data is present
if tag.EncryptedName == "" {
return fmt.Errorf("encrypted tag name is required")
}
if tag.EncryptedColor == "" {
return fmt.Errorf("encrypted tag color is required")
}
if tag.EncryptedTagKey == nil || len(tag.EncryptedTagKey.Ciphertext) == 0 {
return fmt.Errorf("encrypted tag key is required")
}
if tag.State == "" {
return fmt.Errorf("tag state is required")
}
// Backend never sees plaintext - only validates encrypted data exists
if err := uc.tagRepo.Create(ctx, tag); err != nil {
return fmt.Errorf("failed to create tag: %w", err)
}
return nil
}

View file

@ -0,0 +1,128 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/delete.go
package tag
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type DeleteTagUseCase struct {
tagRepo dom_tag.Repository
collectionRepo dom_collection.CollectionRepository
fileRepo dom_file.FileMetadataRepository
logger *zap.Logger
}
func NewDeleteTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
logger *zap.Logger,
) *DeleteTagUseCase {
return &DeleteTagUseCase{
tagRepo: tagRepo,
collectionRepo: collectionRepo,
fileRepo: fileRepo,
logger: logger.Named("DeleteTagUseCase"),
}
}
func (uc *DeleteTagUseCase) Execute(ctx context.Context, userID, id gocql.UUID) error {
// Remove tag from all collections and files before deleting
// This runs synchronously to ensure data consistency
uc.logger.Info("🏷️ TAG DELETE: Starting tag deletion",
zap.String("tag_id", id.String()),
zap.String("user_id", userID.String()))
// Remove from all collections
collections, err := uc.collectionRepo.ListByTagID(ctx, id)
if err != nil {
uc.logger.Error("🏷️ TAG DELETE: Failed to list collections by tag",
zap.String("tag_id", id.String()),
zap.Error(err))
} else {
uc.logger.Info("🏷️ TAG DELETE: Found collections to clean up",
zap.String("tag_id", id.String()),
zap.Int("count", len(collections)))
for _, collection := range collections {
// Remove the tag from the collection's Tags array
newTags := make([]dom_tag.EmbeddedTag, 0, len(collection.Tags))
for _, embeddedTag := range collection.Tags {
if embeddedTag.ID != id {
newTags = append(newTags, embeddedTag)
}
}
if len(newTags) != len(collection.Tags) {
collection.Tags = newTags
collection.ModifiedAt = time.Now()
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
uc.logger.Error("🏷️ TAG DELETE: Failed to update collection",
zap.String("tag_id", id.String()),
zap.String("collection_id", collection.ID.String()),
zap.Error(err))
} else {
uc.logger.Debug("🏷️ TAG DELETE: Removed tag from collection",
zap.String("tag_id", id.String()),
zap.String("collection_id", collection.ID.String()))
}
}
}
}
// Remove from all files
files, err := uc.fileRepo.ListByTagID(ctx, id)
if err != nil {
uc.logger.Error("🏷️ TAG DELETE: Failed to list files by tag",
zap.String("tag_id", id.String()),
zap.Error(err))
} else {
uc.logger.Info("🏷️ TAG DELETE: Found files to clean up",
zap.String("tag_id", id.String()),
zap.Int("count", len(files)))
for _, file := range files {
// Remove the tag from the file's Tags array
newTags := make([]dom_tag.EmbeddedTag, 0, len(file.Tags))
for _, embeddedTag := range file.Tags {
if embeddedTag.ID != id {
newTags = append(newTags, embeddedTag)
}
}
if len(newTags) != len(file.Tags) {
file.Tags = newTags
file.ModifiedAt = time.Now()
if err := uc.fileRepo.Update(file); err != nil {
uc.logger.Error("🏷️ TAG DELETE: Failed to update file",
zap.String("tag_id", id.String()),
zap.String("file_id", file.ID.String()),
zap.Error(err))
} else {
uc.logger.Debug("🏷️ TAG DELETE: Removed tag from file",
zap.String("tag_id", id.String()),
zap.String("file_id", file.ID.String()))
}
}
}
}
// Finally, delete the tag itself
if err := uc.tagRepo.DeleteByID(ctx, userID, id); err != nil {
return fmt.Errorf("failed to delete tag: %w", err)
}
uc.logger.Info("🏷️ TAG DELETE: Completed tag deletion",
zap.String("tag_id", id.String()))
return nil
}

View file

@ -0,0 +1,30 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/getbyid.go
package tag
import (
"context"
"fmt"
"github.com/gocql/gocql"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type GetTagByIDUseCase struct {
tagRepo dom_tag.Repository
}
func NewGetTagByIDUseCase(tagRepo dom_tag.Repository) *GetTagByIDUseCase {
return &GetTagByIDUseCase{
tagRepo: tagRepo,
}
}
func (uc *GetTagByIDUseCase) Execute(ctx context.Context, id gocql.UUID) (*dom_tag.Tag, error) {
tag, err := uc.tagRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get tag: %w", err)
}
return tag, nil
}

View file

@ -0,0 +1,35 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/gettagsforentity.go
package tag
import (
"context"
"fmt"
"github.com/gocql/gocql"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type GetTagsForEntityUseCase struct {
tagRepo dom_tag.Repository
}
func NewGetTagsForEntityUseCase(tagRepo dom_tag.Repository) *GetTagsForEntityUseCase {
return &GetTagsForEntityUseCase{
tagRepo: tagRepo,
}
}
func (uc *GetTagsForEntityUseCase) Execute(ctx context.Context, entityID gocql.UUID, entityType string) ([]*dom_tag.Tag, error) {
// Validate entity type
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
return nil, fmt.Errorf("invalid entity type: %s", entityType)
}
tags, err := uc.tagRepo.GetTagsForEntity(ctx, entityID, entityType)
if err != nil {
return nil, fmt.Errorf("failed to get tags for entity: %w", err)
}
return tags, nil
}

View file

@ -0,0 +1,30 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listbyuser.go
package tag
import (
"context"
"fmt"
"github.com/gocql/gocql"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type ListTagsByUserUseCase struct {
tagRepo dom_tag.Repository
}
func NewListTagsByUserUseCase(tagRepo dom_tag.Repository) *ListTagsByUserUseCase {
return &ListTagsByUserUseCase{
tagRepo: tagRepo,
}
}
func (uc *ListTagsByUserUseCase) Execute(ctx context.Context, userID gocql.UUID) ([]*dom_tag.Tag, error) {
tags, err := uc.tagRepo.ListByUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to list tags: %w", err)
}
return tags, nil
}

View file

@ -0,0 +1,132 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listcollectionsbytag.go
package tag
import (
"context"
"encoding/base64"
"fmt"
"sort"
"github.com/gocql/gocql"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type ListCollectionsByTagUseCase struct {
tagRepo dom_tag.Repository
collectionRepo dom_collection.CollectionRepository
}
func NewListCollectionsByTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
) *ListCollectionsByTagUseCase {
return &ListCollectionsByTagUseCase{
tagRepo: tagRepo,
collectionRepo: collectionRepo,
}
}
// Execute returns collections that have ALL specified tags (AND logic)
func (uc *ListCollectionsByTagUseCase) Execute(
ctx context.Context,
userID gocql.UUID,
tagIDs []gocql.UUID,
limit int,
cursor string,
) ([]*dom_collection.Collection, string, error) {
if len(tagIDs) == 0 {
return []*dom_collection.Collection{}, "", nil
}
if limit <= 0 || limit > 100 {
limit = 50
}
// Verify all tags exist and belong to user
for _, tagID := range tagIDs {
tag, err := uc.tagRepo.GetByID(ctx, tagID)
if err != nil {
return nil, "", fmt.Errorf("tag %s not found: %w", tagID.String(), err)
}
if tag.UserID != userID {
return nil, "", fmt.Errorf("unauthorized: tag %s does not belong to user", tagID.String())
}
}
// Query each tag and build collection ID -> count map
collectionCounts := make(map[gocql.UUID]int)
collectionData := make(map[gocql.UUID]*dom_collection.Collection)
for _, tagID := range tagIDs {
// Get collections for this tag
collections, err := uc.collectionRepo.ListByTagID(ctx, tagID)
if err != nil {
return nil, "", fmt.Errorf("failed to list collections by tag: %w", err)
}
for _, collection := range collections {
// Filter by user ownership
if collection.OwnerID != userID {
continue
}
collectionCounts[collection.ID]++
// Store collection data on first occurrence
if _, exists := collectionData[collection.ID]; !exists {
collectionData[collection.ID] = collection
}
}
}
// Filter to collections that have ALL tags (AND logic)
var intersectionCollections []*dom_collection.Collection
for collectionID, count := range collectionCounts {
if count == len(tagIDs) {
intersectionCollections = append(intersectionCollections, collectionData[collectionID])
}
}
// Sort by ID for consistent ordering
sort.Slice(intersectionCollections, func(i, j int) bool {
return intersectionCollections[i].ID.String() < intersectionCollections[j].ID.String()
})
// Apply cursor pagination
var cursorCollectionID gocql.UUID
if cursor != "" {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err == nil {
cursorCollectionID, _ = gocql.ParseUUID(string(decoded))
}
}
// Filter by cursor
filteredCollections := make([]*dom_collection.Collection, 0, limit)
foundCursor := cursorCollectionID.String() == "00000000-0000-0000-0000-000000000000"
for _, collection := range intersectionCollections {
if !foundCursor {
if collection.ID.String() > cursorCollectionID.String() {
foundCursor = true
} else {
continue
}
}
filteredCollections = append(filteredCollections, collection)
if len(filteredCollections) >= limit {
break
}
}
// Generate next cursor
var nextCursor string
if len(filteredCollections) == limit && len(intersectionCollections) > len(filteredCollections) {
lastCollectionID := filteredCollections[len(filteredCollections)-1].ID
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastCollectionID.String()))
}
return filteredCollections, nextCursor, nil
}

View file

@ -0,0 +1,132 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/listfilesbytag.go
package tag
import (
"context"
"encoding/base64"
"fmt"
"sort"
"github.com/gocql/gocql"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type ListFilesByTagUseCase struct {
tagRepo dom_tag.Repository
fileRepo dom_file.FileMetadataRepository
}
func NewListFilesByTagUseCase(
tagRepo dom_tag.Repository,
fileRepo dom_file.FileMetadataRepository,
) *ListFilesByTagUseCase {
return &ListFilesByTagUseCase{
tagRepo: tagRepo,
fileRepo: fileRepo,
}
}
// Execute returns files that have ALL specified tags (AND logic)
func (uc *ListFilesByTagUseCase) Execute(
ctx context.Context,
userID gocql.UUID,
tagIDs []gocql.UUID,
limit int,
cursor string,
) ([]*dom_file.File, string, error) {
if len(tagIDs) == 0 {
return []*dom_file.File{}, "", nil
}
if limit <= 0 || limit > 100 {
limit = 50
}
// Verify all tags exist and belong to user
for _, tagID := range tagIDs {
tag, err := uc.tagRepo.GetByID(ctx, tagID)
if err != nil {
return nil, "", fmt.Errorf("tag %s not found: %w", tagID.String(), err)
}
if tag.UserID != userID {
return nil, "", fmt.Errorf("unauthorized: tag %s does not belong to user", tagID.String())
}
}
// Query each tag and build file ID -> count map
fileCounts := make(map[gocql.UUID]int)
fileData := make(map[gocql.UUID]*dom_file.File)
for _, tagID := range tagIDs {
// Get files for this tag
files, err := uc.fileRepo.ListByTagID(ctx, tagID)
if err != nil {
return nil, "", fmt.Errorf("failed to list files by tag: %w", err)
}
for _, file := range files {
// Filter by user ownership
if file.OwnerID != userID {
continue
}
fileCounts[file.ID]++
// Store file data on first occurrence
if _, exists := fileData[file.ID]; !exists {
fileData[file.ID] = file
}
}
}
// Filter to files that have ALL tags (AND logic)
var intersectionFiles []*dom_file.File
for fileID, count := range fileCounts {
if count == len(tagIDs) {
intersectionFiles = append(intersectionFiles, fileData[fileID])
}
}
// Sort by ID for consistent ordering
sort.Slice(intersectionFiles, func(i, j int) bool {
return intersectionFiles[i].ID.String() < intersectionFiles[j].ID.String()
})
// Apply cursor pagination
var cursorFileID gocql.UUID
if cursor != "" {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err == nil {
cursorFileID, _ = gocql.ParseUUID(string(decoded))
}
}
// Filter by cursor
filteredFiles := make([]*dom_file.File, 0, limit)
foundCursor := cursorFileID.String() == "00000000-0000-0000-0000-000000000000"
for _, file := range intersectionFiles {
if !foundCursor {
if file.ID.String() > cursorFileID.String() {
foundCursor = true
} else {
continue
}
}
filteredFiles = append(filteredFiles, file)
if len(filteredFiles) >= limit {
break
}
}
// Generate next cursor
var nextCursor string
if len(filteredFiles) == limit && len(intersectionFiles) > len(filteredFiles) {
lastFileID := filteredFiles[len(filteredFiles)-1].ID
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastFileID.String()))
}
return filteredFiles, nextCursor, nil
}

View file

@ -0,0 +1,86 @@
package tag
import (
"go.uber.org/zap"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
// Wire providers for tag use cases
func ProvideCreateTagUseCase(
tagRepo dom_tag.Repository,
) *CreateTagUseCase {
return NewCreateTagUseCase(tagRepo)
}
func ProvideGetTagByIDUseCase(
tagRepo dom_tag.Repository,
) *GetTagByIDUseCase {
return NewGetTagByIDUseCase(tagRepo)
}
func ProvideListTagsByUserUseCase(
tagRepo dom_tag.Repository,
) *ListTagsByUserUseCase {
return NewListTagsByUserUseCase(tagRepo)
}
func ProvideUpdateTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
logger *zap.Logger,
) *UpdateTagUseCase {
return NewUpdateTagUseCase(tagRepo, collectionRepo, fileRepo, logger)
}
func ProvideDeleteTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
logger *zap.Logger,
) *DeleteTagUseCase {
return NewDeleteTagUseCase(tagRepo, collectionRepo, fileRepo, logger)
}
func ProvideAssignTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) *AssignTagUseCase {
return NewAssignTagUseCase(tagRepo, collectionRepo, fileRepo)
}
func ProvideUnassignTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) *UnassignTagUseCase {
return NewUnassignTagUseCase(tagRepo, collectionRepo, fileRepo)
}
func ProvideGetTagsForEntityUseCase(
tagRepo dom_tag.Repository,
) *GetTagsForEntityUseCase {
return NewGetTagsForEntityUseCase(tagRepo)
}
func ProvideListCollectionsByTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
) *ListCollectionsByTagUseCase {
return NewListCollectionsByTagUseCase(tagRepo, collectionRepo)
}
func ProvideListFilesByTagUseCase(
tagRepo dom_tag.Repository,
fileRepo dom_file.FileMetadataRepository,
) *ListFilesByTagUseCase {
return NewListFilesByTagUseCase(tagRepo, fileRepo)
}
// NOTE: CreateDefaultTagsUseCase removed - default tags must be created client-side
// due to E2EE. The client creates default tags after first login using the user's master key.

View file

@ -0,0 +1,125 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/unassigntag.go
package tag
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type UnassignTagUseCase struct {
tagRepo dom_tag.Repository
collectionRepo dom_collection.CollectionRepository
fileRepo dom_file.FileMetadataRepository
}
func NewUnassignTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) *UnassignTagUseCase {
return &UnassignTagUseCase{
tagRepo: tagRepo,
collectionRepo: collectionRepo,
fileRepo: fileRepo,
}
}
func (uc *UnassignTagUseCase) Execute(ctx context.Context, tagID, entityID gocql.UUID, entityType string) error {
// Validate entity type
if entityType != dom_tag.EntityTypeCollection && entityType != dom_tag.EntityTypeFile {
return fmt.Errorf("invalid entity type: %s", entityType)
}
// Handle collection or file unassignment
switch entityType {
case dom_tag.EntityTypeCollection:
return uc.unassignFromCollection(ctx, tagID, entityID)
case dom_tag.EntityTypeFile:
return uc.unassignFromFile(ctx, tagID, entityID)
default:
return fmt.Errorf("unsupported entity type: %s", entityType)
}
}
func (uc *UnassignTagUseCase) unassignFromCollection(ctx context.Context, tagID, collectionID gocql.UUID) error {
// Get collection
collection, err := uc.collectionRepo.Get(ctx, collectionID)
if err != nil {
return fmt.Errorf("collection not found: %w", err)
}
// Remove tag from collection's tag list
newTags := make([]dom_tag.EmbeddedTag, 0, len(collection.Tags))
found := false
for _, existingTag := range collection.Tags {
if existingTag.ID != tagID {
newTags = append(newTags, existingTag)
} else {
found = true
}
}
if !found {
return nil // Tag wasn't assigned, idempotent
}
collection.Tags = newTags
collection.ModifiedAt = time.Now()
// Update collection (this will trigger denormalized table maintenance in the repository)
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
return fmt.Errorf("failed to update collection: %w", err)
}
// Remove lightweight assignment tracking
if err := uc.tagRepo.UnassignTag(ctx, tagID, collectionID, dom_tag.EntityTypeCollection); err != nil {
return fmt.Errorf("failed to remove tag assignment: %w", err)
}
return nil
}
func (uc *UnassignTagUseCase) unassignFromFile(ctx context.Context, tagID, fileID gocql.UUID) error {
// Get file metadata
file, err := uc.fileRepo.Get(fileID)
if err != nil {
return fmt.Errorf("file not found: %w", err)
}
// Remove tag from file's tag list
newTags := make([]dom_tag.EmbeddedTag, 0, len(file.Tags))
found := false
for _, existingTag := range file.Tags {
if existingTag.ID != tagID {
newTags = append(newTags, existingTag)
} else {
found = true
}
}
if !found {
return nil // Tag wasn't assigned, idempotent
}
file.Tags = newTags
file.ModifiedAt = time.Now()
// Update file (this will trigger denormalized table maintenance in the repository)
if err := uc.fileRepo.Update(file); err != nil {
return fmt.Errorf("failed to update file: %w", err)
}
// Remove lightweight assignment tracking
if err := uc.tagRepo.UnassignTag(ctx, tagID, fileID, dom_tag.EntityTypeFile); err != nil {
return fmt.Errorf("failed to remove tag assignment: %w", err)
}
return nil
}

View file

@ -0,0 +1,154 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/tag/update.go
package tag
import (
"context"
"fmt"
"time"
"go.uber.org/zap"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
type UpdateTagUseCase struct {
tagRepo dom_tag.Repository
collectionRepo dom_collection.CollectionRepository
fileRepo dom_file.FileMetadataRepository
logger *zap.Logger
}
func NewUpdateTagUseCase(
tagRepo dom_tag.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
logger *zap.Logger,
) *UpdateTagUseCase {
return &UpdateTagUseCase{
tagRepo: tagRepo,
collectionRepo: collectionRepo,
fileRepo: fileRepo,
logger: logger.Named("UpdateTagUseCase"),
}
}
// Execute updates a tag with new encrypted data (E2EE)
// The client must provide the complete updated tag with newly encrypted fields
// This will propagate the tag update to all collections and files that have this tag embedded
func (uc *UpdateTagUseCase) Execute(ctx context.Context, tag *dom_tag.Tag) error {
// Validate encrypted data is present
if tag.EncryptedName == "" {
return fmt.Errorf("encrypted tag name is required")
}
if tag.EncryptedColor == "" {
return fmt.Errorf("encrypted tag color is required")
}
if tag.EncryptedTagKey == nil || len(tag.EncryptedTagKey.Ciphertext) == 0 {
return fmt.Errorf("encrypted tag key is required")
}
// Update modified timestamp
tag.ModifiedAt = time.Now()
// Backend never sees plaintext - only validates encrypted data exists and updates
if err := uc.tagRepo.Update(ctx, tag); err != nil {
return fmt.Errorf("failed to update tag: %w", err)
}
// Propagate tag updates to all collections and files that have this tag embedded
// This runs asynchronously in the background to avoid blocking the API response
go uc.propagateTagUpdate(context.Background(), tag)
return nil
}
// propagateTagUpdate updates all collections and files that have this tag embedded
// This is called asynchronously after the tag is updated
func (uc *UpdateTagUseCase) propagateTagUpdate(ctx context.Context, tag *dom_tag.Tag) {
uc.logger.Info("🏷️ TAG PROPAGATION: Starting tag update propagation",
zap.String("tag_id", tag.ID.String()))
// Create the updated embedded tag
updatedEmbeddedTag := tag.ToEmbeddedTag()
// Update all collections with this tag
collections, err := uc.collectionRepo.ListByTagID(ctx, tag.ID)
if err != nil {
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to list collections by tag",
zap.String("tag_id", tag.ID.String()),
zap.Error(err))
} else {
uc.logger.Info("🏷️ TAG PROPAGATION: Found collections to update",
zap.String("tag_id", tag.ID.String()),
zap.Int("count", len(collections)))
for _, collection := range collections {
// Update the embedded tag in the collection
updated := false
for i, embeddedTag := range collection.Tags {
if embeddedTag.ID == tag.ID {
collection.Tags[i] = *updatedEmbeddedTag
updated = true
break
}
}
if updated {
collection.ModifiedAt = time.Now()
if err := uc.collectionRepo.Update(ctx, collection); err != nil {
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to update collection",
zap.String("tag_id", tag.ID.String()),
zap.String("collection_id", collection.ID.String()),
zap.Error(err))
} else {
uc.logger.Debug("🏷️ TAG PROPAGATION: Updated collection",
zap.String("tag_id", tag.ID.String()),
zap.String("collection_id", collection.ID.String()))
}
}
}
}
// Update all files with this tag
files, err := uc.fileRepo.ListByTagID(ctx, tag.ID)
if err != nil {
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to list files by tag",
zap.String("tag_id", tag.ID.String()),
zap.Error(err))
} else {
uc.logger.Info("🏷️ TAG PROPAGATION: Found files to update",
zap.String("tag_id", tag.ID.String()),
zap.Int("count", len(files)))
for _, file := range files {
// Update the embedded tag in the file
updated := false
for i, embeddedTag := range file.Tags {
if embeddedTag.ID == tag.ID {
file.Tags[i] = *updatedEmbeddedTag
updated = true
break
}
}
if updated {
file.ModifiedAt = time.Now()
if err := uc.fileRepo.Update(file); err != nil {
uc.logger.Error("🏷️ TAG PROPAGATION: Failed to update file",
zap.String("tag_id", tag.ID.String()),
zap.String("file_id", file.ID.String()),
zap.Error(err))
} else {
uc.logger.Debug("🏷️ TAG PROPAGATION: Updated file",
zap.String("tag_id", tag.ID.String()),
zap.String("file_id", file.ID.String()))
}
}
}
}
uc.logger.Info("🏷️ TAG PROPAGATION: Completed tag update propagation",
zap.String("tag_id", tag.ID.String()))
}

View file

@ -0,0 +1,46 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/anonymize_old_ips.go
package user
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
)
type AnonymizeOldIPsUseCase interface {
Execute(ctx context.Context, cutoffDate time.Time) (int, error)
}
type anonymizeOldIPsUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewAnonymizeOldIPsUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) AnonymizeOldIPsUseCase {
logger = logger.Named("UserAnonymizeOldIPsUseCase")
return &anonymizeOldIPsUseCaseImpl{config, logger, repo}
}
func (uc *anonymizeOldIPsUseCaseImpl) Execute(ctx context.Context, cutoffDate time.Time) (int, error) {
uc.logger.Debug("Anonymizing old IPs in user tables",
zap.Time("cutoff_date", cutoffDate))
count, err := uc.repo.AnonymizeOldIPs(ctx, cutoffDate)
if err != nil {
uc.logger.Error("Failed to anonymize old IPs in user tables",
zap.Error(err),
zap.Time("cutoff_date", cutoffDate))
return 0, err
}
uc.logger.Info("Successfully anonymized old IPs in user tables",
zap.Int("count", count),
zap.Time("cutoff_date", cutoffDate))
return count, nil
}

View file

@ -0,0 +1,123 @@
// monorepo/cloud/backend/internal/maplefile/usecase/user/anonymize_user_ips_immediately.go
package user
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
dom_file "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/file"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// AnonymizeUserIPsImmediatelyUseCase immediately anonymizes all IP addresses for a user
// Used for GDPR right-to-be-forgotten implementation
type AnonymizeUserIPsImmediatelyUseCase interface {
Execute(ctx context.Context, userID gocql.UUID) error
}
type anonymizeUserIPsImmediatelyUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
userRepo dom_user.Repository
collectionRepo dom_collection.CollectionRepository
fileRepo dom_file.FileMetadataRepository
}
func NewAnonymizeUserIPsImmediatelyUseCase(
config *config.Configuration,
logger *zap.Logger,
userRepo dom_user.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) AnonymizeUserIPsImmediatelyUseCase {
logger = logger.Named("AnonymizeUserIPsImmediatelyUseCase")
return &anonymizeUserIPsImmediatelyUseCaseImpl{config, logger, userRepo, collectionRepo, fileRepo}
}
func (uc *anonymizeUserIPsImmediatelyUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating immediate IP anonymization",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
uc.logger.Info("Starting immediate IP anonymization for user (GDPR mode)",
zap.String("user_id", userID.String()))
//
// STEP 2: Anonymize user metadata IPs
//
uc.logger.Debug("Anonymizing user metadata IPs",
zap.String("user_id", userID.String()))
err := uc.userRepo.AnonymizeUserIPs(ctx, userID)
if err != nil {
uc.logger.Error("Failed to anonymize user metadata IPs",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
uc.logger.Debug("✅ User metadata IPs anonymized")
//
// STEP 3: Anonymize collection IPs for all user's collections
//
uc.logger.Debug("Anonymizing collection IPs for user's collections",
zap.String("user_id", userID.String()))
collectionCount, err := uc.collectionRepo.AnonymizeCollectionIPsByOwner(ctx, userID)
if err != nil {
uc.logger.Error("Failed to anonymize collection IPs",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
uc.logger.Debug("✅ Collection IPs anonymized",
zap.Int("collection_count", collectionCount))
//
// STEP 4: Anonymize file IPs for all user's files
//
uc.logger.Debug("Anonymizing file IPs for user's files",
zap.String("user_id", userID.String()))
fileCount, err := uc.fileRepo.AnonymizeFileIPsByOwner(ctx, userID)
if err != nil {
uc.logger.Error("Failed to anonymize file IPs",
zap.String("user_id", userID.String()),
zap.Error(err))
return err
}
uc.logger.Debug("✅ File IPs anonymized",
zap.Int("file_count", fileCount))
//
// SUCCESS
//
uc.logger.Info("✅ Successfully anonymized all IPs for user",
zap.String("user_id", userID.String()),
zap.Int("collections_anonymized", collectionCount),
zap.Int("files_anonymized", fileCount))
return nil
}

View file

@ -0,0 +1,25 @@
package user
import (
"testing"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
// NOTE: Unit tests for AnonymizeUserIPsImmediatelyUseCase would require mocks.
// For now, this use case will be tested via integration tests.
// See Task 1.10 in RIGHT_TO_BE_FORGOTTEN_IMPLEMENTATION.md
func TestAnonymizeUserIPsImmediatelyUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
cfg := &config.Configuration{}
logger := zap.NewNop()
useCase := NewAnonymizeUserIPsImmediatelyUseCase(cfg, logger, nil, nil, nil)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,89 @@
// monorepo/cloud/backend/internal/usecase/user/clear_user_cache.go
package user
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// ClearUserCacheUseCase clears all cache entries for a user
// Used for GDPR right-to-be-forgotten implementation
type ClearUserCacheUseCase interface {
Execute(ctx context.Context, userID gocql.UUID, email string) error
}
type clearUserCacheUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
}
func NewClearUserCacheUseCase(
config *config.Configuration,
logger *zap.Logger,
) ClearUserCacheUseCase {
logger = logger.Named("ClearUserCacheUseCase")
return &clearUserCacheUseCaseImpl{config, logger}
}
func (uc *clearUserCacheUseCaseImpl) Execute(ctx context.Context, userID gocql.UUID, email string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if userID.String() == "" {
e["user_id"] = "User ID is required"
}
if email == "" {
e["email"] = "Email is required"
}
if len(e) != 0 {
uc.logger.Warn("Failed validating clear user cache",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
uc.logger.Info("Clearing user cache for GDPR deletion",
zap.String("user_id", userID.String()),
zap.String("email", email))
//
// STEP 2: Clear cache entries
//
// LIMITATION: The current cache implementation (Cassandra-based) stores sessions
// keyed by refresh token (format: "refresh:{token}"), not by user ID.
// This means we cannot efficiently query and delete all sessions for a specific user.
//
// CURRENT APPROACH:
// - All cache entries have TTL (Time To Live)
// - Sessions expire automatically based on JWT refresh token duration
// - No user data is permanently stored in cache
//
// GDPR COMPLIANCE:
// - Cache data is transient and automatically expires
// - No PII is stored permanently in cache
// - User deletion still complies with GDPR right-to-erasure
//
// FUTURE ENHANCEMENT OPTIONS:
// 1. Add a secondary index/table: user_id → [session_keys]
// 2. Switch to Redis and use SCAN with pattern: "refresh:*" + check user_id
// 3. Implement a logout-all-sessions endpoint that users can call before deletion
// 4. Store session keys in user metadata for easy cleanup
//
// For now, we log this operation and rely on TTL expiration.
uc.logger.Info("✅ User cache cleared (sessions will expire via TTL)",
zap.String("user_id", userID.String()),
zap.String("note", "Active sessions expire based on JWT refresh token duration"))
// TODO: Implement actual cache cleanup when we have a user_id → session_key mapping
// For now, this is a placeholder that documents the limitation
return nil
}

View file

@ -0,0 +1,21 @@
package user
import (
"testing"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
func TestClearUserCacheUseCase_Constructor(t *testing.T) {
// Test that constructor creates use case successfully
cfg := &config.Configuration{}
logger := zap.NewNop()
useCase := NewClearUserCacheUseCase(cfg, logger)
if useCase == nil {
t.Error("Expected use case to be created, got nil")
}
}

View file

@ -0,0 +1,50 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/create.go
package user
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UserCreateUseCase interface {
Execute(ctx context.Context, user *dom_user.User) error
}
type userCreateUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserCreateUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserCreateUseCase {
logger = logger.Named("UserCreateUseCase")
return &userCreateUseCaseImpl{config, logger, repo}
}
func (uc *userCreateUseCaseImpl) Execute(ctx context.Context, user *dom_user.User) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if user == nil {
e["user"] = "missing value"
} else {
//TODO: IMPL.
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Insert into database.
//
return uc.repo.Create(ctx, user)
}

View file

@ -0,0 +1,51 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/getbyid.go
package user
import (
"context"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UserDeleteUserByEmailUseCase interface {
Execute(ctx context.Context, email string) error
}
type userDeleteUserByEmailImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserDeleteUserByEmailUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserDeleteUserByEmailUseCase {
logger = logger.Named("UserDeleteUserByEmailUseCase")
return &userDeleteUserByEmailImpl{config, logger, repo}
}
func (uc *userDeleteUserByEmailImpl) Execute(ctx context.Context, email string) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if email == "" {
e["email"] = "missing value"
} else {
//TODO: IMPL.
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.DeleteByEmail(ctx, email)
}

Some files were not shown because too many files have changed in this diff Show more