feat: kavenegar sdk, otp generation and verifying

This commit is contained in:
2025-09-15 16:36:52 +03:30
parent 36cda3fd5a
commit 5829b471d5
9 changed files with 131 additions and 10 deletions
+4
View File
@@ -24,3 +24,7 @@ type AuthenticateResponse struct {
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
}
type OTPProviderReq struct {
Receptor string `json:"receptor"`
}
+5
View File
@@ -8,6 +8,7 @@ import (
"backend/internal/usecase"
cachePackage "backend/pkg/cache"
"backend/pkg/postgres"
"backend/pkg/sms"
"backend/pkg/zohal"
"fmt"
"log"
@@ -45,13 +46,17 @@ func NewAppContainer(cfg config.Config) (*AppContainer, error) {
sessionRepo := cache.NewSessionRepository(redis)
challengeRepo := cache.NewChallengeRepository(redis)
zohal := zohal.NewZohal(cfg.KYCProvider)
otpRepo := cache.NewOTPRepository(redis)
smsProvider := sms.NewKavenegar(cfg.Kavenegar)
authService := usecase.NewAuthService(
userRepo,
sessionRepo,
challengeRepo,
30,
otpRepo,
zohal,
smsProvider,
cfg,
)
+6 -1
View File
@@ -10,6 +10,7 @@ import (
type OTPRepo interface {
Create(ctx context.Context, phone string, otp *OTP) error
Get(ctx context.Context, phone string) (*OTP, error)
Delete(ctx context.Context, phone string) error
}
@@ -17,7 +18,7 @@ type OTP struct {
ID uuid.UUID
Code string
Phone string
ExpiredAt time.Time
ExpiresAt time.Time
}
func NewOTP(phone string) *OTP {
@@ -27,3 +28,7 @@ func NewOTP(phone string) *OTP {
Phone: phone,
}
}
func (o *OTP) IsExpired() bool {
return time.Now().After(o.ExpiresAt)
}
+27 -1
View File
@@ -7,6 +7,8 @@ import (
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type OTPRepository struct {
@@ -27,7 +29,7 @@ func (r *OTPRepository) Create(ctx context.Context, phone string, otp *domain.OT
return fmt.Errorf("failed to marshal otp: %w", err)
}
ttl := time.Until(otp.ExpiredAt)
ttl := time.Until(otp.ExpiresAt)
if ttl <= 0 {
return fmt.Errorf("OTP expired already")
}
@@ -48,6 +50,30 @@ func (r *OTPRepository) Delete(ctx context.Context, phone string) error {
return nil
}
func (r *OTPRepository) Get(ctx context.Context, phone string) (*domain.OTP, error) {
key := r.getOTPKey(phone)
result, err := r.redis.Client().Get(ctx, key).Result()
if err != nil {
if err == redis.Nil {
return nil, fmt.Errorf("otp code not found")
}
return nil, err
}
var otp domain.OTP
if err := json.Unmarshal([]byte(result), &otp); err != nil {
return nil, err
}
if otp.IsExpired() {
r.Delete(ctx, phone)
return nil, fmt.Errorf("otp is expired")
}
return &otp, nil
}
func (r *OTPRepository) getOTPKey(phone string) string {
return fmt.Sprintf("challenge:%s", phone)
}
+42 -2
View File
@@ -4,6 +4,7 @@ import (
"backend/config"
"backend/internal/domain"
"backend/pkg/jwt"
"backend/pkg/sms"
"backend/pkg/validate/common"
"backend/pkg/zohal"
"context"
@@ -12,6 +13,7 @@ import (
"time"
jwt2 "github.com/golang-jwt/jwt/v5"
"github.com/kavenegar/kavenegar-go"
"github.com/google/uuid"
)
@@ -25,7 +27,9 @@ 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
}
@@ -41,15 +45,19 @@ func NewAuthService(
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,
}
}
@@ -190,6 +198,38 @@ func (s *authService) VerifyKYC(ctx context.Context, userID, nationalID, birthDa
shahkarResp.StatusCode, identityResp.StatusCode)
}
func (*authService) SendOTPVerifaction(ctx context.Context, phone string) (string, error) {
return "", nil
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
}