Igris/internal/usecase/auth_service.go

236 lines
6.1 KiB
Go

package usecase
import (
"backend/config"
"backend/internal/domain"
"backend/pkg/jwt"
"backend/pkg/sms"
"backend/pkg/validate/common"
"backend/pkg/zohal"
"context"
"errors"
"fmt"
"time"
jwt2 "github.com/golang-jwt/jwt/v5"
"github.com/kavenegar/kavenegar-go"
"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
otpRepo domain.OTPRepo
zohal *zohal.Zohal
smsProvider *sms.Kavenegar
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,
otpRepo domain.OTPRepo,
zohal *zohal.Zohal,
smsProvider *sms.Kavenegar,
cfg config.Config,
) AuthService {
return &authService{
userRepo: userRepo,
sessionRepo: sessionRepo,
challengeRepo: challengeRepo,
otpRepo: otpRepo,
challengeExp: challengeExp,
zohal: zohal,
smsProvider: smsProvider,
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 (s *authService) SendOTPCode(ctx context.Context, phone string) (string, error) {
otp := domain.NewOTP(phone)
if err := s.otpRepo.Create(ctx, phone, otp); err != nil {
return "", err
}
otpMsg := &sms.OTPMsg{
Receptor: otp.Phone,
Token: otp.Code,
Template: s.cfg.Kavenegar.Template,
// TODO: make sure when use VerfiyLookup Params
Params: kavenegar.VerifyLookupParam{},
}
err := s.smsProvider.OTP(otpMsg)
if err != nil {
return "", err
}
return otp.Code, err
}
func (s *authService) VerifyOTP(ctx context.Context, phone, code string) error {
otp, err := s.otpRepo.Get(ctx, phone)
if err != nil {
return err
}
if otp == nil {
return errors.New("otp code not found or expired")
}
if code != otp.Code {
return errors.New("otp code is not valid")
}
return nil
}