Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
50
cloud/maplefile-backend/internal/usecase/user/create.go
Normal file
50
cloud/maplefile-backend/internal/usecase/user/create.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
50
cloud/maplefile-backend/internal/usecase/user/deletebyid.go
Normal file
50
cloud/maplefile-backend/internal/usecase/user/deletebyid.go
Normal 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)
|
||||
}
|
||||
49
cloud/maplefile-backend/internal/usecase/user/getbyemail.go
Normal file
49
cloud/maplefile-backend/internal/usecase/user/getbyemail.go
Normal 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)
|
||||
}
|
||||
95
cloud/maplefile-backend/internal/usecase/user/getbyid.go
Normal file
95
cloud/maplefile-backend/internal/usecase/user/getbyid.go
Normal 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
|
||||
}
|
||||
68
cloud/maplefile-backend/internal/usecase/user/getbysesid.go
Normal file
68
cloud/maplefile-backend/internal/usecase/user/getbysesid.go
Normal 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
|
||||
}
|
||||
50
cloud/maplefile-backend/internal/usecase/user/getbyverify.go
Normal file
50
cloud/maplefile-backend/internal/usecase/user/getbyverify.go
Normal 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)
|
||||
}
|
||||
110
cloud/maplefile-backend/internal/usecase/user/provider.go
Normal file
110
cloud/maplefile-backend/internal/usecase/user/provider.go
Normal 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)
|
||||
}
|
||||
119
cloud/maplefile-backend/internal/usecase/user/quota_helper.go
Normal file
119
cloud/maplefile-backend/internal/usecase/user/quota_helper.go
Normal 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
|
||||
}
|
||||
50
cloud/maplefile-backend/internal/usecase/user/update.go
Normal file
50
cloud/maplefile-backend/internal/usecase/user/update.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue