package entities import ( "time" "github.com/google/uuid" "github.com/rwadurian/mpc-system/services/account/domain/value_objects" ) // Account represents a user account with MPC-based authentication type Account struct { ID value_objects.AccountID Username string // Required: auto-generated by identity-service Email *string // Optional: for anonymous accounts Phone *string PublicKey []byte // MPC group public key KeygenSessionID uuid.UUID ThresholdN int ThresholdT int Status value_objects.AccountStatus CreatedAt time.Time UpdatedAt time.Time LastLoginAt *time.Time // SigningParties is a list of party IDs designated for signing operations. // If nil or empty, all active parties will be used for signing. // Must contain exactly ThresholdT parties when set. SigningParties []string } // NewAccount creates a new Account func NewAccount( username string, email string, publicKey []byte, keygenSessionID uuid.UUID, thresholdN int, thresholdT int, ) *Account { now := time.Now().UTC() var emailPtr *string if email != "" { emailPtr = &email } return &Account{ ID: value_objects.NewAccountID(), Username: username, Email: emailPtr, PublicKey: publicKey, KeygenSessionID: keygenSessionID, ThresholdN: thresholdN, ThresholdT: thresholdT, Status: value_objects.AccountStatusActive, CreatedAt: now, UpdatedAt: now, } } // SetPhone sets the phone number func (a *Account) SetPhone(phone string) { a.Phone = &phone a.UpdatedAt = time.Now().UTC() } // UpdateLastLogin updates the last login timestamp func (a *Account) UpdateLastLogin() { now := time.Now().UTC() a.LastLoginAt = &now a.UpdatedAt = now } // Suspend suspends the account func (a *Account) Suspend() error { if a.Status == value_objects.AccountStatusRecovering { return ErrAccountInRecovery } a.Status = value_objects.AccountStatusSuspended a.UpdatedAt = time.Now().UTC() return nil } // Lock locks the account func (a *Account) Lock() error { if a.Status == value_objects.AccountStatusRecovering { return ErrAccountInRecovery } a.Status = value_objects.AccountStatusLocked a.UpdatedAt = time.Now().UTC() return nil } // Activate activates the account func (a *Account) Activate() { a.Status = value_objects.AccountStatusActive a.UpdatedAt = time.Now().UTC() } // StartRecovery marks the account as recovering func (a *Account) StartRecovery() error { if !a.Status.CanInitiateRecovery() { return ErrCannotInitiateRecovery } a.Status = value_objects.AccountStatusRecovering a.UpdatedAt = time.Now().UTC() return nil } // CompleteRecovery completes the recovery process with new public key func (a *Account) CompleteRecovery(newPublicKey []byte, newKeygenSessionID uuid.UUID) { a.PublicKey = newPublicKey a.KeygenSessionID = newKeygenSessionID a.Status = value_objects.AccountStatusActive a.UpdatedAt = time.Now().UTC() } // CanLogin checks if the account can login func (a *Account) CanLogin() bool { return a.Status.CanLogin() } // IsActive checks if the account is active func (a *Account) IsActive() bool { return a.Status == value_objects.AccountStatusActive } // Validate validates the account data func (a *Account) Validate() error { if a.Username == "" { return ErrInvalidUsername } // Email is optional, but if provided must be valid (checked by binding) if len(a.PublicKey) == 0 { return ErrInvalidPublicKey } if a.ThresholdT > a.ThresholdN || a.ThresholdT <= 0 { return ErrInvalidThreshold } return nil } // SetSigningParties sets the designated signing parties for this account // partyIDs must contain exactly ThresholdT + 1 parties (required signers for TSS) // For 2-of-3: ThresholdT=1, so T+1=2 signers required func (a *Account) SetSigningParties(partyIDs []string) error { requiredSigners := a.ThresholdT + 1 if len(partyIDs) != requiredSigners { return ErrInvalidSigningPartiesCount } // Check for duplicates seen := make(map[string]bool) for _, id := range partyIDs { if id == "" { return ErrInvalidPartyID } if seen[id] { return ErrDuplicatePartyID } seen[id] = true } a.SigningParties = partyIDs a.UpdatedAt = time.Now().UTC() return nil } // ClearSigningParties removes the signing parties configuration // After clearing, all active parties will be used for signing func (a *Account) ClearSigningParties() { a.SigningParties = nil a.UpdatedAt = time.Now().UTC() } // HasSigningPartiesConfig returns true if signing parties are configured func (a *Account) HasSigningPartiesConfig() bool { return len(a.SigningParties) > 0 } // GetSigningParties returns the configured signing parties // Returns nil if not configured (meaning all active parties should be used) func (a *Account) GetSigningParties() []string { if len(a.SigningParties) == 0 { return nil } // Return a copy to prevent modification result := make([]string, len(a.SigningParties)) copy(result, a.SigningParties) return result } // Account errors var ( ErrInvalidUsername = &AccountError{Code: "INVALID_USERNAME", Message: "username is required"} ErrInvalidEmail = &AccountError{Code: "INVALID_EMAIL", Message: "email is required"} ErrInvalidPublicKey = &AccountError{Code: "INVALID_PUBLIC_KEY", Message: "public key is required"} ErrInvalidThreshold = &AccountError{Code: "INVALID_THRESHOLD", Message: "invalid threshold configuration"} ErrAccountInRecovery = &AccountError{Code: "ACCOUNT_IN_RECOVERY", Message: "account is in recovery mode"} ErrCannotInitiateRecovery = &AccountError{Code: "CANNOT_INITIATE_RECOVERY", Message: "cannot initiate recovery in current state"} ErrAccountNotActive = &AccountError{Code: "ACCOUNT_NOT_ACTIVE", Message: "account is not active"} ErrAccountNotFound = &AccountError{Code: "ACCOUNT_NOT_FOUND", Message: "account not found"} ErrDuplicateUsername = &AccountError{Code: "DUPLICATE_USERNAME", Message: "username already exists"} ErrDuplicateEmail = &AccountError{Code: "DUPLICATE_EMAIL", Message: "email already exists"} ErrInvalidSigningPartiesCount = &AccountError{Code: "INVALID_SIGNING_PARTIES_COUNT", Message: "signing parties count must equal threshold T + 1 (required signers)"} ErrInvalidPartyID = &AccountError{Code: "INVALID_PARTY_ID", Message: "party ID cannot be empty"} ErrDuplicatePartyID = &AccountError{Code: "DUPLICATE_PARTY_ID", Message: "duplicate party ID in signing parties"} ) // AccountError represents an account domain error type AccountError struct { Code string Message string } func (e *AccountError) Error() string { return e.Message }