feat: kavenegar sdk, otp generation and verifying
This commit is contained in:
parent
36cda3fd5a
commit
5829b471d5
@ -1,12 +1,13 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Server Server `mapstructure:"server"`
|
||||
JWT JWT `mapstructure:"jwt"`
|
||||
DB DB `mapstructure:"db"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
KYCProvider KYC `mapstructure:"kyc"`
|
||||
OTP OTP `mapstructure:"otp"`
|
||||
Server Server `mapstructure:"server"`
|
||||
JWT JWT `mapstructure:"jwt"`
|
||||
DB DB `mapstructure:"db"`
|
||||
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
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
||||
@ -24,3 +24,7 @@ type AuthenticateResponse struct {
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
|
||||
}
|
||||
|
||||
type OTPProviderReq struct {
|
||||
Receptor string `json:"receptor"`
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
28
internal/repository/cache/otp_repo.go
vendored
28
internal/repository/cache/otp_repo.go
vendored
@ -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)
|
||||
}
|
||||
|
||||
@ -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
32
pkg/sms/kavengar.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user