254 lines
7.0 KiB
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)
|
|
}
|