From 6fdcf04e6702acb01785ee2b4c6bad1e39522667 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 25 Mar 2025 23:28:46 -0300 Subject: [PATCH 01/36] :rocket: start project --- .env | 4 +++ cmd/main.go | 33 ++++++++++++++++++++ go.mod | 26 ++++++++++++++++ go.sum | 37 +++++++++++++++++++++++ notes.txt | 1 + pkg/config/env.go | 47 +++++++++++++++++++++++++++++ pkg/models/user.go | 14 +++++++++ pkg/repositories/interfaces.go | 11 +++++++ pkg/repositories/user_repository.go | 32 ++++++++++++++++++++ 9 files changed, 205 insertions(+) create mode 100644 .env create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 notes.txt create mode 100644 pkg/config/env.go create mode 100644 pkg/models/user.go create mode 100644 pkg/repositories/interfaces.go create mode 100644 pkg/repositories/user_repository.go diff --git a/.env b/.env new file mode 100644 index 0000000..ca82f2d --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +POSTGRES_HOST=http://localhost +POSTGRES_PORT=5432 +POSTGRES_USER=root +POSTGRES_PASSWORD=root \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..51bd080 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "gss-backend/pkg/config" + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + config, err := config.NewConfig() + if err != nil { + log.Fatal(err) + } + + fmt.Println(config.POSTGRES_HOST) + fmt.Println(config.POSTGRES_PORT) + fmt.Println(config.POSTGRES_USER) + fmt.Println(config.POSTGRES_PASSWORD) + + app := fiber.New() + app.Use(cors.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("GSS Gateway API is up and running! 🚀") + }) + + log.Fatal(app.Listen(":3000")) + + +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..416cf1a --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module gss-backend + +go 1.24.1 + +require ( + github.com/gofiber/fiber/v2 v2.52.6 + github.com/joho/godotenv v1.5.1 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..17a66ed --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..6498181 --- /dev/null +++ b/notes.txt @@ -0,0 +1 @@ +docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres diff --git a/pkg/config/env.go b/pkg/config/env.go new file mode 100644 index 0000000..048e202 --- /dev/null +++ b/pkg/config/env.go @@ -0,0 +1,47 @@ +package config + +import ( + "log" + "os" + "strconv" + + "github.com/joho/godotenv" +) + +type Config struct { + POSTGRES_HOST string + POSTGRES_PORT int + POSTGRES_USER string + POSTGRES_PASSWORD string +} + +func NewConfig() (*Config, error) { + err := godotenv.Load() + + if err != nil { + log.Fatal("Error loading .env file") + } + config := &Config{ + POSTGRES_HOST: getEnv("POSTGRES_HOST", "localhost"), + POSTGRES_PORT: getEnvAsInt("POSTGRES_PORT", 5432), + POSTGRES_USER: getEnv("POSTGRES_USER", "root"), + POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), + } + + return config, nil +} + +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func getEnvAsInt(name string, defaultValue int) int { + valueStr := getEnv(name, "") + if value, err := strconv.Atoi(valueStr); err == nil { + return value + } + return defaultValue +} diff --git a/pkg/models/user.go b/pkg/models/user.go new file mode 100644 index 0000000..2866de1 --- /dev/null +++ b/pkg/models/user.go @@ -0,0 +1,14 @@ +package models + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model + ID uint `json:"id" gorm:"primary_key"` + Name string `json:"name"` + Email string `json:"email"` + Password string `json:"password"` + PhoneNumber string `json:"phone_number"` +} \ No newline at end of file diff --git a/pkg/repositories/interfaces.go b/pkg/repositories/interfaces.go new file mode 100644 index 0000000..e17dba5 --- /dev/null +++ b/pkg/repositories/interfaces.go @@ -0,0 +1,11 @@ +package repositories + +import ( + "gss-backend/pkg/models" +) + +type UserRepository interface { + FindAll() ([]models.User, error) + FindByID(id uint) (models.User, error) + Create(user models.User) (models.User, error) +} \ No newline at end of file diff --git a/pkg/repositories/user_repository.go b/pkg/repositories/user_repository.go new file mode 100644 index 0000000..898fcdf --- /dev/null +++ b/pkg/repositories/user_repository.go @@ -0,0 +1,32 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +type PostgresUserRepository struct { + db *gorm.DB +} + +func NewPostgresUserRepository(db *gorm.DB) *PostgresUserRepository { + return &PostgresUserRepository{db: db} +} + +func (r *PostgresUserRepository) Create(user models.User) (models.User, error) { + result := r.db.Create(&user) + return user, result.Error +} + +func (r *PostgresUserRepository) FindAll() ([]models.User, error) { + var users []models.User + result := r.db.Find(&users) + return users, result.Error +} + +func (r *PostgresUserRepository) FindByID(id uint) (models.User, error) { + var user models.User + result := r.db.First(&user, id) + return user, result.Error +} \ No newline at end of file From 8ab7df3b95d6686ece71fc0e0238c3c427111eb9 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 18:04:41 -0300 Subject: [PATCH 02/36] :wrench: refactor: change User model --- pkg/models/user.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/models/user.go b/pkg/models/user.go index 2866de1..dc0be80 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -7,8 +7,7 @@ import ( type User struct { gorm.Model ID uint `json:"id" gorm:"primary_key"` - Name string `json:"name"` + FullName string `json:"full_name"` Email string `json:"email"` - Password string `json:"password"` PhoneNumber string `json:"phone_number"` } \ No newline at end of file From 05d95f3646609a77c63a4bf0f463004333eb2bbb Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 18:05:10 -0300 Subject: [PATCH 03/36] :sparkles: feat: create user repository --- pkg/repositories/interfaces.go | 11 ------- pkg/repositories/user/interfaces.go | 17 ++++++++++ .../user/postgres_user_repository.go | 29 +++++++++++++++++ pkg/repositories/user_repository.go | 32 ------------------- 4 files changed, 46 insertions(+), 43 deletions(-) delete mode 100644 pkg/repositories/interfaces.go create mode 100644 pkg/repositories/user/interfaces.go create mode 100644 pkg/repositories/user/postgres_user_repository.go delete mode 100644 pkg/repositories/user_repository.go diff --git a/pkg/repositories/interfaces.go b/pkg/repositories/interfaces.go deleted file mode 100644 index e17dba5..0000000 --- a/pkg/repositories/interfaces.go +++ /dev/null @@ -1,11 +0,0 @@ -package repositories - -import ( - "gss-backend/pkg/models" -) - -type UserRepository interface { - FindAll() ([]models.User, error) - FindByID(id uint) (models.User, error) - Create(user models.User) (models.User, error) -} \ No newline at end of file diff --git a/pkg/repositories/user/interfaces.go b/pkg/repositories/user/interfaces.go new file mode 100644 index 0000000..91f5d20 --- /dev/null +++ b/pkg/repositories/user/interfaces.go @@ -0,0 +1,17 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +type IUserRepository interface { + FindAll() (*[]models.User, error) + FindByID(id uint) (*models.User, error) + Create(user *models.User) (*models.User, error) +} + +type PostgresUserRepository struct { + db *gorm.DB +} \ No newline at end of file diff --git a/pkg/repositories/user/postgres_user_repository.go b/pkg/repositories/user/postgres_user_repository.go new file mode 100644 index 0000000..8c2ceb3 --- /dev/null +++ b/pkg/repositories/user/postgres_user_repository.go @@ -0,0 +1,29 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + + +func NewPostgresUserRepository(db *gorm.DB) *PostgresUserRepository { + return &PostgresUserRepository{db: db} +} + +func (r *PostgresUserRepository) Create(user *models.User) (*models.User, error) { + result := r.db.Create(user) + return user, result.Error +} + +func (r *PostgresUserRepository) FindAll() (*[]models.User, error) { + var users []models.User + result := r.db.Find(&users) + return &users, result.Error +} + +func (r *PostgresUserRepository) FindByID(id uint) (*models.User, error) { + var user models.User + result := r.db.First(&user, id) + return &user, result.Error +} \ No newline at end of file diff --git a/pkg/repositories/user_repository.go b/pkg/repositories/user_repository.go deleted file mode 100644 index 898fcdf..0000000 --- a/pkg/repositories/user_repository.go +++ /dev/null @@ -1,32 +0,0 @@ -package repositories - -import ( - "gss-backend/pkg/models" - - "gorm.io/gorm" -) - -type PostgresUserRepository struct { - db *gorm.DB -} - -func NewPostgresUserRepository(db *gorm.DB) *PostgresUserRepository { - return &PostgresUserRepository{db: db} -} - -func (r *PostgresUserRepository) Create(user models.User) (models.User, error) { - result := r.db.Create(&user) - return user, result.Error -} - -func (r *PostgresUserRepository) FindAll() ([]models.User, error) { - var users []models.User - result := r.db.Find(&users) - return users, result.Error -} - -func (r *PostgresUserRepository) FindByID(id uint) (models.User, error) { - var user models.User - result := r.db.First(&user, id) - return user, result.Error -} \ No newline at end of file From 723bff027ae4d2546fd5bb071f5c6f7954e130aa Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 18:05:27 -0300 Subject: [PATCH 04/36] :sparkles: feat: create user service --- pkg/services/user/interfaces.go | 16 ++++++++++++++++ pkg/services/user/user_service.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 pkg/services/user/interfaces.go create mode 100644 pkg/services/user/user_service.go diff --git a/pkg/services/user/interfaces.go b/pkg/services/user/interfaces.go new file mode 100644 index 0000000..d49561b --- /dev/null +++ b/pkg/services/user/interfaces.go @@ -0,0 +1,16 @@ +package services + +import ( + "gss-backend/pkg/models" + repositories "gss-backend/pkg/repositories/user" +) + +type IUserService interface { + FindAll() (*[]models.User, error) + FindByID(id uint) (*models.User, error) + Create(user *models.User) (*models.User, error) +} + +type UserService struct { + repository repositories.IUserRepository +} \ No newline at end of file diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go new file mode 100644 index 0000000..b51c23d --- /dev/null +++ b/pkg/services/user/user_service.go @@ -0,0 +1,30 @@ +package services + +import ( + "gss-backend/pkg/models" + repositories "gss-backend/pkg/repositories/user" +) + + +func NewUserService(repository repositories.IUserRepository) *UserService { + return &UserService{ + repository: repository, + } +} + +func (s *UserService) Create(user *models.User) (*models.User, error) { + return s.repository.Create(user) +} + +func (s *UserService) FindAll() (*[]models.User, error) { + return s.repository.FindAll() +} + +func (s *UserService) FindByID(id uint) (*models.User, error) { + return s.repository.FindByID(id) +} + + + + + From 2f099dce030c1423d770c147fe09b6e53fb759c1 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 18:06:25 -0300 Subject: [PATCH 05/36] :sparkles: feat: implement user presenters and CreateUser handler --- api/handlers/user.go | 39 ++++++++++++++++++++++++++++++ api/presenter/user.go | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 api/handlers/user.go create mode 100644 api/presenter/user.go diff --git a/api/handlers/user.go b/api/handlers/user.go new file mode 100644 index 0000000..b81c541 --- /dev/null +++ b/api/handlers/user.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "errors" + "gss-backend/api/presenter" + "gss-backend/pkg/models" + services "gss-backend/pkg/services/user" + + "github.com/gofiber/fiber/v2" +) + +func CreateUser(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody models.User + err := c.BodyParser(&requestBody) + + if err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(err)) + } + + if requestBody.FullName == "" || requestBody.Email == "" || requestBody.PhoneNumber == "" { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(errors.New( + "Full Name, Email, and Phone Number are required", + ))) + } + + result, err := user_service.Create(&requestBody) + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + + return c.JSON(presenter.UserSuccessResponse(result)) + + } +} \ No newline at end of file diff --git a/api/presenter/user.go b/api/presenter/user.go new file mode 100644 index 0000000..077e7e6 --- /dev/null +++ b/api/presenter/user.go @@ -0,0 +1,55 @@ +package presenter + +import ( + "gss-backend/pkg/models" + + "github.com/gofiber/fiber/v2" +) + +// UserPresenter is a struct that is going to be used to present the user data in a controlled way +type UserPresenter struct { + ID uint `json:"id"` + FullName string `json:"full_name"` + Email string `json:"email"` +} + +// Functions to transform the user 'raw' data to presenters +func UserSuccessResponse(data *models.User) *fiber.Map { + user := UserPresenter{ + ID: data.ID, + FullName: data.FullName, + Email: data.Email, + } + + return &fiber.Map{ + "status": "success", + "data": user, + "error": nil, + } +} + +func UsersSuccessResponse(data *[]models.User) *fiber.Map { + var users []UserPresenter + + for _, user := range *data { + users = append(users, UserPresenter{ + ID: user.ID, + FullName: user.FullName, + Email: user.Email, + }) + } + + return &fiber.Map{ + "status": "success", + "data": users, + "error": nil, + } +} + +func UserErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": "error", + "data": nil, + "error": err.Error(), + } +} \ No newline at end of file From 1776c16fb72b846e316aedfdc9c77399cd2b3db6 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 18:06:56 -0300 Subject: [PATCH 06/36] :construction: feat: create routes package and user route file --- api/routes/user.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 api/routes/user.go diff --git a/api/routes/user.go b/api/routes/user.go new file mode 100644 index 0000000..836aa6a --- /dev/null +++ b/api/routes/user.go @@ -0,0 +1 @@ +package routes \ No newline at end of file From 4496cb1b0c5b03c0972cd9d3195d8db39dd48e44 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 20:13:54 -0300 Subject: [PATCH 07/36] :bulb: style: add comments to User model --- pkg/models/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/models/user.go b/pkg/models/user.go index dc0be80..75c8b8f 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -4,6 +4,7 @@ import ( "gorm.io/gorm" ) +// User struct to model the user entity type User struct { gorm.Model ID uint `json:"id" gorm:"primary_key"` From f930641b4f3fb204f4249421af3839ab41e47d50 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 20:14:23 -0300 Subject: [PATCH 08/36] :bulb: style: add comments to User repository --- pkg/repositories/user/interfaces.go | 1 + pkg/repositories/user/postgres_user_repository.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/repositories/user/interfaces.go b/pkg/repositories/user/interfaces.go index 91f5d20..3ad46da 100644 --- a/pkg/repositories/user/interfaces.go +++ b/pkg/repositories/user/interfaces.go @@ -6,6 +6,7 @@ import ( "gorm.io/gorm" ) +// Interfaces that I going to use to implement the repository pattern type IUserRepository interface { FindAll() (*[]models.User, error) FindByID(id uint) (*models.User, error) diff --git a/pkg/repositories/user/postgres_user_repository.go b/pkg/repositories/user/postgres_user_repository.go index 8c2ceb3..b9f137d 100644 --- a/pkg/repositories/user/postgres_user_repository.go +++ b/pkg/repositories/user/postgres_user_repository.go @@ -6,7 +6,7 @@ import ( "gorm.io/gorm" ) - +// Concrete implementation of the IUserRepository interface func NewPostgresUserRepository(db *gorm.DB) *PostgresUserRepository { return &PostgresUserRepository{db: db} } From d7a043747502ef4af044a960fa1e7f511d8a37eb Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 20:15:34 -0300 Subject: [PATCH 09/36] :wrench: build: add new .env variable --- .env | 5 +++-- pkg/config/env.go | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.env b/.env index ca82f2d..f758683 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ -POSTGRES_HOST=http://localhost +POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=root -POSTGRES_PASSWORD=root \ No newline at end of file +POSTGRES_PASSWORD=root +POSTGRES_DB=gss-db \ No newline at end of file diff --git a/pkg/config/env.go b/pkg/config/env.go index 048e202..a62e51e 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -8,11 +8,13 @@ import ( "github.com/joho/godotenv" ) +// Config struct to abstract the environment variables type Config struct { POSTGRES_HOST string POSTGRES_PORT int POSTGRES_USER string POSTGRES_PASSWORD string + POSTGRES_DB string } func NewConfig() (*Config, error) { @@ -26,11 +28,14 @@ func NewConfig() (*Config, error) { POSTGRES_PORT: getEnvAsInt("POSTGRES_PORT", 5432), POSTGRES_USER: getEnv("POSTGRES_USER", "root"), POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), + POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), + } return config, nil } +// Auxiliar functions to get the environment variables func getEnv(key, defaultValue string) string { if value, exists := os.LookupEnv(key); exists { return value From 7c41784d6a4f124dd49dfc53eaa17645c5ac98a9 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 20:16:56 -0300 Subject: [PATCH 10/36] :sparkles: feat: finish user handlers, presenters and routes --- api/handlers/user.go | 36 ++++++++++++++++++++++++++++++++++++ api/presenter/user.go | 1 + api/routes/user.go | 16 +++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/api/handlers/user.go b/api/handlers/user.go index b81c541..d838824 100644 --- a/api/handlers/user.go +++ b/api/handlers/user.go @@ -5,6 +5,7 @@ import ( "gss-backend/api/presenter" "gss-backend/pkg/models" services "gss-backend/pkg/services/user" + "strconv" "github.com/gofiber/fiber/v2" ) @@ -36,4 +37,39 @@ func CreateUser(user_service services.IUserService) fiber.Handler { return c.JSON(presenter.UserSuccessResponse(result)) } +} + +func FindAllUsers(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + result, err := user_service.FindAll() + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + + return c.JSON(presenter.UsersSuccessResponse(result)) + } +} + +func FindUserByID(user_service services.IUserService) fiber.Handler { + return func(c *fiber.Ctx) error { + idStr := c.Params("id") + + id, err := strconv.ParseUint(idStr, 10, 32) + + if err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(err)) + } + + result, err := user_service.FindByID(uint(id)) + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + + return c.JSON(presenter.UserSuccessResponse(result)) + } } \ No newline at end of file diff --git a/api/presenter/user.go b/api/presenter/user.go index 077e7e6..622299f 100644 --- a/api/presenter/user.go +++ b/api/presenter/user.go @@ -7,6 +7,7 @@ import ( ) // UserPresenter is a struct that is going to be used to present the user data in a controlled way +// As a minor "business logic", I opted to not return the phone number of the user type UserPresenter struct { ID uint `json:"id"` FullName string `json:"full_name"` diff --git a/api/routes/user.go b/api/routes/user.go index 836aa6a..2c2e5aa 100644 --- a/api/routes/user.go +++ b/api/routes/user.go @@ -1 +1,15 @@ -package routes \ No newline at end of file +package routes + +import ( + "gss-backend/api/handlers" + services "gss-backend/pkg/services/user" + + "github.com/gofiber/fiber/v2" +) + +// UserRouter is a function created to define User related routes +func UserRouter(app fiber.Router, user_service services.IUserService) { + app.Post("/users", handlers.CreateUser(user_service)) + app.Get("/users", handlers.FindAllUsers(user_service)) + app.Get("/users/:id", handlers.FindUserByID(user_service)) +} \ No newline at end of file From 7b5ba201ac1fe6dd77deea544aef6dfabfcebecd Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Wed, 26 Mar 2025 20:18:05 -0300 Subject: [PATCH 11/36] :sparkles: feat: register User routes in the API --- cmd/main.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 7 +++++++ go.sum | 28 ++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/cmd/main.go b/cmd/main.go index 51bd080..56de90c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,24 +2,61 @@ package main import ( "fmt" + "gss-backend/api/routes" "gss-backend/pkg/config" + "gss-backend/pkg/models" + repositories "gss-backend/pkg/repositories/user" + services "gss-backend/pkg/services/user" "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" + "gorm.io/driver/postgres" + "gorm.io/gorm" ) func main() { + // Loading configuration config, err := config.NewConfig() + if err != nil { log.Fatal(err) } + // Printing .env variables to see if they are loaded correctly fmt.Println(config.POSTGRES_HOST) fmt.Println(config.POSTGRES_PORT) fmt.Println(config.POSTGRES_USER) fmt.Println(config.POSTGRES_PASSWORD) + fmt.Println(config.POSTGRES_DB) + + // Setting up Postgres DSN + dsn := fmt.Sprintf(( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=UTC"), + config.POSTGRES_HOST, + config.POSTGRES_PORT, + config.POSTGRES_USER, + config.POSTGRES_PASSWORD, + config.POSTGRES_DB, + ) + + // Connecting to the database + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Connected to the database!") + // Setting up Migrations + err = db.AutoMigrate(&models.User{}) + + if err != nil { + log.Fatal("Error running migrations", err) + } + + // Setting up Fiber app := fiber.New() app.Use(cors.New()) @@ -27,6 +64,15 @@ func main() { return c.SendString("GSS Gateway API is up and running! 🚀") }) + // Instatiating the User Repo + userRepo := repositories.NewPostgresUserRepository(db) + + // Setting up the User Service + userService := services.NewUserService(userRepo) + + api := app.Group("/api") + routes.UserRouter(api, userService) + log.Fatal(app.Listen(":3000")) diff --git a/go.mod b/go.mod index 416cf1a..66e3618 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,17 @@ go 1.24.1 require ( github.com/gofiber/fiber/v2 v2.52.6 github.com/joho/godotenv v1.5.1 + gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.9 // indirect @@ -21,6 +26,8 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 17a66ed..b000cd0 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,20 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -19,19 +30,36 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= From 3514025abf7e818d7652171a75d906c54056dc23 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Thu, 27 Mar 2025 15:24:27 -0300 Subject: [PATCH 12/36] :sparkles: feat: add Points Module --- api/handlers/points.go | 53 +++++++++++++++++++ api/presenter/points.go | 50 +++++++++++++++++ api/routes/points.go | 13 +++++ cmd/main.go | 19 ++++--- pkg/models/points.go | 13 +++++ pkg/repositories/points/interfaces.go | 18 +++++++ .../points/postgres_points_repository.go | 50 +++++++++++++++++ pkg/services/points/interfaces.go | 17 ++++++ pkg/services/points/points_service.go | 29 ++++++++++ 9 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 api/handlers/points.go create mode 100644 api/presenter/points.go create mode 100644 api/routes/points.go create mode 100644 pkg/models/points.go create mode 100644 pkg/repositories/points/interfaces.go create mode 100644 pkg/repositories/points/postgres_points_repository.go create mode 100644 pkg/services/points/interfaces.go create mode 100644 pkg/services/points/points_service.go diff --git a/api/handlers/points.go b/api/handlers/points.go new file mode 100644 index 0000000..f95422c --- /dev/null +++ b/api/handlers/points.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "gss-backend/api/presenter" + services "gss-backend/pkg/services/points" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +func UpdatePoints(points_service services.IPointsService) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody struct { + UserID string `json:"user_id"` + } + + if err := c.BodyParser(&requestBody); err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenter.PointsErrorResponse(err)) + } + + userIdStr := requestBody.UserID + userId, err := strconv.ParseUint(userIdStr, 10, 32) + + if err != nil { + c.Status(fiber.StatusBadRequest) + return c.JSON(presenter.PointsErrorResponse(err)) + } + + result, err := points_service.Update(uint(userId)) + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenter.PointsErrorResponse(err)) + } + + return c.JSON(presenter.PointsSuccessResponse(result)) + } +} + +func FindLeaderboard(points_service services.IPointsService) fiber.Handler { + return func(c *fiber.Ctx) error { + result, err := points_service.FindLeaderboard() + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenter.PointsErrorResponse(err)) + } + + return c.JSON(presenter.PointsLeaderboardSuccessResponse(result)) + } +} + diff --git a/api/presenter/points.go b/api/presenter/points.go new file mode 100644 index 0000000..d1e4109 --- /dev/null +++ b/api/presenter/points.go @@ -0,0 +1,50 @@ +package presenter + +import ( + "gss-backend/pkg/models" + + "github.com/gofiber/fiber/v2" +) + +type PointsPresenter struct { + ID uint `json:"id"` + Points uint `json:"points"` +} + +func PointsSuccessResponse(data *models.Points) *fiber.Map { + points := PointsPresenter{ + ID: data.ID, + Points: data.Points, + } + + return &fiber.Map{ + "status": "success", + "data": points, + "error": nil, + } +} + +func PointsLeaderboardSuccessResponse(data *[]models.Points) *fiber.Map { + var points []PointsPresenter + + for _, point := range *data { + points = append(points, PointsPresenter{ + ID: point.ID, + Points: point.Points, + }) + } + + return &fiber.Map{ + "status": "success", + "data": points, + "error": nil, + } +} + +func PointsErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": "error", + "data": nil, + "error": err.Error(), + } +} \ No newline at end of file diff --git a/api/routes/points.go b/api/routes/points.go new file mode 100644 index 0000000..d0ce61c --- /dev/null +++ b/api/routes/points.go @@ -0,0 +1,13 @@ +package routes + +import ( + "gss-backend/api/handlers" + services "gss-backend/pkg/services/points" + + "github.com/gofiber/fiber/v2" +) + +func PointsRouter(app fiber.Router, points_service services.IPointsService) { + app.Post("/points", handlers.UpdatePoints(points_service)) + app.Get("/points/leaderboard", handlers.FindLeaderboard(points_service)) +} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 56de90c..4eb9715 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,8 +5,10 @@ import ( "gss-backend/api/routes" "gss-backend/pkg/config" "gss-backend/pkg/models" - repositories "gss-backend/pkg/repositories/user" - services "gss-backend/pkg/services/user" + pointsRepo "gss-backend/pkg/repositories/points" + userRepo "gss-backend/pkg/repositories/user" + pointsService "gss-backend/pkg/services/points" + userService "gss-backend/pkg/services/user" "log" "github.com/gofiber/fiber/v2" @@ -50,7 +52,7 @@ func main() { fmt.Println("Connected to the database!") // Setting up Migrations - err = db.AutoMigrate(&models.User{}) + err = db.AutoMigrate(&models.User{}, &models.Points{}) if err != nil { log.Fatal("Error running migrations", err) @@ -64,14 +66,17 @@ func main() { return c.SendString("GSS Gateway API is up and running! 🚀") }) - // Instatiating the User Repo - userRepo := repositories.NewPostgresUserRepository(db) + // Instatiating Repositories + userRepo := userRepo.NewPostgresUserRepository(db) + pointsRepo := pointsRepo.NewPostgresPointsRepository(db) - // Setting up the User Service - userService := services.NewUserService(userRepo) + // Instatiating Services + userService := userService.NewUserService(userRepo) + pointsService := pointsService.NewPointsService(pointsRepo) api := app.Group("/api") routes.UserRouter(api, userService) + routes.PointsRouter(api, pointsService) log.Fatal(app.Listen(":3000")) diff --git a/pkg/models/points.go b/pkg/models/points.go new file mode 100644 index 0000000..8e1dced --- /dev/null +++ b/pkg/models/points.go @@ -0,0 +1,13 @@ +package models + +import ( + "gorm.io/gorm" +) + +type Points struct { + gorm.Model + ID uint `json:"id" gorm:"primary_key"` + UserId uint `json:"user_id"` + User User `gorm:"foreignKey:UserId"` + Points uint `json:"points"` +} \ No newline at end of file diff --git a/pkg/repositories/points/interfaces.go b/pkg/repositories/points/interfaces.go new file mode 100644 index 0000000..d32f467 --- /dev/null +++ b/pkg/repositories/points/interfaces.go @@ -0,0 +1,18 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +type IPointsRepository interface { + Create(user_id uint) (*models.Points, error) + Update(user_id uint) (*models.Points, error) + FindByUserID(user_id uint) (*models.Points, error) + CreateLeaderboard() (*[]models.Points, error) +} + +type PostgresPointsRepository struct { + db *gorm.DB +} \ No newline at end of file diff --git a/pkg/repositories/points/postgres_points_repository.go b/pkg/repositories/points/postgres_points_repository.go new file mode 100644 index 0000000..b99617a --- /dev/null +++ b/pkg/repositories/points/postgres_points_repository.go @@ -0,0 +1,50 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +// Concrete implementation of the IPointsRepository interface +func NewPostgresPointsRepository(db *gorm.DB) *PostgresPointsRepository { + return &PostgresPointsRepository{db: db} +} + +func (r *PostgresPointsRepository) Create(user_id uint) (*models.Points, error) { + points := models.Points{UserId: user_id, Points: 1} + result := r.db.Create(&points) + return &points, result.Error +} + +func (r *PostgresPointsRepository) Update(user_id uint) (*models.Points, error) { + var points models.Points + + result := r.db.Where("user_id = ?", user_id).First(&points) + + if result.Error != nil { + return nil, result.Error + } + + points.Points += 1 + + result = r.db.Save(&points) + + if result.Error != nil { + return nil, result.Error + } + + return &points, result.Error +} + +func (r *PostgresPointsRepository) FindByUserID(user_id uint) (*models.Points, error) { + var points models.Points + result := r.db.Where("user_id = ?", user_id).First(&points) + return &points, result.Error +} + +func (r *PostgresPointsRepository) CreateLeaderboard() (*[]models.Points, error) { + var points []models.Points + result := r.db.Order("points desc").Find(&points).Limit(10) + return &points, result.Error +} \ No newline at end of file diff --git a/pkg/services/points/interfaces.go b/pkg/services/points/interfaces.go new file mode 100644 index 0000000..8a85e4f --- /dev/null +++ b/pkg/services/points/interfaces.go @@ -0,0 +1,17 @@ +package services + +import ( + "gss-backend/pkg/models" + repositories "gss-backend/pkg/repositories/points" +) + +type IPointsService interface { + Create(user_id uint) (*models.Points, error) + Update(user_id uint) (*models.Points, error) + FindByUserID(user_id uint) (*models.Points, error) + FindLeaderboard() (*[]models.Points, error) +} + +type PointsService struct { + repository repositories.IPointsRepository +} \ No newline at end of file diff --git a/pkg/services/points/points_service.go b/pkg/services/points/points_service.go new file mode 100644 index 0000000..0c50209 --- /dev/null +++ b/pkg/services/points/points_service.go @@ -0,0 +1,29 @@ +package services + +import ( + "gss-backend/pkg/models" + repositories "gss-backend/pkg/repositories/points" +) + +func NewPointsService(repository repositories.IPointsRepository) *PointsService { + return &PointsService{ + repository: repository, + } +} + +func (s *PointsService) Create(user_id uint) (*models.Points, error) { + return s.repository.Create(user_id) +} + +func (s *PointsService) Update(user_id uint) (*models.Points, error) { + return s.repository.Update(user_id) +} + + +func (s *PointsService) FindByUserID(user_id uint) (*models.Points, error) { + return s.repository.FindByUserID(user_id) +} + +func (s *PointsService) FindLeaderboard() (*[]models.Points, error) { + return s.repository.CreateLeaderboard() +} \ No newline at end of file From dc0547f1a43bd29657be72e9d033ee132c5bbb3f Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Thu, 27 Mar 2025 15:46:53 -0300 Subject: [PATCH 13/36] :sparkles: feat: change user service to also create points entity on user creation --- cmd/main.go | 2 +- pkg/services/user/interfaces.go | 6 ++++-- pkg/services/user/user_service.go | 32 +++++++++++++++++++++++++------ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 4eb9715..1b7bb48 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -71,7 +71,7 @@ func main() { pointsRepo := pointsRepo.NewPostgresPointsRepository(db) // Instatiating Services - userService := userService.NewUserService(userRepo) + userService := userService.NewUserService(userRepo, pointsRepo) pointsService := pointsService.NewPointsService(pointsRepo) api := app.Group("/api") diff --git a/pkg/services/user/interfaces.go b/pkg/services/user/interfaces.go index d49561b..7e30c40 100644 --- a/pkg/services/user/interfaces.go +++ b/pkg/services/user/interfaces.go @@ -2,7 +2,8 @@ package services import ( "gss-backend/pkg/models" - repositories "gss-backend/pkg/repositories/user" + pointsRepo "gss-backend/pkg/repositories/points" + userRepo "gss-backend/pkg/repositories/user" ) type IUserService interface { @@ -12,5 +13,6 @@ type IUserService interface { } type UserService struct { - repository repositories.IUserRepository + pointsRepo pointsRepo.IPointsRepository + userRepo userRepo.IUserRepository } \ No newline at end of file diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index b51c23d..6bb7a60 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -2,26 +2,46 @@ package services import ( "gss-backend/pkg/models" - repositories "gss-backend/pkg/repositories/user" + pointsRepo "gss-backend/pkg/repositories/points" + userRepo "gss-backend/pkg/repositories/user" ) -func NewUserService(repository repositories.IUserRepository) *UserService { +func NewUserService(userRepo userRepo.IUserRepository, pointsRepo pointsRepo.IPointsRepository) *UserService { return &UserService{ - repository: repository, + userRepo: userRepo, + pointsRepo: pointsRepo, } } +// Register a new user and create a new points record for the user func (s *UserService) Create(user *models.User) (*models.User, error) { - return s.repository.Create(user) + // Create a new user + user, err := s.userRepo.Create(user) + + if err != nil { + return nil, err + } + + // Create a new points record for the user + _, err = s.pointsRepo.Create(user.ID) + + if err != nil { + return nil, err + } + + return user, nil + } +// Find all users (used for developing purposes) func (s *UserService) FindAll() (*[]models.User, error) { - return s.repository.FindAll() + return s.userRepo.FindAll() } +// Find a user by their ID (used for developing purposes) func (s *UserService) FindByID(id uint) (*models.User, error) { - return s.repository.FindByID(id) + return s.userRepo.FindByID(id) } From f1e6b36fb0e422d5fb8827dfb853202be53fc099 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Thu, 27 Mar 2025 16:22:28 -0300 Subject: [PATCH 14/36] :card_file_box: feat: add constraints on Points model --- pkg/models/points.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/models/points.go b/pkg/models/points.go index 8e1dced..59957d1 100644 --- a/pkg/models/points.go +++ b/pkg/models/points.go @@ -7,7 +7,7 @@ import ( type Points struct { gorm.Model ID uint `json:"id" gorm:"primary_key"` - UserId uint `json:"user_id"` - User User `gorm:"foreignKey:UserId"` + UserId uint `json:"user_id" gorm:"not null; unique"` + User User `gorm:"foreignKey:UserId; constraint:OnDelete:CASCADE"` Points uint `json:"points"` } \ No newline at end of file From d67c02186dee3e8b1b4c339cabed7464e6c49dcb Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Thu, 27 Mar 2025 16:23:13 -0300 Subject: [PATCH 15/36] :loud_sound: feat: add logs on API startup --- cmd/main.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 1b7bb48..dfcef79 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -19,18 +19,14 @@ import ( func main() { // Loading configuration + log.Println("Loading configuration...") config, err := config.NewConfig() if err != nil { log.Fatal(err) } - // Printing .env variables to see if they are loaded correctly - fmt.Println(config.POSTGRES_HOST) - fmt.Println(config.POSTGRES_PORT) - fmt.Println(config.POSTGRES_USER) - fmt.Println(config.POSTGRES_PASSWORD) - fmt.Println(config.POSTGRES_DB) + log.Println("Configuration loaded!") // Setting up Postgres DSN dsn := fmt.Sprintf(( @@ -43,21 +39,27 @@ func main() { ) // Connecting to the database + log.Println("Connecting to the database...") + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { log.Fatal(err) } - fmt.Println("Connected to the database!") + log.Println("Connected to the database!") // Setting up Migrations + log.Println("Running migrations...") + err = db.AutoMigrate(&models.User{}, &models.Points{}) if err != nil { log.Fatal("Error running migrations", err) } + log.Println("Migrations complete!") + // Setting up Fiber app := fiber.New() app.Use(cors.New()) From 8bf5b4197e524b8e5e17aab065df7a7aa7b14237 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Thu, 27 Mar 2025 19:20:03 -0300 Subject: [PATCH 16/36] :bulb: style: add comments to Points structure --- pkg/models/points.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/models/points.go b/pkg/models/points.go index 59957d1..9b9d1ba 100644 --- a/pkg/models/points.go +++ b/pkg/models/points.go @@ -4,6 +4,7 @@ import ( "gorm.io/gorm" ) +// Structs that represents the points the user gains every time someone access the user's referral link type Points struct { gorm.Model ID uint `json:"id" gorm:"primary_key"` From 691879014b8e9ecbb7ce7758a96cbf259748bc91 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 10:06:29 -0300 Subject: [PATCH 17/36] :sparkles: :wrench: feat: update points logic --- api/dtos/user_dto.go | 8 +++ api/handlers/points.go | 53 ------------------ api/handlers/user.go | 25 +++++---- api/handlers/user_referral.go | 23 ++++++++ api/presenter/points.go | 50 ----------------- api/{presenter => presenters}/user.go | 11 ++-- api/presenters/user_referral.go | 33 +++++++++++ api/routes/points.go | 13 ----- api/routes/user_referral.go | 13 +++++ cmd/main.go | 14 ++--- go.mod | 2 +- pkg/models/points.go | 14 ----- pkg/models/user.go | 7 ++- pkg/models/user_referral.go | 15 +++++ pkg/repositories/points/interfaces.go | 18 ------ .../points/postgres_points_repository.go | 50 ----------------- pkg/repositories/user/interfaces.go | 1 + .../user/postgres_user_repository.go | 8 ++- pkg/repositories/user_referral/interfaces.go | 21 +++++++ .../postgres_user_referral_repository.go | 36 ++++++++++++ pkg/services/points/interfaces.go | 17 ------ pkg/services/points/points_service.go | 29 ---------- pkg/services/user/interfaces.go | 8 ++- pkg/services/user/user_service.go | 56 +++++++++++++++---- pkg/services/user_referral/interfaces.go | 15 +++++ .../user_referral/user_referral_service.go | 32 +++++++++++ pkg/utils/generate_referral_code .go | 7 +++ 27 files changed, 292 insertions(+), 287 deletions(-) create mode 100644 api/dtos/user_dto.go delete mode 100644 api/handlers/points.go create mode 100644 api/handlers/user_referral.go delete mode 100644 api/presenter/points.go rename api/{presenter => presenters}/user.go (86%) create mode 100644 api/presenters/user_referral.go delete mode 100644 api/routes/points.go create mode 100644 api/routes/user_referral.go delete mode 100644 pkg/models/points.go create mode 100644 pkg/models/user_referral.go delete mode 100644 pkg/repositories/points/interfaces.go delete mode 100644 pkg/repositories/points/postgres_points_repository.go create mode 100644 pkg/repositories/user_referral/interfaces.go create mode 100644 pkg/repositories/user_referral/postgres_user_referral_repository.go delete mode 100644 pkg/services/points/interfaces.go delete mode 100644 pkg/services/points/points_service.go create mode 100644 pkg/services/user_referral/interfaces.go create mode 100644 pkg/services/user_referral/user_referral_service.go create mode 100644 pkg/utils/generate_referral_code .go diff --git a/api/dtos/user_dto.go b/api/dtos/user_dto.go new file mode 100644 index 0000000..a805f60 --- /dev/null +++ b/api/dtos/user_dto.go @@ -0,0 +1,8 @@ +package dtos + +type CreateUserDTO struct { + FullName string `json:"full_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + ReferrerCode string `json:"referrer_code"` +} \ No newline at end of file diff --git a/api/handlers/points.go b/api/handlers/points.go deleted file mode 100644 index f95422c..0000000 --- a/api/handlers/points.go +++ /dev/null @@ -1,53 +0,0 @@ -package handlers - -import ( - "gss-backend/api/presenter" - services "gss-backend/pkg/services/points" - "strconv" - - "github.com/gofiber/fiber/v2" -) - -func UpdatePoints(points_service services.IPointsService) fiber.Handler { - return func(c *fiber.Ctx) error { - var requestBody struct { - UserID string `json:"user_id"` - } - - if err := c.BodyParser(&requestBody); err != nil { - c.Status(fiber.StatusBadRequest) - return c.JSON(presenter.PointsErrorResponse(err)) - } - - userIdStr := requestBody.UserID - userId, err := strconv.ParseUint(userIdStr, 10, 32) - - if err != nil { - c.Status(fiber.StatusBadRequest) - return c.JSON(presenter.PointsErrorResponse(err)) - } - - result, err := points_service.Update(uint(userId)) - - if err != nil { - c.Status(fiber.StatusInternalServerError) - return c.JSON(presenter.PointsErrorResponse(err)) - } - - return c.JSON(presenter.PointsSuccessResponse(result)) - } -} - -func FindLeaderboard(points_service services.IPointsService) fiber.Handler { - return func(c *fiber.Ctx) error { - result, err := points_service.FindLeaderboard() - - if err != nil { - c.Status(fiber.StatusInternalServerError) - return c.JSON(presenter.PointsErrorResponse(err)) - } - - return c.JSON(presenter.PointsLeaderboardSuccessResponse(result)) - } -} - diff --git a/api/handlers/user.go b/api/handlers/user.go index d838824..05548db 100644 --- a/api/handlers/user.go +++ b/api/handlers/user.go @@ -2,8 +2,8 @@ package handlers import ( "errors" - "gss-backend/api/presenter" - "gss-backend/pkg/models" + "gss-backend/api/dtos" + "gss-backend/api/presenters" services "gss-backend/pkg/services/user" "strconv" @@ -12,17 +12,18 @@ import ( func CreateUser(user_service services.IUserService) fiber.Handler { return func(c *fiber.Ctx) error { - var requestBody models.User + var requestBody dtos.CreateUserDTO + err := c.BodyParser(&requestBody) if err != nil { c.Status(fiber.StatusBadRequest) - return c.JSON(presenter.UserErrorResponse(err)) + return c.JSON(presenters.UserErrorResponse(err)) } if requestBody.FullName == "" || requestBody.Email == "" || requestBody.PhoneNumber == "" { c.Status(fiber.StatusBadRequest) - return c.JSON(presenter.UserErrorResponse(errors.New( + return c.JSON(presenters.UserErrorResponse(errors.New( "Full Name, Email, and Phone Number are required", ))) } @@ -31,10 +32,10 @@ func CreateUser(user_service services.IUserService) fiber.Handler { if err != nil { c.Status(fiber.StatusInternalServerError) - return c.JSON(presenter.UserErrorResponse(err)) + return c.JSON(presenters.UserErrorResponse(err)) } - return c.JSON(presenter.UserSuccessResponse(result)) + return c.JSON(presenters.UserSuccessResponse(result)) } } @@ -45,10 +46,10 @@ func FindAllUsers(user_service services.IUserService) fiber.Handler { if err != nil { c.Status(fiber.StatusInternalServerError) - return c.JSON(presenter.UserErrorResponse(err)) + return c.JSON(presenters.UserErrorResponse(err)) } - return c.JSON(presenter.UsersSuccessResponse(result)) + return c.JSON(presenters.UsersSuccessResponse(result)) } } @@ -60,16 +61,16 @@ func FindUserByID(user_service services.IUserService) fiber.Handler { if err != nil { c.Status(fiber.StatusBadRequest) - return c.JSON(presenter.UserErrorResponse(err)) + return c.JSON(presenters.UserErrorResponse(err)) } result, err := user_service.FindByID(uint(id)) if err != nil { c.Status(fiber.StatusInternalServerError) - return c.JSON(presenter.UserErrorResponse(err)) + return c.JSON(presenters.UserErrorResponse(err)) } - return c.JSON(presenter.UserSuccessResponse(result)) + return c.JSON(presenters.UserSuccessResponse(result)) } } \ No newline at end of file diff --git a/api/handlers/user_referral.go b/api/handlers/user_referral.go new file mode 100644 index 0000000..961bc65 --- /dev/null +++ b/api/handlers/user_referral.go @@ -0,0 +1,23 @@ +package handlers + +import ( + "gss-backend/api/presenters" + services "gss-backend/pkg/services/user_referral" + + "github.com/gofiber/fiber/v2" +) + + +func FindLeaderboardScores(userReferralService services.IUserReferralService) fiber.Handler { + return func(c *fiber.Ctx) error { + result, err := userReferralService.FindLeaderboardScores() + + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.JSON(presenters.UserReferralErrorResponse(err)) + } + + return c.JSON(presenters.LeaderboardScoreSuceessResponse(result)) + } +} + diff --git a/api/presenter/points.go b/api/presenter/points.go deleted file mode 100644 index d1e4109..0000000 --- a/api/presenter/points.go +++ /dev/null @@ -1,50 +0,0 @@ -package presenter - -import ( - "gss-backend/pkg/models" - - "github.com/gofiber/fiber/v2" -) - -type PointsPresenter struct { - ID uint `json:"id"` - Points uint `json:"points"` -} - -func PointsSuccessResponse(data *models.Points) *fiber.Map { - points := PointsPresenter{ - ID: data.ID, - Points: data.Points, - } - - return &fiber.Map{ - "status": "success", - "data": points, - "error": nil, - } -} - -func PointsLeaderboardSuccessResponse(data *[]models.Points) *fiber.Map { - var points []PointsPresenter - - for _, point := range *data { - points = append(points, PointsPresenter{ - ID: point.ID, - Points: point.Points, - }) - } - - return &fiber.Map{ - "status": "success", - "data": points, - "error": nil, - } -} - -func PointsErrorResponse(err error) *fiber.Map { - return &fiber.Map{ - "status": "error", - "data": nil, - "error": err.Error(), - } -} \ No newline at end of file diff --git a/api/presenter/user.go b/api/presenters/user.go similarity index 86% rename from api/presenter/user.go rename to api/presenters/user.go index 622299f..00fd2b0 100644 --- a/api/presenter/user.go +++ b/api/presenters/user.go @@ -1,4 +1,4 @@ -package presenter +package presenters import ( "gss-backend/pkg/models" @@ -7,11 +7,11 @@ import ( ) // UserPresenter is a struct that is going to be used to present the user data in a controlled way -// As a minor "business logic", I opted to not return the phone number of the user +// As a minor "business logic", I opted to not return the phone number and the email of the user type UserPresenter struct { ID uint `json:"id"` FullName string `json:"full_name"` - Email string `json:"email"` + ReferralCode string `json:"referral_code"` } // Functions to transform the user 'raw' data to presenters @@ -19,7 +19,8 @@ func UserSuccessResponse(data *models.User) *fiber.Map { user := UserPresenter{ ID: data.ID, FullName: data.FullName, - Email: data.Email, + ReferralCode: data.ReferralCode, + } return &fiber.Map{ @@ -36,7 +37,7 @@ func UsersSuccessResponse(data *[]models.User) *fiber.Map { users = append(users, UserPresenter{ ID: user.ID, FullName: user.FullName, - Email: user.Email, + ReferralCode: user.ReferralCode, }) } diff --git a/api/presenters/user_referral.go b/api/presenters/user_referral.go new file mode 100644 index 0000000..33925f1 --- /dev/null +++ b/api/presenters/user_referral.go @@ -0,0 +1,33 @@ +package presenters + +import ( + repo "gss-backend/pkg/repositories/user_referral" + + "github.com/gofiber/fiber/v2" +) + +type PointsPresenter struct { + ID uint `json:"id"` + Points uint `json:"points"` +} + +type LeaderboardScorePresenter struct { + ReferrerId uint `json:"referrer_id"` + ReferralsCount uint `json:"referrals_count"` +} + +func LeaderboardScoreSuceessResponse(data *[]repo.LeaderboardScore) *fiber.Map { + return &fiber.Map{ + "status": "success", + "data": data, + "error": nil, + } +} + +func UserReferralErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": "error", + "data": nil, + "error": err.Error(), + } +} \ No newline at end of file diff --git a/api/routes/points.go b/api/routes/points.go deleted file mode 100644 index d0ce61c..0000000 --- a/api/routes/points.go +++ /dev/null @@ -1,13 +0,0 @@ -package routes - -import ( - "gss-backend/api/handlers" - services "gss-backend/pkg/services/points" - - "github.com/gofiber/fiber/v2" -) - -func PointsRouter(app fiber.Router, points_service services.IPointsService) { - app.Post("/points", handlers.UpdatePoints(points_service)) - app.Get("/points/leaderboard", handlers.FindLeaderboard(points_service)) -} \ No newline at end of file diff --git a/api/routes/user_referral.go b/api/routes/user_referral.go new file mode 100644 index 0000000..0492575 --- /dev/null +++ b/api/routes/user_referral.go @@ -0,0 +1,13 @@ +package routes + +import ( + "gss-backend/api/handlers" + services "gss-backend/pkg/services/user_referral" + + "github.com/gofiber/fiber/v2" +) + + +func UserReferralRouter(app fiber.Router, user_referral_service services.IUserReferralService) { + app.Get("/leaderboard", handlers.FindLeaderboardScores(user_referral_service)) +} \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index dfcef79..73a527c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,10 +5,10 @@ import ( "gss-backend/api/routes" "gss-backend/pkg/config" "gss-backend/pkg/models" - pointsRepo "gss-backend/pkg/repositories/points" userRepo "gss-backend/pkg/repositories/user" - pointsService "gss-backend/pkg/services/points" + userReferralRepo "gss-backend/pkg/repositories/user_referral" userService "gss-backend/pkg/services/user" + userReferralService "gss-backend/pkg/services/user_referral" "log" "github.com/gofiber/fiber/v2" @@ -52,7 +52,7 @@ func main() { // Setting up Migrations log.Println("Running migrations...") - err = db.AutoMigrate(&models.User{}, &models.Points{}) + err = db.AutoMigrate(&models.User{}, &models.UserReferral{}) if err != nil { log.Fatal("Error running migrations", err) @@ -70,15 +70,15 @@ func main() { // Instatiating Repositories userRepo := userRepo.NewPostgresUserRepository(db) - pointsRepo := pointsRepo.NewPostgresPointsRepository(db) + userReferralRepo := userReferralRepo.NewPostgresUserReferralRepository(db) // Instatiating Services - userService := userService.NewUserService(userRepo, pointsRepo) - pointsService := pointsService.NewPointsService(pointsRepo) + userService := userService.NewUserService(userRepo, userReferralRepo) + userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo) api := app.Group("/api") routes.UserRouter(api, userService) - routes.PointsRouter(api, pointsService) + routes.UserReferralRouter(api, userReferralService) log.Fatal(app.Listen(":3000")) diff --git a/go.mod b/go.mod index 66e3618..c1218ca 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.1 require ( github.com/gofiber/fiber/v2 v2.52.6 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 @@ -11,7 +12,6 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect diff --git a/pkg/models/points.go b/pkg/models/points.go deleted file mode 100644 index 9b9d1ba..0000000 --- a/pkg/models/points.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import ( - "gorm.io/gorm" -) - -// Structs that represents the points the user gains every time someone access the user's referral link -type Points struct { - gorm.Model - ID uint `json:"id" gorm:"primary_key"` - UserId uint `json:"user_id" gorm:"not null; unique"` - User User `gorm:"foreignKey:UserId; constraint:OnDelete:CASCADE"` - Points uint `json:"points"` -} \ No newline at end of file diff --git a/pkg/models/user.go b/pkg/models/user.go index 75c8b8f..35f1fa5 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -8,7 +8,8 @@ import ( type User struct { gorm.Model ID uint `json:"id" gorm:"primary_key"` - FullName string `json:"full_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` + FullName string `json:"full_name" gor:"not null"` + Email string `json:"email" gorm:"not null; unique"` + PhoneNumber string `json:"phone_number" gorm:"not null"` + ReferralCode string `json:"referral_code" gorm:"not null; unique"` } \ No newline at end of file diff --git a/pkg/models/user_referral.go b/pkg/models/user_referral.go new file mode 100644 index 0000000..90a9bc5 --- /dev/null +++ b/pkg/models/user_referral.go @@ -0,0 +1,15 @@ +package models + +import ( + "gorm.io/gorm" +) + +// Struct that represents the connection +type UserReferral struct { + gorm.Model + ID uint `json:"id" gorm:"primary_key"` + ReferrerId uint `json:"referrer_id" gorm:"not null"` // The user that referred the other user + Referrer User `gorm:"foreignKey:ReferrerId; constraint:OnDelete:CASCADE"` + ReferredId uint `json:"referred_id" gorm:"not null"` // The user that was referred + Referred User `gorm:"foreignKey:ReferredId; constraint:OnDelete:CASCADE"` +} \ No newline at end of file diff --git a/pkg/repositories/points/interfaces.go b/pkg/repositories/points/interfaces.go deleted file mode 100644 index d32f467..0000000 --- a/pkg/repositories/points/interfaces.go +++ /dev/null @@ -1,18 +0,0 @@ -package repositories - -import ( - "gss-backend/pkg/models" - - "gorm.io/gorm" -) - -type IPointsRepository interface { - Create(user_id uint) (*models.Points, error) - Update(user_id uint) (*models.Points, error) - FindByUserID(user_id uint) (*models.Points, error) - CreateLeaderboard() (*[]models.Points, error) -} - -type PostgresPointsRepository struct { - db *gorm.DB -} \ No newline at end of file diff --git a/pkg/repositories/points/postgres_points_repository.go b/pkg/repositories/points/postgres_points_repository.go deleted file mode 100644 index b99617a..0000000 --- a/pkg/repositories/points/postgres_points_repository.go +++ /dev/null @@ -1,50 +0,0 @@ -package repositories - -import ( - "gss-backend/pkg/models" - - "gorm.io/gorm" -) - -// Concrete implementation of the IPointsRepository interface -func NewPostgresPointsRepository(db *gorm.DB) *PostgresPointsRepository { - return &PostgresPointsRepository{db: db} -} - -func (r *PostgresPointsRepository) Create(user_id uint) (*models.Points, error) { - points := models.Points{UserId: user_id, Points: 1} - result := r.db.Create(&points) - return &points, result.Error -} - -func (r *PostgresPointsRepository) Update(user_id uint) (*models.Points, error) { - var points models.Points - - result := r.db.Where("user_id = ?", user_id).First(&points) - - if result.Error != nil { - return nil, result.Error - } - - points.Points += 1 - - result = r.db.Save(&points) - - if result.Error != nil { - return nil, result.Error - } - - return &points, result.Error -} - -func (r *PostgresPointsRepository) FindByUserID(user_id uint) (*models.Points, error) { - var points models.Points - result := r.db.Where("user_id = ?", user_id).First(&points) - return &points, result.Error -} - -func (r *PostgresPointsRepository) CreateLeaderboard() (*[]models.Points, error) { - var points []models.Points - result := r.db.Order("points desc").Find(&points).Limit(10) - return &points, result.Error -} \ No newline at end of file diff --git a/pkg/repositories/user/interfaces.go b/pkg/repositories/user/interfaces.go index 3ad46da..13bc64f 100644 --- a/pkg/repositories/user/interfaces.go +++ b/pkg/repositories/user/interfaces.go @@ -10,6 +10,7 @@ import ( type IUserRepository interface { FindAll() (*[]models.User, error) FindByID(id uint) (*models.User, error) + FindByReferralCode(referralCode string) (*models.User, error) Create(user *models.User) (*models.User, error) } diff --git a/pkg/repositories/user/postgres_user_repository.go b/pkg/repositories/user/postgres_user_repository.go index b9f137d..ddcf0d5 100644 --- a/pkg/repositories/user/postgres_user_repository.go +++ b/pkg/repositories/user/postgres_user_repository.go @@ -26,4 +26,10 @@ func (r *PostgresUserRepository) FindByID(id uint) (*models.User, error) { var user models.User result := r.db.First(&user, id) return &user, result.Error -} \ No newline at end of file +} + +func (r *PostgresUserRepository) FindByReferralCode(referralCode string) (*models.User, error) { + var user models.User + result := r.db.Where("referral_code = ?", referralCode).First(&user) + return &user, result.Error +} \ No newline at end of file diff --git a/pkg/repositories/user_referral/interfaces.go b/pkg/repositories/user_referral/interfaces.go new file mode 100644 index 0000000..48a5c81 --- /dev/null +++ b/pkg/repositories/user_referral/interfaces.go @@ -0,0 +1,21 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +type IUserReferralRepository interface { + Create(referrerId uint, referredId uint) (*models.UserReferral, error) + FindLeaderboard() (*[]LeaderboardScore, error) +} + +type PostgresUserReferralRepository struct { + db *gorm.DB +} + +type LeaderboardScore struct { + ReferrerId uint + ReferralsCount int +} \ No newline at end of file diff --git a/pkg/repositories/user_referral/postgres_user_referral_repository.go b/pkg/repositories/user_referral/postgres_user_referral_repository.go new file mode 100644 index 0000000..555775d --- /dev/null +++ b/pkg/repositories/user_referral/postgres_user_referral_repository.go @@ -0,0 +1,36 @@ +package repositories + +import ( + "gss-backend/pkg/models" + + "gorm.io/gorm" +) + +// Concrete implementation of the IPointsRepository interface +func NewPostgresUserReferralRepository(db *gorm.DB) *PostgresUserReferralRepository { + return &PostgresUserReferralRepository{db: db} +} + +// Create a new user referral +func (r *PostgresUserReferralRepository) Create(referrerId uint, referredId uint) (*models.UserReferral, error) { + userReferral := models.UserReferral{ReferrerId: referrerId, ReferredId: referredId} + result := r.db.Create(&userReferral) + return &userReferral, result.Error + +} + +// Find the top 10 users with the most referrals +func (r *PostgresUserReferralRepository) FindLeaderboard() (*[]LeaderboardScore, error) { + var leaderboardScores []LeaderboardScore + + result := r.db.Table("user_referrals"). + Select("referrer_id, COUNT(*) as referrals_count"). + Group("referrer_id"). + Order("referrals_count DESC"). + Limit(10). + Find(&leaderboardScores) + + + return &leaderboardScores, result.Error +} + diff --git a/pkg/services/points/interfaces.go b/pkg/services/points/interfaces.go deleted file mode 100644 index 8a85e4f..0000000 --- a/pkg/services/points/interfaces.go +++ /dev/null @@ -1,17 +0,0 @@ -package services - -import ( - "gss-backend/pkg/models" - repositories "gss-backend/pkg/repositories/points" -) - -type IPointsService interface { - Create(user_id uint) (*models.Points, error) - Update(user_id uint) (*models.Points, error) - FindByUserID(user_id uint) (*models.Points, error) - FindLeaderboard() (*[]models.Points, error) -} - -type PointsService struct { - repository repositories.IPointsRepository -} \ No newline at end of file diff --git a/pkg/services/points/points_service.go b/pkg/services/points/points_service.go deleted file mode 100644 index 0c50209..0000000 --- a/pkg/services/points/points_service.go +++ /dev/null @@ -1,29 +0,0 @@ -package services - -import ( - "gss-backend/pkg/models" - repositories "gss-backend/pkg/repositories/points" -) - -func NewPointsService(repository repositories.IPointsRepository) *PointsService { - return &PointsService{ - repository: repository, - } -} - -func (s *PointsService) Create(user_id uint) (*models.Points, error) { - return s.repository.Create(user_id) -} - -func (s *PointsService) Update(user_id uint) (*models.Points, error) { - return s.repository.Update(user_id) -} - - -func (s *PointsService) FindByUserID(user_id uint) (*models.Points, error) { - return s.repository.FindByUserID(user_id) -} - -func (s *PointsService) FindLeaderboard() (*[]models.Points, error) { - return s.repository.CreateLeaderboard() -} \ No newline at end of file diff --git a/pkg/services/user/interfaces.go b/pkg/services/user/interfaces.go index 7e30c40..34560b1 100644 --- a/pkg/services/user/interfaces.go +++ b/pkg/services/user/interfaces.go @@ -1,18 +1,20 @@ package services import ( + "gss-backend/api/dtos" "gss-backend/pkg/models" - pointsRepo "gss-backend/pkg/repositories/points" userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" ) type IUserService interface { FindAll() (*[]models.User, error) FindByID(id uint) (*models.User, error) - Create(user *models.User) (*models.User, error) + FindByReferralCode(referralCode string) (*models.User, error) + Create(userDto *dtos.CreateUserDTO) (*models.User, error) } type UserService struct { - pointsRepo pointsRepo.IPointsRepository userRepo userRepo.IUserRepository + userReferralRepo userReferralRepo.IUserReferralRepository } \ No newline at end of file diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index 6bb7a60..11fb3b2 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -1,37 +1,66 @@ package services import ( + "gss-backend/api/dtos" "gss-backend/pkg/models" - pointsRepo "gss-backend/pkg/repositories/points" userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + "gss-backend/pkg/utils" ) - -func NewUserService(userRepo userRepo.IUserRepository, pointsRepo pointsRepo.IPointsRepository) *UserService { +// Instatiate a new UserService +func NewUserService(userRepo userRepo.IUserRepository, userReferralRepo userReferralRepo.IUserReferralRepository) *UserService { return &UserService{ userRepo: userRepo, - pointsRepo: pointsRepo, + userReferralRepo: userReferralRepo, } } // Register a new user and create a new points record for the user -func (s *UserService) Create(user *models.User) (*models.User, error) { - // Create a new user - user, err := s.userRepo.Create(user) +func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) { + // Generate new referral code for the user + referralCode := utils.GenerateReferralCode() + + + // Create a new user model + newUser := models.User{ + FullName: userDto.FullName, + Email: userDto.Email, + PhoneNumber: userDto.PhoneNumber, + ReferralCode: referralCode, + } + + // Create a new user record in the database + createdUser, err := s.userRepo.Create(&newUser) if err != nil { return nil, err } - // Create a new points record for the user - _, err = s.pointsRepo.Create(user.ID) + // Create user referral record for the registered user + _, err = s.userReferralRepo.Create(createdUser.ID, createdUser.ID) if err != nil { return nil, err } - return user, nil - + // If user has a referral code, set another record for the referrer + if userDto.ReferrerCode != "" { + referrerUser, err := s.userRepo.FindByReferralCode(userDto.ReferrerCode) + + if err != nil { + return nil, err + } + + _, err = s.userReferralRepo.Create(referrerUser.ID, createdUser.ID) + + if err != nil { + return nil, err + } + + } + + return createdUser, nil } // Find all users (used for developing purposes) @@ -44,6 +73,11 @@ func (s *UserService) FindByID(id uint) (*models.User, error) { return s.userRepo.FindByID(id) } +// Find a user by their referral code +func (s *UserService) FindByReferralCode(referralCode string) (*models.User, error) { + return s.userRepo.FindByReferralCode(referralCode) +} + diff --git a/pkg/services/user_referral/interfaces.go b/pkg/services/user_referral/interfaces.go new file mode 100644 index 0000000..bf90771 --- /dev/null +++ b/pkg/services/user_referral/interfaces.go @@ -0,0 +1,15 @@ +package services + +import ( + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" +) + +type IUserReferralService interface { + FindLeaderboardScores() (*[]userReferralRepo.LeaderboardScore, error) +} + +type UserReferralService struct { + userReferralRepo userReferralRepo.IUserReferralRepository + userRepo userRepo.IUserRepository +} \ No newline at end of file diff --git a/pkg/services/user_referral/user_referral_service.go b/pkg/services/user_referral/user_referral_service.go new file mode 100644 index 0000000..8e8826a --- /dev/null +++ b/pkg/services/user_referral/user_referral_service.go @@ -0,0 +1,32 @@ +package services + +import ( + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" +) + + + +func NewUserReferralService(userRepo userRepo.IUserRepository, userReferralRepo userReferralRepo.IUserReferralRepository) IUserReferralService { + return &UserReferralService{ + userRepo: userRepo, + userReferralRepo: userReferralRepo, + } +} + +// Function that calculates the appropiate score for the leaderboard +func (s *UserReferralService) FindLeaderboardScores() (*[]userReferralRepo.LeaderboardScore, error) { + // Find the top 10 users with the most referrals + leaderboardScores, err := s.userReferralRepo.FindLeaderboard() + + if err != nil { + return nil, err + } + + // Add 1 point to each Referral count + for _, score := range *leaderboardScores { + score.ReferralsCount += 1 + } + + return leaderboardScores, nil +} diff --git a/pkg/utils/generate_referral_code .go b/pkg/utils/generate_referral_code .go new file mode 100644 index 0000000..ade3080 --- /dev/null +++ b/pkg/utils/generate_referral_code .go @@ -0,0 +1,7 @@ +package utils + +import "github.com/google/uuid" + +func GenerateReferralCode() string { + return uuid.NewString() +} \ No newline at end of file From 69bf09ca9aaa8d7c7a51bd1454332c2558053963 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 10:21:24 -0300 Subject: [PATCH 18/36] :see_no_evil: build: add .gitignore --- .env => .env.example | 0 .gitignore | 1 + 2 files changed, 1 insertion(+) rename .env => .env.example (100%) create mode 100644 .gitignore diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file From 3fbb8ba4f42f82b41186d881e27baf5731c6e1a9 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 10:36:54 -0300 Subject: [PATCH 19/36] :rocket: feat: add FIBER_PORT configuration and update server startup logic --- .env.example | 3 ++- cmd/main.go | 5 ++++- pkg/config/env.go | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index f758683..3ab04b5 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USER=root POSTGRES_PASSWORD=root -POSTGRES_DB=gss-db \ No newline at end of file +POSTGRES_DB=gss-db +FIBER_PORT=3000 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 73a527c..7322485 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -76,11 +76,14 @@ func main() { userService := userService.NewUserService(userRepo, userReferralRepo) userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo) + // Setting up routes api := app.Group("/api") routes.UserRouter(api, userService) routes.UserReferralRouter(api, userReferralService) - log.Fatal(app.Listen(":3000")) + // Starting the server + port := fmt.Sprintf(":%s", config.FIBER_PORT) + log.Fatal(app.Listen(port)) } \ No newline at end of file diff --git a/pkg/config/env.go b/pkg/config/env.go index a62e51e..44c537e 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -15,6 +15,7 @@ type Config struct { POSTGRES_USER string POSTGRES_PASSWORD string POSTGRES_DB string + FIBER_PORT int } func NewConfig() (*Config, error) { @@ -29,6 +30,7 @@ func NewConfig() (*Config, error) { POSTGRES_USER: getEnv("POSTGRES_USER", "root"), POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), + FIBER_PORT: getEnvAsInt("FIBER_PORT", 3000), } From ebe7dd35a03ac56462f5f07bccfa8d099ab659d8 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 10:56:19 -0300 Subject: [PATCH 20/36] :bug: feat: fix FIBER_PORT import from .env --- pkg/config/env.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/config/env.go b/pkg/config/env.go index 44c537e..6635668 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -14,8 +14,8 @@ type Config struct { POSTGRES_PORT int POSTGRES_USER string POSTGRES_PASSWORD string - POSTGRES_DB string - FIBER_PORT int + POSTGRES_DB string + FIBER_PORT string } func NewConfig() (*Config, error) { @@ -30,7 +30,7 @@ func NewConfig() (*Config, error) { POSTGRES_USER: getEnv("POSTGRES_USER", "root"), POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), - FIBER_PORT: getEnvAsInt("FIBER_PORT", 3000), + FIBER_PORT: getEnv("FIBER_PORT", "3000"), } From 3696d5da3fa9dd64e6f30c8d9d0ea455a7054ff4 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 10:56:55 -0300 Subject: [PATCH 21/36] :whale: build: add Dockerfile and docker compose --- .dockerignore | 1 + .gitignore | 3 ++- Dockerfile | 20 ++++++++++++++++++++ docker-compose.yaml | 34 ++++++++++++++++++++++++++++++++++ notes.txt | 2 +- 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..347e267 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +volumes/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2eea525..1580bc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +volumes/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3acba58 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Setting up my base image +FROM golang:1.24.1-alpine + +# Set the Current Working Directory inside the container +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Get all dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Expose port 3000 +EXPOSE 3000 + +# Run the application +CMD ["go", "run", "cmd/main.go"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ac999c1 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,34 @@ +services: + gss-api: + container_name: gss-api + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + restart: always + env_file: + - .env + depends_on: + gss-postgres-db: + condition: service_healthy + + gss-postgres-db: + container_name: gss-postgres-db + image: postgres:13 + ports: + - "5432:5432" + restart: always + env_file: + - .env + volumes: + - ./volumes/gss-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + gss-db: + driver: local diff --git a/notes.txt b/notes.txt index 6498181..c3b0bae 100644 --- a/notes.txt +++ b/notes.txt @@ -1 +1 @@ -docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres +docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres:13 From 28a97eb2a6bdbe4af31d4829e09efb1461995b9d Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Fri, 28 Mar 2025 14:26:53 -0300 Subject: [PATCH 22/36] :sparkles: feat: create email service and send email on user register --- cmd/main.go | 11 ++++- go.mod | 2 + go.sum | 4 ++ pkg/config/env.go | 10 +++++ pkg/services/email/email_service.go | 62 +++++++++++++++++++++++++++++ pkg/services/email/interfaces.go | 19 +++++++++ pkg/services/user/interfaces.go | 2 + pkg/services/user/user_service.go | 15 ++++++- 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 pkg/services/email/email_service.go create mode 100644 pkg/services/email/interfaces.go diff --git a/cmd/main.go b/cmd/main.go index 7322485..10fe65b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,6 +7,7 @@ import ( "gss-backend/pkg/models" userRepo "gss-backend/pkg/repositories/user" userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" userService "gss-backend/pkg/services/user" userReferralService "gss-backend/pkg/services/user_referral" "log" @@ -73,7 +74,15 @@ func main() { userReferralRepo := userReferralRepo.NewPostgresUserReferralRepository(db) // Instatiating Services - userService := userService.NewUserService(userRepo, userReferralRepo) + emailConfig := emailService.EmailConfig{ + SMTPEmail: config.SMTP_EMAIL, + SMTPHost: config.SMTP_HOST, + SMTPPort: config.SMTP_PORT, + SMTPUser: config.SMTP_USER, + SMTPPassword: config.SMTP_PASSWORD, + } + emailService := emailService.NewEmailService(emailConfig) + userService := userService.NewUserService(userRepo, userReferralRepo, emailService) userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo) // Setting up routes diff --git a/go.mod b/go.mod index c1218ca..e46d689 100644 --- a/go.mod +++ b/go.mod @@ -30,4 +30,6 @@ require ( golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.23.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/mail.v2 v2.3.1 // indirect ) diff --git a/go.sum b/go.sum index b000cd0..a1573d8 100644 --- a/go.sum +++ b/go.sum @@ -55,7 +55,11 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/env.go b/pkg/config/env.go index 6635668..8870633 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -16,6 +16,11 @@ type Config struct { POSTGRES_PASSWORD string POSTGRES_DB string FIBER_PORT string + SMTP_EMAIL string + SMTP_HOST string + SMTP_PORT int + SMTP_USER string + SMTP_PASSWORD string } func NewConfig() (*Config, error) { @@ -31,6 +36,11 @@ func NewConfig() (*Config, error) { POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), FIBER_PORT: getEnv("FIBER_PORT", "3000"), + SMTP_EMAIL: getEnv("SMTP_EMAIL", ""), + SMTP_HOST: getEnv("SMTP_HOST", ""), + SMTP_PORT: getEnvAsInt("SMTP_PORT", 587), + SMTP_USER: getEnv("SMTP_USER", ""), + SMTP_PASSWORD: getEnv("SMTP_PASSWORD", ""), } diff --git a/pkg/services/email/email_service.go b/pkg/services/email/email_service.go new file mode 100644 index 0000000..776ef77 --- /dev/null +++ b/pkg/services/email/email_service.go @@ -0,0 +1,62 @@ +package services + +import ( + gomail "gopkg.in/mail.v2" +) + +// Instantiate a new EmailService +func NewEmailService(emailConfig EmailConfig) *EmailService { + return &EmailService{ + EmailConfig: emailConfig, + } +} + +// Send a welcome email to the user upon registration +func (s *EmailService) SendWelcomeEmail(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Welcome to GSS Eco News!") + + // Setting email body + message.SetBody("text/plain", "Welcome to GSS Eco News! We are excited to have you on board!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPUser, + s.EmailConfig.SMTPPassword, + ) + + // Sending the email + return dialer.DialAndSend(message) +} + +// Send email to user when someones is registered using their referral link +func (s *EmailService) SendReferralLinkAccess(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Someone has registered using your referral link!") + + // Setting email body + message.SetBody("text/plain", "Someone has registered using your referral link! You have earned 1 point in the competition!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPUser, + s.EmailConfig.SMTPPassword, + ) + + // Sending the email + return dialer.DialAndSend(message) +} \ No newline at end of file diff --git a/pkg/services/email/interfaces.go b/pkg/services/email/interfaces.go new file mode 100644 index 0000000..35909a3 --- /dev/null +++ b/pkg/services/email/interfaces.go @@ -0,0 +1,19 @@ +package services + + +type IEmailService interface { + SendWelcomeEmail(email string) error + SendReferralLinkAccess(email string) error +} + +type EmailConfig struct { + SMTPEmail string + SMTPHost string + SMTPPort int + SMTPUser string + SMTPPassword string +} + +type EmailService struct { + EmailConfig EmailConfig +} diff --git a/pkg/services/user/interfaces.go b/pkg/services/user/interfaces.go index 34560b1..6e7b4ad 100644 --- a/pkg/services/user/interfaces.go +++ b/pkg/services/user/interfaces.go @@ -5,6 +5,7 @@ import ( "gss-backend/pkg/models" userRepo "gss-backend/pkg/repositories/user" userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" ) type IUserService interface { @@ -17,4 +18,5 @@ type IUserService interface { type UserService struct { userRepo userRepo.IUserRepository userReferralRepo userReferralRepo.IUserReferralRepository + emailService emailService.IEmailService } \ No newline at end of file diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index 11fb3b2..f72b3a8 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -5,14 +5,20 @@ import ( "gss-backend/pkg/models" userRepo "gss-backend/pkg/repositories/user" userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" "gss-backend/pkg/utils" ) // Instatiate a new UserService -func NewUserService(userRepo userRepo.IUserRepository, userReferralRepo userReferralRepo.IUserReferralRepository) *UserService { +func NewUserService( + userRepo userRepo.IUserRepository, + userReferralRepo userReferralRepo.IUserReferralRepository, + emailService emailService.IEmailService ) *UserService { return &UserService{ userRepo: userRepo, userReferralRepo: userReferralRepo, + emailService: emailService, + } } @@ -37,6 +43,13 @@ func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) return nil, err } + // Send welcome email to the user + err = s.emailService.SendWelcomeEmail(createdUser.Email) + + if err != nil { + return nil, err + } + // Create user referral record for the registered user _, err = s.userReferralRepo.Create(createdUser.ID, createdUser.ID) From f9bbb8e0bf2ec7ce728e55a83861b2e279a8993b Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Sat, 29 Mar 2025 18:05:11 -0300 Subject: [PATCH 23/36] :sparkles: feat: use gmail as smtp --- cmd/main.go | 3 +-- notes.txt | 2 +- pkg/config/env.go | 3 +-- pkg/services/email/email_service.go | 4 ++-- pkg/services/email/interfaces.go | 1 - pkg/services/user/user_service.go | 7 +++++++ 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 10fe65b..22eb338 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -75,10 +75,9 @@ func main() { // Instatiating Services emailConfig := emailService.EmailConfig{ - SMTPEmail: config.SMTP_EMAIL, SMTPHost: config.SMTP_HOST, SMTPPort: config.SMTP_PORT, - SMTPUser: config.SMTP_USER, + SMTPEmail: config.SMTP_EMAIL, SMTPPassword: config.SMTP_PASSWORD, } emailService := emailService.NewEmailService(emailConfig) diff --git a/notes.txt b/notes.txt index c3b0bae..2d07e7d 100644 --- a/notes.txt +++ b/notes.txt @@ -1 +1 @@ -docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres:13 +docker run --name gss-dev-db -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=gss-db -p 5432:5432 -d postgres:13 \ No newline at end of file diff --git a/pkg/config/env.go b/pkg/config/env.go index 8870633..32439b9 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -37,9 +37,8 @@ func NewConfig() (*Config, error) { POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), FIBER_PORT: getEnv("FIBER_PORT", "3000"), SMTP_EMAIL: getEnv("SMTP_EMAIL", ""), - SMTP_HOST: getEnv("SMTP_HOST", ""), SMTP_PORT: getEnvAsInt("SMTP_PORT", 587), - SMTP_USER: getEnv("SMTP_USER", ""), + SMTP_HOST: getEnv("SMTP_HOST", ""), SMTP_PASSWORD: getEnv("SMTP_PASSWORD", ""), } diff --git a/pkg/services/email/email_service.go b/pkg/services/email/email_service.go index 776ef77..cdf0866 100644 --- a/pkg/services/email/email_service.go +++ b/pkg/services/email/email_service.go @@ -28,7 +28,7 @@ func (s *EmailService) SendWelcomeEmail(email string) error { dialer := gomail.NewDialer( s.EmailConfig.SMTPHost, s.EmailConfig.SMTPPort, - s.EmailConfig.SMTPUser, + s.EmailConfig.SMTPEmail, s.EmailConfig.SMTPPassword, ) @@ -53,7 +53,7 @@ func (s *EmailService) SendReferralLinkAccess(email string) error { dialer := gomail.NewDialer( s.EmailConfig.SMTPHost, s.EmailConfig.SMTPPort, - s.EmailConfig.SMTPUser, + s.EmailConfig.SMTPEmail, s.EmailConfig.SMTPPassword, ) diff --git a/pkg/services/email/interfaces.go b/pkg/services/email/interfaces.go index 35909a3..2505cb7 100644 --- a/pkg/services/email/interfaces.go +++ b/pkg/services/email/interfaces.go @@ -10,7 +10,6 @@ type EmailConfig struct { SMTPEmail string SMTPHost string SMTPPort int - SMTPUser string SMTPPassword string } diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index f72b3a8..5506b01 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -70,6 +70,13 @@ func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) if err != nil { return nil, err } + + // Send email to the referrer user + err = s.emailService.SendReferralLinkAccess(referrerUser.Email) + + if err != nil { + return nil, err + } } From 27102b656f36524677473e24e80b889dc8076806 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 11:49:54 -0300 Subject: [PATCH 24/36] :wrench: build: change .env.example variables --- .env.example | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 3ab04b5..2581b7d 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,15 @@ -POSTGRES_HOST=localhost +# Postgres configuration +POSTGRES_HOST=gss-postgres-db POSTGRES_PORT=5432 POSTGRES_USER=root POSTGRES_PASSWORD=root POSTGRES_DB=gss-db -FIBER_PORT=3000 \ No newline at end of file + +# Fiber configuration +FIBER_PORT=3000 + +# SMTP configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_EMAIL= +SMTP_PASSWORD= \ No newline at end of file From b19d1a6b850d429189de7e80bda52d43c3463675 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 18:33:41 -0300 Subject: [PATCH 25/36] :sparkles: feat: alter leaderboard query to add user full name --- pkg/repositories/user_referral/interfaces.go | 1 + .../user_referral/postgres_user_referral_repository.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/repositories/user_referral/interfaces.go b/pkg/repositories/user_referral/interfaces.go index 48a5c81..c236e36 100644 --- a/pkg/repositories/user_referral/interfaces.go +++ b/pkg/repositories/user_referral/interfaces.go @@ -17,5 +17,6 @@ type PostgresUserReferralRepository struct { type LeaderboardScore struct { ReferrerId uint + FullName string ReferralsCount int } \ No newline at end of file diff --git a/pkg/repositories/user_referral/postgres_user_referral_repository.go b/pkg/repositories/user_referral/postgres_user_referral_repository.go index 555775d..4d2080b 100644 --- a/pkg/repositories/user_referral/postgres_user_referral_repository.go +++ b/pkg/repositories/user_referral/postgres_user_referral_repository.go @@ -24,8 +24,9 @@ func (r *PostgresUserReferralRepository) FindLeaderboard() (*[]LeaderboardScore, var leaderboardScores []LeaderboardScore result := r.db.Table("user_referrals"). - Select("referrer_id, COUNT(*) as referrals_count"). - Group("referrer_id"). + Select("user_referrals.referrer_id, users.full_name, COUNT(user_referrals.referrer_id) as referrals_count"). + Joins("JOIN users ON users.id = user_referrals.referrer_id"). + Group("user_referrals.referrer_id, users.full_name"). Order("referrals_count DESC"). Limit(10). Find(&leaderboardScores) From afeb386f956273bca3ac07e28652ef7c1dace9b1 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 18:46:00 -0300 Subject: [PATCH 26/36] :bug: feat: now properly using user_referral presenter --- api/presenters/user_referral.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/presenters/user_referral.go b/api/presenters/user_referral.go index 33925f1..4305bcf 100644 --- a/api/presenters/user_referral.go +++ b/api/presenters/user_referral.go @@ -13,13 +13,23 @@ type PointsPresenter struct { type LeaderboardScorePresenter struct { ReferrerId uint `json:"referrer_id"` + FullName string `json:"full_name"` ReferralsCount uint `json:"referrals_count"` } func LeaderboardScoreSuceessResponse(data *[]repo.LeaderboardScore) *fiber.Map { + var leaderboardScores []LeaderboardScorePresenter + + for _, score := range *data { + leaderboardScores = append(leaderboardScores, LeaderboardScorePresenter{ + ReferrerId: score.ReferrerId, + FullName: score.FullName, + ReferralsCount: score.ReferralsCount, + }) + } return &fiber.Map{ "status": "success", - "data": data, + "data": leaderboardScores, "error": nil, } } From c391d8e7e31e013fea5b51e574aec2e37b98bfd8 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 18:46:59 -0300 Subject: [PATCH 27/36] :wrench: refactor: change LeaderboardScore struct --- pkg/repositories/user_referral/interfaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/repositories/user_referral/interfaces.go b/pkg/repositories/user_referral/interfaces.go index c236e36..68e6d4e 100644 --- a/pkg/repositories/user_referral/interfaces.go +++ b/pkg/repositories/user_referral/interfaces.go @@ -18,5 +18,5 @@ type PostgresUserReferralRepository struct { type LeaderboardScore struct { ReferrerId uint FullName string - ReferralsCount int + ReferralsCount uint } \ No newline at end of file From 9f0e12f855b0b2fbbc8f34038cead625f5d899f0 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 20:21:53 -0300 Subject: [PATCH 28/36] :whale: feat: new docker compose to start frontend app --- .gitignore | 1 + docker-compose.yaml | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1580bc3..cde8a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env +.env.production volumes/ \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index ac999c1..9e582c2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,13 +5,15 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - "3001:3001" restart: always env_file: - - .env + - .env.production depends_on: gss-postgres-db: condition: service_healthy + networks: + - gss-network gss-postgres-db: container_name: gss-postgres-db @@ -20,7 +22,7 @@ services: - "5432:5432" restart: always env_file: - - .env + - .env.production volumes: - ./volumes/gss-db:/var/lib/postgresql/data healthcheck: @@ -28,7 +30,26 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - gss-network + + gss-frontend: + container_name: gss-frontend + build: + context: ../gss-frontend + dockerfile: ../gss-frontend/Dockerfile + ports: + - "3000:3000" + restart: always + env_file: + - ../gss-frontend/.env.production + networks: + - gss-network volumes: gss-db: driver: local + +networks: + gss-network: + driver: bridge From f162008c82d89ed7ae710fa6b565549575b4499a Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Mon, 31 Mar 2025 20:30:40 -0300 Subject: [PATCH 29/36] :fire: refactor: delete one more counter in leaderboard calculation --- pkg/services/user_referral/user_referral_service.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/services/user_referral/user_referral_service.go b/pkg/services/user_referral/user_referral_service.go index 8e8826a..cd630a5 100644 --- a/pkg/services/user_referral/user_referral_service.go +++ b/pkg/services/user_referral/user_referral_service.go @@ -23,10 +23,5 @@ func (s *UserReferralService) FindLeaderboardScores() (*[]userReferralRepo.Leade return nil, err } - // Add 1 point to each Referral count - for _, score := range *leaderboardScores { - score.ReferralsCount += 1 - } - return leaderboardScores, nil } From 3874df47e45f696d1cc3b138569362566e1fba21 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 00:35:10 -0300 Subject: [PATCH 30/36] :sparkles: feat: add email to leaderboard users and send emails asynchronously --- cmd/main.go | 2 +- pkg/services/email/email_service.go | 25 +++++++++++ pkg/services/email/interfaces.go | 1 + pkg/services/user/user_service.go | 28 +++++++----- pkg/services/user_referral/interfaces.go | 2 + .../user_referral/user_referral_service.go | 45 ++++++++++++++++++- 6 files changed, 89 insertions(+), 14 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 22eb338..cb884a0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -82,7 +82,7 @@ func main() { } emailService := emailService.NewEmailService(emailConfig) userService := userService.NewUserService(userRepo, userReferralRepo, emailService) - userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo) + userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo, emailService) // Setting up routes api := app.Group("/api") diff --git a/pkg/services/email/email_service.go b/pkg/services/email/email_service.go index cdf0866..0053cc4 100644 --- a/pkg/services/email/email_service.go +++ b/pkg/services/email/email_service.go @@ -57,6 +57,31 @@ func (s *EmailService) SendReferralLinkAccess(email string) error { s.EmailConfig.SMTPPassword, ) + // Sending the email + return dialer.DialAndSend(message) +} + +// Send email to user when they are in the top 10 of the leaderboard +func (s *EmailService) SendLeaderboardEmail(email string) error { + // Creating a new message + message := gomail.NewMessage() + + // Setting email headers + message.SetHeader("From", s.EmailConfig.SMTPEmail) + message.SetHeader("To", email) + message.SetHeader("Subject", "Congratulations! You are in the top 10 of the leaderboard!") + + // Setting email body + message.SetBody("text/plain", "Congratulations! You are in the top 10 of the leaderboard! Thanks for participating the competition!") + + // Setting up SMTP configuration + dialer := gomail.NewDialer( + s.EmailConfig.SMTPHost, + s.EmailConfig.SMTPPort, + s.EmailConfig.SMTPEmail, + s.EmailConfig.SMTPPassword, + ) + // Sending the email return dialer.DialAndSend(message) } \ No newline at end of file diff --git a/pkg/services/email/interfaces.go b/pkg/services/email/interfaces.go index 2505cb7..22ace25 100644 --- a/pkg/services/email/interfaces.go +++ b/pkg/services/email/interfaces.go @@ -4,6 +4,7 @@ package services type IEmailService interface { SendWelcomeEmail(email string) error SendReferralLinkAccess(email string) error + SendLeaderboardEmail(email string) error } type EmailConfig struct { diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index 5506b01..ee45823 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -1,6 +1,7 @@ package services import ( + "fmt" "gss-backend/api/dtos" "gss-backend/pkg/models" userRepo "gss-backend/pkg/repositories/user" @@ -43,12 +44,15 @@ func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) return nil, err } - // Send welcome email to the user - err = s.emailService.SendWelcomeEmail(createdUser.Email) + // Send welcome email to the user asynchronously + go func() { + err := s.emailService.SendWelcomeEmail(createdUser.Email) - if err != nil { - return nil, err - } + if err != nil { + fmt.Printf("Failed to send welcome email to %s: %v\n", createdUser.Email, err) + } + }() + // Create user referral record for the registered user _, err = s.userReferralRepo.Create(createdUser.ID, createdUser.ID) @@ -71,13 +75,13 @@ func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) return nil, err } - // Send email to the referrer user - err = s.emailService.SendReferralLinkAccess(referrerUser.Email) - - if err != nil { - return nil, err - } - + // Send email to the referrer user asynchronously + go func() { + // Rewrote in another way so I can remind this also works hehe + if err := s.emailService.SendReferralLinkAccess(referrerUser.Email); err != nil { + fmt.Printf("Error sending referral link access email to %s: %v\n", referrerUser.Email, err) + } + }() } return createdUser, nil diff --git a/pkg/services/user_referral/interfaces.go b/pkg/services/user_referral/interfaces.go index bf90771..7c864d5 100644 --- a/pkg/services/user_referral/interfaces.go +++ b/pkg/services/user_referral/interfaces.go @@ -3,6 +3,7 @@ package services import ( userRepo "gss-backend/pkg/repositories/user" userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" ) type IUserReferralService interface { @@ -12,4 +13,5 @@ type IUserReferralService interface { type UserReferralService struct { userReferralRepo userReferralRepo.IUserReferralRepository userRepo userRepo.IUserRepository + emailService emailService.IEmailService } \ No newline at end of file diff --git a/pkg/services/user_referral/user_referral_service.go b/pkg/services/user_referral/user_referral_service.go index cd630a5..0c06d29 100644 --- a/pkg/services/user_referral/user_referral_service.go +++ b/pkg/services/user_referral/user_referral_service.go @@ -1,16 +1,21 @@ package services import ( + "fmt" userRepo "gss-backend/pkg/repositories/user" userReferralRepo "gss-backend/pkg/repositories/user_referral" + emailService "gss-backend/pkg/services/email" ) -func NewUserReferralService(userRepo userRepo.IUserRepository, userReferralRepo userReferralRepo.IUserReferralRepository) IUserReferralService { +func NewUserReferralService(userRepo userRepo.IUserRepository, + userReferralRepo userReferralRepo.IUserReferralRepository, + emailService emailService.IEmailService) IUserReferralService { return &UserReferralService{ userRepo: userRepo, userReferralRepo: userReferralRepo, + emailService: emailService, } } @@ -23,5 +28,43 @@ func (s *UserReferralService) FindLeaderboardScores() (*[]userReferralRepo.Leade return nil, err } + // Instatiating error channel + errChan := make(chan error, len(*leaderboardScores)) + + // For every user in the leaderboard, send an email to say he is in the leaderboard + for _, leaderboardScore := range *leaderboardScores { + // Get user data and send email asynchronously + // If any step causes an error, send the error to the error channel + go func(leaderboardScore userReferralRepo.LeaderboardScore) { + user, err := s.userRepo.FindByID(leaderboardScore.ReferrerId) + + if err != nil { + errChan <- fmt.Errorf("Failed to find user with ID %d: %v", leaderboardScore.ReferrerId, err) + return + } + + err = s.emailService.SendLeaderboardEmail(user.Email) + + if err != nil { + errChan <- fmt.Errorf("Failed to send leaderboard email to %s: %v", user.Email, err) + return + } + + errChan <- nil // Signal that the email was sent successfully + }(leaderboardScore) + + } + + // Processing errors in another goroutine + go func() { + for range *leaderboardScores { + if err := <-errChan; err != nil { + fmt.Println("Error processing leaderboard email: ", err) + } + } + + close(errChan) + }() + return leaderboardScores, nil } From f3191c8aea8c89b35d35e5a0ab803a06f0d4a2b7 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 01:14:41 -0300 Subject: [PATCH 31/36] :sparkles: feat: validate if user is already registered by email --- pkg/repositories/user/interfaces.go | 1 + pkg/repositories/user/postgres_user_repository.go | 6 ++++++ pkg/services/user/user_service.go | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/pkg/repositories/user/interfaces.go b/pkg/repositories/user/interfaces.go index 13bc64f..01f728a 100644 --- a/pkg/repositories/user/interfaces.go +++ b/pkg/repositories/user/interfaces.go @@ -11,6 +11,7 @@ type IUserRepository interface { FindAll() (*[]models.User, error) FindByID(id uint) (*models.User, error) FindByReferralCode(referralCode string) (*models.User, error) + FindByEmail(email string) (*models.User, error) Create(user *models.User) (*models.User, error) } diff --git a/pkg/repositories/user/postgres_user_repository.go b/pkg/repositories/user/postgres_user_repository.go index ddcf0d5..8428771 100644 --- a/pkg/repositories/user/postgres_user_repository.go +++ b/pkg/repositories/user/postgres_user_repository.go @@ -28,6 +28,12 @@ func (r *PostgresUserRepository) FindByID(id uint) (*models.User, error) { return &user, result.Error } +func (r *PostgresUserRepository) FindByEmail(email string) (*models.User, error) { + var user models.User + result := r.db.Where("email = ?", email).First(&user) + return &user, result.Error +} + func (r *PostgresUserRepository) FindByReferralCode(referralCode string) (*models.User, error) { var user models.User result := r.db.Where("referral_code = ?", referralCode).First(&user) diff --git a/pkg/services/user/user_service.go b/pkg/services/user/user_service.go index ee45823..6233cb6 100644 --- a/pkg/services/user/user_service.go +++ b/pkg/services/user/user_service.go @@ -15,6 +15,7 @@ func NewUserService( userRepo userRepo.IUserRepository, userReferralRepo userReferralRepo.IUserReferralRepository, emailService emailService.IEmailService ) *UserService { + return &UserService{ userRepo: userRepo, userReferralRepo: userReferralRepo, @@ -25,6 +26,13 @@ func NewUserService( // Register a new user and create a new points record for the user func (s *UserService) Create(userDto *dtos.CreateUserDTO) (*models.User, error) { + // Check if user already exists + user, err := s.userRepo.FindByEmail(userDto.Email) + + if err == nil && user != nil { + return nil, fmt.Errorf("user with email %s already exists", userDto.Email) + } + // Generate new referral code for the user referralCode := utils.GenerateReferralCode() From 42580f7c852760256a051da6eeb10cb649799732 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 01:50:57 -0300 Subject: [PATCH 32/36] :sparkles: feat: add application containers --- Dockerfile | 2 +- cmd/main.go | 16 +++++----------- cmd/repository_container.go | 20 ++++++++++++++++++++ cmd/service_container.go | 26 ++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 cmd/repository_container.go create mode 100644 cmd/service_container.go diff --git a/Dockerfile b/Dockerfile index 3acba58..38c2370 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ COPY . . EXPOSE 3000 # Run the application -CMD ["go", "run", "cmd/main.go"] \ No newline at end of file +CMD ["go", "run", "./cmd"] \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index cb884a0..baeeaa7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,11 +5,7 @@ import ( "gss-backend/api/routes" "gss-backend/pkg/config" "gss-backend/pkg/models" - userRepo "gss-backend/pkg/repositories/user" - userReferralRepo "gss-backend/pkg/repositories/user_referral" emailService "gss-backend/pkg/services/email" - userService "gss-backend/pkg/services/user" - userReferralService "gss-backend/pkg/services/user_referral" "log" "github.com/gofiber/fiber/v2" @@ -70,8 +66,8 @@ func main() { }) // Instatiating Repositories - userRepo := userRepo.NewPostgresUserRepository(db) - userReferralRepo := userReferralRepo.NewPostgresUserReferralRepository(db) + repoContainer := NewRepositoryContainer(db) + // Instatiating Services emailConfig := emailService.EmailConfig{ @@ -80,14 +76,12 @@ func main() { SMTPEmail: config.SMTP_EMAIL, SMTPPassword: config.SMTP_PASSWORD, } - emailService := emailService.NewEmailService(emailConfig) - userService := userService.NewUserService(userRepo, userReferralRepo, emailService) - userReferralService := userReferralService.NewUserReferralService(userRepo, userReferralRepo, emailService) + serviceContainer := NewServiceContainer(repoContainer, emailConfig) // Setting up routes api := app.Group("/api") - routes.UserRouter(api, userService) - routes.UserReferralRouter(api, userReferralService) + routes.UserRouter(api, serviceContainer.UserService) + routes.UserReferralRouter(api, serviceContainer.UserReferralService) // Starting the server port := fmt.Sprintf(":%s", config.FIBER_PORT) diff --git a/cmd/repository_container.go b/cmd/repository_container.go new file mode 100644 index 0000000..22db8dd --- /dev/null +++ b/cmd/repository_container.go @@ -0,0 +1,20 @@ +package main + +import ( + userRepo "gss-backend/pkg/repositories/user" + userReferralRepo "gss-backend/pkg/repositories/user_referral" + + "gorm.io/gorm" +) + +type RepositoryContainer struct { + UserRepository userRepo.IUserRepository + UserReferralRepository userReferralRepo.IUserReferralRepository +} + +func NewRepositoryContainer(db *gorm.DB) *RepositoryContainer { + return &RepositoryContainer{ + UserRepository: userRepo.NewPostgresUserRepository(db), + UserReferralRepository: userReferralRepo.NewPostgresUserReferralRepository(db), + } +} diff --git a/cmd/service_container.go b/cmd/service_container.go new file mode 100644 index 0000000..72dd017 --- /dev/null +++ b/cmd/service_container.go @@ -0,0 +1,26 @@ +package main + +import ( + emailService "gss-backend/pkg/services/email" + userService "gss-backend/pkg/services/user" + userReferralService "gss-backend/pkg/services/user_referral" +) + + +type ServiceContainer struct { + EmailService emailService.IEmailService + UserService userService.IUserService + UserReferralService userReferralService.IUserReferralService +} + +func NewServiceContainer(repoContainer *RepositoryContainer, emailConfig emailService.EmailConfig) *ServiceContainer { + emailService := emailService.NewEmailService(emailConfig) + userService := userService.NewUserService(repoContainer.UserRepository, repoContainer.UserReferralRepository, emailService) + userReferralService := userReferralService.NewUserReferralService(repoContainer.UserRepository, repoContainer.UserReferralRepository, emailService) + + return &ServiceContainer{ + EmailService: emailService, + UserService: userService, + UserReferralService: userReferralService, + } +} \ No newline at end of file From 316f6b5e5e4fde2072e2153b0e196012bc0da1ba Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 14:38:06 -0300 Subject: [PATCH 33/36] :fire: feat: remove frontend from compose --- docker-compose.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 9e582c2..2d34f9e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,19 +33,6 @@ services: networks: - gss-network - gss-frontend: - container_name: gss-frontend - build: - context: ../gss-frontend - dockerfile: ../gss-frontend/Dockerfile - ports: - - "3000:3000" - restart: always - env_file: - - ../gss-frontend/.env.production - networks: - - gss-network - volumes: gss-db: driver: local From 58af37946c201ff349de46c3fdb1c7e62eafccef Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 15:02:17 -0300 Subject: [PATCH 34/36] :wrench: build: change port on Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 38c2370..ae9dfbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download COPY . . # Expose port 3000 -EXPOSE 3000 +EXPOSE 3001 # Run the application CMD ["go", "run", "./cmd"] \ No newline at end of file From ba78546750d7d0748a25c11595fdad0892e65d6b Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 15:55:09 -0300 Subject: [PATCH 35/36] :wrench: build: change port name --- Dockerfile | 2 +- pkg/config/env.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae9dfbe..000b381 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN go mod download # Copy source code COPY . . -# Expose port 3000 +# Expose port 3001 EXPOSE 3001 # Run the application diff --git a/pkg/config/env.go b/pkg/config/env.go index 32439b9..ac14d44 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -35,7 +35,7 @@ func NewConfig() (*Config, error) { POSTGRES_USER: getEnv("POSTGRES_USER", "root"), POSTGRES_PASSWORD: getEnv("POSTGRES_PASSWORD", "root"), POSTGRES_DB: getEnv("POSTGRES_DB", "gss-db"), - FIBER_PORT: getEnv("FIBER_PORT", "3000"), + FIBER_PORT: getEnv("PORT", "3000"), SMTP_EMAIL: getEnv("SMTP_EMAIL", ""), SMTP_PORT: getEnvAsInt("SMTP_PORT", 587), SMTP_HOST: getEnv("SMTP_HOST", ""), From c0030961270b2083948619e1bb1e69ace6d9b9e0 Mon Sep 17 00:00:00 2001 From: HiIamZeref Date: Tue, 1 Apr 2025 16:57:42 -0300 Subject: [PATCH 36/36] :whale: feat: change Dockerfile to build go app --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 000b381..77f4998 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,11 @@ RUN go mod download # Copy source code COPY . . +# Build the Go app +RUN go build -o gss-backend ./cmd + # Expose port 3001 EXPOSE 3001 # Run the application -CMD ["go", "run", "./cmd"] \ No newline at end of file +CMD ["./gss-backend"] \ No newline at end of file