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,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)
}

View file

@ -0,0 +1,50 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/getbyid.go
package user
import (
"context"
"go.uber.org/zap"
"github.com/gocql/gocql"
"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 UserDeleteByIDUseCase interface {
Execute(ctx context.Context, id gocql.UUID) error
}
type userDeleteByIDImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserDeleteByIDUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserDeleteByIDUseCase {
logger = logger.Named("UserDeleteByIDUseCase")
return &userDeleteByIDImpl{config, logger, repo}
}
func (uc *userDeleteByIDImpl) Execute(ctx context.Context, id gocql.UUID) error {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "missing value"
}
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.DeleteByID(ctx, id)
}

View file

@ -0,0 +1,49 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/getbyemail.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 UserGetByEmailUseCase interface {
Execute(ctx context.Context, email string) (*dom_user.User, error)
}
type userGetByEmailUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserGetByEmailUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserGetByEmailUseCase {
logger = logger.Named("UserGetByEmailUseCase")
return &userGetByEmailUseCaseImpl{config, logger, repo}
}
func (uc *userGetByEmailUseCaseImpl) Execute(ctx context.Context, email string) (*dom_user.User, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if email == "" {
e["email"] = "missing value"
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
return uc.repo.GetByEmail(ctx, email)
}

View file

@ -0,0 +1,95 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user/getbyid.go
package user
import (
"context"
"errors"
"go.uber.org/zap"
"github.com/gocql/gocql"
"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"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
type UserGetByIDUseCase interface {
Execute(ctx context.Context, id gocql.UUID) (*dom_user.User, error)
}
type userGetByIDUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserGetByIDUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserGetByIDUseCase {
logger = logger.Named("UserGetByIDUseCase")
// Defensive check: ensure dependencies are not nil
if config == nil {
panic("config cannot be nil")
}
if logger == nil {
panic("logger cannot be nil")
}
if repo == nil {
panic("repository cannot be nil")
}
return &userGetByIDUseCaseImpl{
config: config,
logger: logger,
repo: repo,
}
}
func (uc *userGetByIDUseCaseImpl) Execute(ctx context.Context, id gocql.UUID) (*dom_user.User, error) {
// Defensive check: ensure use case was properly initialized
if uc.repo == nil {
uc.logger.Error("repository is nil - use case was not properly initialized")
return nil, errors.New("internal error: repository not available")
}
//
// STEP 1: Validation.
//
e := make(map[string]string)
if id.String() == "" {
e["id"] = "missing value"
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for get by ID",
zap.Any("error", e),
zap.String("id", id.String()))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2: Get from database.
//
uc.logger.Debug("Getting user by ID",
zap.String("user_id", id.String()))
user, err := uc.repo.GetByID(ctx, id)
if err != nil {
uc.logger.Error("Failed to get user from repository",
zap.String("user_id", id.String()),
zap.Any("error", err))
return nil, err
}
if user != nil {
uc.logger.Debug("Successfully retrieved user",
zap.String("user_id", id.String()),
zap.String("email", validation.MaskEmail(user.Email)))
} else {
uc.logger.Debug("User not found",
zap.String("user_id", id.String()))
}
return user, nil
}

View file

@ -0,0 +1,68 @@
package user
import (
"context"
"encoding/json"
"errors"
"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"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/cache/cassandracache"
)
type UserGetBySessionIDUseCase interface {
Execute(ctx context.Context, sessionID string) (*dom_user.User, error)
}
type userGetBySessionIDUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
cache cassandracache.CassandraCacher
}
func NewUserGetBySessionIDUseCase(config *config.Configuration, logger *zap.Logger, ca cassandracache.CassandraCacher) UserGetBySessionIDUseCase {
logger = logger.Named("UserGetBySessionIDUseCase")
return &userGetBySessionIDUseCaseImpl{config, logger, ca}
}
func (uc *userGetBySessionIDUseCaseImpl) Execute(ctx context.Context, sessionID string) (*dom_user.User, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if sessionID == "" {
e["session_id"] = "missing value"
} else {
//TODO: IMPL.
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for upsert",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 2:
//
userBytes, err := uc.cache.Get(ctx, sessionID)
if err != nil {
return nil, err
}
if userBytes == nil {
uc.logger.Warn("record not found")
return nil, errors.New("record not found")
}
var user dom_user.User
err = json.Unmarshal(userBytes, &user)
if err != nil {
uc.logger.Error("unmarshalling failed", zap.Any("err", err))
return nil, err
}
return &user, nil
}

View file

@ -0,0 +1,50 @@
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 UserGetByVerificationCodeUseCase interface {
Execute(ctx context.Context, verificationCode string) (*dom_user.User, error)
}
type userGetByVerificationCodeUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserGetByVerificationCodeUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserGetByVerificationCodeUseCase {
logger = logger.Named("UserGetByVerificationCodeUseCase")
return &userGetByVerificationCodeUseCaseImpl{config, logger, repo}
}
func (uc *userGetByVerificationCodeUseCaseImpl) Execute(ctx context.Context, verificationCode string) (*dom_user.User, error) {
//
// STEP 1: Validation.
//
e := make(map[string]string)
if verificationCode == "" {
e["verification_code"] = "missing value"
} else {
//TODO: IMPL.
}
if len(e) != 0 {
uc.logger.Warn("Validation failed for get by verification",
zap.Any("error", e))
return nil, httperror.NewForBadRequest(&e)
}
//
// STEP 3: Get from database.
//
return uc.repo.GetByVerificationCode(ctx, verificationCode)
}

View file

@ -0,0 +1,110 @@
package user
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"
dom_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
dom_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/cache/cassandracache"
)
// Wire providers for user use cases
func ProvideUserCreateUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserCreateUseCase {
return NewUserCreateUseCase(cfg, logger, repo)
}
func ProvideUserUpdateUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserUpdateUseCase {
return NewUserUpdateUseCase(cfg, logger, repo)
}
func ProvideUserGetByIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserGetByIDUseCase {
return NewUserGetByIDUseCase(cfg, logger, repo)
}
func ProvideUserGetByEmailUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserGetByEmailUseCase {
return NewUserGetByEmailUseCase(cfg, logger, repo)
}
func ProvideUserGetBySessionIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
cache cassandracache.CassandraCacher,
) UserGetBySessionIDUseCase {
return NewUserGetBySessionIDUseCase(cfg, logger, cache)
}
func ProvideUserGetByVerificationCodeUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserGetByVerificationCodeUseCase {
return NewUserGetByVerificationCodeUseCase(cfg, logger, repo)
}
func ProvideUserDeleteByIDUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserDeleteByIDUseCase {
return NewUserDeleteByIDUseCase(cfg, logger, repo)
}
func ProvideUserDeleteUserByEmailUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) UserDeleteUserByEmailUseCase {
return NewUserDeleteUserByEmailUseCase(cfg, logger, repo)
}
func ProvideUserStorageQuotaHelperUseCase(
logger *zap.Logger,
storageDailyUsageRepository dom_storagedailyusage.StorageDailyUsageRepository,
) UserStorageQuotaHelperUseCase {
return NewUserStorageQuotaHelperUseCase(logger, storageDailyUsageRepository)
}
func ProvideAnonymizeOldIPsUseCase(
cfg *config.Configuration,
logger *zap.Logger,
repo dom_user.Repository,
) AnonymizeOldIPsUseCase {
return NewAnonymizeOldIPsUseCase(cfg, logger, repo)
}
func ProvideAnonymizeUserIPsImmediatelyUseCase(
cfg *config.Configuration,
logger *zap.Logger,
userRepo dom_user.Repository,
collectionRepo dom_collection.CollectionRepository,
fileRepo dom_file.FileMetadataRepository,
) AnonymizeUserIPsImmediatelyUseCase {
return NewAnonymizeUserIPsImmediatelyUseCase(cfg, logger, userRepo, collectionRepo, fileRepo)
}
func ProvideClearUserCacheUseCase(
cfg *config.Configuration,
logger *zap.Logger,
) ClearUserCacheUseCase {
return NewClearUserCacheUseCase(cfg, logger)
}

View file

@ -0,0 +1,119 @@
// monorepo/cloud/maplefile-backend/internal/usecase/user/quota_helper.go
package user
import (
"context"
"fmt"
"time"
"github.com/gocql/gocql"
"go.uber.org/zap"
dom_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
// Default storage quota limit: 10GB
const DefaultStorageQuotaBytes int64 = 10 * 1024 * 1024 * 1024
// UserStorageQuotaHelperUseCase provides storage quota validation
type UserStorageQuotaHelperUseCase interface {
HasEnoughQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) (bool, error)
CheckAndReserveQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error
ReleaseQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error
OnFileDeleted(ctx context.Context, userID gocql.UUID, sizeBytes int64) error
}
type userStorageQuotaHelperUseCaseImpl struct {
logger *zap.Logger
storageDailyUsageRepository dom_storagedailyusage.StorageDailyUsageRepository
}
// NewUserStorageQuotaHelperUseCase creates a new storage quota helper use case
func NewUserStorageQuotaHelperUseCase(
logger *zap.Logger,
storageDailyUsageRepository dom_storagedailyusage.StorageDailyUsageRepository,
) UserStorageQuotaHelperUseCase {
return &userStorageQuotaHelperUseCaseImpl{
logger: logger.Named("UserStorageQuotaHelper"),
storageDailyUsageRepository: storageDailyUsageRepository,
}
}
// HasEnoughQuota checks if user has enough storage quota
func (uc *userStorageQuotaHelperUseCaseImpl) HasEnoughQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) (bool, error) {
// Get current storage usage from most recent day
today := time.Now().UTC().Truncate(24 * time.Hour)
usage, err := uc.storageDailyUsageRepository.GetByUserAndDay(ctx, userID, today)
var currentUsage int64 = 0
if err == nil && usage != nil {
currentUsage = usage.TotalBytes
}
// Check if adding the new size would exceed the quota
newTotal := currentUsage + sizeBytes
hasQuota := newTotal <= DefaultStorageQuotaBytes
uc.logger.Debug("Quota check",
zap.String("user_id", userID.String()),
zap.Int64("current_usage", currentUsage),
zap.Int64("requested_size", sizeBytes),
zap.Int64("new_total", newTotal),
zap.Int64("quota_limit", DefaultStorageQuotaBytes),
zap.Bool("has_quota", hasQuota))
return hasQuota, nil
}
// CheckAndReserveQuota reserves storage quota for a user
func (uc *userStorageQuotaHelperUseCaseImpl) CheckAndReserveQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error {
hasQuota, err := uc.HasEnoughQuota(ctx, userID, sizeBytes)
if err != nil {
uc.logger.Error("Failed to check quota",
zap.String("user_id", userID.String()),
zap.Int64("size_bytes", sizeBytes),
zap.Error(err))
return httperror.NewForInternalServerErrorWithSingleField("message", "Failed to check storage quota")
}
if !hasQuota {
uc.logger.Warn("User exceeded storage quota",
zap.String("user_id", userID.String()),
zap.Int64("requested_size", sizeBytes))
return httperror.NewForBadRequestWithSingleField(
"storage_quota",
fmt.Sprintf("Storage quota exceeded. You are trying to upload %d bytes, but your quota limit is %d GB. Please delete some files or upgrade your plan.",
sizeBytes,
DefaultStorageQuotaBytes/(1024*1024*1024)))
}
// Note: Actual quota reservation would be tracked in a separate table
// For now, we just validate and rely on the storage events to track actual usage
uc.logger.Info("Quota check passed",
zap.String("user_id", userID.String()),
zap.Int64("size_bytes", sizeBytes))
return nil
}
// ReleaseQuota releases previously reserved storage quota
func (uc *userStorageQuotaHelperUseCaseImpl) ReleaseQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error {
// Note: In a full implementation, this would release a reservation
// For now, we just log the release since we're not tracking reservations separately
uc.logger.Debug("Quota release requested",
zap.String("user_id", userID.String()),
zap.Int64("size_bytes", sizeBytes))
return nil
}
// OnFileDeleted handles quota updates when a file is deleted
func (uc *userStorageQuotaHelperUseCaseImpl) OnFileDeleted(ctx context.Context, userID gocql.UUID, sizeBytes int64) error {
// Note: This is a no-op because storage usage tracking is handled by storage events
// The actual storage decrease is recorded via IncrementUsage with negative values
uc.logger.Debug("File deleted notification",
zap.String("user_id", userID.String()),
zap.Int64("size_bytes", sizeBytes))
return nil
}

View file

@ -0,0 +1,50 @@
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 UserUpdateUseCase interface {
Execute(ctx context.Context, user *dom_user.User) error
}
type userUpdateUseCaseImpl struct {
config *config.Configuration
logger *zap.Logger
repo dom_user.Repository
}
func NewUserUpdateUseCase(config *config.Configuration, logger *zap.Logger, repo dom_user.Repository) UserUpdateUseCase {
logger = logger.Named("UserUpdateUseCase")
return &userUpdateUseCaseImpl{config, logger, repo}
}
func (uc *userUpdateUseCaseImpl) 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: Update in database.
//
return uc.repo.UpdateByID(ctx, user)
}