package use_cases import ( "context" "encoding/hex" "time" "github.com/rwadurian/mpc-system/pkg/crypto" "github.com/rwadurian/mpc-system/services/account/application/ports" "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" ) // LoginError represents a login error type LoginError struct { Code string Message string } func (e *LoginError) Error() string { return e.Message } var ( ErrInvalidCredentials = &LoginError{Code: "INVALID_CREDENTIALS", Message: "invalid username or signature"} ErrAccountLocked = &LoginError{Code: "ACCOUNT_LOCKED", Message: "account is locked"} ErrAccountSuspended = &LoginError{Code: "ACCOUNT_SUSPENDED", Message: "account is suspended"} ErrSignatureInvalid = &LoginError{Code: "SIGNATURE_INVALID", Message: "signature verification failed"} ) // LoginUseCase handles user login with MPC signature verification type LoginUseCase struct { accountRepo repositories.AccountRepository shareRepo repositories.AccountShareRepository tokenService ports.TokenService eventPublisher ports.EventPublisher } // NewLoginUseCase creates a new LoginUseCase func NewLoginUseCase( accountRepo repositories.AccountRepository, shareRepo repositories.AccountShareRepository, tokenService ports.TokenService, eventPublisher ports.EventPublisher, ) *LoginUseCase { return &LoginUseCase{ accountRepo: accountRepo, shareRepo: shareRepo, tokenService: tokenService, eventPublisher: eventPublisher, } } // Execute performs login with signature verification func (uc *LoginUseCase) Execute(ctx context.Context, input ports.LoginInput) (*ports.LoginOutput, error) { // Get account by username account, err := uc.accountRepo.GetByUsername(ctx, input.Username) if err != nil { return nil, ErrInvalidCredentials } // Check account status if !account.CanLogin() { switch account.Status.String() { case "locked": return nil, ErrAccountLocked case "suspended": return nil, ErrAccountSuspended default: return nil, entities.ErrAccountNotActive } } // Parse public key pubKey, err := crypto.ParsePublicKey(account.PublicKey) if err != nil { return nil, ErrSignatureInvalid } // Verify signature (hash the challenge first, as SignMessage does) challengeHash := crypto.HashMessage(input.Challenge) if !crypto.VerifySignature(pubKey, challengeHash, input.Signature) { return nil, ErrSignatureInvalid } // Update last login account.UpdateLastLogin() if err := uc.accountRepo.Update(ctx, account); err != nil { return nil, err } // Generate tokens accessToken, err := uc.tokenService.GenerateAccessToken(account.ID.String(), account.Username) if err != nil { return nil, err } refreshToken, err := uc.tokenService.GenerateRefreshToken(account.ID.String()) if err != nil { return nil, err } // Publish login event if uc.eventPublisher != nil { _ = uc.eventPublisher.Publish(ctx, ports.AccountEvent{ Type: ports.EventTypeAccountLogin, AccountID: account.ID.String(), Data: map[string]interface{}{ "username": account.Username, "timestamp": time.Now().UTC(), }, }) } return &ports.LoginOutput{ Account: account, AccessToken: accessToken, RefreshToken: refreshToken, }, nil } // RefreshTokenInput represents input for refreshing tokens type RefreshTokenInput struct { RefreshToken string } // RefreshTokenOutput represents output from refreshing tokens type RefreshTokenOutput struct { AccessToken string RefreshToken string } // RefreshTokenUseCase handles token refresh type RefreshTokenUseCase struct { accountRepo repositories.AccountRepository tokenService ports.TokenService } // NewRefreshTokenUseCase creates a new RefreshTokenUseCase func NewRefreshTokenUseCase( accountRepo repositories.AccountRepository, tokenService ports.TokenService, ) *RefreshTokenUseCase { return &RefreshTokenUseCase{ accountRepo: accountRepo, tokenService: tokenService, } } // Execute refreshes the access token func (uc *RefreshTokenUseCase) Execute(ctx context.Context, input RefreshTokenInput) (*RefreshTokenOutput, error) { // Validate refresh token and get account ID accountIDStr, err := uc.tokenService.ValidateRefreshToken(input.RefreshToken) if err != nil { return nil, err } // Get account to verify it still exists and is active accountID, err := parseAccountID(accountIDStr) if err != nil { return nil, err } account, err := uc.accountRepo.GetByID(ctx, accountID) if err != nil { return nil, err } if !account.CanLogin() { return nil, entities.ErrAccountNotActive } // Generate new access token accessToken, err := uc.tokenService.GenerateAccessToken(account.ID.String(), account.Username) if err != nil { return nil, err } // Generate new refresh token refreshToken, err := uc.tokenService.GenerateRefreshToken(account.ID.String()) if err != nil { return nil, err } return &RefreshTokenOutput{ AccessToken: accessToken, RefreshToken: refreshToken, }, nil } // GenerateChallengeUseCase handles challenge generation for login type GenerateChallengeUseCase struct { cacheService ports.CacheService } // NewGenerateChallengeUseCase creates a new GenerateChallengeUseCase func NewGenerateChallengeUseCase(cacheService ports.CacheService) *GenerateChallengeUseCase { return &GenerateChallengeUseCase{ cacheService: cacheService, } } // GenerateChallengeInput represents input for generating a challenge type GenerateChallengeInput struct { Username string } // GenerateChallengeOutput represents output from generating a challenge type GenerateChallengeOutput struct { Challenge []byte ChallengeID string ExpiresAt time.Time } // Execute generates a challenge for login func (uc *GenerateChallengeUseCase) Execute(ctx context.Context, input GenerateChallengeInput) (*GenerateChallengeOutput, error) { // Generate random challenge challenge, err := crypto.GenerateRandomBytes(32) if err != nil { return nil, err } // Generate challenge ID challengeID, err := crypto.GenerateRandomBytes(16) if err != nil { return nil, err } challengeIDStr := hex.EncodeToString(challengeID) expiresAt := time.Now().UTC().Add(5 * time.Minute) // Store challenge in cache cacheKey := "login_challenge:" + challengeIDStr if uc.cacheService != nil { _ = uc.cacheService.Set(ctx, cacheKey, map[string]interface{}{ "username": input.Username, "challenge": hex.EncodeToString(challenge), "expiresAt": expiresAt, }, 300) // 5 minutes TTL } return &GenerateChallengeOutput{ Challenge: challenge, ChallengeID: challengeIDStr, ExpiresAt: expiresAt, }, nil } // helper function to parse account ID func parseAccountID(s string) (value_objects.AccountID, error) { return value_objects.AccountIDFromString(s) }