package usecase import ( "backend/config" "backend/internal/domain" "backend/pkg/jwt" "backend/pkg/validate/common" "backend/pkg/zohal" "context" "errors" "fmt" "time" jwt2 "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) type AuthService interface { GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) } type authService struct { userRepo domain.UserRepo sessionRepo domain.SessionRepo challengeRepo domain.ChallengeRepo zohal *zohal.Zohal challengeExp uint cfg config.Config } type UserToken struct { AuthorizationToken string RefreshToken string ExpiresAt int64 } func NewAuthService( userRepo domain.UserRepo, sessionRepo domain.SessionRepo, challengeRepo domain.ChallengeRepo, challengeExp uint, zohal *zohal.Zohal, cfg config.Config, ) AuthService { return &authService{ userRepo: userRepo, sessionRepo: sessionRepo, challengeRepo: challengeRepo, challengeExp: challengeExp, zohal: zohal, cfg: cfg, } } func (s *authService) GenerateChallenge(ctx context.Context, pubKey string) (*domain.Challenge, error) { _, err := common.IsValidPublicKey(pubKey) if err != nil { return nil, err } challenge := &domain.Challenge{ Message: uuid.New().String(), TimeStamp: time.Now().UTC(), ExpiresAt: time.Now().Add(time.Duration(s.challengeExp) * time.Minute), } // Save challenge to Redis err = s.challengeRepo.Create(ctx, pubKey, challenge) if err != nil { return nil, fmt.Errorf("failed to save challenge: %w", err) } return challenge, nil } func (s *authService) Authenticate(ctx context.Context, pubKey string, signature string, chainID uint, ipAddress, userAgent string) (*UserToken, error) { _, err := common.IsValidPublicKey(pubKey) if err != nil { return nil, fmt.Errorf("invalid public key: %w", err) } // Retrieve challenge from Redis challenge, err := s.challengeRepo.GetByPubKey(ctx, pubKey) if err != nil { return nil, fmt.Errorf("failed to retrieve challenge: %w", err) } isValid, err := challenge.Verify(pubKey, signature) if err != nil { return nil, fmt.Errorf("signature verification failed: %w", err) } if !isValid { return nil, errors.New("invalid signature") } // Delete the challenge after successful verification to prevent replay attacks err = s.challengeRepo.Delete(ctx, pubKey) if err != nil { return nil, fmt.Errorf("failed to delete challenge: %w", err) } user, err := s.userRepo.GetOrCreateUserByPubKey(ctx, pubKey) if err != nil { return nil, fmt.Errorf("failed to get or create user: %w", err) } user.UpdateLastLogin() if err := s.userRepo.Update(ctx, user); err != nil { return nil, fmt.Errorf("failed to update user: %w", err) } expiresAt := time.Now().Add(time.Duration(s.cfg.JWT.TokenExpMinutes) * time.Minute) session := domain.NewSession(user.ID, user.PubKey, expiresAt) err = s.sessionRepo.Create(ctx, session) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } //FIXME: KYC level will be set to 1 claims := &jwt.UserClaims{ UserID: uint(user.ID.ID()), KYCLevel: int(user.KYCLevel), Sections: []string{}, } claims.ExpiresAt = jwt2.NewNumericDate(expiresAt) claims.IssuedAt = jwt2.NewNumericDate(time.Now()) authToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), claims) if err != nil { return nil, err } refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.JWT.RefreshTokenExpMinutes) * time.Minute) refreshClaims := &jwt.UserClaims{ UserID: uint(user.ID.ID()), KYCLevel: int(user.KYCLevel), Sections: []string{"refresh"}, } refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt) refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now()) refreshToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), refreshClaims) if err != nil { return nil, fmt.Errorf("failed to create refresh token: %w", err) } return &UserToken{ AuthorizationToken: authToken, RefreshToken: refreshToken, ExpiresAt: expiresAt.Unix(), }, nil } func (s *authService) VerifyKYC(ctx context.Context, userID, nationalID, birthDate string) error { uID, err := uuid.Parse(userID) if err != nil { return fmt.Errorf("invalid user ID format: %w", err) } user, err := s.userRepo.GetByID(ctx, uID) if err != nil { return fmt.Errorf("failed to get user: %w", err) } shahkarResp, err := s.zohal.Shahkar(ctx, user.PhoneNumber, user.NationalID) if err != nil { return fmt.Errorf("shahkar verification failed: %w", err) } identityResp, err := s.zohal.GetPerson(ctx, nationalID, birthDate) if err != nil { return fmt.Errorf("identity verification failed: %w", err) } if shahkarResp.StatusCode == 200 && identityResp.StatusCode == 200 { user.UpdateKYCLevel(domain.KYCLevel1) err = s.userRepo.Update(ctx, user) if err != nil { return fmt.Errorf("failed to update user KYC level: %w", err) } return nil } return fmt.Errorf("KYC verification failed - shahkar status: %d, identity status: %d", shahkarResp.StatusCode, identityResp.StatusCode) } func (*authService) SendOTPVerifaction(ctx context.Context, phone string) (string, error) { return "", nil }