From 6651daca670664f3de8af9c7bcb74b1e7c6c6be9 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Thu, 7 Mar 2024 19:16:07 -0500 Subject: Add CORS middleware and authentication middleware to the API server. The `setupRouter` function in `main.go` now includes a CORS middleware and a token authentication middleware. The CORS middleware allows cross-origin resource sharing by setting the appropriate response headers. The token authentication middleware checks for the presence of an `Authorization` header with a valid bearer token. If the token is missing or invalid, an unauthorized response is returned. In addition to these changes, a new test file `main_test.go` has been added to test the `/api/v1/auth` route. This test suite includes two test cases: one for successful authentication and one for failed authentication. Update go.mod to include new dependencies. The `go.mod` file has been modified to include two new dependencies: `github.com/spf13/viper` and `github.com/stretchr/testify`. Ignore go.sum changes. Ignore changes in the `go.sum` file, as they only include updates to existing dependencies. --- api/cmd/main.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ api/cmd/main_test.go | 56 +++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 api/cmd/main.go create mode 100644 api/cmd/main_test.go (limited to 'api/cmd') diff --git a/api/cmd/main.go b/api/cmd/main.go new file mode 100644 index 0000000..1924556 --- /dev/null +++ b/api/cmd/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "errors" + "log" + "net/http" + "strings" + "water/api/internal/database" + "water/api/internal/controllers" + + "github.com/gin-gonic/gin" + _ "github.com/mattn/go-sqlite3" +) + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") + + if c.Request.Method == "OPTIONS" { + log.Println(c.Request.Header) + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} + +func checkForTokenInContext(c *gin.Context) (string, error) { + authorizationHeader := c.GetHeader("Authorization") + if authorizationHeader == "" { + return "", errors.New("authorization header is missing") + } + + parts := strings.Split(authorizationHeader, " ") + + if len(parts) != 2 || parts[0] != "Bearer" { + return "", errors.New("invalid Authorization header format") + } + + return parts[1], nil +} + +func TokenRequired() gin.HandlerFunc { + return func(c *gin.Context) { + _, err := checkForTokenInContext(c) + + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } + + c.Next() + } +} + +func setupRouter() *gin.Engine { + // Disable Console Color + // gin.DisableConsoleColor() + r := gin.Default() + r.Use(CORSMiddleware()) + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + api := r.Group("api/v1") + + api.POST("/auth", controllers.AuthHandler) + + user := api.Group("/user/:uuid") + user.Use(TokenRequired()) + { + user.GET("", controllers.GetUser) + user.GET("preferences", controllers.GetUserPreferences) + user.PATCH("preferences", controllers.UpdateUserPreferences) + } + + stats := api.Group("/stats") + stats.Use(TokenRequired()) + { + stats.GET("/", controllers.GetAllStatistics) + stats.POST("/", controllers.PostNewStatistic) + stats.GET("weekly/", controllers.GetWeeklyStatistics) + stats.GET("daily/", controllers.GetDailyUserStatistics) + stats.GET("user/:uuid", controllers.GetUserStatistics) + stats.PATCH("user/:uuid", controllers.UpdateUserStatistic) + stats.DELETE("user/:uuid", controllers.DeleteUserStatistic) + } + + return r +} + +func main() { + database.SetupDatabase() + r := setupRouter() + // Listen and Server in 0.0.0.0:8080 + err := r.Run(":8080") + if err != nil { + return + } +} diff --git a/api/cmd/main_test.go b/api/cmd/main_test.go new file mode 100644 index 0000000..8d0df8d --- /dev/null +++ b/api/cmd/main_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func getTestUserCredentials() (string, string) { + viper.SetConfigName(".env") + viper.AddConfigPath(".") + err := viper.ReadInConfig() + if err != nil { + log.Fatalf("Error while reading config file %s", err) + } + + testUser := viper.GetString("TEST_USER") + testPass := viper.GetString("TEST_PASS") + return testUser, testPass +} + +func TestAuthRoute(t *testing.T) { + router := setupRouter() + + username, password := getTestUserCredentials() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/auth", nil) + req.SetBasicAuth(username, password) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, "response should return a 200 code") + + var response map[string]interface{} + _ = json.Unmarshal(w.Body.Bytes(), &response) + _, exists := response["token"] + assert.True(t, exists, "response should return a token") +} + +func TestAuthRouteFailure(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/auth", nil) + req.SetBasicAuth("asdf", "asdf") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusUnauthorized, w.Code, "should return a 401 code") +} + +func Test \ No newline at end of file -- cgit v1.1 From 29f83e05270d0012ad9f273ac3364106fcff5f50 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Thu, 7 Mar 2024 19:56:34 -0500 Subject: chore: Update paths in test and database configuration This commit updates the paths in the test suite and database configuration to reflect the new directory structure. In the `api/cmd/main_test.go` file, the path for the config file is changed from `viper.SetConfigName(".env")` to `viper.SetConfigFile("../.env")` and `viper.AddConfigPath(".")` is added for configuration purposes. Additionally, `viper.AutomaticEnv()` is added to enable automatic environment variable configuration. In the same file, error handling is improved by adding explicit checks and error messages. The `api/internal/database/database.go` file is also modified, updating the path for database initialization from `"../db/water.sqlite3"` to `"../../db/water.sqlite3"`. These changes ensure proper configuration and functioning of the application. --- api/cmd/main_test.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'api/cmd') diff --git a/api/cmd/main_test.go b/api/cmd/main_test.go index 8d0df8d..a6c8381 100644 --- a/api/cmd/main_test.go +++ b/api/cmd/main_test.go @@ -12,8 +12,9 @@ import ( ) func getTestUserCredentials() (string, string) { - viper.SetConfigName(".env") + viper.SetConfigFile("../.env") viper.AddConfigPath(".") + viper.AutomaticEnv() err := viper.ReadInConfig() if err != nil { log.Fatalf("Error while reading config file %s", err) @@ -29,17 +30,29 @@ func TestAuthRoute(t *testing.T) { username, password := getTestUserCredentials() + log.Println("username", username) + log.Println("password", password) + w := httptest.NewRecorder() - req, _ := http.NewRequest("POST", "/api/v1/auth", nil) + req, err := http.NewRequest("POST", "/api/v1/auth", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } req.SetBasicAuth(username, password) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "response should return a 200 code") var response map[string]interface{} - _ = json.Unmarshal(w.Body.Bytes(), &response) + err = json.Unmarshal(w.Body.Bytes(), &response) + if err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } _, exists := response["token"] assert.True(t, exists, "response should return a token") + if !exists { + t.Fatalf("response did not contain token") + } } func TestAuthRouteFailure(t *testing.T) { @@ -52,5 +65,3 @@ func TestAuthRouteFailure(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, w.Code, "should return a 401 code") } - -func Test \ No newline at end of file -- cgit v1.1 From 831b6f0167b9c1747d128b4a5a648d4de42ff0a9 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Thu, 7 Mar 2024 20:20:36 -0500 Subject: Refactor router and middleware packages - Move middleware functions from `main.go` to `middleware.go` in the `middleware` package. - Update import statements in `main.go` and use the `router` package instead of the `controllers` package. ``` Refactor router and middleware packages Move middleware functions from `main.go` to `middleware.go` in the `middleware` package. Update import statements in `main.go` and use the `router` package instead of the `controllers` package. ``` --- api/cmd/main.go | 92 ++------------------------------------------------------- 1 file changed, 2 insertions(+), 90 deletions(-) (limited to 'api/cmd') diff --git a/api/cmd/main.go b/api/cmd/main.go index 1924556..d97c942 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -1,101 +1,13 @@ package main import ( - "errors" - "log" - "net/http" - "strings" "water/api/internal/database" - "water/api/internal/controllers" - - "github.com/gin-gonic/gin" - _ "github.com/mattn/go-sqlite3" + "water/api/internal/router" ) -func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") - c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") - - if c.Request.Method == "OPTIONS" { - log.Println(c.Request.Header) - c.AbortWithStatus(http.StatusNoContent) - return - } - - c.Next() - } -} - -func checkForTokenInContext(c *gin.Context) (string, error) { - authorizationHeader := c.GetHeader("Authorization") - if authorizationHeader == "" { - return "", errors.New("authorization header is missing") - } - - parts := strings.Split(authorizationHeader, " ") - - if len(parts) != 2 || parts[0] != "Bearer" { - return "", errors.New("invalid Authorization header format") - } - - return parts[1], nil -} - -func TokenRequired() gin.HandlerFunc { - return func(c *gin.Context) { - _, err := checkForTokenInContext(c) - - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } - - c.Next() - } -} - -func setupRouter() *gin.Engine { - // Disable Console Color - // gin.DisableConsoleColor() - r := gin.Default() - r.Use(CORSMiddleware()) - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - api := r.Group("api/v1") - - api.POST("/auth", controllers.AuthHandler) - - user := api.Group("/user/:uuid") - user.Use(TokenRequired()) - { - user.GET("", controllers.GetUser) - user.GET("preferences", controllers.GetUserPreferences) - user.PATCH("preferences", controllers.UpdateUserPreferences) - } - - stats := api.Group("/stats") - stats.Use(TokenRequired()) - { - stats.GET("/", controllers.GetAllStatistics) - stats.POST("/", controllers.PostNewStatistic) - stats.GET("weekly/", controllers.GetWeeklyStatistics) - stats.GET("daily/", controllers.GetDailyUserStatistics) - stats.GET("user/:uuid", controllers.GetUserStatistics) - stats.PATCH("user/:uuid", controllers.UpdateUserStatistic) - stats.DELETE("user/:uuid", controllers.DeleteUserStatistic) - } - - return r -} - func main() { database.SetupDatabase() - r := setupRouter() + r := router.SetupRouter() // Listen and Server in 0.0.0.0:8080 err := r.Run(":8080") if err != nil { -- cgit v1.1 From 8fab2d03bce82e4dee798ebffb1e93c557f62a4b Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Thu, 7 Mar 2024 23:16:22 -0500 Subject: feat: Update authentication route and add comments to exported members - The authentication route in the API has been updated to use a new router setup function. - Comments have been added to all exported members of the `auth.go` module in the internal controllers package. --- api/cmd/main_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'api/cmd') diff --git a/api/cmd/main_test.go b/api/cmd/main_test.go index a6c8381..049cf6e 100644 --- a/api/cmd/main_test.go +++ b/api/cmd/main_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "water/api/internal/router" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -26,7 +27,7 @@ func getTestUserCredentials() (string, string) { } func TestAuthRoute(t *testing.T) { - router := setupRouter() + r := router.SetupRouter() username, password := getTestUserCredentials() @@ -39,7 +40,7 @@ func TestAuthRoute(t *testing.T) { t.Fatalf("Failed to create request: %v", err) } req.SetBasicAuth(username, password) - router.ServeHTTP(w, req) + r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "response should return a 200 code") @@ -56,12 +57,12 @@ func TestAuthRoute(t *testing.T) { } func TestAuthRouteFailure(t *testing.T) { - router := setupRouter() + r := router.SetupRouter() w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/auth", nil) req.SetBasicAuth("asdf", "asdf") - router.ServeHTTP(w, req) + r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "should return a 401 code") } -- cgit v1.1 From c4e5776f9e174fe6bf91721649c0541a9fb310ae Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 15 Mar 2024 21:41:12 -0400 Subject: add env samples, move files --- api/cmd/main.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'api/cmd') diff --git a/api/cmd/main.go b/api/cmd/main.go index d97c942..c23eff1 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -1,16 +1,21 @@ package main import ( - "water/api/internal/database" + "log" + "water/api/internal/config" "water/api/internal/router" ) func main() { - database.SetupDatabase() + c, err := config.Load() + if err != nil { + log.Fatalf("Error while reading config file %s", err) + } + r := router.SetupRouter() // Listen and Server in 0.0.0.0:8080 - err := r.Run(":8080") + err = r.Run(c.GetString("PORT")) if err != nil { - return + log.Fatal(err) } } -- cgit v1.1 From cc49361bcbf689510035e7bbdcce9d8467a36282 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 15 Mar 2024 21:45:01 -0400 Subject: add newline, clean up test --- api/cmd/main_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'api/cmd') diff --git a/api/cmd/main_test.go b/api/cmd/main_test.go index 049cf6e..a4db57a 100644 --- a/api/cmd/main_test.go +++ b/api/cmd/main_test.go @@ -8,15 +8,12 @@ import ( "testing" "water/api/internal/router" - "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "water/api/internal/config" ) func getTestUserCredentials() (string, string) { - viper.SetConfigFile("../.env") - viper.AddConfigPath(".") - viper.AutomaticEnv() - err := viper.ReadInConfig() + viper, err := config.Load() if err != nil { log.Fatalf("Error while reading config file %s", err) } @@ -31,9 +28,6 @@ func TestAuthRoute(t *testing.T) { username, password := getTestUserCredentials() - log.Println("username", username) - log.Println("password", password) - w := httptest.NewRecorder() req, err := http.NewRequest("POST", "/api/v1/auth", nil) if err != nil { -- cgit v1.1