├── cmd ├── migrate │ ├── migrations │ │ ├── 20240922135358_add-user-table.down.sql │ │ ├── 20240922141434_add-orders-table.down.sql │ │ ├── 20240922141343_add-product-table.down.sql │ │ ├── 20240922141508_add-order-items-table.down.sql │ │ ├── 20240922135358_add-user-table.up.sql │ │ ├── 20240922141343_add-product-table.up.sql │ │ ├── 20240922141508_add-order-items-table.up.sql │ │ └── 20240922141434_add-orders-table.up.sql │ └── main.go ├── api │ └── api.go └── main.go ├── .env ├── .env.example ├── db └── db.go ├── Makefile ├── service ├── auth │ ├── jwt_test.go │ ├── password.go │ ├── password_test.go │ └── jwt.go ├── order │ └── store.go ├── user │ ├── store.go │ ├── routes_test.go │ └── routes.go ├── cart │ ├── routes.go │ ├── service.go │ └── routes_test.go └── product │ ├── routes.go │ ├── store.go │ └── routes_test.go ├── .gitignore ├── DockerFile ├── docker-compose.yml ├── README.md ├── go.mod ├── utils └── utils.go ├── config └── env.go ├── types └── types.go └── go.sum /cmd/migrate/migrations/20240922135358_add-user-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141434_add-orders-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS orders; -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141343_add-product-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS products; -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141508_add-order-items-table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS order_items; -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Server 2 | PUBLIC_HOST=http://localhost 3 | PORT=8080 4 | 5 | # Database 6 | DB_USER=root 7 | DB_PASSWORD=My7Pass@Word_9_8A_zE 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_NAME=ecom -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server 2 | PUBLIC_HOST=http://localhost 3 | PORT=8080 4 | 5 | # Database config 6 | DB_USER=root 7 | DB_PASSWORD=mypassword 8 | DB_HOST=127.0.0.1 9 | DB_PORT=3306 10 | DB_NAME=ecom 11 | JWT_SECRET= 12 | JWT_EXPIRATION_IN_SECONDS= 13 | 14 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/go-sql-driver/mysql" 8 | ) 9 | 10 | // db part 11 | func NewMySQLStorage(cfg mysql.Config) (*sql.DB, error) { 12 | db, err := sql.Open("mysql", cfg.FormatDSN()) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | return db, nil 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @go build -o bin/ecom cmd/main.go 3 | 4 | test: 5 | @go test -v ./...; 6 | 7 | run: 8 | @go run ./... ; 9 | 10 | 11 | migration: 12 | @migrate create -ext sql -dir cmd/migrate/migrations $(filter-out $@,$(MAKECMDGOALS)) 13 | 14 | migrate-up: 15 | @go run cmd/migrate/main.go up 16 | 17 | migrate-down: 18 | @go run cmd/migrate/main.go down -------------------------------------------------------------------------------- /service/auth/jwt_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCreateJWT(t *testing.T) { 8 | secret := []byte("secret") 9 | 10 | token, err := CreateJWT(secret, 1) 11 | if err != nil { 12 | t.Errorf("error creating JWT: %v", err) 13 | } 14 | 15 | if token == "" { 16 | t.Error("expected token to be not empty") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | .env 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | .idea/ 18 | vendor/ 19 | .vscode/ 20 | tmp/ -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922135358_add-user-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `firstName` VARCHAR(255) NOT NULL, 4 | `lastName` VARCHAR(255) NOT NULL, 5 | `email` VARCHAR(255) NOT NULL, 6 | `password` VARCHAR(255) NOT NULL, 7 | `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | PRIMARY KEY(id), 10 | UNIQUE KEY (email) 11 | 12 | ); -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141343_add-product-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS products( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(255) NOT NULL, 4 | `description` TEXT NOT NULL, 5 | `image` VARCHAR(255) NOT NULL, 6 | `price` DECIMAL(10, 2) NOT NULL, 7 | `quantity` INT UNSIGNED NOT NULL, 8 | `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | PRIMARY KEY(id) 11 | 12 | ); -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141508_add-order-items-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `order_items` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `orderId` INT UNSIGNED NOT NULL, 4 | `productId` INT UNSIGNED NOT NULL, 5 | `quantity` INT NOT NULL, 6 | `price` DECIMAL(10, 2) NOT NULL, 7 | PRIMARY KEY (`id`), 8 | FOREIGN KEY (`orderId`) REFERENCES `orders` (`id`), 9 | FOREIGN KEY (`productId`) REFERENCES `products` (`id`) 10 | ); -------------------------------------------------------------------------------- /service/auth/password.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | func HashPassword(password string) (string, error) { 6 | hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 7 | 8 | if err != nil { 9 | return "", err 10 | } 11 | 12 | return string(hash), nil 13 | 14 | } 15 | 16 | func ComparePasswords(hashed string, plain []byte) bool { 17 | err := bcrypt.CompareHashAndPassword([]byte(hashed), plain) 18 | return err == nil 19 | } 20 | -------------------------------------------------------------------------------- /cmd/migrate/migrations/20240922141434_add-orders-table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS orders( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `userId` INT UNSIGNED NOT NULL, 4 | `total` DECIMAL(10, 2) NOT NULL, 5 | `status` ENUM('pending', 'completed', 'cancelled') NOT NULL DEFAULT 'pending', 6 | `address` TEXT NOT NULL, 7 | `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | PRIMARY KEY(`id`), 10 | FOREIGN KEY(`userId`) REFERENCES users(`id`) 11 | 12 | 13 | ); -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build the application from source 4 | FROM golang:1.22.0 AS build-stage 5 | WORKDIR /app 6 | 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | COPY . . 11 | 12 | RUN CGO_ENABLED=0 GOOS=linux go build -o /api ./cmd/main.go 13 | 14 | # Run the tests in the container 15 | FROM build-stage AS run-test-stage 16 | RUN go test -v ./... 17 | 18 | # Deploy the application binary into a lean image 19 | FROM scratch AS build-release-stage 20 | WORKDIR / 21 | 22 | COPY --from=build-stage /api /api 23 | 24 | EXPOSE 8080 25 | 26 | ENTRYPOINT ["/api"] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | db: 5 | image: mysql:8.0 6 | healthcheck: 7 | test: "exit 0" 8 | volumes: 9 | - db_data:/var/lib/mysql 10 | ports: 11 | - "3306:3306" 12 | environment: 13 | MYSQL_ROOT_PASSWORD: mypassword 14 | MYSQL_DATABASE: ecom 15 | 16 | api: 17 | build: 18 | context: . 19 | dockerfile: Dockerfile 20 | restart: on-failure 21 | volumes: 22 | - .:/go/src/api 23 | ports: 24 | - "8080:8080" 25 | environment: 26 | DB_HOST: db 27 | DB_USER: root 28 | DB_PASSWORD: mypassword 29 | DB_NAME: ecom 30 | links: 31 | - db 32 | depends_on: 33 | - db 34 | 35 | volumes: 36 | db_data: -------------------------------------------------------------------------------- /cmd/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "database/sql" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/iamrubayet/ecom/service/user" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | type APISERVER struct { 13 | addr string 14 | db *sql.DB 15 | } 16 | 17 | func NewAPIServer(addr string, db *sql.DB) *APISERVER { 18 | return &APISERVER{ 19 | addr: addr, 20 | db: db, 21 | } 22 | 23 | } 24 | 25 | func (s *APISERVER) Run() error { 26 | router := mux.NewRouter() 27 | subrouter := router.PathPrefix("/api/v1").Subrouter() 28 | userStore := user.NewStore(s.db) 29 | userHandler := user.NewHandler(userStore) 30 | userHandler.RegisterRoutes(subrouter) 31 | log.Print("Server is running on port ", s.addr) 32 | 33 | return http.ListenAndServe(s.addr, router) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /service/auth/password_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHashPassword(t *testing.T) { 8 | hash, err := HashPassword("password") 9 | if err != nil { 10 | t.Errorf("error hashing password: %v", err) 11 | } 12 | 13 | if hash == "" { 14 | t.Error("expected hash to be not empty") 15 | } 16 | 17 | if hash == "password" { 18 | t.Error("expected hash to be different from password") 19 | } 20 | } 21 | 22 | func TestComparePasswords(t *testing.T) { 23 | hash, err := HashPassword("password") 24 | if err != nil { 25 | t.Errorf("error hashing password: %v", err) 26 | } 27 | 28 | if !ComparePasswords(hash, []byte("password")) { 29 | t.Errorf("expected password to match hash") 30 | } 31 | if ComparePasswords(hash, []byte("notpassword")) { 32 | t.Errorf("expected password to not match hash") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## E-commerce REST API in Go 2 | 3 | ### Installation 4 | 5 | There are a few tools that you need to install to run the project. 6 | So make sure you have the following tools installed on your machine. 7 | 8 | - [Migrate (for DB migrations)](https://github.com/golang-migrate/migrate/tree/v4.17.0/cmd/migrate) 9 | 10 | ## Running the project 11 | 12 | Firstly make sure you have a MySQL database running on your machine or just swap for any storage you like under `/db`. 13 | 14 | Then create a database with the name you want *(`ecom` is the default)* and run the migrations. 15 | 16 | ```bash 17 | make migrate-up 18 | ``` 19 | 20 | After that, you can run the project with the following command: 21 | 22 | ```bash 23 | make run 24 | ``` 25 | 26 | ## Running the tests 27 | 28 | To run the tests, you can use the following command: 29 | 30 | ```bash 31 | make test 32 | ``` -------------------------------------------------------------------------------- /service/order/store.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/iamrubayet/ecom/types" 7 | ) 8 | 9 | type Store struct { 10 | db *sql.DB 11 | } 12 | 13 | func NewStore(db *sql.DB) *Store { 14 | return &Store{db: db} 15 | } 16 | 17 | func (s *Store) CreateOrder(order types.Order) (int, error) { 18 | res, err := s.db.Exec("INSERT INTO orders (userId, total, status, address) VALUES (?, ?, ?, ?)", order.UserID, order.Total, order.Status, order.Address) 19 | if err != nil { 20 | return 0, err 21 | } 22 | 23 | id, err := res.LastInsertId() 24 | if err != nil { 25 | return 0, err 26 | } 27 | 28 | return int(id), nil 29 | } 30 | 31 | func (s *Store) CreateOrderItem(orderItem types.OrderItem) error { 32 | _, err := s.db.Exec("INSERT INTO order_items (orderId, productId, quantity, price) VALUES (?, ?, ?, ?)", orderItem.OrderID, orderItem.ProductID, orderItem.Quantity, orderItem.Price) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | 7 | "github.com/go-sql-driver/mysql" 8 | "github.com/iamrubayet/ecom/cmd/api" 9 | "github.com/iamrubayet/ecom/config" 10 | "github.com/iamrubayet/ecom/db" 11 | ) 12 | 13 | func main() { 14 | db, err := db.NewMySQLStorage(mysql.Config{ 15 | User: config.Envs.DBUser, 16 | Passwd: config.Envs.DBPassword, 17 | Addr: config.Envs.DBAddress, 18 | DBName: config.Envs.DBName, 19 | Net: "tcp", 20 | AllowNativePasswords: true, 21 | ParseTime: true, 22 | }) 23 | 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | initStorage(db) 29 | 30 | server := api.NewAPIServer(":8080", db) 31 | 32 | if err := server.Run(); err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | } 37 | 38 | // db connect 39 | func initStorage(db *sql.DB) { 40 | err := db.Ping() 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | log.Println("Database Connected Successfully") 46 | 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iamrubayet/ecom 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.7 6 | 7 | require ( 8 | github.com/go-playground/validator/v10 v10.22.1 9 | github.com/go-sql-driver/mysql v1.8.1 10 | github.com/goccy/go-json v0.10.3 11 | github.com/golang-migrate/migrate/v4 v4.18.1 12 | github.com/gorilla/mux v1.8.1 13 | github.com/joho/godotenv v1.5.1 14 | github.com/rs/zerolog v1.33.0 15 | golang.org/x/crypto v0.27.0 16 | ) 17 | 18 | require ( 19 | filippo.io/edwards25519 v1.1.0 // indirect 20 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 24 | github.com/hashicorp/errwrap v1.1.0 // indirect 25 | github.com/hashicorp/go-multierror v1.1.1 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.19 // indirect 29 | go.uber.org/atomic v1.7.0 // indirect 30 | golang.org/x/net v0.29.0 // indirect 31 | golang.org/x/sys v0.25.0 // indirect 32 | golang.org/x/text v0.18.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/go-playground/validator/v10" 9 | ) 10 | 11 | var Validate = validator.New() 12 | 13 | func ParseJSON(r *http.Request, payload any) error { 14 | 15 | if r.Body == nil { 16 | return errors.New("request body is empty") 17 | } 18 | 19 | return json.NewDecoder(r.Body).Decode(payload) // Decode the request body(json) into the payload(struct) 20 | 21 | } 22 | 23 | func WriteJSON(w http.ResponseWriter, status int, v any) error { 24 | w.Header().Set("Content-Type", "application/json") 25 | w.WriteHeader(status) 26 | return json.NewEncoder(w).Encode(v) // Encode the response into json format 27 | 28 | } 29 | 30 | func WriteError(w http.ResponseWriter, status int, err error) { 31 | WriteJSON(w, status, map[string]string{"error": err.Error()}) 32 | 33 | } 34 | 35 | func GetTokenFromRequest(r *http.Request) string { 36 | tokenAuth := r.Header.Get("Authorization") 37 | tokenQuery := r.URL.Query().Get("token") 38 | 39 | if tokenAuth != "" { 40 | return tokenAuth 41 | } 42 | 43 | if tokenQuery != "" { 44 | return tokenQuery 45 | } 46 | 47 | return "" 48 | } 49 | -------------------------------------------------------------------------------- /cmd/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | mysqlCfg "github.com/go-sql-driver/mysql" 8 | "github.com/golang-migrate/migrate/v4" 9 | "github.com/golang-migrate/migrate/v4/database/mysql" 10 | _ "github.com/golang-migrate/migrate/v4/source/file" 11 | "github.com/iamrubayet/ecom/config" 12 | "github.com/iamrubayet/ecom/db" 13 | ) 14 | 15 | func main() { 16 | db, err := db.NewMySQLStorage(mysqlCfg.Config{ 17 | User: config.Envs.DBUser, 18 | Passwd: config.Envs.DBPassword, 19 | Addr: config.Envs.DBAddress, 20 | DBName: config.Envs.DBName, 21 | Net: "tcp", 22 | AllowNativePasswords: true, 23 | ParseTime: true, 24 | }) 25 | 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | driver, err := mysql.WithInstance(db, &mysql.Config{}) 31 | 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | m, err := migrate.NewWithDatabaseInstance( 37 | "file://cmd/migrate/migrations", 38 | "mysql", 39 | driver, 40 | ) 41 | 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | cmd := os.Args[(len(os.Args) - 1)] 47 | 48 | if cmd == "up" { 49 | if err := m.Up(); err != nil && err != migrate.ErrNoChange { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | if cmd == "down" { 55 | if err := m.Down(); err != nil && err != migrate.ErrNoChange { 56 | log.Fatal(err) 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/joho/godotenv" 9 | ) 10 | 11 | type Config struct { 12 | PublicHost string 13 | Port string 14 | DBUser string 15 | DBPassword string 16 | DBAddress string 17 | DBName string 18 | JWTSecret string 19 | JWTExpirationInSeconds int64 20 | } 21 | 22 | var Envs = initConfig() 23 | 24 | func initConfig() Config { 25 | godotenv.Load() 26 | 27 | return Config{ 28 | PublicHost: getEnv("PUBLIC_HOST", "HTTP://localhost"), 29 | Port: getEnv("PORT", "8080"), 30 | DBUser: getEnv("DB_USER", "root"), 31 | DBPassword: getEnv("DB_PASSWORD", "My7Pass@Word_9_8A_zE"), 32 | DBAddress: fmt.Sprintf("%s:%s", getEnv("DB_HOST", "127.0.0.1"), getEnv("DB_PORT", "3306")), 33 | DBName: getEnv("DB_NAME", "ecom"), 34 | JWTSecret: getEnv("JWT_SECRET", "not-so-secret-now-is-it?"), 35 | JWTExpirationInSeconds: getEnvAsInt("JWT_EXPIRATION_IN_SECONDS", 3600*24*7), 36 | } 37 | 38 | } 39 | 40 | func getEnv(key, fallback string) string { 41 | if value, ok := os.LookupEnv(key); ok { 42 | return value 43 | } 44 | return fallback 45 | 46 | } 47 | 48 | func getEnvAsInt(key string, fallback int64) int64 { 49 | if value, ok := os.LookupEnv(key); ok { 50 | i, err := strconv.ParseInt(value, 10, 64) 51 | if err != nil { 52 | return fallback 53 | } 54 | 55 | return i 56 | } 57 | 58 | return fallback 59 | } 60 | -------------------------------------------------------------------------------- /service/user/store.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/iamrubayet/ecom/types" 8 | ) 9 | 10 | type Store struct { 11 | db *sql.DB 12 | } 13 | 14 | func NewStore(db *sql.DB) *Store { 15 | return &Store{ 16 | db: db, 17 | } 18 | } 19 | 20 | // Get user by email 21 | func (s *Store) GetUserByEmail(email string) (*types.User, error) { 22 | rows, err := s.db.Query("SELECT * FROM users WHERE email = ?", email) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | u := new(types.User) 28 | for rows.Next() { 29 | u, err = scanRowIntoUser(rows) 30 | if err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | if u.ID == 0 { 36 | return nil, fmt.Errorf("user not found") 37 | 38 | } 39 | return u, nil 40 | 41 | } 42 | 43 | func scanRowIntoUser(rows *sql.Rows) (*types.User, error) { 44 | user := new(types.User) 45 | err := rows.Scan( 46 | &user.ID, 47 | &user.FirstName, 48 | &user.LastName, 49 | &user.Email, 50 | &user.Password, 51 | &user.CreatedAt, 52 | ) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return user, nil 57 | 58 | } 59 | 60 | // Get user by ID 61 | func (s *Store) GetUserByID(id int) (*types.User, error) { 62 | rows, err := s.db.Query("SELECT * FROM users WHERE id = ?", id) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | u := new(types.User) 68 | for rows.Next() { 69 | u, err = scanRowIntoUser(rows) 70 | if err != nil { 71 | return nil, err 72 | } 73 | } 74 | 75 | if u.ID == 0 { 76 | return nil, fmt.Errorf("user not found") 77 | 78 | } 79 | return u, nil 80 | } 81 | 82 | // create user 83 | func (s *Store) CreateUser(user types.User) error { 84 | _, err := s.db.Exec("INSERT INTO users (firstName,lastName,email,password) VALUES (?,?,?,?)", user.FirstName, user.LastName, user.Email, user.Password) 85 | 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /service/cart/routes.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-playground/validator/v10" 8 | "github.com/gorilla/mux" 9 | "github.com/iamrubayet/ecom/service/auth" 10 | "github.com/iamrubayet/ecom/types" 11 | "github.com/iamrubayet/ecom/utils" 12 | ) 13 | 14 | type Handler struct { 15 | store types.ProductStore 16 | orderStore types.OrderStore 17 | userStore types.UserStore 18 | } 19 | 20 | func NewHandler( 21 | store types.ProductStore, 22 | orderStore types.OrderStore, 23 | userStore types.UserStore, 24 | ) *Handler { 25 | return &Handler{ 26 | store: store, 27 | orderStore: orderStore, 28 | userStore: userStore, 29 | } 30 | } 31 | 32 | func (h *Handler) RegisterRoutes(router *mux.Router) { 33 | router.HandleFunc("/cart/checkout", auth.WithJWTAuth(h.handleCheckout, h.userStore)).Methods(http.MethodPost) 34 | } 35 | 36 | func (h *Handler) handleCheckout(w http.ResponseWriter, r *http.Request) { 37 | userID := auth.GetUserIDFromContext(r.Context()) 38 | 39 | var cart types.CartCheckoutPayload 40 | if err := utils.ParseJSON(r, &cart); err != nil { 41 | utils.WriteError(w, http.StatusBadRequest, err) 42 | return 43 | } 44 | 45 | if err := utils.Validate.Struct(cart); err != nil { 46 | errors := err.(validator.ValidationErrors) 47 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors)) 48 | return 49 | } 50 | 51 | productIds, err := getCartItemsIDs(cart.Items) 52 | if err != nil { 53 | utils.WriteError(w, http.StatusBadRequest, err) 54 | return 55 | } 56 | 57 | // get products 58 | products, err := h.store.GetProductsByID(productIds) 59 | if err != nil { 60 | utils.WriteError(w, http.StatusInternalServerError, err) 61 | return 62 | } 63 | 64 | orderID, totalPrice, err := h.createOrder(products, cart.Items, userID) 65 | if err != nil { 66 | utils.WriteError(w, http.StatusBadRequest, err) 67 | return 68 | } 69 | 70 | utils.WriteJSON(w, http.StatusOK, map[string]interface{}{ 71 | "total_price": totalPrice, 72 | "order_id": orderID, 73 | }) 74 | } -------------------------------------------------------------------------------- /service/user/routes_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/goccy/go-json" 11 | "github.com/gorilla/mux" 12 | "github.com/iamrubayet/ecom/types" 13 | ) 14 | 15 | func TestUserServiceHanlders(t *testing.T) { 16 | userStore := &mockUserStore{} 17 | handler := NewHandler(userStore) 18 | 19 | t.Run("should fail if the user payload is invalid", func(t *testing.T) { 20 | 21 | payload := types.RegisterUserPayload{ 22 | FirstName: "John", 23 | LastName: "Doe", 24 | Email: "invalid", 25 | Password: "password", 26 | } 27 | 28 | marshalled, _ := json.Marshal(payload) 29 | 30 | req, err := http.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(marshalled)) 31 | 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | rr := httptest.NewRecorder() 37 | router := mux.NewRouter() 38 | 39 | router.HandleFunc("/register", handler.handleRegister).Methods(http.MethodPost) 40 | router.ServeHTTP(rr, req) 41 | 42 | if rr.Code != http.StatusBadRequest { 43 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 44 | } 45 | 46 | }) 47 | 48 | t.Run("should fail if the user payload is invalid", func(t *testing.T) { 49 | 50 | payload := types.RegisterUserPayload{ 51 | FirstName: "John", 52 | LastName: "Doe", 53 | Email: "valid@gmail.com", 54 | Password: "password", 55 | } 56 | 57 | marshalled, _ := json.Marshal(payload) 58 | 59 | req, err := http.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(marshalled)) 60 | 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | rr := httptest.NewRecorder() 66 | router := mux.NewRouter() 67 | 68 | router.HandleFunc("/register", handler.handleRegister).Methods(http.MethodPost) 69 | router.ServeHTTP(rr, req) 70 | 71 | if rr.Code != http.StatusCreated { 72 | t.Errorf("expected status code %d, got %d", http.StatusCreated, rr.Code) 73 | } 74 | 75 | }) 76 | 77 | } 78 | 79 | type mockUserStore struct { 80 | } 81 | 82 | func (m *mockUserStore) GetUserByEmail(email string) (*types.User, error) { 83 | return &types.User{}, fmt.Errorf("user not found") 84 | } 85 | 86 | func (m *mockUserStore) CreateUser(u types.User) error { 87 | return nil 88 | } 89 | 90 | func (m *mockUserStore) GetUserByID(id int) (*types.User, error) { 91 | return &types.User{}, nil 92 | } 93 | -------------------------------------------------------------------------------- /service/product/routes.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/go-playground/validator/v10" 9 | "github.com/gorilla/mux" 10 | "github.com/iamrubayet/ecom/service/auth" 11 | "github.com/iamrubayet/ecom/types" 12 | "github.com/iamrubayet/ecom/utils" 13 | ) 14 | 15 | type Handler struct { 16 | store types.ProductStore 17 | userStore types.UserStore 18 | } 19 | 20 | func NewHandler(store types.ProductStore, userStore types.UserStore) *Handler { 21 | return &Handler{store: store, userStore: userStore} 22 | } 23 | 24 | func (h *Handler) RegisterRoutes(router *mux.Router) { 25 | router.HandleFunc("/products", h.handleGetProducts).Methods(http.MethodGet) 26 | router.HandleFunc("/products/{productID}", h.handleGetProduct).Methods(http.MethodGet) 27 | 28 | // admin routes 29 | router.HandleFunc("/products", auth.WithJWTAuth(h.handleCreateProduct, h.userStore)).Methods(http.MethodPost) 30 | } 31 | 32 | func (h *Handler) handleGetProducts(w http.ResponseWriter, r *http.Request) { 33 | products, err := h.store.GetProducts() 34 | if err != nil { 35 | utils.WriteError(w, http.StatusInternalServerError, err) 36 | return 37 | } 38 | 39 | utils.WriteJSON(w, http.StatusOK, products) 40 | } 41 | 42 | func (h *Handler) handleGetProduct(w http.ResponseWriter, r *http.Request) { 43 | vars := mux.Vars(r) 44 | str, ok := vars["productID"] 45 | if !ok { 46 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("missing product ID")) 47 | return 48 | } 49 | 50 | productID, err := strconv.Atoi(str) 51 | if err != nil { 52 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid product ID")) 53 | return 54 | } 55 | 56 | product, err := h.store.GetProductByID(productID) 57 | if err != nil { 58 | utils.WriteError(w, http.StatusInternalServerError, err) 59 | return 60 | } 61 | 62 | utils.WriteJSON(w, http.StatusOK, product) 63 | } 64 | 65 | func (h *Handler) handleCreateProduct(w http.ResponseWriter, r *http.Request) { 66 | var product types.CreateProductPayload 67 | if err := utils.ParseJSON(r, &product); err != nil { 68 | utils.WriteError(w, http.StatusBadRequest, err) 69 | return 70 | } 71 | 72 | if err := utils.Validate.Struct(product); err != nil { 73 | errors := err.(validator.ValidationErrors) 74 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid payload: %v", errors)) 75 | return 76 | } 77 | 78 | err := h.store.CreateProduct(product) 79 | if err != nil { 80 | utils.WriteError(w, http.StatusInternalServerError, err) 81 | return 82 | } 83 | 84 | utils.WriteJSON(w, http.StatusCreated, product) 85 | } 86 | -------------------------------------------------------------------------------- /service/auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/golang-jwt/jwt/v5" 12 | "github.com/iamrubayet/ecom/config" 13 | "github.com/iamrubayet/ecom/types" 14 | "github.com/iamrubayet/ecom/utils" 15 | ) 16 | 17 | type contextKey string 18 | 19 | const UserKey contextKey = "userID" 20 | 21 | func WithJWTAuth(handlerFunc http.HandlerFunc, store types.UserStore) http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | tokenString := utils.GetTokenFromRequest(r) 24 | 25 | token, err := validateJWT(tokenString) 26 | if err != nil { 27 | log.Printf("failed to validate token: %v", err) 28 | permissionDenied(w) 29 | return 30 | } 31 | 32 | if !token.Valid { 33 | log.Println("invalid token") 34 | permissionDenied(w) 35 | return 36 | } 37 | 38 | claims := token.Claims.(jwt.MapClaims) 39 | str := claims["userID"].(string) 40 | 41 | userID, err := strconv.Atoi(str) 42 | if err != nil { 43 | log.Printf("failed to convert userID to int: %v", err) 44 | permissionDenied(w) 45 | return 46 | } 47 | 48 | u, err := store.GetUserByID(userID) 49 | if err != nil { 50 | log.Printf("failed to get user by id: %v", err) 51 | permissionDenied(w) 52 | return 53 | } 54 | 55 | // Add the user to the context 56 | ctx := r.Context() 57 | ctx = context.WithValue(ctx, UserKey, u.ID) 58 | r = r.WithContext(ctx) 59 | 60 | // Call the function if the token is valid 61 | handlerFunc(w, r) 62 | } 63 | } 64 | 65 | func CreateJWT(secret []byte, userID int) (string, error) { 66 | expiration := time.Second * time.Duration(config.Envs.JWTExpirationInSeconds) 67 | 68 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 69 | "userID": strconv.Itoa(int(userID)), 70 | "expiresAt": time.Now().Add(expiration).Unix(), 71 | }) 72 | 73 | tokenString, err := token.SignedString(secret) 74 | if err != nil { 75 | return "", err 76 | } 77 | 78 | return tokenString, err 79 | } 80 | 81 | func validateJWT(tokenString string) (*jwt.Token, error) { 82 | return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 83 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 84 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 85 | } 86 | 87 | return []byte(config.Envs.JWTSecret), nil 88 | }) 89 | } 90 | 91 | func permissionDenied(w http.ResponseWriter) { 92 | utils.WriteError(w, http.StatusForbidden, fmt.Errorf("permission denied")) 93 | } 94 | 95 | func GetUserIDFromContext(ctx context.Context) int { 96 | userID, ok := ctx.Value(UserKey).(int) 97 | if !ok { 98 | return -1 99 | } 100 | 101 | return userID 102 | } -------------------------------------------------------------------------------- /service/cart/service.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iamrubayet/ecom/types" 7 | ) 8 | 9 | func getCartItemsIDs(items []types.CartCheckoutItem) ([]int, error) { 10 | productIds := make([]int, len(items)) 11 | for i, item := range items { 12 | if item.Quantity <= 0 { 13 | return nil, fmt.Errorf("invalid quantity for product %d", item.ProductID) 14 | } 15 | 16 | productIds[i] = item.ProductID 17 | } 18 | 19 | return productIds, nil 20 | } 21 | 22 | func checkIfCartIsInStock(cartItems []types.CartCheckoutItem, products map[int]types.Product) error { 23 | if len(cartItems) == 0 { 24 | return fmt.Errorf("cart is empty") 25 | } 26 | 27 | for _, item := range cartItems { 28 | product, ok := products[item.ProductID] 29 | if !ok { 30 | return fmt.Errorf("product %d is not available in the store, please refresh your cart", item.ProductID) 31 | } 32 | 33 | if product.Quantity < item.Quantity { 34 | return fmt.Errorf("product %s is not available in the quantity requested", product.Name) 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func calculateTotalPrice(cartItems []types.CartCheckoutItem, products map[int]types.Product) float64 { 42 | var total float64 43 | 44 | for _, item := range cartItems { 45 | product := products[item.ProductID] 46 | total += product.Price * float64(item.Quantity) 47 | } 48 | 49 | return total 50 | } 51 | 52 | func (h *Handler) createOrder(products []types.Product, cartItems []types.CartCheckoutItem, userID int) (int, float64, error) { 53 | // create a map of products for easier access 54 | productsMap := make(map[int]types.Product) 55 | for _, product := range products { 56 | productsMap[product.ID] = product 57 | } 58 | 59 | // check if all products are available 60 | if err := checkIfCartIsInStock(cartItems, productsMap); err != nil { 61 | return 0, 0, err 62 | } 63 | 64 | // calculate total price 65 | totalPrice := calculateTotalPrice(cartItems, productsMap) 66 | 67 | // reduce the quantity of products in the store 68 | for _, item := range cartItems { 69 | product := productsMap[item.ProductID] 70 | product.Quantity -= item.Quantity 71 | h.store.UpdateProduct(product) 72 | } 73 | 74 | // create order record 75 | orderID, err := h.orderStore.CreateOrder(types.Order{ 76 | UserID: userID, 77 | Total: totalPrice, 78 | Status: "pending", 79 | Address: "some address", // could fetch address from a user addresses table 80 | }) 81 | if err != nil { 82 | return 0, 0, err 83 | } 84 | 85 | // create order the items records 86 | for _, item := range cartItems { 87 | h.orderStore.CreateOrderItem(types.OrderItem{ 88 | OrderID: orderID, 89 | ProductID: item.ProductID, 90 | Quantity: item.Quantity, 91 | Price: productsMap[item.ProductID].Price, 92 | }) 93 | } 94 | 95 | return orderID, totalPrice, nil 96 | } -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // for json marshaling 4 | 5 | import "time" 6 | 7 | type UserStore interface { 8 | GetUserByEmail(email string) (*User, error) 9 | GetUserByID(id int) (*User, error) 10 | CreateUser(User) error 11 | } 12 | 13 | type User struct { 14 | ID int `json:"id"` 15 | FirstName string `json:"firstName"` 16 | LastName string `json:"lastName"` 17 | Email string `json:"email"` 18 | Password string `json:"-"` 19 | CreatedAt time.Time `json:"createdAt"` 20 | } 21 | 22 | type RegisterUserPayload struct { 23 | FirstName string `json:"firstName" validate:"required"` 24 | LastName string `json:"lastName" validate:"required"` 25 | Email string `json:"email" validate:"required,email"` 26 | Password string `json:"password" validate:"required,min=3,max=10"` 27 | } 28 | 29 | type LoginUserPayload struct { 30 | Email string `json:"email" validate:"required,email"` 31 | Password string `json:"password" validate:"required"` 32 | } 33 | 34 | type Product struct { 35 | ID int `json:"id"` 36 | Name string `json:"name"` 37 | Description string `json:"description"` 38 | Image string `json:"image"` 39 | Price float64 `json:"price"` 40 | // note that this isn't the best way to handle quantity 41 | // because it's not atomic (in ACID), but it's good enough for this example 42 | Quantity int `json:"quantity"` 43 | CreatedAt time.Time `json:"createdAt"` 44 | } 45 | 46 | type CartCheckoutItem struct { 47 | ProductID int `json:"productID"` 48 | Quantity int `json:"quantity"` 49 | } 50 | 51 | type Order struct { 52 | ID int `json:"id"` 53 | UserID int `json:"userID"` 54 | Total float64 `json:"total"` 55 | Status string `json:"status"` 56 | Address string `json:"address"` 57 | CreatedAt time.Time `json:"createdAt"` 58 | } 59 | 60 | type OrderItem struct { 61 | ID int `json:"id"` 62 | OrderID int `json:"orderID"` 63 | ProductID int `json:"productID"` 64 | Quantity int `json:"quantity"` 65 | Price float64 `json:"price"` 66 | CreatedAt time.Time `json:"createdAt"` 67 | } 68 | 69 | type ProductStore interface { 70 | GetProductByID(id int) (*Product, error) 71 | GetProductsByID(ids []int) ([]Product, error) 72 | GetProducts() ([]*Product, error) 73 | CreateProduct(CreateProductPayload) error 74 | UpdateProduct(Product) error 75 | } 76 | 77 | type OrderStore interface { 78 | CreateOrder(Order) (int, error) 79 | CreateOrderItem(OrderItem) error 80 | } 81 | type CreateProductPayload struct { 82 | Name string `json:"name" validate:"required"` 83 | Description string `json:"description"` 84 | Image string `json:"image"` 85 | Price float64 `json:"price" validate:"required"` 86 | Quantity int `json:"quantity" validate:"required"` 87 | } 88 | 89 | type CartCheckoutPayload struct { 90 | Items []CartCheckoutItem `json:"items" validate:"required"` 91 | } 92 | -------------------------------------------------------------------------------- /service/product/store.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/iamrubayet/ecom/types" 9 | ) 10 | 11 | type Store struct { 12 | db *sql.DB 13 | } 14 | 15 | func NewStore(db *sql.DB) *Store { 16 | return &Store{db: db} 17 | } 18 | 19 | func (s *Store) GetProductByID(productID int) (*types.Product, error) { 20 | rows, err := s.db.Query("SELECT * FROM products WHERE id = ?", productID) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | p := new(types.Product) 26 | for rows.Next() { 27 | p, err = scanRowsIntoProduct(rows) 28 | if err != nil { 29 | return nil, err 30 | } 31 | } 32 | 33 | return p, nil 34 | } 35 | 36 | func (s *Store) GetProductsByID(productIDs []int) ([]types.Product, error) { 37 | placeholders := strings.Repeat(",?", len(productIDs)-1) 38 | query := fmt.Sprintf("SELECT * FROM products WHERE id IN (?%s)", placeholders) 39 | 40 | // Convert productIDs to []interface{} 41 | args := make([]interface{}, len(productIDs)) 42 | for i, v := range productIDs { 43 | args[i] = v 44 | } 45 | 46 | rows, err := s.db.Query(query, args...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | products := []types.Product{} 52 | for rows.Next() { 53 | p, err := scanRowsIntoProduct(rows) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | products = append(products, *p) 59 | } 60 | 61 | return products, nil 62 | 63 | } 64 | 65 | func (s *Store) GetProducts() ([]*types.Product, error) { 66 | rows, err := s.db.Query("SELECT * FROM products") 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | products := make([]*types.Product, 0) 72 | for rows.Next() { 73 | p, err := scanRowsIntoProduct(rows) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | products = append(products, p) 79 | } 80 | 81 | return products, nil 82 | } 83 | 84 | func (s *Store) CreateProduct(product types.CreateProductPayload) error { 85 | _, err := s.db.Exec("INSERT INTO products (name, price, image, description, quantity) VALUES (?, ?, ?, ?, ?)", product.Name, product.Price, product.Image, product.Description, product.Quantity) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (s *Store) UpdateProduct(product types.Product) error { 94 | _, err := s.db.Exec("UPDATE products SET name = ?, price = ?, image = ?, description = ?, quantity = ? WHERE id = ?", product.Name, product.Price, product.Image, product.Description, product.Quantity, product.ID) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func scanRowsIntoProduct(rows *sql.Rows) (*types.Product, error) { 103 | product := new(types.Product) 104 | 105 | err := rows.Scan( 106 | &product.ID, 107 | &product.Name, 108 | &product.Description, 109 | &product.Image, 110 | &product.Price, 111 | &product.Quantity, 112 | &product.CreatedAt, 113 | ) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return product, nil 119 | } 120 | -------------------------------------------------------------------------------- /service/user/routes.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-playground/validator/v10" 8 | "github.com/gorilla/mux" 9 | "github.com/iamrubayet/ecom/config" 10 | "github.com/iamrubayet/ecom/service/auth" 11 | "github.com/iamrubayet/ecom/types" 12 | "github.com/iamrubayet/ecom/utils" 13 | ) 14 | 15 | type Handler struct { 16 | store types.UserStore 17 | } 18 | 19 | func NewHandler(store types.UserStore) *Handler { 20 | return &Handler{store: store} 21 | } 22 | 23 | // register routes 24 | func (h *Handler) RegisterRoutes(router *mux.Router) { 25 | router.HandleFunc("/login", h.handleLogin).Methods("POST") 26 | router.HandleFunc("/register", h.handleRegister).Methods("POST") 27 | 28 | } 29 | 30 | // login route 31 | func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { 32 | // handle login user 33 | 34 | var payload types.LoginUserPayload 35 | 36 | if err := utils.ParseJSON(r, &payload); err != nil { 37 | utils.WriteError(w, http.StatusBadRequest, err) 38 | return 39 | } 40 | 41 | // validate the payload 42 | if err := utils.Validate.Struct(payload); err != nil { 43 | error := err.(validator.ValidationErrors) 44 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("validation error: %s", error)) 45 | return 46 | 47 | } 48 | 49 | u, err := h.store.GetUserByEmail(payload.Email) 50 | 51 | if err != nil { 52 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("not found, invalid email or password")) 53 | return 54 | } 55 | 56 | if !auth.ComparePasswords(u.Password, []byte(payload.Password)) { 57 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("invalid email or password")) 58 | return 59 | } 60 | 61 | secret := []byte(config.Envs.JWTSecret) 62 | token, err := auth.CreateJWT(secret, u.ID) 63 | if err != nil { 64 | utils.WriteError(w, http.StatusInternalServerError, err) 65 | return 66 | } 67 | 68 | utils.WriteJSON(w, http.StatusOK, map[string]string{"token": token}) 69 | 70 | } 71 | 72 | // register route 73 | func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request) { 74 | var payload types.RegisterUserPayload 75 | 76 | if err := utils.ParseJSON(r, &payload); err != nil { 77 | utils.WriteError(w, http.StatusBadRequest, err) 78 | return 79 | 80 | } 81 | // validate the payload 82 | if err := utils.Validate.Struct(payload); err != nil { 83 | error := err.(validator.ValidationErrors) 84 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("validation error: %s", error)) 85 | return 86 | 87 | } 88 | 89 | // if the user is exists or not 90 | _, err := h.store.GetUserByEmail(payload.Email) 91 | if err == nil { 92 | utils.WriteError(w, http.StatusBadRequest, fmt.Errorf("user with email %s already exists", payload.Email)) 93 | return 94 | } 95 | 96 | // hashing password from payload 97 | hashedPassword, err := auth.HashPassword(payload.Password) 98 | 99 | if err != nil { 100 | utils.WriteError(w, http.StatusInternalServerError, err) 101 | return 102 | } 103 | 104 | // if user does not exists then create the user 105 | err = h.store.CreateUser(types.User{ 106 | FirstName: payload.FirstName, 107 | LastName: payload.LastName, 108 | Email: payload.Email, 109 | Password: hashedPassword, 110 | }) 111 | 112 | if err != nil { 113 | utils.WriteError(w, http.StatusInternalServerError, err) 114 | return 115 | } 116 | 117 | utils.WriteJSON(w, http.StatusCreated, nil) 118 | 119 | } 120 | -------------------------------------------------------------------------------- /service/product/routes_test.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/iamrubayet/ecom/types" 12 | ) 13 | 14 | func TestProductServiceHandlers(t *testing.T) { 15 | productStore := &mockProductStore{} 16 | userStore := &mockUserStore{} 17 | handler := NewHandler(productStore, userStore) 18 | 19 | t.Run("should handle get products", func(t *testing.T) { 20 | req, err := http.NewRequest(http.MethodGet, "/products", nil) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | rr := httptest.NewRecorder() 26 | router := mux.NewRouter() 27 | 28 | router.HandleFunc("/products", handler.handleGetProducts).Methods(http.MethodGet) 29 | 30 | router.ServeHTTP(rr, req) 31 | 32 | if rr.Code != http.StatusOK { 33 | t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code) 34 | } 35 | }) 36 | 37 | t.Run("should fail if the product ID is not a number", func(t *testing.T) { 38 | req, err := http.NewRequest(http.MethodGet, "/products/abc", nil) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | rr := httptest.NewRecorder() 44 | router := mux.NewRouter() 45 | 46 | router.HandleFunc("/products/{productID}", handler.handleGetProduct).Methods(http.MethodGet) 47 | 48 | router.ServeHTTP(rr, req) 49 | 50 | if rr.Code != http.StatusBadRequest { 51 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 52 | } 53 | }) 54 | 55 | t.Run("should handle get product by ID", func(t *testing.T) { 56 | req, err := http.NewRequest(http.MethodGet, "/products/42", nil) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | rr := httptest.NewRecorder() 62 | router := mux.NewRouter() 63 | 64 | router.HandleFunc("/products/{productID}", handler.handleGetProduct).Methods(http.MethodGet) 65 | 66 | router.ServeHTTP(rr, req) 67 | 68 | if rr.Code != http.StatusOK { 69 | t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code) 70 | } 71 | }) 72 | 73 | t.Run("should fail creating a product if the payload is missing", func(t *testing.T) { 74 | req, err := http.NewRequest(http.MethodPost, "/products", nil) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | rr := httptest.NewRecorder() 80 | router := mux.NewRouter() 81 | 82 | router.HandleFunc("/products", handler.handleCreateProduct).Methods(http.MethodPost) 83 | 84 | router.ServeHTTP(rr, req) 85 | 86 | if rr.Code != http.StatusBadRequest { 87 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 88 | } 89 | }) 90 | 91 | t.Run("should handle creating a product", func(t *testing.T) { 92 | payload := types.CreateProductPayload{ 93 | Name: "test", 94 | Price: 100, 95 | Image: "test.jpg", 96 | Description: "test description", 97 | Quantity: 10, 98 | } 99 | 100 | marshalled, err := json.Marshal(payload) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | req, err := http.NewRequest(http.MethodPost, "/products", bytes.NewBuffer(marshalled)) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | rr := httptest.NewRecorder() 111 | router := mux.NewRouter() 112 | 113 | router.HandleFunc("/products", handler.handleCreateProduct).Methods(http.MethodPost) 114 | 115 | router.ServeHTTP(rr, req) 116 | 117 | if rr.Code != http.StatusCreated { 118 | t.Errorf("expected status code %d, got %d", http.StatusCreated, rr.Code) 119 | } 120 | }) 121 | } 122 | 123 | type mockProductStore struct{} 124 | 125 | func (m *mockProductStore) GetProductByID(productID int) (*types.Product, error) { 126 | return &types.Product{}, nil 127 | } 128 | 129 | func (m *mockProductStore) GetProducts() ([]*types.Product, error) { 130 | return []*types.Product{}, nil 131 | } 132 | 133 | func (m *mockProductStore) CreateProduct(product types.CreateProductPayload) error { 134 | return nil 135 | } 136 | 137 | func (m *mockProductStore) UpdateProduct(product types.Product) error { 138 | return nil 139 | } 140 | 141 | func (m *mockProductStore) GetProductsByID(ids []int) ([]types.Product, error) { 142 | return []types.Product{}, nil 143 | } 144 | 145 | type mockUserStore struct{} 146 | 147 | func (m *mockUserStore) GetUserByID(userID int) (*types.User, error) { 148 | return &types.User{}, nil 149 | } 150 | 151 | func (m *mockUserStore) CreateUser(user types.User) error { 152 | return nil 153 | } 154 | 155 | func (m *mockUserStore) GetUserByEmail(email string) (*types.User, error) { 156 | return &types.User{}, nil 157 | } 158 | -------------------------------------------------------------------------------- /service/cart/routes_test.go: -------------------------------------------------------------------------------- 1 | package cart 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/iamrubayet/ecom/types" 12 | ) 13 | 14 | var mockProducts = []types.Product{ 15 | {ID: 1, Name: "product 1", Price: 10, Quantity: 100}, 16 | {ID: 2, Name: "product 2", Price: 20, Quantity: 200}, 17 | {ID: 3, Name: "product 3", Price: 30, Quantity: 300}, 18 | {ID: 4, Name: "empty stock", Price: 30, Quantity: 0}, 19 | {ID: 5, Name: "almost stock", Price: 30, Quantity: 1}, 20 | } 21 | 22 | func TestCartServiceHandler(t *testing.T) { 23 | productStore := &mockProductStore{} 24 | orderStore := &mockOrderStore{} 25 | handler := NewHandler(productStore, orderStore, nil) 26 | 27 | t.Run("should fail to checkout if the cart items do not exist", func(t *testing.T) { 28 | payload := types.CartCheckoutPayload{ 29 | Items: []types.CartCheckoutItem{ 30 | {ProductID: 99, Quantity: 100}, 31 | }, 32 | } 33 | 34 | marshalled, err := json.Marshal(payload) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled)) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | rr := httptest.NewRecorder() 45 | router := mux.NewRouter() 46 | 47 | router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost) 48 | 49 | router.ServeHTTP(rr, req) 50 | 51 | if rr.Code != http.StatusBadRequest { 52 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 53 | } 54 | }) 55 | 56 | t.Run("should fail to checkout if the cart has negative quantities", func(t *testing.T) { 57 | payload := types.CartCheckoutPayload{ 58 | Items: []types.CartCheckoutItem{ 59 | {ProductID: 1, Quantity: 0}, // invalid quantity 60 | }, 61 | } 62 | 63 | marshalled, err := json.Marshal(payload) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled)) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | rr := httptest.NewRecorder() 74 | router := mux.NewRouter() 75 | 76 | router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost) 77 | 78 | router.ServeHTTP(rr, req) 79 | 80 | if rr.Code != http.StatusBadRequest { 81 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 82 | } 83 | }) 84 | 85 | t.Run("should fail to checkout if there is no stock for an item", func(t *testing.T) { 86 | payload := types.CartCheckoutPayload{ 87 | Items: []types.CartCheckoutItem{ 88 | {ProductID: 4, Quantity: 2}, 89 | }, 90 | } 91 | 92 | marshalled, err := json.Marshal(payload) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled)) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | rr := httptest.NewRecorder() 103 | router := mux.NewRouter() 104 | 105 | router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost) 106 | 107 | router.ServeHTTP(rr, req) 108 | 109 | if rr.Code != http.StatusBadRequest { 110 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 111 | } 112 | }) 113 | 114 | t.Run("should fail to checkout if there is not enough stock", func(t *testing.T) { 115 | payload := types.CartCheckoutPayload{ 116 | Items: []types.CartCheckoutItem{ 117 | {ProductID: 5, Quantity: 2}, 118 | }, 119 | } 120 | 121 | marshalled, err := json.Marshal(payload) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled)) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | rr := httptest.NewRecorder() 132 | router := mux.NewRouter() 133 | 134 | router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost) 135 | 136 | router.ServeHTTP(rr, req) 137 | 138 | if rr.Code != http.StatusBadRequest { 139 | t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code) 140 | } 141 | }) 142 | 143 | t.Run("should checkout and calculate the price correctly", func(t *testing.T) { 144 | payload := types.CartCheckoutPayload{ 145 | Items: []types.CartCheckoutItem{ 146 | {ProductID: 1, Quantity: 10}, 147 | {ProductID: 2, Quantity: 20}, 148 | {ProductID: 5, Quantity: 1}, 149 | }, 150 | } 151 | 152 | marshalled, err := json.Marshal(payload) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | req, err := http.NewRequest(http.MethodPost, "/cart/checkout", bytes.NewBuffer(marshalled)) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | 162 | rr := httptest.NewRecorder() 163 | router := mux.NewRouter() 164 | 165 | router.HandleFunc("/cart/checkout", handler.handleCheckout).Methods(http.MethodPost) 166 | 167 | router.ServeHTTP(rr, req) 168 | 169 | if rr.Code != http.StatusOK { 170 | t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code) 171 | } 172 | 173 | var response map[string]interface{} 174 | if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | if response["total_price"] != 530.0 { 179 | t.Errorf("expected total price to be 530, got %f", response["total_price"]) 180 | } 181 | }) 182 | } 183 | 184 | type mockProductStore struct{} 185 | 186 | func (m *mockProductStore) GetProductByID(productID int) (*types.Product, error) { 187 | return &types.Product{}, nil 188 | } 189 | 190 | func (m *mockProductStore) GetProducts() ([]*types.Product, error) { 191 | return []*types.Product{}, nil 192 | } 193 | 194 | func (m *mockProductStore) CreateProduct(product types.CreateProductPayload) error { 195 | return nil 196 | } 197 | 198 | func (m *mockProductStore) GetProductsByID(ids []int) ([]types.Product, error) { 199 | return mockProducts, nil 200 | } 201 | 202 | func (m *mockProductStore) UpdateProduct(product types.Product) error { 203 | return nil 204 | } 205 | 206 | type mockOrderStore struct{} 207 | 208 | func (m *mockOrderStore) CreateOrder(order types.Order) (int, error) { 209 | return 0, nil 210 | } 211 | 212 | func (m *mockOrderStore) CreateOrderItem(orderItem types.OrderItem) error { 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 4 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= 12 | github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= 13 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 14 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 15 | github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= 16 | github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 17 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 18 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 19 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 20 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 21 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 22 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 23 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 24 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 25 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 26 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 27 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 28 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 29 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 30 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 32 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 33 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 34 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 35 | github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= 36 | github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 37 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 38 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 39 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 40 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 41 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 42 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 43 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 44 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 45 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 46 | github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= 47 | github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= 48 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 49 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 50 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 51 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 52 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 53 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 54 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 55 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 56 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 57 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 58 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 59 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 60 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 61 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 62 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 63 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 64 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 65 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 66 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 67 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 68 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 69 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 70 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 71 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 72 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 73 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 74 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 75 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 76 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 77 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 79 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 81 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 82 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 86 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 88 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 89 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 90 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 91 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 92 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 93 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 94 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 95 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 96 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 97 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 98 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 99 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 100 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 101 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 105 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 106 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 107 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 108 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 109 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | --------------------------------------------------------------------------------