From 6054f8bf0ff88010accd12e284acfff26f2ccc23 Mon Sep 17 00:00:00 2001 From: kemvl Date: Wed, 27 Mar 2024 23:44:50 +0500 Subject: [PATCH] first commit --- .gitignore | 1 + cmd/main.go | 55 +++++++++++++++++++ config/config.yaml.example | 15 +++++ go.mod | 14 +++++ go.sum | 17 ++++++ internal/config/config.go | 49 +++++++++++++++++ .../delivery/handlers/smpp-otp-handlers.go | 41 ++++++++++++++ internal/delivery/routers/smpp-otp-routers.go | 19 +++++++ .../interfaces/otp-smpp-repository.go | 5 ++ internal/repository/smpp-otp-repository.go | 44 +++++++++++++++ .../service/interfaces/smpp-otp-service.go | 9 +++ internal/service/smpp-otp-service.go | 34 ++++++++++++ logs/Error.log | 0 logs/Info.log | 4 ++ pkg/database/database.go | 39 +++++++++++++ pkg/lib/errs/errs.go | 1 + pkg/lib/logger/logger.go | 44 +++++++++++++++ pkg/lib/status/status.go | 35 ++++++++++++ pkg/lib/utils/utils.go | 35 ++++++++++++ 19 files changed, 461 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/main.go create mode 100644 config/config.yaml.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/delivery/handlers/smpp-otp-handlers.go create mode 100644 internal/delivery/routers/smpp-otp-routers.go create mode 100644 internal/repository/interfaces/otp-smpp-repository.go create mode 100644 internal/repository/smpp-otp-repository.go create mode 100644 internal/service/interfaces/smpp-otp-service.go create mode 100644 internal/service/smpp-otp-service.go create mode 100644 logs/Error.log create mode 100644 logs/Info.log create mode 100644 pkg/database/database.go create mode 100644 pkg/lib/errs/errs.go create mode 100644 pkg/lib/logger/logger.go create mode 100644 pkg/lib/status/status.go create mode 100644 pkg/lib/utils/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a539470 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.yaml \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a6f8826 --- /dev/null +++ b/cmd/main.go @@ -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) + } +} diff --git a/config/config.yaml.example b/config/config.yaml.example new file mode 100644 index 0000000..1ace2c7 --- /dev/null +++ b/config/config.yaml.example @@ -0,0 +1,15 @@ +env: "local" # local, dev, prod + +database: + address: "" + password: "" + db: 0 + +http_server: + address: "" + +smpp: + address: "" + user: "" + password: "" + src_phone_num: "" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3113f3f --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7774e5b --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d378220 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/delivery/handlers/smpp-otp-handlers.go b/internal/delivery/handlers/smpp-otp-handlers.go new file mode 100644 index 0000000..52199c5 --- /dev/null +++ b/internal/delivery/handlers/smpp-otp-handlers.go @@ -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) +} diff --git a/internal/delivery/routers/smpp-otp-routers.go b/internal/delivery/routers/smpp-otp-routers.go new file mode 100644 index 0000000..53bca4f --- /dev/null +++ b/internal/delivery/routers/smpp-otp-routers.go @@ -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 +} diff --git a/internal/repository/interfaces/otp-smpp-repository.go b/internal/repository/interfaces/otp-smpp-repository.go new file mode 100644 index 0000000..1bea2f7 --- /dev/null +++ b/internal/repository/interfaces/otp-smpp-repository.go @@ -0,0 +1,5 @@ +package repository + +type OTPRepository interface { + SaveOTP(phoneNumber string, otp string) error +} diff --git a/internal/repository/smpp-otp-repository.go b/internal/repository/smpp-otp-repository.go new file mode 100644 index 0000000..0dab0a7 --- /dev/null +++ b/internal/repository/smpp-otp-repository.go @@ -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 +} diff --git a/internal/service/interfaces/smpp-otp-service.go b/internal/service/interfaces/smpp-otp-service.go new file mode 100644 index 0000000..bbc3b41 --- /dev/null +++ b/internal/service/interfaces/smpp-otp-service.go @@ -0,0 +1,9 @@ +package service + +import ( + "smpp-otp/internal/config" +) + +type OTPService interface { + GenerateAndSendOTP(cfg config.Config, phoneNumber string) error +} diff --git a/internal/service/smpp-otp-service.go b/internal/service/smpp-otp-service.go new file mode 100644 index 0000000..f0684d7 --- /dev/null +++ b/internal/service/smpp-otp-service.go @@ -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 +} diff --git a/logs/Error.log b/logs/Error.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/Info.log b/logs/Info.log new file mode 100644 index 0000000..c4e1bc9 --- /dev/null +++ b/logs/Info.log @@ -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..." diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..260777d --- /dev/null +++ b/pkg/database/database.go @@ -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 +} diff --git a/pkg/lib/errs/errs.go b/pkg/lib/errs/errs.go new file mode 100644 index 0000000..6a51dc1 --- /dev/null +++ b/pkg/lib/errs/errs.go @@ -0,0 +1 @@ +package errs \ No newline at end of file diff --git a/pkg/lib/logger/logger.go b/pkg/lib/logger/logger.go new file mode 100644 index 0000000..fd7da62 --- /dev/null +++ b/pkg/lib/logger/logger.go @@ -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 +} diff --git a/pkg/lib/status/status.go b/pkg/lib/status/status.go new file mode 100644 index 0000000..42d6a5f --- /dev/null +++ b/pkg/lib/status/status.go @@ -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) +} diff --git a/pkg/lib/utils/utils.go b/pkg/lib/utils/utils.go new file mode 100644 index 0000000..42d6a5f --- /dev/null +++ b/pkg/lib/utils/utils.go @@ -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) +}