first commit

This commit is contained in:
kemvl 2024-03-27 23:44:50 +05:00
commit 6054f8bf0f
19 changed files with 461 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.yaml

55
cmd/main.go Normal file
View File

@ -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)
}
}

View File

@ -0,0 +1,15 @@
env: "local" # local, dev, prod
database:
address: ""
password: ""
db: 0
http_server:
address: ""
smpp:
address: ""
user: ""
password: ""
src_phone_num: ""

14
go.mod Normal file
View File

@ -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
)

17
go.sum Normal file
View File

@ -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=

49
internal/config/config.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package repository
type OTPRepository interface {
SaveOTP(phoneNumber string, otp string) error
}

View File

@ -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
}

View File

@ -0,0 +1,9 @@
package service
import (
"smpp-otp/internal/config"
)
type OTPService interface {
GenerateAndSendOTP(cfg config.Config, phoneNumber string) error
}

View File

@ -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
logs/Error.log Normal file
View File

4
logs/Info.log Normal file
View File

@ -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..."

39
pkg/database/database.go Normal file
View File

@ -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
}

1
pkg/lib/errs/errs.go Normal file
View File

@ -0,0 +1 @@
package errs

44
pkg/lib/logger/logger.go Normal file
View File

@ -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
}

35
pkg/lib/status/status.go Normal file
View File

@ -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)
}

35
pkg/lib/utils/utils.go Normal file
View File

@ -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)
}