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

273 lines
7.1 KiB
Go

package services
import (
"context"
"github.com/google/uuid"
"github.com/rwadurian/mpc-system/pkg/crypto"
"github.com/rwadurian/mpc-system/services/account/domain/entities"
"github.com/rwadurian/mpc-system/services/account/domain/repositories"
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
)
// AccountDomainService provides domain logic for accounts
type AccountDomainService struct {
accountRepo repositories.AccountRepository
shareRepo repositories.AccountShareRepository
recoveryRepo repositories.RecoverySessionRepository
}
// NewAccountDomainService creates a new AccountDomainService
func NewAccountDomainService(
accountRepo repositories.AccountRepository,
shareRepo repositories.AccountShareRepository,
recoveryRepo repositories.RecoverySessionRepository,
) *AccountDomainService {
return &AccountDomainService{
accountRepo: accountRepo,
shareRepo: shareRepo,
recoveryRepo: recoveryRepo,
}
}
// CreateAccountInput represents input for creating an account
type CreateAccountInput struct {
Username string
Email string
Phone *string
PublicKey []byte
KeygenSessionID uuid.UUID
ThresholdN int
ThresholdT int
Shares []ShareInfo
}
// ShareInfo represents information about a key share
type ShareInfo struct {
ShareType value_objects.ShareType
PartyID string
PartyIndex int
DeviceType *string
DeviceID *string
}
// CreateAccount creates a new account with shares
func (s *AccountDomainService) CreateAccount(ctx context.Context, input CreateAccountInput) (*entities.Account, error) {
// Check username uniqueness
exists, err := s.accountRepo.ExistsByUsername(ctx, input.Username)
if err != nil {
return nil, err
}
if exists {
return nil, entities.ErrDuplicateUsername
}
// Check email uniqueness
exists, err = s.accountRepo.ExistsByEmail(ctx, input.Email)
if err != nil {
return nil, err
}
if exists {
return nil, entities.ErrDuplicateEmail
}
// Create account
account := entities.NewAccount(
input.Username,
input.Email,
input.PublicKey,
input.KeygenSessionID,
input.ThresholdN,
input.ThresholdT,
)
if input.Phone != nil {
account.SetPhone(*input.Phone)
}
// Validate account
if err := account.Validate(); err != nil {
return nil, err
}
// Create account in repository
if err := s.accountRepo.Create(ctx, account); err != nil {
return nil, err
}
// Create shares
for _, shareInfo := range input.Shares {
share := entities.NewAccountShare(
account.ID,
shareInfo.ShareType,
shareInfo.PartyID,
shareInfo.PartyIndex,
)
if shareInfo.DeviceType != nil && shareInfo.DeviceID != nil {
share.SetDeviceInfo(*shareInfo.DeviceType, *shareInfo.DeviceID)
}
if err := share.Validate(); err != nil {
return nil, err
}
if err := s.shareRepo.Create(ctx, share); err != nil {
return nil, err
}
}
return account, nil
}
// VerifySignature verifies a signature against an account's public key
func (s *AccountDomainService) VerifySignature(ctx context.Context, accountID value_objects.AccountID, message, signature []byte) (bool, error) {
account, err := s.accountRepo.GetByID(ctx, accountID)
if err != nil {
return false, err
}
// Parse public key
pubKey, err := crypto.ParsePublicKey(account.PublicKey)
if err != nil {
return false, err
}
// Verify signature
valid := crypto.VerifySignature(pubKey, message, signature)
return valid, nil
}
// InitiateRecovery initiates account recovery
func (s *AccountDomainService) InitiateRecovery(ctx context.Context, accountID value_objects.AccountID, recoveryType value_objects.RecoveryType, oldShareType *value_objects.ShareType) (*entities.RecoverySession, error) {
// Get account
account, err := s.accountRepo.GetByID(ctx, accountID)
if err != nil {
return nil, err
}
// Check if recovery can be initiated
if err := account.StartRecovery(); err != nil {
return nil, err
}
// Update account status
if err := s.accountRepo.Update(ctx, account); err != nil {
return nil, err
}
// Create recovery session
recoverySession := entities.NewRecoverySession(accountID, recoveryType)
if oldShareType != nil {
recoverySession.SetOldShareType(*oldShareType)
}
if err := recoverySession.Validate(); err != nil {
return nil, err
}
if err := s.recoveryRepo.Create(ctx, recoverySession); err != nil {
return nil, err
}
return recoverySession, nil
}
// CompleteRecovery completes the recovery process
func (s *AccountDomainService) CompleteRecovery(ctx context.Context, recoverySessionID string, newPublicKey []byte, newKeygenSessionID uuid.UUID, newShares []ShareInfo) error {
// Get recovery session
recovery, err := s.recoveryRepo.GetByID(ctx, recoverySessionID)
if err != nil {
return err
}
// Start keygen if still in requested state (transitions to in_progress)
if recovery.Status == value_objects.RecoveryStatusRequested {
if err := recovery.StartKeygen(newKeygenSessionID); err != nil {
return err
}
}
// Complete recovery session
if err := recovery.Complete(); err != nil {
return err
}
// Get account
account, err := s.accountRepo.GetByID(ctx, recovery.AccountID)
if err != nil {
return err
}
// Complete account recovery
account.CompleteRecovery(newPublicKey, newKeygenSessionID)
// Deactivate old shares
if err := s.shareRepo.DeactivateByAccountID(ctx, account.ID); err != nil {
return err
}
// Create new shares
for _, shareInfo := range newShares {
share := entities.NewAccountShare(
account.ID,
shareInfo.ShareType,
shareInfo.PartyID,
shareInfo.PartyIndex,
)
if shareInfo.DeviceType != nil && shareInfo.DeviceID != nil {
share.SetDeviceInfo(*shareInfo.DeviceType, *shareInfo.DeviceID)
}
if err := s.shareRepo.Create(ctx, share); err != nil {
return err
}
}
// Update account
if err := s.accountRepo.Update(ctx, account); err != nil {
return err
}
// Update recovery session
if err := s.recoveryRepo.Update(ctx, recovery); err != nil {
return err
}
return nil
}
// GetActiveShares returns active shares for an account
func (s *AccountDomainService) GetActiveShares(ctx context.Context, accountID value_objects.AccountID) ([]*entities.AccountShare, error) {
return s.shareRepo.GetActiveByAccountID(ctx, accountID)
}
// CanAccountSign checks if an account has enough active shares to sign
func (s *AccountDomainService) CanAccountSign(ctx context.Context, accountID value_objects.AccountID) (bool, error) {
account, err := s.accountRepo.GetByID(ctx, accountID)
if err != nil {
return false, err
}
if !account.CanLogin() {
return false, nil
}
shares, err := s.shareRepo.GetActiveByAccountID(ctx, accountID)
if err != nil {
return false, err
}
// Count active shares
activeCount := 0
for _, share := range shares {
if share.IsActive {
activeCount++
}
}
// Check if we have enough shares for threshold
return activeCount >= account.ThresholdT, nil
}