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