first commit
This commit is contained in:
commit
6054f8bf0f
|
|
@ -0,0 +1 @@
|
||||||
|
config.yaml
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"smpp-otp/internal/config"
|
||||||
|
"smpp-otp/internal/delivery/routers"
|
||||||
|
"smpp-otp/internal/repository"
|
||||||
|
"smpp-otp/internal/service"
|
||||||
|
db "smpp-otp/pkg/database"
|
||||||
|
"smpp-otp/pkg/lib/logger"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
|
logger, err := logger.SetupLogger(cfg.Env)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to set up logger: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.InfoLogger.Info("Server is up and running")
|
||||||
|
slog.Info("Server is up and running")
|
||||||
|
|
||||||
|
database, err := db.InitDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorLogger.Error("failed to initialize database: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
repo := repository.NewOTPRepository(database.GetClient(), logger)
|
||||||
|
otpService := service.NewOTPService(repo, logger)
|
||||||
|
|
||||||
|
r := routers.SetupOTPRoutes(otpService, logger)
|
||||||
|
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-stop
|
||||||
|
logger.InfoLogger.Info("Shutting down the server gracefully...")
|
||||||
|
if err := database.Close(); err != nil {
|
||||||
|
logger.ErrorLogger.Error("Error closing database:", err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = http.ListenAndServe(cfg.HTTPServer.Address, r)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorLogger.Error("Server failed to start:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
env: "local" # local, dev, prod
|
||||||
|
|
||||||
|
database:
|
||||||
|
address: ""
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
|
||||||
|
http_server:
|
||||||
|
address: ""
|
||||||
|
|
||||||
|
smpp:
|
||||||
|
address: ""
|
||||||
|
user: ""
|
||||||
|
password: ""
|
||||||
|
src_phone_num: ""
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
module smpp-otp
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
|
github.com/go-chi/chi v1.5.5 // indirect
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible // indirect
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||||
|
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||||
|
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"smpp-otp/pkg/lib/utils"
|
||||||
|
|
||||||
|
"github.com/ilyakaznacheev/cleanenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Env string `yaml:"env"`
|
||||||
|
Database `yaml:"database"`
|
||||||
|
HTTPServer `yaml:"http_server"`
|
||||||
|
SMPP `yaml:"smpp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
DB int `yaml:"db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPServer struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SMPP struct {
|
||||||
|
Addr string `yaml:"addr"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Pass string `yaml:"pass"`
|
||||||
|
Src_Phone_Number string `yaml:"src_phone_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() *Config {
|
||||||
|
configPath := "./config/config.yaml"
|
||||||
|
|
||||||
|
if configPath == "" {
|
||||||
|
log.Fatalf("config path is not set or config file does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
|
||||||
|
if err := cleanenv.ReadConfig(configPath, &cfg); err != nil {
|
||||||
|
log.Fatalf("Cannot read config: %v", utils.Err(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"smpp-otp/internal/service"
|
||||||
|
"smpp-otp/pkg/lib/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPHandler struct {
|
||||||
|
Service service.OTPService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOTPHandler(s service.OTPService) *OTPHandler {
|
||||||
|
return &OTPHandler{Service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OTPHandler) GenerateAndSaveOTPHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var request struct {
|
||||||
|
PhoneNumber string `json:"phone_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||||||
|
if err != nil {
|
||||||
|
utils.RespondWithErrorJSON(w, http.StatusBadRequest, "Invalid request payload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.PhoneNumber == "" {
|
||||||
|
utils.RespondWithErrorJSON(w, http.StatusBadRequest, "Phone number is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.Service.GenerateAndSaveOTP(request.PhoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
utils.RespondWithErrorJSON(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package routers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"smpp-otp/internal/delivery/handlers"
|
||||||
|
"smpp-otp/internal/service"
|
||||||
|
"smpp-otp/pkg/lib/logger"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupOTPRoutes(otpService service.OTPService, logger *logger.Loggers) http.Handler {
|
||||||
|
otpRouter := chi.NewRouter()
|
||||||
|
otpHandler := handlers.NewOTPHandler(otpService)
|
||||||
|
|
||||||
|
otpRouter.Post("/sendOTP", otpHandler.GenerateAndSaveOTPHandler)
|
||||||
|
|
||||||
|
return otpRouter
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
type OTPRepository interface {
|
||||||
|
SaveOTP(phoneNumber string, otp string) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"smpp-otp/pkg/lib/logger"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPRepository struct {
|
||||||
|
Client *redis.Client
|
||||||
|
logger *logger.Loggers
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOTPRepository(client *redis.Client, logger *logger.Loggers) *OTPRepository {
|
||||||
|
return &OTPRepository{
|
||||||
|
Client: client,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateOTP() string {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
otp := fmt.Sprintf("%06d", r.Intn(1000000))
|
||||||
|
return otp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OTPRepository) SaveOTP(phoneNumber string, otp string) error {
|
||||||
|
err := r.Client.Watch(func(tx *redis.Tx) error {
|
||||||
|
err := tx.Set(phoneNumber, otp, 0).Err()
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorLogger.Error("Error setting up values into redis: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, phoneNumber)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"smpp-otp/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPService interface {
|
||||||
|
GenerateAndSendOTP(cfg config.Config, phoneNumber string) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
repository "smpp-otp/internal/repository/interfaces"
|
||||||
|
"smpp-otp/pkg/lib/logger"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OTPService struct {
|
||||||
|
repository repository.OTPRepository
|
||||||
|
logger *logger.Loggers
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOTPService(repo repository.OTPRepository, logger *logger.Loggers) OTPService {
|
||||||
|
return OTPService{repository: repo, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateOTP() string {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
otp := fmt.Sprintf("%06d", r.Intn(1000000))
|
||||||
|
return otp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OTPService) GenerateAndSaveOTP(phoneNumber string) error {
|
||||||
|
otp := GenerateOTP()
|
||||||
|
err := s.repository.SaveOTP(phoneNumber, otp)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.ErrorLogger.Error("Error saving OTP to repository: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
time=2024-03-27T23:24:15.715+05:00 level=INFO msg="Shutting down the server gracefully..."
|
||||||
|
time=2024-03-27T23:31:35.765+05:00 level=INFO msg="Shutting down the server gracefully..."
|
||||||
|
time=2024-03-27T23:39:10.332+05:00 level=INFO msg="Server is up and running"
|
||||||
|
time=2024-03-27T23:39:22.414+05:00 level=INFO msg="Shutting down the server gracefully..."
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"smpp-otp/internal/config"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
client *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDB(cfg *config.Config) (*Database, error) {
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.Database.Address,
|
||||||
|
Password: cfg.Database.Password,
|
||||||
|
DB: cfg.Database.DB,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := client.Ping().Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize database: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Close() error {
|
||||||
|
if d.client != nil {
|
||||||
|
return d.client.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetClient() *redis.Client {
|
||||||
|
return d.client
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
package errs
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Loggers struct {
|
||||||
|
InfoLogger *slog.Logger
|
||||||
|
ErrorLogger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupLogger(env string) (*Loggers, error) {
|
||||||
|
var infoHandler slog.Handler
|
||||||
|
var errorHandler slog.Handler
|
||||||
|
|
||||||
|
if env == "test" {
|
||||||
|
infoHandler = slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelInfo})
|
||||||
|
errorHandler = slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError})
|
||||||
|
} else {
|
||||||
|
infoFile, err := os.OpenFile("logs/Info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
errorFile, err := os.OpenFile("logs/Error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
infoFile.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
infoHandler = slog.NewTextHandler(infoFile, &slog.HandlerOptions{Level: slog.LevelInfo})
|
||||||
|
errorHandler = slog.NewTextHandler(errorFile, &slog.HandlerOptions{Level: slog.LevelError})
|
||||||
|
}
|
||||||
|
|
||||||
|
infoLogger := slog.New(infoHandler)
|
||||||
|
errorLogger := slog.New(errorHandler)
|
||||||
|
|
||||||
|
return &Loggers{
|
||||||
|
InfoLogger: infoLogger,
|
||||||
|
ErrorLogger: errorLogger,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Err(err error) slog.Attr {
|
||||||
|
return slog.Attr{
|
||||||
|
Key: "error",
|
||||||
|
Value: slog.StringValue(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithErrorJSON(w http.ResponseWriter, status int, message string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
jsonError := struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(jsonError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Err(err error) slog.Attr {
|
||||||
|
return slog.Attr{
|
||||||
|
Key: "error",
|
||||||
|
Value: slog.StringValue(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithErrorJSON(w http.ResponseWriter, status int, message string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
jsonError := struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(jsonError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue