feat: add kyc layer service
This commit is contained in:
parent
667d2ce2f4
commit
2da912739b
@ -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/zohal"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
@ -43,13 +44,15 @@ func NewAppContainer(cfg config.Config) (*AppContainer, error) {
|
|||||||
userRepo := storage.NewUserRepository(dbConn)
|
userRepo := storage.NewUserRepository(dbConn)
|
||||||
sessionRepo := cache.NewSessionRepository(redis)
|
sessionRepo := cache.NewSessionRepository(redis)
|
||||||
challengeRepo := cache.NewChallengeRepository(redis)
|
challengeRepo := cache.NewChallengeRepository(redis)
|
||||||
|
zohal := zohal.NewZohal(cfg.KYCProvider)
|
||||||
|
|
||||||
authService := usecase.NewAuthService(
|
authService := usecase.NewAuthService(
|
||||||
userRepo,
|
userRepo,
|
||||||
sessionRepo,
|
sessionRepo,
|
||||||
challengeRepo,
|
challengeRepo,
|
||||||
30,
|
30,
|
||||||
cfg.JWT,
|
zohal,
|
||||||
|
cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
return &AppContainer{
|
return &AppContainer{
|
||||||
|
|||||||
@ -39,12 +39,19 @@ type ChallengeRepo interface {
|
|||||||
Delete(ctx context.Context, pubKey string) error
|
Delete(ctx context.Context, pubKey string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KYCLevel int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
KYCLevel0 KYCLevel = iota
|
||||||
|
KYCLevel1
|
||||||
|
KYCLevel2
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
PubKey string
|
PubKey string
|
||||||
Name string
|
Name string
|
||||||
//TODO: add Kyc Embedded struct
|
KYCLevel KYCLevel
|
||||||
//TODO: add NFT Embedded struct
|
|
||||||
LastName string
|
LastName string
|
||||||
PhoneNumber string
|
PhoneNumber string
|
||||||
Email *string
|
Email *string
|
||||||
@ -56,7 +63,54 @@ type User struct {
|
|||||||
DeletedAt *time.Time
|
DeletedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to another file?
|
func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) {
|
||||||
|
|
||||||
|
_, err := validate.IsValidPhone(phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validate.IsValidNationalID(nationalID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = validate.IsValidPublicKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &User{
|
||||||
|
ID: uuid.New(),
|
||||||
|
PubKey: pubKey,
|
||||||
|
Name: name,
|
||||||
|
LastName: lastName,
|
||||||
|
PhoneNumber: phoneNumber,
|
||||||
|
NationalID: nationalID,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateLastLogin() {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
u.LastLogin = &now
|
||||||
|
u.UpdatedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateInfo(email string) {
|
||||||
|
addr, err := mail.ParseAddress(email)
|
||||||
|
if err == nil {
|
||||||
|
u.Email = &addr.Address
|
||||||
|
}
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateKYCLevel(level KYCLevel) {
|
||||||
|
u.KYCLevel = level
|
||||||
|
u.UpdatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
Message string
|
Message string
|
||||||
TimeStamp time.Time
|
TimeStamp time.Time
|
||||||
@ -106,46 +160,3 @@ func (c *Challenge) VerifySignedBytes(expectedAddress string, msg []byte, sig []
|
|||||||
|
|
||||||
return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil
|
return strings.EqualFold(expectedAddr.Hex(), recoveredAddr.Hex()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(pubKey, name, lastName, phoneNumber, email, nationalID string) (*User, error) {
|
|
||||||
|
|
||||||
_, err := validate.IsValidPhone(phoneNumber)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = validate.IsValidNationalID(nationalID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = validate.IsValidPublicKey(pubKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &User{
|
|
||||||
ID: uuid.New(),
|
|
||||||
PubKey: pubKey,
|
|
||||||
Name: name,
|
|
||||||
LastName: lastName,
|
|
||||||
PhoneNumber: phoneNumber,
|
|
||||||
NationalID: nationalID,
|
|
||||||
CreatedAt: time.Now().UTC(),
|
|
||||||
UpdatedAt: time.Now().UTC(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) UpdateLastLogin() {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
u.LastLogin = &now
|
|
||||||
u.UpdatedAt = now
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) UpdateInfo(email string) {
|
|
||||||
addr, err := mail.ParseAddress(email)
|
|
||||||
if err == nil {
|
|
||||||
u.Email = &addr.Address
|
|
||||||
}
|
|
||||||
u.UpdatedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type User struct {
|
|||||||
PubKey string
|
PubKey string
|
||||||
Name string
|
Name string
|
||||||
LastName string
|
LastName string
|
||||||
//TODO: add Kyc Embedded struct
|
KYCLevel int
|
||||||
//TODO: add NFT Embedded struct
|
//TODO: add NFT Embedded struct
|
||||||
Phone string
|
Phone string
|
||||||
NationalID string
|
NationalID string
|
||||||
@ -30,6 +30,7 @@ func CastUserToStorage(u domain.User) *User {
|
|||||||
PubKey: u.PubKey,
|
PubKey: u.PubKey,
|
||||||
Name: u.Name,
|
Name: u.Name,
|
||||||
LastName: u.LastName,
|
LastName: u.LastName,
|
||||||
|
KYCLevel: int(u.KYCLevel),
|
||||||
Phone: u.PhoneNumber,
|
Phone: u.PhoneNumber,
|
||||||
NationalID: u.NationalID,
|
NationalID: u.NationalID,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
@ -44,6 +45,7 @@ func CastUserToDomain(u User) *domain.User {
|
|||||||
PubKey: u.PubKey,
|
PubKey: u.PubKey,
|
||||||
Name: u.Name,
|
Name: u.Name,
|
||||||
LastName: u.LastName,
|
LastName: u.LastName,
|
||||||
|
KYCLevel: domain.KYCLevel(u.KYCLevel),
|
||||||
PhoneNumber: u.Phone,
|
PhoneNumber: u.Phone,
|
||||||
NationalID: u.NationalID,
|
NationalID: u.NationalID,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"backend/internal/domain"
|
"backend/internal/domain"
|
||||||
"backend/pkg/jwt"
|
"backend/pkg/jwt"
|
||||||
"backend/pkg/validate/common"
|
"backend/pkg/validate/common"
|
||||||
|
"backend/pkg/zohal"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -24,8 +25,9 @@ type authService struct {
|
|||||||
userRepo domain.UserRepo
|
userRepo domain.UserRepo
|
||||||
sessionRepo domain.SessionRepo
|
sessionRepo domain.SessionRepo
|
||||||
challengeRepo domain.ChallengeRepo
|
challengeRepo domain.ChallengeRepo
|
||||||
|
zohal *zohal.Zohal
|
||||||
challengeExp uint
|
challengeExp uint
|
||||||
cfg config.JWT
|
cfg config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserToken struct {
|
type UserToken struct {
|
||||||
@ -39,13 +41,15 @@ func NewAuthService(
|
|||||||
sessionRepo domain.SessionRepo,
|
sessionRepo domain.SessionRepo,
|
||||||
challengeRepo domain.ChallengeRepo,
|
challengeRepo domain.ChallengeRepo,
|
||||||
challengeExp uint,
|
challengeExp uint,
|
||||||
cfg config.JWT,
|
zohal *zohal.Zohal,
|
||||||
|
cfg config.Config,
|
||||||
) AuthService {
|
) AuthService {
|
||||||
return &authService{
|
return &authService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
sessionRepo: sessionRepo,
|
sessionRepo: sessionRepo,
|
||||||
challengeRepo: challengeRepo,
|
challengeRepo: challengeRepo,
|
||||||
challengeExp: challengeExp,
|
challengeExp: challengeExp,
|
||||||
|
zohal: zohal,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +112,7 @@ func (s *authService) Authenticate(ctx context.Context, pubKey string, signature
|
|||||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expiresAt := time.Now().Add(time.Duration(s.cfg.TokenExpMinutes) * time.Minute)
|
expiresAt := time.Now().Add(time.Duration(s.cfg.JWT.TokenExpMinutes) * time.Minute)
|
||||||
session := domain.NewSession(user.ID, user.PubKey, expiresAt)
|
session := domain.NewSession(user.ID, user.PubKey, expiresAt)
|
||||||
|
|
||||||
err = s.sessionRepo.Create(ctx, session)
|
err = s.sessionRepo.Create(ctx, session)
|
||||||
@ -116,28 +120,31 @@ func (s *authService) Authenticate(ctx context.Context, pubKey string, signature
|
|||||||
return nil, fmt.Errorf("failed to create session: %w", err)
|
return nil, fmt.Errorf("failed to create session: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//FIXME: KYC level will be set to 1
|
||||||
claims := &jwt.UserClaims{
|
claims := &jwt.UserClaims{
|
||||||
UserID: uint(user.ID.ID()),
|
UserID: uint(user.ID.ID()),
|
||||||
|
KYCLevel: int(user.KYCLevel),
|
||||||
Sections: []string{},
|
Sections: []string{},
|
||||||
}
|
}
|
||||||
claims.ExpiresAt = jwt2.NewNumericDate(expiresAt)
|
claims.ExpiresAt = jwt2.NewNumericDate(expiresAt)
|
||||||
claims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
claims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||||
|
|
||||||
authToken, err := jwt.CreateToken([]byte(s.cfg.TokenSecret), claims)
|
authToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.RefreshTokenExpMinutes) * time.Minute)
|
refreshExpiresAt := time.Now().Add(time.Duration(s.cfg.JWT.RefreshTokenExpMinutes) * time.Minute)
|
||||||
refreshClaims := &jwt.UserClaims{
|
refreshClaims := &jwt.UserClaims{
|
||||||
UserID: uint(user.ID.ID()),
|
UserID: uint(user.ID.ID()),
|
||||||
|
KYCLevel: int(user.KYCLevel),
|
||||||
Sections: []string{"refresh"},
|
Sections: []string{"refresh"},
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt)
|
refreshClaims.ExpiresAt = jwt2.NewNumericDate(refreshExpiresAt)
|
||||||
refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
refreshClaims.IssuedAt = jwt2.NewNumericDate(time.Now())
|
||||||
|
|
||||||
refreshToken, err := jwt.CreateToken([]byte(s.cfg.TokenSecret), refreshClaims)
|
refreshToken, err := jwt.CreateToken([]byte(s.cfg.JWT.TokenSecret), refreshClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create refresh token: %w", err)
|
return nil, fmt.Errorf("failed to create refresh token: %w", err)
|
||||||
}
|
}
|
||||||
@ -148,3 +155,37 @@ func (s *authService) Authenticate(ctx context.Context, pubKey string, signature
|
|||||||
ExpiresAt: expiresAt.Unix(),
|
ExpiresAt: expiresAt.Unix(),
|
||||||
}, nil
|
}, 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -5,5 +5,6 @@ import jwt2 "github.com/golang-jwt/jwt/v5"
|
|||||||
type UserClaims struct {
|
type UserClaims struct {
|
||||||
jwt2.RegisteredClaims
|
jwt2.RegisteredClaims
|
||||||
UserID uint
|
UserID uint
|
||||||
|
KYCLevel int
|
||||||
Sections []string
|
Sections []string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,13 @@ type Zohal struct {
|
|||||||
cfg config.KYC
|
cfg config.KYC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewZohal(cfg config.KYC) *Zohal {
|
||||||
|
return &Zohal{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check Phone and NationalID
|
||||||
func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalShahkarResp, error) {
|
func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalShahkarResp, error) {
|
||||||
var req ZohalShahkarReq
|
var req ZohalShahkarReq
|
||||||
var resp ZohalShahkarResp
|
var resp ZohalShahkarResp
|
||||||
@ -28,7 +35,6 @@ func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalSha
|
|||||||
if !ok {
|
if !ok {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
header := make(map[string]string)
|
header := make(map[string]string)
|
||||||
header["Content-Type"] = "application/json"
|
header["Content-Type"] = "application/json"
|
||||||
header["Accept"] = "application/json"
|
header["Accept"] = "application/json"
|
||||||
@ -58,6 +64,42 @@ func (z *Zohal) Shahkar(ctx context.Context, phone, nationalID string) (ZohalSha
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (z *Zohal) GetPerson(ctx context.Context, nationalID, birthDate string) (ZohalIdentityResp, error) {
|
||||||
|
var req ZohalIdentityReq
|
||||||
|
var resp ZohalIdentityResp
|
||||||
|
|
||||||
|
header := make(map[string]string)
|
||||||
|
header["Content-Type"] = "application/json"
|
||||||
|
header["Accept"] = "application/json"
|
||||||
|
header["Authorization"] = fmt.Sprintf("Bearer %s", z.cfg.APIKey)
|
||||||
|
|
||||||
|
req.BirthDate = birthDate
|
||||||
|
req.NationalID = nationalID
|
||||||
|
|
||||||
|
u, err := url.Parse(z.cfg.URL)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
u = u.JoinPath("inquiry", "shahkar")
|
||||||
|
|
||||||
|
client := util.NewHttpClient()
|
||||||
|
clientResp, err := client.HttpRequest(ctx, http.MethodPost, u.String(), req, header)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(clientResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bodyBytes, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user