feat: kavenegar sdk, otp generation and verifying
This commit is contained in:
parent
36cda3fd5a
commit
5829b471d5
@ -1,12 +1,13 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server Server `mapstructure:"server"`
|
Server Server `mapstructure:"server"`
|
||||||
JWT JWT `mapstructure:"jwt"`
|
JWT JWT `mapstructure:"jwt"`
|
||||||
DB DB `mapstructure:"db"`
|
DB DB `mapstructure:"db"`
|
||||||
Redis Redis `mapstructure:"redis"`
|
Redis Redis `mapstructure:"redis"`
|
||||||
KYCProvider KYC `mapstructure:"kyc"`
|
KYCProvider KYC `mapstructure:"kyc"`
|
||||||
OTP OTP `mapstructure:"otp"`
|
OTP OTP `mapstructure:"otp"`
|
||||||
|
Kavenegar SMSProvider `mapstructure:"kavenegar"`
|
||||||
}
|
}
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
@ -41,3 +42,8 @@ type KYC struct {
|
|||||||
type OTP struct {
|
type OTP struct {
|
||||||
CodeExpMinutes uint `mapstructure:"code_exp_minutes"`
|
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/gofiber/fiber/v2 v2.52.9
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.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/redis/go-redis/v9 v9.13.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/http-swagger v1.3.4
|
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/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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
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 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
|||||||
@ -24,3 +24,7 @@ type AuthenticateResponse struct {
|
|||||||
type RefreshTokenRequest struct {
|
type RefreshTokenRequest struct {
|
||||||
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
|
RefreshToken string `json:"refreshToken" validate:"required,jwt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OTPProviderReq struct {
|
||||||
|
Receptor string `json:"receptor"`
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"backend/internal/usecase"
|
"backend/internal/usecase"
|
||||||
cachePackage "backend/pkg/cache"
|
cachePackage "backend/pkg/cache"
|
||||||
"backend/pkg/postgres"
|
"backend/pkg/postgres"
|
||||||
|
"backend/pkg/sms"
|
||||||
"backend/pkg/zohal"
|
"backend/pkg/zohal"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -45,13 +46,17 @@ func NewAppContainer(cfg config.Config) (*AppContainer, error) {
|
|||||||
sessionRepo := cache.NewSessionRepository(redis)
|
sessionRepo := cache.NewSessionRepository(redis)
|
||||||
challengeRepo := cache.NewChallengeRepository(redis)
|
challengeRepo := cache.NewChallengeRepository(redis)
|
||||||
zohal := zohal.NewZohal(cfg.KYCProvider)
|
zohal := zohal.NewZohal(cfg.KYCProvider)
|
||||||
|
otpRepo := cache.NewOTPRepository(redis)
|
||||||
|
smsProvider := sms.NewKavenegar(cfg.Kavenegar)
|
||||||
|
|
||||||
authService := usecase.NewAuthService(
|
authService := usecase.NewAuthService(
|
||||||
userRepo,
|
userRepo,
|
||||||
sessionRepo,
|
sessionRepo,
|
||||||
challengeRepo,
|
challengeRepo,
|
||||||
30,
|
30,
|
||||||
|
otpRepo,
|
||||||
zohal,
|
zohal,
|
||||||
|
smsProvider,
|
||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type OTPRepo interface {
|
type OTPRepo interface {
|
||||||
Create(ctx context.Context, phone string, otp *OTP) error
|
Create(ctx context.Context, phone string, otp *OTP) error
|
||||||
|
Get(ctx context.Context, phone string) (*OTP, error)
|
||||||
Delete(ctx context.Context, phone string) error
|
Delete(ctx context.Context, phone string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ type OTP struct {
|
|||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
Code string
|
Code string
|
||||||
Phone string
|
Phone string
|
||||||
ExpiredAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOTP(phone string) *OTP {
|
func NewOTP(phone string) *OTP {
|
||||||
@ -27,3 +28,7 @@ func NewOTP(phone string) *OTP {
|
|||||||
Phone: phone,
|
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"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OTPRepository struct {
|
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)
|
return fmt.Errorf("failed to marshal otp: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ttl := time.Until(otp.ExpiredAt)
|
ttl := time.Until(otp.ExpiresAt)
|
||||||
if ttl <= 0 {
|
if ttl <= 0 {
|
||||||
return fmt.Errorf("OTP expired already")
|
return fmt.Errorf("OTP expired already")
|
||||||
}
|
}
|
||||||
@ -48,6 +50,30 @@ func (r *OTPRepository) Delete(ctx context.Context, phone string) error {
|
|||||||
return nil
|
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 {
|
func (r *OTPRepository) getOTPKey(phone string) string {
|
||||||
return fmt.Sprintf("challenge:%s", phone)
|
return fmt.Sprintf("challenge:%s", phone)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"backend/config"
|
"backend/config"
|
||||||
"backend/internal/domain"
|
"backend/internal/domain"
|
||||||
"backend/pkg/jwt"
|
"backend/pkg/jwt"
|
||||||
|
"backend/pkg/sms"
|
||||||
"backend/pkg/validate/common"
|
"backend/pkg/validate/common"
|
||||||
"backend/pkg/zohal"
|
"backend/pkg/zohal"
|
||||||
"context"
|
"context"
|
||||||
@ -12,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwt2 "github.com/golang-jwt/jwt/v5"
|
jwt2 "github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/kavenegar/kavenegar-go"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -25,7 +27,9 @@ type authService struct {
|
|||||||
userRepo domain.UserRepo
|
userRepo domain.UserRepo
|
||||||
sessionRepo domain.SessionRepo
|
sessionRepo domain.SessionRepo
|
||||||
challengeRepo domain.ChallengeRepo
|
challengeRepo domain.ChallengeRepo
|
||||||
|
otpRepo domain.OTPRepo
|
||||||
zohal *zohal.Zohal
|
zohal *zohal.Zohal
|
||||||
|
smsProvider *sms.Kavenegar
|
||||||
challengeExp uint
|
challengeExp uint
|
||||||
cfg config.Config
|
cfg config.Config
|
||||||
}
|
}
|
||||||
@ -41,15 +45,19 @@ func NewAuthService(
|
|||||||
sessionRepo domain.SessionRepo,
|
sessionRepo domain.SessionRepo,
|
||||||
challengeRepo domain.ChallengeRepo,
|
challengeRepo domain.ChallengeRepo,
|
||||||
challengeExp uint,
|
challengeExp uint,
|
||||||
|
otpRepo domain.OTPRepo,
|
||||||
zohal *zohal.Zohal,
|
zohal *zohal.Zohal,
|
||||||
|
smsProvider *sms.Kavenegar,
|
||||||
cfg config.Config,
|
cfg config.Config,
|
||||||
) AuthService {
|
) AuthService {
|
||||||
return &authService{
|
return &authService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
sessionRepo: sessionRepo,
|
sessionRepo: sessionRepo,
|
||||||
challengeRepo: challengeRepo,
|
challengeRepo: challengeRepo,
|
||||||
|
otpRepo: otpRepo,
|
||||||
challengeExp: challengeExp,
|
challengeExp: challengeExp,
|
||||||
zohal: zohal,
|
zohal: zohal,
|
||||||
|
smsProvider: smsProvider,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,6 +198,38 @@ func (s *authService) VerifyKYC(ctx context.Context, userID, nationalID, birthDa
|
|||||||
shahkarResp.StatusCode, identityResp.StatusCode)
|
shahkarResp.StatusCode, identityResp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*authService) SendOTPVerifaction(ctx context.Context, phone string) (string, error) {
|
func (s *authService) SendOTPCode(ctx context.Context, phone string) (string, error) {
|
||||||
return "", nil
|
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