feat: kavenegar sdk, otp generation and verifying

This commit is contained in:
AmirMahdi Qiasvand 2025-09-15 16:36:52 +03:30
parent 36cda3fd5a
commit 5829b471d5
9 changed files with 131 additions and 10 deletions

View File

@ -7,6 +7,7 @@ type Config struct {
Redis Redis `mapstructure:"redis"`
KYCProvider KYC `mapstructure:"kyc"`
OTP OTP `mapstructure:"otp"`
Kavenegar SMSProvider `mapstructure:"kavenegar"`
}
type Server struct {
Host string `mapstructure:"host"`
@ -41,3 +42,8 @@ type KYC struct {
type OTP struct {
CodeExpMinutes uint `mapstructure:"code_exp_minutes"`
}
type SMSProvider struct {
APIKey string `mapstructure:"api_key"`
Template string `mapstructure:"template"`
}

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/gofiber/fiber/v2 v2.52.9
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d
github.com/redis/go-redis/v9 v9.13.0
github.com/spf13/viper v1.21.0
github.com/swaggo/http-swagger v1.3.4

2
go.sum
View File

@ -149,6 +149,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d h1:5yPyBSS28Nojbr7pAkiXADGj6VpTXx73o6SsprKbSoo=
github.com/kavenegar/kavenegar-go v0.0.0-20240205151018-77039f51467d/go.mod h1:CRhvvr4KNAyrg+ewrutOf+/QoHs7lztSoLjp+GqhYlA=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=

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"`
}

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

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

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

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
}

32
pkg/sms/kavengar.go Normal file
View File

@ -0,0 +1,32 @@
package sms
import (
"backend/config"
"github.com/kavenegar/kavenegar-go"
)
type Kavenegar struct {
*kavenegar.Kavenegar
}
type OTPMsg struct {
Receptor string
Token string
Template string
Params kavenegar.VerifyLookupParam
}
func NewKavenegar(cfg config.SMSProvider) *Kavenegar {
instance := kavenegar.New(cfg.APIKey)
return &Kavenegar{
Kavenegar: instance,
}
}
func (k *Kavenegar) OTP(otpMsg *OTPMsg) error {
if _, err := k.Verify.Lookup(otpMsg.Receptor, otpMsg.Template, otpMsg.Token, &otpMsg.Params); err != nil {
return err
}
return nil
}