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,17 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail/entity.go
package blockedemail
import (
"time"
"github.com/gocql/gocql"
)
// BlockedEmail represents a blocked email entry for a user
type BlockedEmail struct {
UserID gocql.UUID `json:"user_id"`
BlockedEmail string `json:"blocked_email"`
BlockedUserID gocql.UUID `json:"blocked_user_id"`
Reason string `json:"reason"`
CreatedAt time.Time `json:"created_at"`
}

View file

@ -0,0 +1,29 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/blockedemail/interface.go
package blockedemail
import (
"context"
"github.com/gocql/gocql"
)
// BlockedEmailRepository defines the interface for blocked email data access
type BlockedEmailRepository interface {
// Create adds a new blocked email entry
Create(ctx context.Context, blockedEmail *BlockedEmail) error
// Get retrieves a specific blocked email entry
Get(ctx context.Context, userID gocql.UUID, blockedEmail string) (*BlockedEmail, error)
// List retrieves all blocked emails for a user
List(ctx context.Context, userID gocql.UUID) ([]*BlockedEmail, error)
// Delete removes a blocked email entry
Delete(ctx context.Context, userID gocql.UUID, blockedEmail string) error
// IsBlocked checks if an email is blocked by a user
IsBlocked(ctx context.Context, userID gocql.UUID, email string) (bool, error)
// Count returns the number of blocked emails for a user
Count(ctx context.Context, userID gocql.UUID) (int, error)
}

View file

@ -0,0 +1,24 @@
// monorepo/cloud/backend/internal/maplefile/domain/collection/constants.go
package collection
const (
CollectionTypeFolder = "folder"
CollectionTypeAlbum = "album"
)
const ( // Permission levels
CollectionPermissionReadOnly = "read_only"
CollectionPermissionReadWrite = "read_write"
CollectionPermissionAdmin = "admin"
)
const (
CollectionStateActive = "active"
CollectionStateDeleted = "deleted"
CollectionStateArchived = "archived"
)
const (
CollectionAccessTypeOwner = "owner"
CollectionAccessTypeMember = "member"
)

View file

@ -0,0 +1,43 @@
// monorepo/cloud/backend/internal/maplefile/domain/collection/filter.go
package collection
import "github.com/gocql/gocql"
// CollectionFilterOptions defines the filtering options for retrieving collections
type CollectionFilterOptions struct {
// IncludeOwned includes collections where the user is the owner
IncludeOwned bool `json:"include_owned"`
// IncludeShared includes collections where the user is a member (shared with them)
IncludeShared bool `json:"include_shared"`
// UserID is the user for whom we're filtering collections
UserID gocql.UUID `json:"user_id"`
}
// CollectionFilterResult represents the result of a filtered collection query
type CollectionFilterResult struct {
// OwnedCollections are collections where the user is the owner
OwnedCollections []*Collection `json:"owned_collections"`
// SharedCollections are collections shared with the user
SharedCollections []*Collection `json:"shared_collections"`
// TotalCount is the total number of collections returned
TotalCount int `json:"total_count"`
}
// GetAllCollections returns all collections (owned + shared) in a single slice
func (r *CollectionFilterResult) GetAllCollections() []*Collection {
allCollections := make([]*Collection, 0, len(r.OwnedCollections)+len(r.SharedCollections))
allCollections = append(allCollections, r.OwnedCollections...)
allCollections = append(allCollections, r.SharedCollections...)
return allCollections
}
// IsValid checks if the filter options are valid
func (options *CollectionFilterOptions) IsValid() bool {
// At least one filter option must be enabled
return options.IncludeOwned || options.IncludeShared
}
// ShouldIncludeAll returns true if both owned and shared collections should be included
func (options *CollectionFilterOptions) ShouldIncludeAll() bool {
return options.IncludeOwned && options.IncludeShared
}

View file

@ -0,0 +1,89 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/collection/interface.go
package collection
import (
"context"
"time"
"github.com/gocql/gocql"
)
// CollectionRepository defines the interface for collection persistence operations
type CollectionRepository interface {
// Collection CRUD operations
Create(ctx context.Context, collection *Collection) error
Get(ctx context.Context, id gocql.UUID) (*Collection, error)
Update(ctx context.Context, collection *Collection) error
SoftDelete(ctx context.Context, id gocql.UUID) error // Now soft delete
HardDelete(ctx context.Context, id gocql.UUID) error
// State management operations
Archive(ctx context.Context, id gocql.UUID) error
Restore(ctx context.Context, id gocql.UUID) error
// Hierarchical queries (now state-aware)
FindByParent(ctx context.Context, parentID gocql.UUID) ([]*Collection, error)
FindRootCollections(ctx context.Context, ownerID gocql.UUID) ([]*Collection, error)
FindDescendants(ctx context.Context, collectionID gocql.UUID) ([]*Collection, error)
// GetFullHierarchy(ctx context.Context, rootID gocql.UUID) (*Collection, error) // DEPRECATED AND WILL BE REMOVED
// Move collection to a new parent
MoveCollection(ctx context.Context, collectionID, newParentID gocql.UUID, updatedAncestors []gocql.UUID, updatedPathSegments []string) error
// Collection ownership and access queries (now state-aware)
CheckIfExistsByID(ctx context.Context, id gocql.UUID) (bool, error)
GetAllByUserID(ctx context.Context, ownerID gocql.UUID) ([]*Collection, error)
GetCollectionsSharedWithUser(ctx context.Context, userID gocql.UUID) ([]*Collection, error)
IsCollectionOwner(ctx context.Context, collectionID, userID gocql.UUID) (bool, error)
CheckAccess(ctx context.Context, collectionID, userID gocql.UUID, requiredPermission string) (bool, error)
GetUserPermissionLevel(ctx context.Context, collectionID, userID gocql.UUID) (string, error)
// Filtered collection queries (now state-aware)
GetCollectionsWithFilter(ctx context.Context, options CollectionFilterOptions) (*CollectionFilterResult, error)
// Collection membership operations
AddMember(ctx context.Context, collectionID gocql.UUID, membership *CollectionMembership) error
RemoveMember(ctx context.Context, collectionID, recipientID gocql.UUID) error
RemoveUserFromAllCollections(ctx context.Context, userID gocql.UUID, userEmail string) ([]gocql.UUID, error)
UpdateMemberPermission(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error
GetCollectionMembership(ctx context.Context, collectionID, recipientID gocql.UUID) (*CollectionMembership, error)
// Hierarchical sharing
AddMemberToHierarchy(ctx context.Context, rootID gocql.UUID, membership *CollectionMembership) error
RemoveMemberFromHierarchy(ctx context.Context, rootID, recipientID gocql.UUID) error
// GetCollectionSyncData retrieves collection sync data with pagination for the specified user
GetCollectionSyncData(ctx context.Context, userID gocql.UUID, cursor *CollectionSyncCursor, limit int64) (*CollectionSyncResponse, error)
GetCollectionSyncDataByAccessType(ctx context.Context, userID gocql.UUID, cursor *CollectionSyncCursor, limit int64, accessType string) (*CollectionSyncResponse, error)
// Count operations for all collection types (folders + albums)
CountOwnedCollections(ctx context.Context, userID gocql.UUID) (int, error)
CountSharedCollections(ctx context.Context, userID gocql.UUID) (int, error)
CountOwnedFolders(ctx context.Context, userID gocql.UUID) (int, error)
CountSharedFolders(ctx context.Context, userID gocql.UUID) (int, error)
CountTotalUniqueFolders(ctx context.Context, userID gocql.UUID) (int, error)
// IP Anonymization for GDPR compliance
AnonymizeOldIPs(ctx context.Context, cutoffDate time.Time) (int, error)
AnonymizeCollectionIPsByOwner(ctx context.Context, ownerID gocql.UUID) (int, error) // For GDPR right-to-be-forgotten
// File count maintenance operations
IncrementFileCount(ctx context.Context, collectionID gocql.UUID) error
DecrementFileCount(ctx context.Context, collectionID gocql.UUID) error
// RecalculateAllFileCounts recalculates file_count for all collections
// by counting active files. Used for data migration/repair.
RecalculateAllFileCounts(ctx context.Context) (*RecalculateAllFileCountsResult, error)
// Tag-related operations
// ListByTagID retrieves all collections that have the specified tag assigned
// Used for tag update propagation (updating embedded tag data across all collections)
ListByTagID(ctx context.Context, tagID gocql.UUID) ([]*Collection, error)
}
// RecalculateAllFileCountsResult holds the results of the recalculation operation
type RecalculateAllFileCountsResult struct {
TotalCollections int
UpdatedCount int
ErrorCount int
}

View file

@ -0,0 +1,124 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/collection/model.go
package collection
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
"github.com/gocql/gocql"
)
// Collection represents a folder or album.
// Can be used for both root collections and embedded subcollections
type Collection struct {
// Identifiers
// ID is the unique identifier for the collection in the cloud backend.
ID gocql.UUID `bson:"_id" json:"id"`
// OwnerID is the ID of the user who originally created and owns this collection.
// The owner has administrative privileges by default.
OwnerID gocql.UUID `bson:"owner_id" json:"owner_id"`
// Encryption and Content Details
// EncryptedName is the name of the collection, encrypted using the collection's unique key.
// Stored and transferred in encrypted form.
EncryptedName string `bson:"encrypted_name" json:"encrypted_name"`
// CollectionType indicates the nature of the collection, either "folder" or "album".
// Defined by CollectionTypeFolder and CollectionTypeAlbum constants.
CollectionType string `bson:"collection_type" json:"collection_type"` // "folder" or "album"
// EncryptedCollectionKey is the unique symmetric key used to encrypt the collection's data (like name and file metadata).
// This key is encrypted with the owner's master key for storage and transmission,
// allowing the owner's device to decrypt it using their master key.
EncryptedCollectionKey *crypto.EncryptedCollectionKey `bson:"encrypted_collection_key" json:"encrypted_collection_key"`
// EncryptedCustomIcon stores the custom icon for this collection, encrypted with the collection key.
// Empty string means use default folder/album icon.
// Contains either an emoji character (e.g., "📷") or "icon:<identifier>" for predefined icons.
EncryptedCustomIcon string `bson:"encrypted_custom_icon" json:"encrypted_custom_icon"`
// Sharing
// Collection members (users with access)
Members []CollectionMembership `bson:"members" json:"members"`
// Hierarchical structure fields
// ParentID is the ID of the parent collection if this is a subcollection.
// It is omitted (nil) for root collections. Used to reconstruct the hierarchy.
ParentID gocql.UUID `bson:"parent_id,omitempty" json:"parent_id,omitempty"` // Parent collection ID, not stored for root collections
// AncestorIDs is an array containing the IDs of all parent collections up to the root.
// This field is used for efficient querying and traversal of the collection hierarchy without joins.
AncestorIDs []gocql.UUID `bson:"ancestor_ids,omitempty" json:"ancestor_ids,omitempty"` // Array of ancestor IDs for efficient querying
// File count for performance optimization
// FileCount stores the number of active files in this collection.
// This denormalized field eliminates N+1 queries when listing collections.
FileCount int64 `bson:"file_count" json:"file_count"`
// DEPRECATED: Replaced by Tags field below
// TagIDs []gocql.UUID `bson:"tag_ids,omitempty" json:"tag_ids,omitempty"`
// Tags stores full embedded tag data (eliminates frontend API lookups)
// Stored as JSON text in database, marshaled/unmarshaled automatically
Tags []tag.EmbeddedTag `bson:"tags,omitempty" json:"tags,omitempty"`
// Ownership, timestamps and conflict resolution
// CreatedAt is the timestamp when the collection was initially created.
// Recorded on the local device and synced.
CreatedAt time.Time `bson:"created_at" json:"created_at"`
// CreatedByUserID is the ID of the user who created this file.
CreatedByUserID gocql.UUID `bson:"created_by_user_id" json:"created_by_user_id"`
// ModifiedAt is the timestamp of the last modification to the collection's metadata or content.
// Updated on the local device and synced.
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
ModifiedByUserID gocql.UUID `bson:"modified_by_user_id" json:"modified_by_user_id"`
// The current version of the file.
Version uint64 `bson:"version" json:"version"` // Every mutation (create, update, delete, etc) is a versioned operation, keep track of the version number with this variable
// State management
State string `bson:"state" json:"state"` // active, deleted, archived
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"` // The `version` number that this collection was deleted at.
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
}
// CollectionMembership represents a user's access to a collection
type CollectionMembership struct {
ID gocql.UUID `bson:"_id" json:"id"`
CollectionID gocql.UUID `bson:"collection_id" json:"collection_id"` // ID of the collection (redundant but helpful for queries)
RecipientID gocql.UUID `bson:"recipient_id" json:"recipient_id"` // User receiving access
RecipientEmail string `bson:"recipient_email" json:"recipient_email"` // Email for display purposes
GrantedByID gocql.UUID `bson:"granted_by_id" json:"granted_by_id"` // User who shared the collection
// Collection key encrypted with recipient's public key using box_seal. This matches the box_seal format which doesn't need a separate nonce.
EncryptedCollectionKey []byte `bson:"encrypted_collection_key" json:"encrypted_collection_key"`
// Access details
PermissionLevel string `bson:"permission_level" json:"permission_level"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
// Sharing origin tracking
IsInherited bool `bson:"is_inherited" json:"is_inherited"` // Tracks whether access was granted directly or inherited from a parent
InheritedFromID gocql.UUID `bson:"inherited_from_id,omitempty" json:"inherited_from_id,omitempty"` // InheritedFromID identifies which parent collection granted this access
}
// CollectionSyncCursor represents cursor-based pagination for sync operations
type CollectionSyncCursor struct {
LastModified time.Time `json:"last_modified" bson:"last_modified"`
LastID gocql.UUID `json:"last_id" bson:"last_id"`
}
// CollectionSyncItem represents minimal collection data for sync operations
type CollectionSyncItem struct {
ID gocql.UUID `json:"id" bson:"_id"`
Version uint64 `json:"version" bson:"version"`
ModifiedAt time.Time `json:"modified_at" bson:"modified_at"`
State string `json:"state" bson:"state"`
ParentID *gocql.UUID `json:"parent_id,omitempty" bson:"parent_id,omitempty"`
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"`
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
EncryptedCustomIcon string `json:"encrypted_custom_icon,omitempty" bson:"encrypted_custom_icon,omitempty"`
}
// CollectionSyncResponse represents the response for collection sync data
type CollectionSyncResponse struct {
Collections []CollectionSyncItem `json:"collections"`
NextCursor *CollectionSyncCursor `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}

View file

@ -0,0 +1,37 @@
// monorepo/cloud/backend/internal/maplefile/domain/collection/state_validator.go
package collection
import "errors"
// StateTransition validates collection state transitions
type StateTransition struct {
From string
To string
}
// IsValidStateTransition checks if a state transition is allowed
func IsValidStateTransition(from, to string) error {
validTransitions := map[StateTransition]bool{
// From active
{CollectionStateActive, CollectionStateDeleted}: true,
{CollectionStateActive, CollectionStateArchived}: true,
// From deleted (cannot be restored nor archived)
{CollectionStateDeleted, CollectionStateActive}: false,
{CollectionStateDeleted, CollectionStateArchived}: false,
// From archived (can only be restored to active)
{CollectionStateArchived, CollectionStateActive}: true,
// Same state transitions (no-op)
{CollectionStateActive, CollectionStateActive}: true,
{CollectionStateDeleted, CollectionStateDeleted}: true,
{CollectionStateArchived, CollectionStateArchived}: true,
}
if !validTransitions[StateTransition{from, to}] {
return errors.New("invalid state transition from " + from + " to " + to)
}
return nil
}

View file

@ -0,0 +1,69 @@
// monorepo/cloud/maplefile-backend/internal/domain/crypto/domain/keys/kdf.go
package crypto
import (
"fmt"
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/crypto"
)
// KDFParams stores the key derivation function parameters
type KDFParams struct {
Algorithm string `json:"algorithm" bson:"algorithm"` // "argon2id", "pbkdf2", "scrypt"
Version string `json:"version" bson:"version"` // "1.0", "1.1", etc.
Iterations uint32 `json:"iterations" bson:"iterations"` // For PBKDF2 or Argon2 time cost
Memory uint32 `json:"memory" bson:"memory"` // For Argon2 memory in KB
Parallelism uint8 `json:"parallelism" bson:"parallelism"` // For Argon2 threads
SaltLength uint32 `json:"salt_length" bson:"salt_length"` // Salt size in bytes
KeyLength uint32 `json:"key_length" bson:"key_length"` // Output key size in bytes
}
// DefaultKDFParams returns the current recommended KDF parameters
func DefaultKDFParams() KDFParams {
return KDFParams{
Algorithm: crypto.Argon2IDAlgorithm,
Version: "1.0", // Always starts at 1.0
Iterations: crypto.Argon2OpsLimit, // Time cost
Memory: crypto.Argon2MemLimit,
Parallelism: crypto.Argon2Parallelism,
SaltLength: crypto.Argon2SaltSize,
KeyLength: crypto.Argon2KeySize,
}
}
// Validate checks if KDF parameters are valid
func (k KDFParams) Validate() error {
switch k.Algorithm {
case crypto.Argon2IDAlgorithm:
if k.Iterations < 1 {
return fmt.Errorf("argon2id time cost must be >= 1")
}
if k.Memory < 1024 {
return fmt.Errorf("argon2id memory must be >= 1024 KB")
}
if k.Parallelism < 1 {
return fmt.Errorf("argon2id parallelism must be >= 1")
}
default:
return fmt.Errorf("unsupported KDF algorithm: %s", k.Algorithm)
}
if k.SaltLength < 8 {
return fmt.Errorf("salt length must be >= 8 bytes")
}
if k.KeyLength < 16 {
return fmt.Errorf("key length must be >= 16 bytes")
}
return nil
}
// KDFUpgradePolicy defines when to upgrade KDF parameters
type KDFUpgradePolicy struct {
MinimumParams KDFParams `json:"minimum_params" bson:"minimum_params"`
RecommendedParams KDFParams `json:"recommended_params" bson:"recommended_params"`
MaxPasswordAge time.Duration `json:"max_password_age" bson:"max_password_age"`
UpgradeOnNextLogin bool `json:"upgrade_on_next_login" bson:"upgrade_on_next_login"`
LastUpgradeCheck time.Time `json:"last_upgrade_check" bson:"last_upgrade_check"`
}

View file

@ -0,0 +1,355 @@
package crypto
import (
"encoding/base64"
"encoding/json"
"fmt"
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/crypto"
)
// tryDecodeBase64 attempts to decode a base64 string using multiple encodings.
// It tries URL-safe without padding first (libsodium's URLSAFE_NO_PADDING),
// then standard base64 with padding, then standard without padding.
func tryDecodeBase64(s string) ([]byte, error) {
var lastErr error
// Try URL-safe base64 without padding (libsodium's URLSAFE_NO_PADDING)
if data, err := base64.RawURLEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try standard base64 with padding (Go's default for []byte)
if data, err := base64.StdEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try standard base64 without padding
if data, err := base64.RawStdEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
// Try URL-safe base64 with padding
if data, err := base64.URLEncoding.DecodeString(s); err == nil {
return data, nil
} else {
lastErr = err
}
return nil, fmt.Errorf("failed to decode base64 with any encoding: %w", lastErr)
}
// MasterKey represents the root encryption key for a user
type MasterKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedMasterKey is the master key encrypted with the key encryption key
type EncryptedMasterKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (emk *EncryptedMasterKey) GetCurrentVersion() int {
return emk.KeyVersion
}
func (emk *EncryptedMasterKey) GetKeyByVersion(version int) *EncryptedHistoricalKey {
if version == emk.KeyVersion {
// Return current key as historical format
return &EncryptedHistoricalKey{
KeyVersion: emk.KeyVersion,
Ciphertext: emk.Ciphertext,
Nonce: emk.Nonce,
Algorithm: crypto.ChaCha20Poly1305Algorithm, // ✅ Updated to ChaCha20-Poly1305
}
}
for _, key := range emk.PreviousKeys {
if key.KeyVersion == version {
return &key
}
}
return nil
}
// KeyEncryptionKey derived from user password
type KeyEncryptionKey struct {
Key []byte `json:"key" bson:"key"`
Salt []byte `json:"salt" bson:"salt"`
}
// PublicKey for asymmetric encryption
type PublicKey struct {
Key []byte `json:"key" bson:"key"`
VerificationID string `json:"verification_id" bson:"verification_id"`
}
// PrivateKey for asymmetric decryption
type PrivateKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedPrivateKey is the private key encrypted with the master key
type EncryptedPrivateKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}
// RecoveryKey for account recovery
type RecoveryKey struct {
Key []byte `json:"key" bson:"key"`
}
// EncryptedRecoveryKey is the recovery key encrypted with the master key
type EncryptedRecoveryKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}
// CollectionKey encrypts files in a collection
type CollectionKey struct {
Key []byte `json:"key" bson:"key"`
CollectionID string `json:"collection_id" bson:"collection_id"`
}
// EncryptedCollectionKey is the collection key encrypted with master key
type EncryptedCollectionKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (eck *EncryptedCollectionKey) NeedsRotation(policy KeyRotationPolicy) bool {
if eck.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*eck.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedCollectionKey to serialize bytes as base64 strings.
func (eck *EncryptedCollectionKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(eck.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(eck.Nonce),
KeyVersion: eck.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedCollectionKey to handle URL-safe base64 strings.
func (eck *EncryptedCollectionKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedCollectionKey into alias: %w", err)
}
// Set KeyVersion
eck.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedCollectionKey.Ciphertext: %w", err)
}
eck.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedCollectionKey.Nonce: %w", err)
}
eck.Nonce = nonceBytes
}
return nil
}
// FileKey encrypts a specific file
type FileKey struct {
Key []byte `json:"key" bson:"key"`
FileID string `json:"file_id" bson:"file_id"`
}
// EncryptedFileKey is the file key encrypted with collection key
type EncryptedFileKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (eck *EncryptedFileKey) NeedsRotation(policy KeyRotationPolicy) bool {
if eck.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*eck.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedFileKey to serialize bytes as base64 strings.
func (efk *EncryptedFileKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(efk.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(efk.Nonce),
KeyVersion: efk.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedFileKey to handle URL-safe base64 strings.
func (efk *EncryptedFileKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedFileKey into alias: %w", err)
}
// Set KeyVersion
efk.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedFileKey.Ciphertext: %w", err)
}
efk.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedFileKey.Nonce: %w", err)
}
efk.Nonce = nonceBytes
}
return nil
}
// TagKey encrypts tag data (name and color)
type TagKey struct {
Key []byte `json:"key" bson:"key"`
TagID string `json:"tag_id" bson:"tag_id"`
}
// EncryptedTagKey is the tag key encrypted with user's master key
type EncryptedTagKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
KeyVersion int `json:"key_version" bson:"key_version"`
RotatedAt *time.Time `json:"rotated_at,omitempty" bson:"rotated_at,omitempty"`
PreviousKeys []EncryptedHistoricalKey `json:"previous_keys,omitempty" bson:"previous_keys,omitempty"`
}
func (etk *EncryptedTagKey) NeedsRotation(policy KeyRotationPolicy) bool {
if etk.RotatedAt == nil {
return true // Never rotated
}
keyAge := time.Since(*etk.RotatedAt)
return keyAge > policy.MaxKeyAge
}
// MarshalJSON custom marshaller for EncryptedTagKey to serialize bytes as base64 strings.
func (etk *EncryptedTagKey) MarshalJSON() ([]byte, error) {
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
alias := Alias{
Ciphertext: base64.StdEncoding.EncodeToString(etk.Ciphertext),
Nonce: base64.StdEncoding.EncodeToString(etk.Nonce),
KeyVersion: etk.KeyVersion,
}
return json.Marshal(alias)
}
// UnmarshalJSON custom unmarshaller for EncryptedTagKey to handle URL-safe base64 strings.
func (etk *EncryptedTagKey) UnmarshalJSON(data []byte) error {
// Temporary struct to unmarshal into string fields
type Alias struct {
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
KeyVersion int `json:"key_version"`
}
var alias Alias
if err := json.Unmarshal(data, &alias); err != nil {
return fmt.Errorf("failed to unmarshal EncryptedTagKey into alias: %w", err)
}
// Set KeyVersion
etk.KeyVersion = alias.KeyVersion
// Decode Ciphertext - try multiple base64 encodings
if alias.Ciphertext != "" {
ciphertextBytes, err := tryDecodeBase64(alias.Ciphertext)
if err != nil {
return fmt.Errorf("failed to decode EncryptedTagKey.Ciphertext: %w", err)
}
etk.Ciphertext = ciphertextBytes
}
// Decode Nonce - try multiple base64 encodings
if alias.Nonce != "" {
nonceBytes, err := tryDecodeBase64(alias.Nonce)
if err != nil {
return fmt.Errorf("failed to decode EncryptedTagKey.Nonce: %w", err)
}
etk.Nonce = nonceBytes
}
return nil
}
// MasterKeyEncryptedWithRecoveryKey allows account recovery
type MasterKeyEncryptedWithRecoveryKey struct {
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
}

View file

@ -0,0 +1,39 @@
// monorepo/cloud/maplefile-backend/internal/domain/crypto/domain/keys/rotation.go
package crypto
import (
"time"
"github.com/gocql/gocql"
)
// EncryptedHistoricalKey represents a previous version of a key
type EncryptedHistoricalKey struct {
KeyVersion int `json:"key_version" bson:"key_version"`
Ciphertext []byte `json:"ciphertext" bson:"ciphertext"`
Nonce []byte `json:"nonce" bson:"nonce"`
RotatedAt time.Time `json:"rotated_at" bson:"rotated_at"`
RotatedReason string `json:"rotated_reason" bson:"rotated_reason"`
// Algorithm used for this key version
Algorithm string `json:"algorithm" bson:"algorithm"`
}
// KeyRotationPolicy defines when and how to rotate keys
type KeyRotationPolicy struct {
MaxKeyAge time.Duration `json:"max_key_age" bson:"max_key_age"`
MaxKeyUsageCount int64 `json:"max_key_usage_count" bson:"max_key_usage_count"`
ForceRotateOnBreach bool `json:"force_rotate_on_breach" bson:"force_rotate_on_breach"`
}
// KeyRotationRecord tracks rotation events
type KeyRotationRecord struct {
ID gocql.UUID `bson:"_id" json:"id"`
EntityType string `bson:"entity_type" json:"entity_type"` // "user", "collection", "file"
EntityID gocql.UUID `bson:"entity_id" json:"entity_id"`
FromVersion int `bson:"from_version" json:"from_version"`
ToVersion int `bson:"to_version" json:"to_version"`
RotatedAt time.Time `bson:"rotated_at" json:"rotated_at"`
RotatedBy gocql.UUID `bson:"rotated_by" json:"rotated_by"`
Reason string `bson:"reason" json:"reason"`
AffectedItems int64 `bson:"affected_items" json:"affected_items"`
}

View file

@ -0,0 +1,54 @@
// cloud/maplefile-backend/internal/maplefile/domain/dashboard/model.go
package dashboard
import (
"time"
)
// Dashboard represents the main dashboard data structure
type Dashboard struct {
Dashboard DashboardData `json:"dashboard"`
}
// DashboardData contains all the dashboard information
type DashboardData struct {
Summary Summary `json:"summary"`
StorageUsageTrend StorageUsageTrend `json:"storageUsageTrend"`
RecentFiles []RecentFile `json:"recentFiles"`
}
// Summary contains the main dashboard statistics
type Summary struct {
TotalFiles int `json:"totalFiles"`
TotalFolders int `json:"totalFolders"`
StorageUsed StorageAmount `json:"storageUsed"`
StorageLimit StorageAmount `json:"storageLimit"`
StorageUsagePercentage int `json:"storageUsagePercentage"`
}
// StorageAmount represents a storage value with its unit
type StorageAmount struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
// StorageUsageTrend contains the trend chart data
type StorageUsageTrend struct {
Period string `json:"period"`
DataPoints []DataPoint `json:"dataPoints"`
}
// DataPoint represents a single point in the storage usage trend
type DataPoint struct {
Date string `json:"date"`
Usage StorageAmount `json:"usage"`
}
// RecentFile represents a file in the recent files list
type RecentFile struct {
FileName string `json:"fileName"`
Uploaded string `json:"uploaded"`
UploadedTimestamp time.Time `json:"uploadedTimestamp"`
Type string `json:"type"`
Size StorageAmount `json:"size"`
}

View file

@ -0,0 +1,13 @@
// monorepo/cloud/backend/internal/maplefile/domain/file/constants.go
package file
const (
// FileStatePending is the initial state of a file before it is uploaded.
FileStatePending = "pending"
// FileStateActive indicates that the file is fully uploaded and ready for use.
FileStateActive = "active"
// FileStateDeleted marks the file as deleted, but still accessible for a period but will eventually be permanently removed.
FileStateDeleted = "deleted"
// FileStateArchived indicates that the file is no longer accessible.
FileStateArchived = "archived"
)

View file

@ -0,0 +1,95 @@
// monorepo/cloud/backend/internal/maplefile/domain/file/interface.go
package file
import (
"context"
"time"
"github.com/gocql/gocql"
)
// FileMetadataRepository defines the interface for interacting with file metadata storage.
// It handles operations related to storing, retrieving, updating, and deleting file information (metadata).
type FileMetadataRepository interface {
// Create saves a single File metadata record to the storage.
Create(file *File) error
// CreateMany saves multiple File metadata records to the storage.
CreateMany(files []*File) error
// Get retrieves a single File metadata record (regardless of its state) by its unique identifier (ID) .
Get(id gocql.UUID) (*File, error)
// GetByIDs retrieves multiple File metadata records by their unique identifiers (IDs).
GetByIDs(ids []gocql.UUID) ([]*File, error)
// GetByCollection retrieves all File metadata records associated with a specific collection ID.
GetByCollection(collectionID gocql.UUID) ([]*File, error)
// Update modifies an existing File metadata record in the storage.
Update(file *File) error
// SoftDelete removes a single File metadata record by its unique identifier (ID) by setting its state to deleted.
SoftDelete(id gocql.UUID) error
// HardDelete permanently removes a file metadata record
HardDelete(id gocql.UUID) error
// SoftDeleteMany removes multiple File metadata records by their unique identifiers (IDs) by setting its state to deleted.
SoftDeleteMany(ids []gocql.UUID) error
// HardDeleteMany permanently removes multiple file metadata records
HardDeleteMany(ids []gocql.UUID) error
// CheckIfExistsByID verifies if a File metadata record with the given ID exists in the storage.
CheckIfExistsByID(id gocql.UUID) (bool, error)
// CheckIfUserHasAccess determines if a specific user (userID) has access permissions for a given file (fileID).
CheckIfUserHasAccess(fileID gocql.UUID, userID gocql.UUID) (bool, error)
GetByCreatedByUserID(createdByUserID gocql.UUID) ([]*File, error)
GetByOwnerID(ownerID gocql.UUID) ([]*File, error)
// State management operations
Archive(id gocql.UUID) error
Restore(id gocql.UUID) error
RestoreMany(ids []gocql.UUID) error
// ListSyncData retrieves file sync data with pagination for the specified user and accessible collections
ListSyncData(ctx context.Context, userID gocql.UUID, cursor *FileSyncCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*FileSyncResponse, error)
// ListRecentFiles retrieves recent files with pagination for the specified user and accessible collections
ListRecentFiles(ctx context.Context, userID gocql.UUID, cursor *RecentFilesCursor, limit int64, accessibleCollectionIDs []gocql.UUID) (*RecentFilesResponse, error)
// CountFilesByUser counts all active files accessible to the user
CountFilesByUser(ctx context.Context, userID gocql.UUID, accessibleCollectionIDs []gocql.UUID) (int, error)
// CountFilesByCollection counts active files in a specific collection
CountFilesByCollection(ctx context.Context, collectionID gocql.UUID) (int, error)
// Storage size calculation methods
GetTotalStorageSizeByOwner(ctx context.Context, ownerID gocql.UUID) (int64, error)
GetTotalStorageSizeByUser(ctx context.Context, userID gocql.UUID, accessibleCollectionIDs []gocql.UUID) (int64, error)
GetTotalStorageSizeByCollection(ctx context.Context, collectionID gocql.UUID) (int64, error)
// IP Anonymization for GDPR compliance
AnonymizeOldIPs(ctx context.Context, cutoffDate time.Time) (int, error)
AnonymizeFileIPsByOwner(ctx context.Context, ownerID gocql.UUID) (int, error) // For GDPR right-to-be-forgotten
// Tag-related operations
// ListByTagID retrieves all files that have the specified tag assigned
// Used for tag update propagation (updating embedded tag data across all files)
ListByTagID(ctx context.Context, tagID gocql.UUID) ([]*File, error)
}
// FileObjectStorageRepository defines the interface for interacting with the actual encrypted file data storage.
// It handles operations related to storing, retrieving, deleting, and generating access URLs for encrypted data.
type FileObjectStorageRepository interface {
// StoreEncryptedData saves encrypted file data to the storage system. It takes the owner's ID,
// the file's ID (metadata ID), and the encrypted byte slice. It returns the storage path
// where the data was saved, or an error.
StoreEncryptedData(ownerID string, fileID string, encryptedData []byte) (string, error)
// GetEncryptedData retrieves encrypted file data from the storage system using its storage path.
// It returns the encrypted data as a byte slice, or an error.
GetEncryptedData(storagePath string) ([]byte, error)
// DeleteEncryptedData removes encrypted file data from the storage system using its storage path.
DeleteEncryptedData(storagePath string) error
// GeneratePresignedDownloadURL creates a temporary, time-limited URL that allows direct download
// of the file data located at the given storage path, with proper content disposition headers.
GeneratePresignedDownloadURL(storagePath string, duration time.Duration) (string, error)
// GeneratePresignedUploadURL creates a temporary, time-limited URL that allows clients to upload
// encrypted file data directly to the storage system at the specified storage path.
GeneratePresignedUploadURL(storagePath string, duration time.Duration) (string, error)
// VerifyObjectExists checks if an object exists at the given storage path.
VerifyObjectExists(storagePath string) (bool, error)
// GetObjectSize returns the size in bytes of the object at the given storage path.
GetObjectSize(storagePath string) (int64, error)
}

View file

@ -0,0 +1,136 @@
// monorepo/cloud/backend/internal/maplefile/domain/file/model.go
package file
import (
"time"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)
// File represents an encrypted file entity stored in the backend database (MongoDB).
// This entity holds metadata and pointers to the actual file content and thumbnail,
// which are stored separately in S3. All sensitive file metadata and the file itself
// are encrypted client-side before being uploaded. The backend stores only encrypted
// data and necessary non-sensitive identifiers or sizes for management.
type File struct {
// Identifiers
// Unique identifier for this specific file entity.
ID gocql.UUID `bson:"_id" json:"id"`
// Identifier of the collection this file belongs to. Used for grouping and key management.
CollectionID gocql.UUID `bson:"collection_id" json:"collection_id"`
// Identifier of the user who owns this file.
OwnerID gocql.UUID `bson:"owner_id" json:"owner_id"`
// Encryption and Content Details
// Client-side encrypted JSON blob containing file-specific metadata like the original file name,
// MIME type, size of the *unencrypted* data, etc. Encrypted by the client using the file key.
EncryptedMetadata string `bson:"encrypted_metadata" json:"encrypted_metadata"`
// The file-specific data encryption key (DEK) used to encrypt the file content and metadata.
// This key is encrypted by the client using the collection's key (a KEK). The backend
// stores this encrypted key; only a user with access to the KEK can decrypt it.
EncryptedFileKey crypto.EncryptedFileKey `bson:"encrypted_file_key" json:"encrypted_file_key"`
// Version identifier for the encryption scheme or client application version used to
// encrypt this file. Useful for migration or compatibility checks.
EncryptionVersion string `bson:"encryption_version" json:"encryption_version"`
// Cryptographic hash of the *encrypted* file content stored in S3. Used for integrity
// verification upon download *before* decryption.
EncryptedHash string `bson:"encrypted_hash" json:"encrypted_hash"`
// File Storage Object Details
// The unique key or path within the S3 bucket where the main encrypted file content is stored.
// This is an internal backend detail and is not exposed to the client API.
EncryptedFileObjectKey string `bson:"encrypted_file_object_key" json:"-"`
// The size of the *encrypted* file content stored in S3, in bytes. This size is not sensitive
// and is used by the backend for storage accounting, billing, and transfer management.
EncryptedFileSizeInBytes int64 `bson:"encrypted_file_size_in_bytes" json:"encrypted_file_size_in_bytes"`
// Thumbnail Storage Object Details (Optional)
// The unique key or path within the S3 bucket where the encrypted thumbnail image (if generated
// and uploaded) is stored. Internal backend detail, not exposed to the client API.
EncryptedThumbnailObjectKey string `bson:"encrypted_thumbnail_object_key" json:"-"`
// The size of the *encrypted* thumbnail image stored in S3, in bytes. Used for accounting.
// Value will be 0 if no thumbnail exists.
EncryptedThumbnailSizeInBytes int64 `bson:"encrypted_thumbnail_size_in_bytes" json:"encrypted_thumbnail_size_in_bytes"`
// DEPRECATED: Replaced by Tags field below
// TagIDs []gocql.UUID `bson:"tag_ids,omitempty" json:"tag_ids,omitempty"`
// Tags stores full embedded tag data (eliminates frontend API lookups)
// Stored as JSON text in database, marshaled/unmarshaled automatically
Tags []tag.EmbeddedTag `bson:"tags,omitempty" json:"tags,omitempty"`
// Timestamps and conflict resolution
// Timestamp when this file entity was created/uploaded.
CreatedAt time.Time `bson:"created_at" json:"created_at"`
// CreatedByUserID is the ID of the user who created this file.
CreatedByUserID gocql.UUID `bson:"created_by_user_id" json:"created_by_user_id"`
// Timestamp when this file entity's metadata or content was last modified.
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
// ModifiedByUserID is the ID of the user whom has last modified this file.
ModifiedByUserID gocql.UUID `bson:"modified_by_user_id" json:"modified_by_user_id"`
// The current version of the file.
Version uint64 `bson:"version" json:"version"` // Every mutation (create, update, delete) is a versioned operation, keep track of the version number with this variable
// State management.
State string `bson:"state" json:"state"` // pending, active, deleted, archived
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"` // The `version` number that this collection was deleted at.
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
}
// FileSyncCursor represents cursor-based pagination for sync operations
type FileSyncCursor struct {
LastModified time.Time `json:"last_modified" bson:"last_modified"`
LastID gocql.UUID `json:"last_id" bson:"last_id"`
}
// FileSyncItem represents minimal file data for sync operations
type FileSyncItem struct {
ID gocql.UUID `json:"id" bson:"_id"`
CollectionID gocql.UUID `json:"collection_id" bson:"collection_id"`
Version uint64 `json:"version" bson:"version"`
ModifiedAt time.Time `json:"modified_at" bson:"modified_at"`
State string `json:"state" bson:"state"`
TombstoneVersion uint64 `bson:"tombstone_version" json:"tombstone_version"`
TombstoneExpiry time.Time `bson:"tombstone_expiry" json:"tombstone_expiry"`
EncryptedFileSizeInBytes int64 `bson:"encrypted_file_size_in_bytes" json:"encrypted_file_size_in_bytes"`
}
// FileSyncResponse represents the response for file sync data
type FileSyncResponse struct {
Files []FileSyncItem `json:"files"`
NextCursor *FileSyncCursor `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}
// RecentFilesCursor represents cursor-based pagination for recent files
type RecentFilesCursor struct {
LastModified time.Time `json:"last_modified" bson:"last_modified"`
LastID gocql.UUID `json:"last_id" bson:"last_id"`
}
// RecentFilesItem represents a file item for recent files listing
type RecentFilesItem struct {
ID gocql.UUID `json:"id" bson:"_id"`
CollectionID gocql.UUID `json:"collection_id" bson:"collection_id"`
OwnerID gocql.UUID `json:"owner_id" bson:"owner_id"`
EncryptedMetadata string `json:"encrypted_metadata" bson:"encrypted_metadata"`
EncryptedFileKey string `json:"encrypted_file_key" bson:"encrypted_file_key"`
EncryptionVersion string `json:"encryption_version" bson:"encryption_version"`
EncryptedHash string `json:"encrypted_hash" bson:"encrypted_hash"`
EncryptedFileSizeInBytes int64 `json:"encrypted_file_size_in_bytes" bson:"encrypted_file_size_in_bytes"`
EncryptedThumbnailSizeInBytes int64 `json:"encrypted_thumbnail_size_in_bytes" bson:"encrypted_thumbnail_size_in_bytes"`
Tags []tag.EmbeddedTag `json:"tags,omitempty" bson:"tags,omitempty"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
ModifiedAt time.Time `json:"modified_at" bson:"modified_at"`
Version uint64 `json:"version" bson:"version"`
State string `json:"state" bson:"state"`
}
// RecentFilesResponse represents the response for recent files listing
type RecentFilesResponse struct {
Files []RecentFilesItem `json:"files"`
NextCursor *RecentFilesCursor `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}

View file

@ -0,0 +1,45 @@
// monorepo/cloud/backend/internal/maplefile/domain/file/state_validator.go
package file
import "errors"
// StateTransition validates file state transitions
type StateTransition struct {
From string
To string
}
// IsValidStateTransition checks if a file state transition is allowed
func IsValidStateTransition(from, to string) error {
validTransitions := map[StateTransition]bool{
// From pending
{FileStatePending, FileStateActive}: true,
{FileStatePending, FileStateDeleted}: true,
{FileStatePending, FileStateArchived}: false,
// From active
{FileStateActive, FileStatePending}: false,
{FileStateActive, FileStateDeleted}: true,
{FileStateActive, FileStateArchived}: true,
// From deleted (cannot be restored nor archived)
{FileStateDeleted, FileStatePending}: false,
{FileStateDeleted, FileStateActive}: false,
{FileStateDeleted, FileStateArchived}: false,
// From archived (can only be restored to active)
{FileStateArchived, FileStateActive}: true,
// Same state transitions (no-op)
{FileStatePending, FileStatePending}: true,
{FileStateActive, FileStateActive}: true,
{FileStateDeleted, FileStateDeleted}: true,
{FileStateArchived, FileStateArchived}: true,
}
if !validTransitions[StateTransition{from, to}] {
return errors.New("invalid state transition from " + from + " to " + to)
}
return nil
}

View file

@ -0,0 +1,7 @@
// Package inviteemail provides domain types and constants for invitation emails
// sent to non-registered users when someone wants to share a collection with them.
package inviteemail
// DefaultMaxInviteEmailsPerDay is the fallback limit if the environment variable is not set.
// This conservative limit protects email domain reputation.
const DefaultMaxInviteEmailsPerDay = 3

View file

@ -0,0 +1,53 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/storagedailyusage/interface.go
package storagedailyusage
import (
"context"
"time"
"github.com/gocql/gocql"
)
// StorageDailyUsageRepository defines the interface for daily storage usage aggregates
type StorageDailyUsageRepository interface {
Create(ctx context.Context, usage *StorageDailyUsage) error
CreateMany(ctx context.Context, usages []*StorageDailyUsage) error
GetByUserAndDay(ctx context.Context, userID gocql.UUID, usageDay time.Time) (*StorageDailyUsage, error)
GetByUserDateRange(ctx context.Context, userID gocql.UUID, startDay, endDay time.Time) ([]*StorageDailyUsage, error)
UpdateOrCreate(ctx context.Context, usage *StorageDailyUsage) error
IncrementUsage(ctx context.Context, userID gocql.UUID, usageDay time.Time, totalBytes, addBytes, removeBytes int64) error
DeleteByUserAndDay(ctx context.Context, userID gocql.UUID, usageDay time.Time) error
DeleteByUserID(ctx context.Context, userID gocql.UUID) error
GetLast7DaysTrend(ctx context.Context, userID gocql.UUID) (*StorageUsageTrend, error)
GetMonthlyTrend(ctx context.Context, userID gocql.UUID, year int, month time.Month) (*StorageUsageTrend, error)
GetYearlyTrend(ctx context.Context, userID gocql.UUID, year int) (*StorageUsageTrend, error)
GetCurrentMonthUsage(ctx context.Context, userID gocql.UUID) (*StorageUsageSummary, error)
GetCurrentYearUsage(ctx context.Context, userID gocql.UUID) (*StorageUsageSummary, error)
}
// StorageUsageTrend represents usage trend over a period
type StorageUsageTrend struct {
UserID gocql.UUID `json:"user_id"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
DailyUsages []*StorageDailyUsage `json:"daily_usages"`
TotalAdded int64 `json:"total_added"`
TotalRemoved int64 `json:"total_removed"`
NetChange int64 `json:"net_change"`
AverageDailyAdd int64 `json:"average_daily_add"`
PeakUsageDay *time.Time `json:"peak_usage_day,omitempty"`
PeakUsageBytes int64 `json:"peak_usage_bytes"`
}
// StorageUsageSummary represents a summary of storage usage
type StorageUsageSummary struct {
UserID gocql.UUID `json:"user_id"`
Period string `json:"period"` // "month" or "year"
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
CurrentUsage int64 `json:"current_usage_bytes"`
TotalAdded int64 `json:"total_added_bytes"`
TotalRemoved int64 `json:"total_removed_bytes"`
NetChange int64 `json:"net_change_bytes"`
DaysWithData int `json:"days_with_data"`
}

View file

@ -0,0 +1,26 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/storagedailyusage/model.go
package storagedailyusage
import (
"time"
"github.com/gocql/gocql"
)
type StorageDailyUsage struct {
UserID gocql.UUID `json:"user_id"` // Partition key
UsageDay time.Time `json:"usage_day"` // Clustering key (date only)
TotalBytes int64 `json:"total_bytes"`
TotalAddBytes int64 `json:"total_add_bytes"`
TotalRemoveBytes int64 `json:"total_remove_bytes"`
}
//
// Use gocql.UUID from the github.com/gocql/gocql driver.
//
// For consistency, always store and retrieve DATE fields (like event_day and usage_day) as time.Time, but truncate to date only before inserting:
//
// ```go
// usageDay := time.Now().Truncate(24 * time.Hour)
// ```
//

View file

@ -0,0 +1,23 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/storageusageevent/interface.go
package storageusageevent
import (
"context"
"time"
"github.com/gocql/gocql"
)
// StorageUsageEventRepository defines the interface for storage usage events
type StorageUsageEventRepository interface {
Create(ctx context.Context, event *StorageUsageEvent) error
CreateMany(ctx context.Context, events []*StorageUsageEvent) error
GetByUserAndDay(ctx context.Context, userID gocql.UUID, eventDay time.Time) ([]*StorageUsageEvent, error)
GetByUserDateRange(ctx context.Context, userID gocql.UUID, startDay, endDay time.Time) ([]*StorageUsageEvent, error)
DeleteByUserAndDay(ctx context.Context, userID gocql.UUID, eventDay time.Time) error
DeleteByUserID(ctx context.Context, userID gocql.UUID) error
GetLast7DaysEvents(ctx context.Context, userID gocql.UUID) ([]*StorageUsageEvent, error)
GetLastNDaysEvents(ctx context.Context, userID gocql.UUID, days int) ([]*StorageUsageEvent, error)
GetMonthlyEvents(ctx context.Context, userID gocql.UUID, year int, month time.Month) ([]*StorageUsageEvent, error)
GetYearlyEvents(ctx context.Context, userID gocql.UUID, year int) ([]*StorageUsageEvent, error)
}

View file

@ -0,0 +1,16 @@
// monorepo/cloud/maplefile-backend/internal/maplefile/domain/storageusageevent/model.go
package storageusageevent
import (
"time"
"github.com/gocql/gocql"
)
type StorageUsageEvent struct {
UserID gocql.UUID `json:"user_id"` // Partition key
EventDay time.Time `json:"event_day"` // Partition key (date only)
EventTime time.Time `json:"event_time"` // Clustering key
FileSize int64 `json:"file_size"` // Bytes
Operation string `json:"operation"` // "add" or "remove"
}

View file

@ -0,0 +1,23 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag/constants.go
package tag
const (
// Tag States
TagStateActive = "active"
TagStateDeleted = "deleted"
TagStateArchived = "archived"
// Entity Types
EntityTypeCollection = "collection"
EntityTypeFile = "file"
// Default Tag Names
DefaultTagImportant = "Important"
DefaultTagWork = "Work"
DefaultTagPersonal = "Personal"
// Default Tag Colors (hex format)
DefaultColorImportant = "#EF4444" // Red
DefaultColorWork = "#3B82F6" // Blue
DefaultColorPersonal = "#10B981" // Green
)

View file

@ -0,0 +1,26 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag/interface.go
package tag
import (
"context"
"github.com/gocql/gocql"
)
// Repository defines the interface for tag data access operations
type Repository interface {
// Tag CRUD operations
Create(ctx context.Context, tag *Tag) error
GetByID(ctx context.Context, id gocql.UUID) (*Tag, error)
ListByUser(ctx context.Context, userID gocql.UUID) ([]*Tag, error)
Update(ctx context.Context, tag *Tag) error
DeleteByID(ctx context.Context, userID, id gocql.UUID) error
// Tag Assignment operations
AssignTag(ctx context.Context, assignment *TagAssignment) error
UnassignTag(ctx context.Context, tagID, entityID gocql.UUID, entityType string) error
GetTagsForEntity(ctx context.Context, entityID gocql.UUID, entityType string) ([]*Tag, error)
GetEntitiesWithTag(ctx context.Context, tagID gocql.UUID, entityType string) ([]gocql.UUID, error)
GetAssignmentsByTag(ctx context.Context, tagID gocql.UUID) ([]*TagAssignment, error)
GetAssignmentsByEntity(ctx context.Context, entityID gocql.UUID, entityType string) ([]*TagAssignment, error)
}

View file

@ -0,0 +1,89 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag/model.go
package tag
import (
"time"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
)
// Tag represents a user-defined label with color that can be assigned to collections or files
// All sensitive data (name, color) is encrypted end-to-end using the tag's unique encryption key
type Tag struct {
// Identifiers
ID gocql.UUID `bson:"_id" json:"id"`
UserID gocql.UUID `bson:"user_id" json:"user_id"` // Owner of the tag
// Encrypted Tag Details
// EncryptedName is the tag label (e.g., "Important", "Work") encrypted with the tag key
EncryptedName string `bson:"encrypted_name" json:"encrypted_name"`
// EncryptedColor is the hex color code (e.g., "#FF5733") encrypted with the tag key
EncryptedColor string `bson:"encrypted_color" json:"encrypted_color"`
// EncryptedTagKey is the unique symmetric key used to encrypt this tag's data (name and color)
// This key is encrypted with the user's master key for storage and transmission
EncryptedTagKey *crypto.EncryptedTagKey `bson:"encrypted_tag_key" json:"encrypted_tag_key"`
// Timestamps and versioning
CreatedAt time.Time `bson:"created_at" json:"created_at"`
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
Version uint64 `bson:"version" json:"version"` // Versioning for sync
// State management
State string `bson:"state" json:"state"` // active, deleted, archived
}
// TagAssignment represents the assignment of a tag to a collection or file
type TagAssignment struct {
// Identifiers
ID gocql.UUID `bson:"_id" json:"id"`
UserID gocql.UUID `bson:"user_id" json:"user_id"` // User who assigned the tag
TagID gocql.UUID `bson:"tag_id" json:"tag_id"` // Reference to the tag
EntityID gocql.UUID `bson:"entity_id" json:"entity_id"` // Collection or File ID
// EntityType indicates whether this is a "collection" or "file"
EntityType string `bson:"entity_type" json:"entity_type"`
// Timestamps
CreatedAt time.Time `bson:"created_at" json:"created_at"`
}
// TagListFilter represents filter criteria for listing tags
type TagListFilter struct {
UserID gocql.UUID
State string // Optional: filter by state
}
// TagAssignmentFilter represents filter criteria for tag assignments
type TagAssignmentFilter struct {
TagID *gocql.UUID
EntityID *gocql.UUID
EntityType *string
UserID *gocql.UUID
}
// EmbeddedTag represents tag data that is embedded in collections and files
// This eliminates the need for frontend API lookups to get tag colors
type EmbeddedTag struct {
// Core identifiers and data
ID gocql.UUID `bson:"id" json:"id"`
EncryptedName string `bson:"encrypted_name" json:"encrypted_name"`
EncryptedColor string `bson:"encrypted_color" json:"encrypted_color"`
EncryptedTagKey *crypto.EncryptedTagKey `bson:"encrypted_tag_key" json:"encrypted_tag_key"`
// For cache invalidation - detect stale embedded data
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
}
// ToEmbeddedTag converts a Tag to an EmbeddedTag for embedding in collections/files
func (t *Tag) ToEmbeddedTag() *EmbeddedTag {
if t == nil {
return nil
}
return &EmbeddedTag{
ID: t.ID,
EncryptedName: t.EncryptedName,
EncryptedColor: t.EncryptedColor,
EncryptedTagKey: t.EncryptedTagKey,
ModifiedAt: t.ModifiedAt,
}
}

View file

@ -0,0 +1,23 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user/interface.go
package user
import (
"context"
"time"
"github.com/gocql/gocql"
)
// Repository Interface for user management.
type Repository interface {
Create(ctx context.Context, m *User) error
GetByID(ctx context.Context, id gocql.UUID) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
GetByVerificationCode(ctx context.Context, verificationCode string) (*User, error)
DeleteByID(ctx context.Context, id gocql.UUID) error
DeleteByEmail(ctx context.Context, email string) error
CheckIfExistsByEmail(ctx context.Context, email string) (bool, error)
UpdateByID(ctx context.Context, m *User) error
AnonymizeOldIPs(ctx context.Context, cutoffDate time.Time) (int, error)
AnonymizeUserIPs(ctx context.Context, userID gocql.UUID) error // For GDPR right-to-be-forgotten
}

View file

@ -0,0 +1,153 @@
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/user/model.go
package user
import (
"time"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
"github.com/gocql/gocql"
)
const (
UserStatusActive = 1 // User is active and can log in.
UserStatusLocked = 50 // User account is locked, typically due to too many failed login attempts.
UserStatusArchived = 100 // User account is archived and cannot log in.
UserRoleRoot = 1 // Root user, has all permissions
UserRoleCompany = 2 // Company user, has permissions for company-related operations
UserRoleIndividual = 3 // Individual user, has permissions for individual-related operations
UserProfileVerificationStatusUnverified = 1 // The user's profile has not yet been submitted for verification.
UserProfileVerificationStatusSubmittedForReview = 2 // The user's profile has been submitted and is awaiting review.
UserProfileVerificationStatusApproved = 3 // The user's profile has been approved.
UserProfileVerificationStatusRejected = 4 // The user's profile has been rejected.
// StorePendingStatus indicates this store needs to be reviewed by CPS and approved / rejected.
StorePendingStatus = 1 // Store is pending review.
StoreActiveStatus = 2 // Store is active and can be used.
StoreRejectedStatus = 3 // Store has been rejected.
StoreErrorStatus = 4 // Store has encountered an error.
StoreArchivedStatus = 5 // Store has been archived.
EstimatedSubmissionsPerMonth1To10 = 1 // Estimated submissions per month: 1 to 10
EstimatedSubmissionsPerMonth10To25 = 2 // Estimated submissions per month: 10 to 25
EstimatedSubmissionsPerMonth25To50 = 3 // Estimated submissions per month: 25 to 50
EstimatedSubmissionsPerMonth50To10 = 4 // Estimated submissions per month: 50 to 100
EstimatedSubmissionsPerMonth100Plus = 5 // Estimated submissions per month: 100+
HasOtherGradingServiceYes = 1 // Has other grading service: Yes
HasOtherGradingServiceNo = 2 // Has other grading service: No
RequestWelcomePackageYes = 1 // Request welcome package: Yes
RequestWelcomePackageNo = 2 // Request welcome package: No
SpecialCollection040001 = 1
UserCodeTypeEmailVerification = "email_verification"
UserCodeTypePasswordReset = "password_reset"
)
type UserProfileData struct {
Phone string `bson:"phone" json:"phone,omitempty"`
Country string `bson:"country" json:"country,omitempty"`
Region string `bson:"region" json:"region,omitempty"`
City string `bson:"city" json:"city,omitempty"`
PostalCode string `bson:"postal_code" json:"postal_code,omitempty"`
AddressLine1 string `bson:"address_line1" json:"address_line1,omitempty"`
AddressLine2 string `bson:"address_line2" json:"address_line2,omitempty"`
HasShippingAddress bool `bson:"has_shipping_address" json:"has_shipping_address,omitempty"`
ShippingName string `bson:"shipping_name" json:"shipping_name,omitempty"`
ShippingPhone string `bson:"shipping_phone" json:"shipping_phone,omitempty"`
ShippingCountry string `bson:"shipping_country" json:"shipping_country,omitempty"`
ShippingRegion string `bson:"shipping_region" json:"shipping_region,omitempty"`
ShippingCity string `bson:"shipping_city" json:"shipping_city,omitempty"`
ShippingPostalCode string `bson:"shipping_postal_code" json:"shipping_postal_code,omitempty"`
ShippingAddressLine1 string `bson:"shipping_address_line1" json:"shipping_address_line1,omitempty"`
ShippingAddressLine2 string `bson:"shipping_address_line2" json:"shipping_address_line2,omitempty"`
Timezone string `bson:"timezone" json:"timezone"`
AgreeTermsOfService bool `bson:"agree_terms_of_service" json:"agree_terms_of_service,omitempty"`
AgreePromotions bool `bson:"agree_promotions" json:"agree_promotions,omitempty"`
AgreeToTrackingAcrossThirdPartyAppsAndServices bool `bson:"agree_to_tracking_across_third_party_apps_and_services" json:"agree_to_tracking_across_third_party_apps_and_services,omitempty"`
// Email share notification preferences
ShareNotificationsEnabled *bool `bson:"share_notifications_enabled" json:"share_notifications_enabled,omitempty"`
}
type UserSecurityData struct {
WasEmailVerified bool `bson:"was_email_verified" json:"was_email_verified,omitempty"`
Code string `bson:"code,omitempty" json:"code,omitempty"`
CodeType string `bson:"code_type,omitempty" json:"code_type,omitempty"` // -- 'email_verification' or 'password_reset'
CodeExpiry time.Time `bson:"code_expiry,omitempty" json:"code_expiry"`
// --- E2EE Related ---
PasswordSalt []byte `json:"password_salt" bson:"password_salt"`
// KDFParams stores the key derivation function parameters used to derive the user's password hash.
KDFParams crypto.KDFParams `json:"kdf_params" bson:"kdf_params"`
EncryptedMasterKey crypto.EncryptedMasterKey `json:"encrypted_master_key" bson:"encrypted_master_key"`
PublicKey crypto.PublicKey `json:"public_key" bson:"public_key"`
EncryptedPrivateKey crypto.EncryptedPrivateKey `json:"encrypted_private_key" bson:"encrypted_private_key"`
EncryptedRecoveryKey crypto.EncryptedRecoveryKey `json:"encrypted_recovery_key" bson:"encrypted_recovery_key"`
MasterKeyEncryptedWithRecoveryKey crypto.MasterKeyEncryptedWithRecoveryKey `json:"master_key_encrypted_with_recovery_key" bson:"master_key_encrypted_with_recovery_key"`
EncryptedChallenge []byte `json:"encrypted_challenge,omitempty" bson:"encrypted_challenge,omitempty"`
VerificationID string `json:"verification_id" bson:"verification_id"`
// Track KDF upgrade status
LastPasswordChange time.Time `json:"last_password_change" bson:"last_password_change"`
KDFParamsNeedUpgrade bool `json:"kdf_params_need_upgrade" bson:"kdf_params_need_upgrade"`
// Key rotation tracking fields
CurrentKeyVersion int `json:"current_key_version" bson:"current_key_version"`
LastKeyRotation *time.Time `json:"last_key_rotation,omitempty" bson:"last_key_rotation,omitempty"`
KeyRotationPolicy *crypto.KeyRotationPolicy `json:"key_rotation_policy,omitempty" bson:"key_rotation_policy,omitempty"`
// OTPEnabled controls whether we force 2FA or not during login.
OTPEnabled bool `bson:"otp_enabled" json:"otp_enabled"`
// OTPVerified indicates user has successfully validated their opt token afer enabling 2FA thus turning it on.
OTPVerified bool `bson:"otp_verified" json:"otp_verified"`
// OTPValidated automatically gets set as `false` on successful login and then sets `true` once successfully validated by 2FA.
OTPValidated bool `bson:"otp_validated" json:"otp_validated"`
// OTPSecret the unique one-time password secret to be shared between our
// backend and 2FA authenticator sort of apps that support `TOPT`.
OTPSecret string `bson:"otp_secret" json:"-"`
// OTPAuthURL is the URL used to share.
OTPAuthURL string `bson:"otp_auth_url" json:"-"`
// OTPBackupCodeHash is the one-time use backup code which resets the 2FA settings and allow the user to setup 2FA from scratch for the user.
OTPBackupCodeHash string `bson:"otp_backup_code_hash" json:"-"`
// OTPBackupCodeHashAlgorithm tracks the hashing algorithm used.
OTPBackupCodeHashAlgorithm string `bson:"otp_backup_code_hash_algorithm" json:"-"`
}
type UserMetadata struct {
CreatedFromIPAddress string `bson:"created_from_ip_address" json:"created_from_ip_address"`
CreatedByUserID gocql.UUID `bson:"created_by_user_id" json:"created_by_user_id"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
CreatedByName string `bson:"created_by_name" json:"created_by_name"`
ModifiedFromIPAddress string `bson:"modified_from_ip_address" json:"modified_from_ip_address"`
ModifiedByUserID gocql.UUID `bson:"modified_by_user_id" json:"modified_by_user_id"`
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
ModifiedByName string `bson:"modified_by_name" json:"modified_by_name"`
LastLoginAt time.Time `json:"last_login_at" bson:"last_login_at"`
}
type User struct {
ID gocql.UUID `bson:"_id" json:"id"`
Email string `bson:"email" json:"email"`
FirstName string `bson:"first_name" json:"first_name"`
LastName string `bson:"last_name" json:"last_name"`
Name string `bson:"name" json:"name"`
LexicalName string `bson:"lexical_name" json:"lexical_name"`
Role int8 `bson:"role" json:"role"`
Status int8 `bson:"status" json:"status"`
Timezone string `bson:"timezone" json:"timezone"`
ProfileData *UserProfileData `bson:"profile_data" json:"profile_data"`
SecurityData *UserSecurityData `bson:"security_data" json:"security_data"`
Metadata *UserMetadata `bson:"metadata" json:"metadata"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
ModifiedAt time.Time `bson:"modified_at" json:"modified_at"`
}