diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 0f99312..9a17644 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,55 +1,61 @@ -name: Go Build and Test with Database +name: User Service CI on: push: branches: - - "master" - - "develop" + - master + - develop pull_request: branches: - - "master" - - "develop" - + - master + - develop + jobs: test: runs-on: ubuntu-latest + services: postgres: image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb + POSTGRES_DB: userdb ports: - 5432:5432 options: >- - --health-cmd "pg_isready -U postgres -d testdb" - --health-interval 5s + --health-cmd pg_isready + --health-interval 10s --health-timeout 5s --health-retries 5 - + steps: - - name: Checkout Code + - name: Checkout Repository uses: actions/checkout@v4 - + - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v4 with: - go-version: 1.23.4 - + go-version: 1.21 + - name: Install Dependencies run: go mod tidy - - - name: Wait for PostgreSQL to Start - run: sleep 10 # Ensure PostgreSQL is fully initialized - - - name: Create .env file for testing + + - name: Create .env file for Testing run: | - echo "DB_USER=postgres" > .env - echo "DB_PASSWORD=postgres" >> .env - echo "DB_NAME=testdb" >> .env + echo "JWT_SECRET=testsecret" > .env echo "DB_HOST=localhost" >> .env echo "DB_PORT=5432" >> .env - - - name: Run Tests with Database - run: go test -v ./... \ No newline at end of file + echo "DB_USER=postgres" >> .env + echo "DB_PASSWORD=postgres" >> .env + echo "DB_NAME=userdb" >> .env + + - name: Run Tests + env: + JWT_SECRET: testsecret + DB_HOST: localhost + DB_PORT: 5432 + DB_USER: postgres + DB_PASSWORD: postgres + DB_NAME: userdb + run: go test ./tests/ -v diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b9c819 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.21 + +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod tidy + +COPY . . + +RUN go build -o main ./cmd/main.go + +CMD ["./main"] diff --git a/main.go b/cmd/main.go similarity index 78% rename from main.go rename to cmd/main.go index a3aeac5..d3be97e 100644 --- a/main.go +++ b/cmd/main.go @@ -1,7 +1,8 @@ -package main +package cmd import ( "user_crud/config" + "user_crud/routes" "github.com/gin-gonic/gin" ) @@ -13,7 +14,7 @@ func main() { config.ConnectDB() // Register Routes - UserRoutes(r) + routes.UserRoutes(r) // Start Server r.Run(":8080") diff --git a/controllers/user.go b/controllers/user.go index 3561acc..4b58440 100644 --- a/controllers/user.go +++ b/controllers/user.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" "user_crud/models" + "user_crud/repository" "user_crud/utils" "github.com/gin-gonic/gin" @@ -17,7 +18,7 @@ func CreateUser(c *gin.Context) { return } - err := models.CreateUser(user) + err := repository.CreateUser(user) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to create user") return @@ -28,7 +29,7 @@ func CreateUser(c *gin.Context) { // Get All Users func GetAllUsers(c *gin.Context) { - users, err := models.GetAllUsers() + users, err := repository.GetAllUsers() if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to fetch users") return @@ -39,7 +40,7 @@ func GetAllUsers(c *gin.Context) { // Get User by ID func GetUserByID(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - user, err := models.GetUserByID(id) + user, err := repository.GetUserByID(id) if err != nil { utils.SendErrorResponse(c, http.StatusNotFound, "User not found") return @@ -55,7 +56,7 @@ func UpdateUser(c *gin.Context) { return } - err := models.UpdateUser(user) + err := repository.UpdateUser(user) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to update user") return @@ -68,7 +69,7 @@ func UpdateUser(c *gin.Context) { func DeleteUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - err := models.DeleteUser(id) + err := repository.DeleteUser(id) if err != nil { utils.SendErrorResponse(c, http.StatusInternalServerError, "Failed to delete user") return diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9f2330a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.8" + +services: + user-service: + build: . + ports: + - "8080:8080" + depends_on: + - db + env_file: + - .env + + db: + image: postgres:15 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: userdb + ports: + - "5432:5432" diff --git a/go.mod b/go.mod index 354061c..7da45ba 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module user_crud go 1.23.4 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-gonic/gin v1.10.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + golang.org/x/time v0.10.0 ) require ( diff --git a/go.sum b/go.sum index e9d3e92..2ec116a 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -80,6 +82,8 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..7e8cc65 --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + "user_crud/utils" + + "github.com/gin-gonic/gin" +) + +// AuthMiddleware - Protect routes with JWT authentication +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" || !utils.ValidateJWT(token) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/ratelimit.go b/middleware/ratelimit.go new file mode 100644 index 0000000..baea2a0 --- /dev/null +++ b/middleware/ratelimit.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +var limiter = rate.NewLimiter(1, 5) // 1 request per second, burst up to 5 + +func RateLimit() gin.HandlerFunc { + return func(c *gin.Context) { + if !limiter.Allow() { + c.AbortWithStatusJSON(429, gin.H{"error": "Too many requests"}) + return + } + c.Next() + } +} diff --git a/models/user.go b/models/user.go index 7bb6639..010ae0c 100644 --- a/models/user.go +++ b/models/user.go @@ -1,56 +1,8 @@ package models -import "user_crud/config" - type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` } - -// Create User -func CreateUser(user User) error { - _, err := config.DB.Exec("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)", user.Name, user.Email, user.Age) - return err -} - -// Get All Users -func GetAllUsers() ([]User, error) { - rows, err := config.DB.Query("SELECT id, name, email, age FROM users") - if err != nil { - return nil, err - } - defer rows.Close() - - var users []User - for rows.Next() { - var user User - err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age) - if err != nil { - return nil, err - } - users = append(users, user) - } - - return users, nil -} - -// Get User by ID -func GetUserByID(id int) (User, error) { - var user User - err := config.DB.QueryRow("SELECT id, name, email, age FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email, &user.Age) - return user, err -} - -// Update User -func UpdateUser(user User) error { - _, err := config.DB.Exec("UPDATE users SET name=$1, email=$2, age=$3 WHERE id=$4", user.Name, user.Email, user.Age, user.ID) - return err -} - -// Delete User -func DeleteUser(id int) error { - _, err := config.DB.Exec("DELETE FROM users WHERE id = $1", id) - return err -} diff --git a/repository/user_repository.go b/repository/user_repository.go new file mode 100644 index 0000000..044c119 --- /dev/null +++ b/repository/user_repository.go @@ -0,0 +1,52 @@ +package repository + +import ( + "user_crud/config" + "user_crud/models" +) + +// Create User +func CreateUser(user models.User) error { + _, err := config.DB.Exec("INSERT INTO users (name, email, age) VALUES ($1, $2, $3)", user.Name, user.Email, user.Age) + return err +} + +// Get All Users +func GetAllUsers() ([]models.User, error) { + rows, err := config.DB.Query("SELECT id, name, email, age FROM users") + if err != nil { + return nil, err + } + defer rows.Close() + + var users []models.User + for rows.Next() { + var user models.User + err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age) + if err != nil { + return nil, err + } + users = append(users, user) + } + + return users, nil +} + +// Get User by ID +func GetUserByID(id int) (models.User, error) { + var user models.User + err := config.DB.QueryRow("SELECT id, name, email, age FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email, &user.Age) + return user, err +} + +// Update User +func UpdateUser(user models.User) error { + _, err := config.DB.Exec("UPDATE users SET name=$1, email=$2, age=$3 WHERE id=$4", user.Name, user.Email, user.Age, user.ID) + return err +} + +// Delete User +func DeleteUser(id int) error { + _, err := config.DB.Exec("DELETE FROM users WHERE id = $1", id) + return err +} diff --git a/routes.go b/routes/routes.go similarity index 96% rename from routes.go rename to routes/routes.go index eb94245..4a151da 100644 --- a/routes.go +++ b/routes/routes.go @@ -1,4 +1,4 @@ -package main +package routes import ( "user_crud/controllers" diff --git a/models/user_test.go b/tests/user_test.go similarity index 85% rename from models/user_test.go rename to tests/user_test.go index 5d15fc4..30afa5e 100644 --- a/models/user_test.go +++ b/tests/user_test.go @@ -1,4 +1,4 @@ -package models +package tests import ( "database/sql" @@ -9,6 +9,8 @@ import ( "testing" "user_crud/config" + "user_crud/models" + "user_crud/repository" "github.com/joho/godotenv" _ "github.com/lib/pq" // PostgreSQL driver @@ -19,8 +21,8 @@ func setupTestDB() { if err != nil { log.Fatal(err) } - dir := strings.Replace(currentDir, "/models", "", 1) - + dir := strings.Replace(currentDir, "/tests", "", 1) + fmt.Println("PRint: ", dir) // Load environment variables err = godotenv.Load(dir + "/.env") if err != nil { @@ -65,8 +67,8 @@ func TestCreateUser(t *testing.T) { setupTestDB() defer teardownTestDB() - user := User{Name: "Alice", Email: "alice@example.com", Age: 28} - err := CreateUser(user) + user := models.User{Name: "Alice", Email: "alice@example.com", Age: 28} + err := repository.CreateUser(user) if err != nil { t.Errorf("Failed to create user: %v", err) } @@ -78,7 +80,7 @@ func TestCreateUser(t *testing.T) { } // Delete user - err = DeleteUser(user.ID) + err = repository.DeleteUser(user.ID) if err != nil { t.Errorf("Failed to delete user: %v", err) } @@ -91,7 +93,7 @@ func TestGetAllUsers(t *testing.T) { // Insert test users config.DB.Exec("INSERT INTO users (name, email, age) VALUES ('Bob', 'bob@example.com', 25), ('Charlie', 'charlie@example.com', 29)") - users, err := GetAllUsers() + users, err := repository.GetAllUsers() if err != nil { t.Errorf("Failed to fetch users: %v", err) } @@ -102,7 +104,7 @@ func TestGetAllUsers(t *testing.T) { for _, user := range users { // Delete user - err = DeleteUser(user.ID) + err = repository.DeleteUser(user.ID) if err != nil { t.Errorf("Failed to delete user: %v", err) } @@ -117,7 +119,7 @@ func TestGetUserByID(t *testing.T) { var id int config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('David', 'david@example.com', 40) RETURNING id").Scan(&id) - user, err := GetUserByID(id) + user, err := repository.GetUserByID(id) if err != nil { t.Errorf("Failed to get user by ID: %v", err) } @@ -126,7 +128,7 @@ func TestGetUserByID(t *testing.T) { t.Errorf("Expected email 'david@example.com', got %s", user.Email) } // Delete user - err = DeleteUser(id) + err = repository.DeleteUser(id) if err != nil { t.Errorf("Failed to delete user: %v", err) } @@ -141,8 +143,8 @@ func TestUpdateUser(t *testing.T) { config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('Eve', 'eve@example.com', 22) RETURNING id").Scan(&id) // Update the user - updatedUser := User{ID: id, Name: "Eve Adams", Email: "eve@example.com", Age: 30} - err := UpdateUser(updatedUser) + updatedUser := models.User{ID: id, Name: "Eve Adams", Email: "eve@example.com", Age: 30} + err := repository.UpdateUser(updatedUser) if err != nil { t.Errorf("Failed to update user: %v", err) } @@ -157,7 +159,7 @@ func TestUpdateUser(t *testing.T) { } // Delete user - err = DeleteUser(id) + err = repository.DeleteUser(id) if err != nil { t.Errorf("Failed to delete user: %v", err) } @@ -172,7 +174,7 @@ func TestDeleteUser(t *testing.T) { config.DB.QueryRow("INSERT INTO users (name, email, age) VALUES ('Frank', 'frank@example.com', 45) RETURNING id").Scan(&id) // Delete user - err := DeleteUser(id) + err := repository.DeleteUser(id) if err != nil { t.Errorf("Failed to delete user: %v", err) } diff --git a/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..8c8989e --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,39 @@ +package utils + +import ( + "errors" + "os" + "time" + + "github.com/dgrijalva/jwt-go" +) + +// Secret key (Use environment variables in production) +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +// Claims structure +type Claims struct { + UserID uint `json:"user_id"` + jwt.StandardClaims +} + +// ValidateJWT verifies and parses the token +func ValidateJWT(tokenString string) bool { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return jwtSecret, nil + }) + + if err != nil { + return false + } + + // Check if token is valid and not expired + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims.ExpiresAt > time.Now().Unix() + } + + return false +}