rwadurian/backend/mpc-system/services/account/application/use_cases/login.go

254 lines
7.0 KiB
Go

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)
}