forgot password - sms and email
This commit is contained in:
parent
6dbce0725d
commit
4c865d4d91
|
|
@ -18,8 +18,7 @@ import (
|
||||||
// mongologger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
// mongologger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
|
||||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
||||||
|
|
@ -109,10 +108,8 @@ func main() {
|
||||||
v := customvalidator.NewCustomValidator(validator.New())
|
v := customvalidator.NewCustomValidator(validator.New())
|
||||||
|
|
||||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||||
mockSms := mocksms.NewMockSMS()
|
|
||||||
mockEmail := mockemail.NewMockEmail()
|
|
||||||
|
|
||||||
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
userSvc := user.NewService(store, store, cfg)
|
||||||
|
|
||||||
eventSvc := event.New(cfg.Bet365Token, store)
|
eventSvc := event.New(cfg.Bet365Token, store)
|
||||||
oddsSvc := odds.New(store, cfg, logger)
|
oddsSvc := odds.New(store, cfg, logger)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS wallet_threshold_notifications (
|
||||||
PRIMARY KEY (company_id, threshold)
|
PRIMARY KEY (company_id, threshold)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_wallet_threshold_notifications_company ON wallet_threshold_notifications(company_id)
|
CREATE INDEX idx_wallet_threshold_notifications_company ON wallet_threshold_notifications(company_id);
|
||||||
|
|
||||||
CREATE INDEX idx_notifications_recipient_id ON notifications (recipient_id);
|
CREATE INDEX idx_notifications_recipient_id ON notifications (recipient_id);
|
||||||
|
|
||||||
|
|
|
||||||
5
go.mod
5
go.mod
|
|
@ -76,4 +76,7 @@ require (
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require go.uber.org/multierr v1.10.0 // indirect
|
require (
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
)
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -110,6 +110,8 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
|
||||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ var (
|
||||||
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
|
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
|
||||||
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
|
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
|
||||||
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
|
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
|
||||||
|
ErrMissingResendApiKey = errors.New("missing Resend Api key")
|
||||||
|
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
|
||||||
)
|
)
|
||||||
|
|
||||||
type AleaPlayConfig struct {
|
type AleaPlayConfig struct {
|
||||||
|
|
@ -75,6 +77,8 @@ type Config struct {
|
||||||
PopOK domain.PopOKConfig
|
PopOK domain.PopOKConfig
|
||||||
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
||||||
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
|
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
|
||||||
|
ResendApiKey string
|
||||||
|
ResendSenderEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -287,6 +291,19 @@ func (c *Config) loadEnv() error {
|
||||||
return ErrMissingBetToken
|
return ErrMissingBetToken
|
||||||
}
|
}
|
||||||
c.Bet365Token = betToken
|
c.Bet365Token = betToken
|
||||||
|
|
||||||
|
resendApiKey := os.Getenv("RESEND_API_KEY")
|
||||||
|
if resendApiKey == "" {
|
||||||
|
return ErrMissingResendApiKey
|
||||||
|
}
|
||||||
|
c.ResendApiKey = resendApiKey
|
||||||
|
|
||||||
|
resendSenderEmail := os.Getenv("RESEND_SENDER_EMAIL")
|
||||||
|
if resendSenderEmail == "" {
|
||||||
|
return ErrMissingResendSenderEmail
|
||||||
|
}
|
||||||
|
c.ResendSenderEmail = resendSenderEmail
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
func GenerateID() string {
|
func GenerateID() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateOTP() string {
|
||||||
|
num := 100000 + rand.UintN(899999)
|
||||||
|
return fmt.Sprintf("%d", num) // 6 digit random number [100,000 - 999,999]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin
|
||||||
// If old refresh token is not revoked, revoke it
|
// If old refresh token is not revoked, revoke it
|
||||||
if err == nil && !oldRefreshToken.Revoked {
|
if err == nil && !oldRefreshToken.Revoked {
|
||||||
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token)
|
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token)
|
||||||
if(err != nil) {
|
if err != nil {
|
||||||
return LoginSuccess{}, err
|
return LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,32 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
||||||
|
afro "github.com/amanuelabay/afrosms-go"
|
||||||
|
"github.com/resend/resend-go/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
|
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
|
||||||
otpCode := "123456" // Generate OTP code
|
otpCode := helpers.GenerateOTP()
|
||||||
|
|
||||||
|
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
|
||||||
|
|
||||||
|
switch medium {
|
||||||
|
case domain.OtpMediumSms:
|
||||||
|
if err := s.SendSMSOTP(ctx, sentTo, message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case domain.OtpMediumEmail:
|
||||||
|
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
otp := domain.Otp{
|
otp := domain.Otp{
|
||||||
SentTo: sentTo,
|
SentTo: sentTo,
|
||||||
|
|
@ -21,19 +39,9 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
|
||||||
ExpiresAt: time.Now().Add(OtpExpiry),
|
ExpiresAt: time.Now().Add(OtpExpiry),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.otpStore.CreateOtp(ctx, otp)
|
return s.otpStore.CreateOtp(ctx, otp)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch medium {
|
|
||||||
case domain.OtpMediumSms:
|
|
||||||
return s.smsGateway.SendSMSOTP(ctx, sentTo, otpCode)
|
|
||||||
case domain.OtpMediumEmail:
|
|
||||||
return s.emailGateway.SendEmailOTP(ctx, sentTo, otpCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashPassword(plaintextPassword string) ([]byte, error) {
|
func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -42,3 +50,50 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
|
|
||||||
return hash, nil
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
apiKey := s.config.AFRO_SMS_API_KEY
|
||||||
|
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||||
|
hostURL := s.config.ADRO_SMS_HOST_URL
|
||||||
|
endpoint := "/api/send"
|
||||||
|
|
||||||
|
// API endpoint has been updated
|
||||||
|
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||||
|
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||||
|
request.BaseURL = "https://api.afromessage.com/api/send"
|
||||||
|
|
||||||
|
request.Method = "GET"
|
||||||
|
request.Sender(senderName)
|
||||||
|
request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response["acknowledge"] == "success" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
fmt.Println(response["response"].(map[string]interface{}))
|
||||||
|
return errors.New("SMS delivery failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
|
||||||
|
apiKey := s.config.ResendApiKey
|
||||||
|
client := resend.NewClient(apiKey)
|
||||||
|
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
|
||||||
|
params := &resend.SendEmailRequest{
|
||||||
|
From: formattedSenderEmail,
|
||||||
|
To: []string{receiverEmail},
|
||||||
|
Subject: "FortuneBets - One Time Password",
|
||||||
|
Text: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Emails.Send(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -11,19 +13,17 @@ const (
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userStore UserStore
|
userStore UserStore
|
||||||
otpStore OtpStore
|
otpStore OtpStore
|
||||||
smsGateway SmsGateway
|
config *config.Config
|
||||||
emailGateway EmailGateway
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
userStore UserStore,
|
userStore UserStore,
|
||||||
otpStore OtpStore, smsGateway SmsGateway,
|
otpStore OtpStore,
|
||||||
emailGateway EmailGateway,
|
cfg *config.Config,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
otpStore: otpStore,
|
otpStore: otpStore,
|
||||||
smsGateway: smsGateway,
|
config: cfg,
|
||||||
emailGateway: emailGateway,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
|
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
|
||||||
type loginCustomerReq struct {
|
type loginCustomerReq struct {
|
||||||
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
|
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
|
||||||
|
|
@ -21,6 +22,7 @@ type loginCustomerRes struct {
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginCustomer godoc
|
// LoginCustomer godoc
|
||||||
// @Summary Login customer
|
// @Summary Login customer
|
||||||
// @Description Login customer
|
// @Description Login customer
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -243,6 +244,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
|
||||||
|
|
||||||
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
|
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
|
||||||
h.logger.Error("Failed to send reset code", "error", err)
|
h.logger.Error("Failed to send reset code", "error", err)
|
||||||
|
fmt.Println(err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,8 +252,8 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetPasswordReq struct {
|
type ResetPasswordReq struct {
|
||||||
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
|
Email string `json:"email,omitempty" validate:"required_without=PhoneNumber,omitempty,email" example:"john.doe@example.com"`
|
||||||
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
|
PhoneNumber string `json:"phone_number,omitempty" validate:"required_without=Email,omitempty" example:"1234567890"`
|
||||||
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
|
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
|
||||||
Otp string `json:"otp" validate:"required" example:"123456"`
|
Otp string `json:"otp" validate:"required" example:"123456"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user