216 lines
6.9 KiB
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
|
|
}
|