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,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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
43
cloud/maplefile-backend/internal/domain/collection/filter.go
Normal file
43
cloud/maplefile-backend/internal/domain/collection/filter.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
124
cloud/maplefile-backend/internal/domain/collection/model.go
Normal file
124
cloud/maplefile-backend/internal/domain/collection/model.go
Normal 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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
69
cloud/maplefile-backend/internal/domain/crypto/kdf.go
Normal file
69
cloud/maplefile-backend/internal/domain/crypto/kdf.go
Normal 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"`
|
||||
}
|
||||
355
cloud/maplefile-backend/internal/domain/crypto/model.go
Normal file
355
cloud/maplefile-backend/internal/domain/crypto/model.go
Normal 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"`
|
||||
}
|
||||
39
cloud/maplefile-backend/internal/domain/crypto/rotation.go
Normal file
39
cloud/maplefile-backend/internal/domain/crypto/rotation.go
Normal 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"`
|
||||
}
|
||||
54
cloud/maplefile-backend/internal/domain/dashboard/model.go
Normal file
54
cloud/maplefile-backend/internal/domain/dashboard/model.go
Normal 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"`
|
||||
}
|
||||
13
cloud/maplefile-backend/internal/domain/file/constants.go
Normal file
13
cloud/maplefile-backend/internal/domain/file/constants.go
Normal 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"
|
||||
)
|
||||
95
cloud/maplefile-backend/internal/domain/file/interface.go
Normal file
95
cloud/maplefile-backend/internal/domain/file/interface.go
Normal 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)
|
||||
}
|
||||
136
cloud/maplefile-backend/internal/domain/file/model.go
Normal file
136
cloud/maplefile-backend/internal/domain/file/model.go
Normal 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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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)
|
||||
// ```
|
||||
//
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
23
cloud/maplefile-backend/internal/domain/tag/constants.go
Normal file
23
cloud/maplefile-backend/internal/domain/tag/constants.go
Normal 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
|
||||
)
|
||||
26
cloud/maplefile-backend/internal/domain/tag/interface.go
Normal file
26
cloud/maplefile-backend/internal/domain/tag/interface.go
Normal 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)
|
||||
}
|
||||
89
cloud/maplefile-backend/internal/domain/tag/model.go
Normal file
89
cloud/maplefile-backend/internal/domain/tag/model.go
Normal 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,
|
||||
}
|
||||
}
|
||||
23
cloud/maplefile-backend/internal/domain/user/interface.go
Normal file
23
cloud/maplefile-backend/internal/domain/user/interface.go
Normal 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
|
||||
}
|
||||
153
cloud/maplefile-backend/internal/domain/user/model.go
Normal file
153
cloud/maplefile-backend/internal/domain/user/model.go
Normal 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"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue