rwadurian/backend/mpc-system/services/account/domain/entities/account.go

216 lines
6.9 KiB
Go

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
}