Skip to content
This repository has been archived by the owner on Dec 4, 2024. It is now read-only.

Commit

Permalink
Merge pull request #23 from swibly/feat/user-feature-handlers
Browse files Browse the repository at this point in the history
feat: User handlers (incomplete)
  • Loading branch information
devkcud authored Jun 4, 2024
2 parents ec3a922 + 8d1c0d1 commit 06163dc
Show file tree
Hide file tree
Showing 26 changed files with 769 additions and 105 deletions.
17 changes: 12 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
JWT_SECRET=
POSTGRES_HOST=
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
# Postgres Config
POSTGRES_HOST=localhost
POSTGRES_DB=arkhon-db
POSTGRES_USER=arkhon-db_owner
POSTGRES_PASSWORD=arkhon
POSTGRES_SSLMODE=disable

# API Config
JWT_SECRET=jwtsecret

# TIP: You can generate a JWT_SECRET with the command:
# openssl rand -hex 256
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,21 @@ internal/service/repository/mock_%.go: internal/service/repository/%.go
@if [ "$(@F)" != "mock_$(*F)" ]; then \
mockgen -source="$<" -destination="$@" -package=repository; \
fi

# Generating users

USERS := \
'{"firstname": "John", "lastname": "Doe", "username": "johndoe", "email": "johndoe@example.com", "password": "T3st1ngP4$$w0rd"}', \
'{"firstname": "Jane", "lastname": "Smith", "username": "janesmith", "email": "janesmith@example.com", "password": "P@ssw0rd123"}', \
'{"firstname": "Alice", "lastname": "Johnson", "username": "alicejohnson", "email": "alicejohnson@example.com", "password": "qwerty"}', \
'{"firstname": "Bob", "lastname": "Brown", "username": "bobbrown", "email": "bobbrown@example.com", "password": "password123"}'
ENDPOINT=http://localhost:8080/v1/auth/register

create_users:
@echo "Creating users..."
@for user_data in $(USERS); do \
curl --silent --request POST \
--url $(ENDPOINT) \
--header 'Content-Type: application/json' \
--data "$$user_data"; \
done
9 changes: 9 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/devkcud/arkhon-foundation/arkhon-api/config"
v1 "github.com/devkcud/arkhon-foundation/arkhon-api/internal/controller/http/v1"
"github.com/devkcud/arkhon-foundation/arkhon-api/internal/service"
"github.com/devkcud/arkhon-foundation/arkhon-api/pkg/db"
"github.com/gin-gonic/gin"
)
Expand All @@ -17,12 +18,18 @@ func main() {
config.Parse()
db.Load()

service.Init()

gin.SetMode(config.Router.GinMode)

router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())

router.GET("/healthz", func(ctx *gin.Context) {
ctx.Writer.WriteString("Hello, world!")
})

v1.NewRouter(router)

// NOTE: Prioritize the PORT env variable, as some web services may set it
Expand All @@ -33,6 +40,8 @@ func main() {
port = fmt.Sprint(config.Router.Port)
}

log.Printf("Using port %s", port)

go func() {
log.Print("Starting API")

Expand Down
8 changes: 5 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ var (
DB string `yaml:"db"`
User string `yaml:"user"`
Password string `yaml:"password"`
SSLMode string `yaml:"sslmode"`
}

JWT struct {
Secret string `yaml:"secret"`
Security struct {
BcryptCost int `yaml:"bcrypt_cost"`
JWTSecret string `yaml:"jwt_secret"`
}
)

Expand All @@ -41,7 +43,7 @@ func Parse() {
log.Fatalf("error: %v", err)
}

if err := yaml.Unmarshal(read("jwt.yaml"), &JWT); err != nil {
if err := yaml.Unmarshal(read("security.yaml"), &Security); err != nil {
log.Fatalf("error: %v", err)
}

Expand Down
1 change: 0 additions & 1 deletion config/jwt.yaml

This file was deleted.

1 change: 1 addition & 0 deletions config/postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ host: $POSTGRES_HOST
db: $POSTGRES_DB
user: $POSTGRES_USER
password: $POSTGRES_PASSWORD
sslmode: $POSTGRES_SSLMODE
2 changes: 2 additions & 0 deletions config/security.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bcrypt_cost: 10 # min: 4 | max: 31
jwt_secret: $JWT_SECRET
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.14.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/jackc/pgx/v5 v5.5.4
github.com/joho/godotenv v1.5.1
go.uber.org/mock v0.4.0
golang.org/x/crypto v0.21.0
Expand All @@ -24,7 +25,6 @@ require (
github.com/goccy/go-json v0.10.2 // 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.4 // 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
Expand Down
101 changes: 84 additions & 17 deletions internal/controller/http/v1/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,37 @@ import (
"log"
"net/http"

"github.com/devkcud/arkhon-foundation/arkhon-api/internal/model"
"github.com/devkcud/arkhon-foundation/arkhon-api/config"
"github.com/devkcud/arkhon-foundation/arkhon-api/internal/model/dto"
"github.com/devkcud/arkhon-foundation/arkhon-api/internal/service/usecase"
"github.com/devkcud/arkhon-foundation/arkhon-api/pkg/middleware"
"github.com/devkcud/arkhon-foundation/arkhon-api/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)

func newAuthRoutes(handler *gin.RouterGroup) {
usecase := usecase.NewUserUseCase()

h := handler.Group("/auth")
{
h.POST("/register", func(ctx *gin.Context) {
RegisterHandler(ctx, usecase)
})
h.POST("/login", func(ctx *gin.Context) {
LoginHandler(ctx, usecase)
})
h.POST("/register", RegisterHandler)
h.POST("/login", LoginHandler)
h.PATCH("/update", middleware.AuthMiddleware, UpdateUserHandler)
h.DELETE("/delete", middleware.AuthMiddleware, DeleteUserHandler)
}
}

func RegisterHandler(ctx *gin.Context, usecase usecase.UserUseCase) {
var body model.UserRegister
func RegisterHandler(ctx *gin.Context) {
var body dto.UserRegister

if err := ctx.BindJSON(&body); err != nil {
log.Print(err)
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"})
return
}

user, err := usecase.CreateUser(body.FirstName, body.LastName, body.Username, body.Email, body.Password)
user, err := usecase.UserInstance.CreateUser(body.FirstName, body.LastName, body.Username, body.Email, body.Password)

if err == nil {
if token, err := utils.GenerateJWT(user.ID); err != nil {
Expand All @@ -49,24 +48,25 @@ func RegisterHandler(ctx *gin.Context, usecase usecase.UserUseCase) {
return
}

// Just print every time there is an error, no need to check what is the "context"
log.Print(err)

if validationErr, ok := err.(utils.ParamError); ok {
ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{validationErr.Param: validationErr.Message}})
return
}

if errors.Is(err, gorm.ErrDuplicatedKey) {
var pgErr *pgconn.PgError
// 23505 => duplicated key value violates unique constraint
if errors.Is(err, gorm.ErrDuplicatedKey) || (errors.As(err, &pgErr) && pgErr.Code == "23505") {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "An user with that username or email already exists."})
return
}

ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."})
}

func LoginHandler(ctx *gin.Context, usecase usecase.UserUseCase) {
var body model.UserLogin
func LoginHandler(ctx *gin.Context) {
var body dto.UserLogin

if err := ctx.BindJSON(&body); err != nil {
log.Print(err)
Expand All @@ -82,7 +82,7 @@ func LoginHandler(ctx *gin.Context, usecase usecase.UserUseCase) {
return
}

user, err := usecase.GetByUsernameOrEmail(body.Username, body.Email)
user, err := usecase.UserInstance.UnsafeGetByUsernameOrEmail(body.Username, body.Email)

if err != nil {
log.Print(err)
Expand Down Expand Up @@ -116,3 +116,70 @@ func LoginHandler(ctx *gin.Context, usecase usecase.UserUseCase) {
ctx.JSON(http.StatusOK, gin.H{"message": "User logged in", "token": token})
}
}

func UpdateUserHandler(ctx *gin.Context) {
issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch)

var body dto.UserUpdate

if err := ctx.BindJSON(&body); err != nil {
log.Print(err)
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Bad body format"})
return
}

if errs := utils.ValidateStruct(&body); errs != nil {
err := utils.ValidateErrorMessage(errs[0])

log.Print(err)
ctx.JSON(http.StatusBadRequest, gin.H{"error": gin.H{err.Param: err.Message}})
return
}

if body.Username != "" {
if profile, err := usecase.UserInstance.GetByUsername(body.Username); profile != nil && err == nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "An user with that username already exists"})
return
}
}

if body.Email != "" {
if profile, err := usecase.UserInstance.GetByEmail(body.Email); profile != nil && err == nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "An user with that email already exists"})
return
}
}

if body.Password != "" {
if hashedPassword, err := bcrypt.GenerateFromPassword([]byte(body.Password), config.Security.BcryptCost); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."})
} else {
body.Password = string(hashedPassword)
}
}

if err := usecase.UserInstance.Update(issuer.ID, &body); err != nil {
log.Print(err)
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."})
return
}

ctx.JSON(http.StatusOK, gin.H{"message": "User updated"})
}

func DeleteUserHandler(ctx *gin.Context) {
issuer := ctx.Keys["auth_user"].(*dto.ProfileSearch)

if err := usecase.UserInstance.DeleteUser(issuer.ID); err != nil {
log.Print(err)
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "User not found."})
return
}

ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."})
return
}

ctx.JSON(http.StatusOK, gin.H{"message": "User deleted"})
}
2 changes: 2 additions & 0 deletions internal/controller/http/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ func NewRouter(handler *gin.Engine) {
g := handler.Group("/v1")
{
newAuthRoutes(g)
newUserRoutes(g)
newSearchRoutes(g)
}
}
41 changes: 41 additions & 0 deletions internal/controller/http/v1/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package v1

import (
"errors"
"log"
"net/http"

"github.com/devkcud/arkhon-foundation/arkhon-api/internal/service/usecase"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

func newSearchRoutes(handler *gin.RouterGroup) {
h := handler.Group("/search")

h.GET("/user", SearchByNameHandler)
}

func SearchByNameHandler(ctx *gin.Context) {
name := ctx.Query("name")
log.Println(name)

if name == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Cannot find by empty name"})
return
}

users, err := usecase.UserInstance.GetBySimilarName(name)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusNotFound, gin.H{"error": "No user found with that name."})
return
}

log.Print(err)
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error. Please, try again later."})
return
}

ctx.JSON(http.StatusOK, users)
}
Loading

0 comments on commit 06163dc

Please sign in to comment.