aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZach Berwaldt <zberwaldt@tutamail.com>2024-03-07 19:16:07 -0500
committerZach Berwaldt <zberwaldt@tutamail.com>2024-03-07 19:16:07 -0500
commit6651daca670664f3de8af9c7bcb74b1e7c6c6be9 (patch)
treef1bb35ef8b0e1d498842a17b84c87c6ee996274e
parent5fa57845052655883120ba4d19a85d8756fb8d8c (diff)
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.
-rw-r--r--api/cmd/main.go104
-rw-r--r--api/cmd/main_test.go56
-rw-r--r--api/go.mod19
-rw-r--r--api/go.sum47
-rw-r--r--api/internal/controllers/auth.go66
-rw-r--r--api/internal/controllers/stats.go164
-rw-r--r--api/internal/controllers/user.go17
-rw-r--r--api/internal/database/database.go22
-rw-r--r--api/internal/models/auth.go11
-rw-r--r--api/internal/models/preferences.go14
-rw-r--r--api/internal/models/statistics.go26
-rw-r--r--api/internal/models/user.go10
-rw-r--r--api/main.go301
-rw-r--r--api/models.go57
14 files changed, 551 insertions, 363 deletions
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 @@
1package main
2
3import (
4 "errors"
5 "log"
6 "net/http"
7 "strings"
8 "water/api/internal/database"
9 "water/api/internal/controllers"
10
11 "github.com/gin-gonic/gin"
12 _ "github.com/mattn/go-sqlite3"
13)
14
15func CORSMiddleware() gin.HandlerFunc {
16 return func(c *gin.Context) {
17 c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
18 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
19 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")
20 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
21
22 if c.Request.Method == "OPTIONS" {
23 log.Println(c.Request.Header)
24 c.AbortWithStatus(http.StatusNoContent)
25 return
26 }
27
28 c.Next()
29 }
30}
31
32func checkForTokenInContext(c *gin.Context) (string, error) {
33 authorizationHeader := c.GetHeader("Authorization")
34 if authorizationHeader == "" {
35 return "", errors.New("authorization header is missing")
36 }
37
38 parts := strings.Split(authorizationHeader, " ")
39
40 if len(parts) != 2 || parts[0] != "Bearer" {
41 return "", errors.New("invalid Authorization header format")
42 }
43
44 return parts[1], nil
45}
46
47func TokenRequired() gin.HandlerFunc {
48 return func(c *gin.Context) {
49 _, err := checkForTokenInContext(c)
50
51 if err != nil {
52 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
53 c.Abort()
54 return
55 }
56
57 c.Next()
58 }
59}
60
61func setupRouter() *gin.Engine {
62 // Disable Console Color
63 // gin.DisableConsoleColor()
64 r := gin.Default()
65 r.Use(CORSMiddleware())
66 r.Use(gin.Logger())
67 r.Use(gin.Recovery())
68
69 api := r.Group("api/v1")
70
71 api.POST("/auth", controllers.AuthHandler)
72
73 user := api.Group("/user/:uuid")
74 user.Use(TokenRequired())
75 {
76 user.GET("", controllers.GetUser)
77 user.GET("preferences", controllers.GetUserPreferences)
78 user.PATCH("preferences", controllers.UpdateUserPreferences)
79 }
80
81 stats := api.Group("/stats")
82 stats.Use(TokenRequired())
83 {
84 stats.GET("/", controllers.GetAllStatistics)
85 stats.POST("/", controllers.PostNewStatistic)
86 stats.GET("weekly/", controllers.GetWeeklyStatistics)
87 stats.GET("daily/", controllers.GetDailyUserStatistics)
88 stats.GET("user/:uuid", controllers.GetUserStatistics)
89 stats.PATCH("user/:uuid", controllers.UpdateUserStatistic)
90 stats.DELETE("user/:uuid", controllers.DeleteUserStatistic)
91 }
92
93 return r
94}
95
96func main() {
97 database.SetupDatabase()
98 r := setupRouter()
99 // Listen and Server in 0.0.0.0:8080
100 err := r.Run(":8080")
101 if err != nil {
102 return
103 }
104}
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 @@
1package main
2
3import (
4 "encoding/json"
5 "log"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/spf13/viper"
11 "github.com/stretchr/testify/assert"
12)
13
14func getTestUserCredentials() (string, string) {
15 viper.SetConfigName(".env")
16 viper.AddConfigPath(".")
17 err := viper.ReadInConfig()
18 if err != nil {
19 log.Fatalf("Error while reading config file %s", err)
20 }
21
22 testUser := viper.GetString("TEST_USER")
23 testPass := viper.GetString("TEST_PASS")
24 return testUser, testPass
25}
26
27func TestAuthRoute(t *testing.T) {
28 router := setupRouter()
29
30 username, password := getTestUserCredentials()
31
32 w := httptest.NewRecorder()
33 req, _ := http.NewRequest("POST", "/api/v1/auth", nil)
34 req.SetBasicAuth(username, password)
35 router.ServeHTTP(w, req)
36
37 assert.Equal(t, http.StatusOK, w.Code, "response should return a 200 code")
38
39 var response map[string]interface{}
40 _ = json.Unmarshal(w.Body.Bytes(), &response)
41 _, exists := response["token"]
42 assert.True(t, exists, "response should return a token")
43}
44
45func TestAuthRouteFailure(t *testing.T) {
46 router := setupRouter()
47
48 w := httptest.NewRecorder()
49 req, _ := http.NewRequest("POST", "/api/v1/auth", nil)
50 req.SetBasicAuth("asdf", "asdf")
51 router.ServeHTTP(w, req)
52
53 assert.Equal(t, http.StatusUnauthorized, w.Code, "should return a 401 code")
54}
55
56func Test \ No newline at end of file
diff --git a/api/go.mod b/api/go.mod
index 6a326bc..6c414bc 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -6,6 +6,8 @@ require (
6 github.com/gin-gonic/gin v1.9.1 6 github.com/gin-gonic/gin v1.9.1
7 github.com/google/uuid v1.6.0 7 github.com/google/uuid v1.6.0
8 github.com/mattn/go-sqlite3 v1.14.22 8 github.com/mattn/go-sqlite3 v1.14.22
9 github.com/spf13/viper v1.18.2
10 github.com/stretchr/testify v1.8.4
9 golang.org/x/crypto v0.19.0 11 golang.org/x/crypto v0.19.0
10) 12)
11 13
@@ -13,25 +15,42 @@ require (
13 github.com/bytedance/sonic v1.11.0 // indirect 15 github.com/bytedance/sonic v1.11.0 // indirect
14 github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 16 github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
15 github.com/chenzhuoyu/iasm v0.9.1 // indirect 17 github.com/chenzhuoyu/iasm v0.9.1 // indirect
18 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
19 github.com/fsnotify/fsnotify v1.7.0 // indirect
16 github.com/gabriel-vasile/mimetype v1.4.3 // indirect 20 github.com/gabriel-vasile/mimetype v1.4.3 // indirect
17 github.com/gin-contrib/sse v0.1.0 // indirect 21 github.com/gin-contrib/sse v0.1.0 // indirect
18 github.com/go-playground/locales v0.14.1 // indirect 22 github.com/go-playground/locales v0.14.1 // indirect
19 github.com/go-playground/universal-translator v0.18.1 // indirect 23 github.com/go-playground/universal-translator v0.18.1 // indirect
20 github.com/go-playground/validator/v10 v10.18.0 // indirect 24 github.com/go-playground/validator/v10 v10.18.0 // indirect
21 github.com/goccy/go-json v0.10.2 // indirect 25 github.com/goccy/go-json v0.10.2 // indirect
26 github.com/hashicorp/hcl v1.0.0 // indirect
22 github.com/json-iterator/go v1.1.12 // indirect 27 github.com/json-iterator/go v1.1.12 // indirect
23 github.com/klauspost/cpuid/v2 v2.2.6 // indirect 28 github.com/klauspost/cpuid/v2 v2.2.6 // indirect
24 github.com/leodido/go-urn v1.4.0 // indirect 29 github.com/leodido/go-urn v1.4.0 // indirect
30 github.com/magiconair/properties v1.8.7 // indirect
25 github.com/mattn/go-isatty v0.0.20 // indirect 31 github.com/mattn/go-isatty v0.0.20 // indirect
32 github.com/mitchellh/mapstructure v1.5.0 // indirect
26 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
27 github.com/modern-go/reflect2 v1.0.2 // indirect 34 github.com/modern-go/reflect2 v1.0.2 // indirect
28 github.com/pelletier/go-toml/v2 v2.1.1 // indirect 35 github.com/pelletier/go-toml/v2 v2.1.1 // indirect
36 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
37 github.com/sagikazarmark/locafero v0.4.0 // indirect
38 github.com/sagikazarmark/slog-shim v0.1.0 // indirect
39 github.com/sourcegraph/conc v0.3.0 // indirect
40 github.com/spf13/afero v1.11.0 // indirect
41 github.com/spf13/cast v1.6.0 // indirect
42 github.com/spf13/pflag v1.0.5 // indirect
43 github.com/subosito/gotenv v1.6.0 // indirect
29 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 44 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
30 github.com/ugorji/go/codec v1.2.12 // indirect 45 github.com/ugorji/go/codec v1.2.12 // indirect
46 go.uber.org/atomic v1.9.0 // indirect
47 go.uber.org/multierr v1.9.0 // indirect
31 golang.org/x/arch v0.7.0 // indirect 48 golang.org/x/arch v0.7.0 // indirect
49 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
32 golang.org/x/net v0.21.0 // indirect 50 golang.org/x/net v0.21.0 // indirect
33 golang.org/x/sys v0.17.0 // indirect 51 golang.org/x/sys v0.17.0 // indirect
34 golang.org/x/text v0.14.0 // indirect 52 golang.org/x/text v0.14.0 // indirect
35 google.golang.org/protobuf v1.32.0 // indirect 53 google.golang.org/protobuf v1.32.0 // indirect
54 gopkg.in/ini.v1 v1.67.0 // indirect
36 gopkg.in/yaml.v3 v3.0.1 // indirect 55 gopkg.in/yaml.v3 v3.0.1 // indirect
37) 56)
diff --git a/api/go.sum b/api/go.sum
index 115a832..20771f7 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -10,8 +10,12 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
10github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 10github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
11github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 11github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
12github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
14github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
15github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
17github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
18github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
15github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 19github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
16github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 20github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
17github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 21github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -27,22 +31,30 @@ github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtP
27github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 31github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
28github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 32github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
29github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 33github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
30github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 34github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
31github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 35github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
32github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 36github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
33github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 37github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
38github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
39github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
34github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 40github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
35github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 41github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
36github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 42github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
37github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= 43github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
38github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 44github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
39github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 45github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
46github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
47github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
40github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 48github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
41github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 49github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
50github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
51github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
42github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 52github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
43github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 53github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
44github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 54github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
45github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 55github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
56github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
57github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
46github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
47github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 59github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
48github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 60github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -50,8 +62,24 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
50github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 62github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
51github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 63github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
52github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 64github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
53github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
54github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
67github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
68github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
69github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
70github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
71github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
72github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
73github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
74github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
75github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
76github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
77github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
78github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
79github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
80github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
81github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
82github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
55github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 83github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
56github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 84github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
57github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 85github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -62,15 +90,23 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
62github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 90github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
63github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 91github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
64github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 92github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
93github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
94github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
65github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 95github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
66github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 96github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
67github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 97github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
68github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 98github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
99go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
100go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
101go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
102go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
69golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 103golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
70golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= 104golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
71golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 105golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
72golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 106golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
73golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 107golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
108golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
109golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
74golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 110golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
75golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 111golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
76golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -79,11 +115,12 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
79golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 115golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
80golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 116golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
81golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 117golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
82golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
83google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 118google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
84google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 119google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
85gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
86gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
121gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
122gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
123gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
87gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 124gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
88gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 125gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
89gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 126gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go
new file mode 100644
index 0000000..744a884
--- /dev/null
+++ b/api/internal/controllers/auth.go
@@ -0,0 +1,66 @@
1package controllers
2
3import (
4 "encoding/base64"
5 "net/http"
6 "github.com/gin-gonic/gin"
7 "water/api/database"
8 "errors"
9 "crypto/rand"
10 "database/sql"
11
12 "water/api/models"
13 _ "github.com/mattn/go-sqlite3"
14 "golang.org/x/crypto/bcrypt"
15)
16
17func AuthHandler (c *gin.Context) {
18 username, password, ok := c.Request.BasicAuth()
19 if !ok {
20 c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`)
21 c.AbortWithStatus(http.StatusUnauthorized)
22 return
23 }
24
25 db := database.EstablishDBConnection()
26 defer func(db *sql.DB) {
27 err := db.Close()
28 if err != nil {
29 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
30 return
31 }
32 }(db)
33
34 var user models.User
35 var preference models.Preference
36 var size models.Size
37
38 row := db.QueryRow("SELECT name, uuid, password, color, size, unit FROM Users u INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Sizes s ON p.size_id = s.id WHERE u.name = ?", username)
39 if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil {
40 if errors.Is(err, sql.ErrNoRows) {
41 c.AbortWithStatus(http.StatusUnauthorized)
42 return
43 }
44 }
45
46 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
47 c.AbortWithStatus(http.StatusUnauthorized)
48 return
49 }
50
51 preference.Size = size
52
53 // Generate a simple API token
54 apiToken := generateToken()
55 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference})
56}
57
58// generatToken will g
59func generateToken() string {
60 token := make([]byte, 32)
61 _, err := rand.Read(token)
62 if err != nil {
63 return ""
64 }
65 return base64.StdEncoding.EncodeToString(token)
66} \ No newline at end of file
diff --git a/api/internal/controllers/stats.go b/api/internal/controllers/stats.go
new file mode 100644
index 0000000..9808ace
--- /dev/null
+++ b/api/internal/controllers/stats.go
@@ -0,0 +1,164 @@
1package controllers
2
3import (
4 "database/sql"
5 "github.com/gin-gonic/gin"
6 "net/http"
7 "water/api/database"
8 "water/api/models"
9)
10
11func GetAllStatistics(c *gin.Context) {
12 db := database.EstablishDBConnection()
13 defer func(db *sql.DB) {
14 err := db.Close()
15 if err != nil {
16 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
17 return
18 }
19 }(db)
20
21 rows, err := db.Query("SELECT s.date, s.quantity, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id")
22 if err != nil {
23 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
24 return
25 }
26 defer func(rows *sql.Rows) {
27 err := rows.Close()
28 if err != nil {
29 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
30 return
31 }
32 }(rows)
33
34 var data []models.Statistic
35
36 for rows.Next() {
37 var stat models.Statistic
38 var user models.User
39 if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil {
40 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
41 return
42 }
43 stat.User = user
44 data = append(data, stat)
45 }
46
47 c.JSON(http.StatusOK, data)
48}
49
50func PostNewStatistic(c *gin.Context) {
51 var stat models.StatisticPost
52
53 if err := c.BindJSON(&stat); err != nil {
54 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
55 return
56 }
57
58 db := database.EstablishDBConnection()
59 defer func(db *sql.DB) {
60 err := db.Close()
61 if err != nil {
62 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
63 return
64 }
65 }(db)
66
67 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity)
68
69 if err != nil {
70 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
71 }
72
73 id, err := result.LastInsertId()
74 if err != nil {
75 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
76 }
77
78 c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id})
79}
80
81func GetWeeklyStatistics (c *gin.Context) {
82 db := database.EstablishDBConnection()
83 defer func(db *sql.DB) {
84 err := db.Close()
85 if err != nil {
86 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
87 return
88 }
89 }(db)
90
91 rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`")
92 if err != nil {
93 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
94 return
95 }
96 defer func(rows *sql.Rows) {
97 err := rows.Close()
98 if err != nil {
99 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
100 return
101 }
102 }(rows)
103
104 var data []models.WeeklyStatistic
105 for rows.Next() {
106 var weeklyStat models.WeeklyStatistic
107 if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil {
108 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
109 }
110 data = append(data, weeklyStat)
111 }
112
113 c.JSON(http.StatusOK, data)
114}
115
116func GetDailyUserStatistics(c *gin.Context) {
117 db := database.EstablishDBConnection()
118 defer func(db *sql.DB) {
119 err := db.Close()
120 if err != nil {
121 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
122 return
123 }
124 }(db)
125
126 rows, err := db.Query("SELECT name, total FROM DailyUserStatistics")
127
128 if err != nil {
129 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
130 return
131 }
132 defer func(rows *sql.Rows) {
133 err := rows.Close()
134 if err != nil {
135 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
136 return
137 }
138 }(rows)
139
140 var data []models.DailyUserTotals
141 for rows.Next() {
142 var stat models.DailyUserTotals
143 if err := rows.Scan(&stat.Name, &stat.Total); err != nil {
144 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
145 return
146 }
147 data = append(data, stat)
148 }
149
150 c.JSON(http.StatusOK, data)
151
152}
153
154func GetUserStatistics(c *gin.Context) {
155 c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")})
156}
157
158func UpdateUserStatistic(c *gin.Context) {
159 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
160}
161
162func DeleteUserStatistic(c *gin.Context) {
163 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
164}
diff --git a/api/internal/controllers/user.go b/api/internal/controllers/user.go
new file mode 100644
index 0000000..1f3f813
--- /dev/null
+++ b/api/internal/controllers/user.go
@@ -0,0 +1,17 @@
1package controllers
2
3import (
4 "net/http"
5 "github.com/gin-gonic/gin"
6)
7
8func GetUser(c *gin.Context) {
9 c.JSON(http.StatusOK, gin.H{"message": "User found"})
10}
11func GetUserPreferences(c *gin.Context) {
12 c.JSON(http.StatusOK, gin.H{"message": "Preferences fetched successfully"})
13}
14
15func UpdateUserPreferences(c *gin.Context) {
16 c.JSON(http.StatusOK, gin.H{"message": "Preferences updated successfully"})
17} \ No newline at end of file
diff --git a/api/internal/database/database.go b/api/internal/database/database.go
new file mode 100644
index 0000000..e313af5
--- /dev/null
+++ b/api/internal/database/database.go
@@ -0,0 +1,22 @@
1package database
2
3import (
4 "database/sql"
5 _ "github.com/mattn/go-sqlite3"
6 "log"
7)
8
9func SetupDatabase() {
10 _, err := sql.Open("sqlite3", "water.db")
11 if err != nil {
12 log.Fatal(err)
13 }
14}
15
16func EstablishDBConnection() *sql.DB {
17 db, err := sql.Open("sqlite3", "../db/water.sqlite3")
18 if err != nil {
19 panic(err)
20 }
21 return db
22} \ No newline at end of file
diff --git a/api/internal/models/auth.go b/api/internal/models/auth.go
new file mode 100644
index 0000000..41344d5
--- /dev/null
+++ b/api/internal/models/auth.go
@@ -0,0 +1,11 @@
1package models
2
3import "time"
4
5type Token struct {
6 ID int64 `json:"-"`
7 UserID int64 `json:"user_id"`
8 Token string `json:"token"`
9 CreatedAt time.Time `json:"created_at"`
10 ExpiredAt time.Time `json:"expired_at"`
11}
diff --git a/api/internal/models/preferences.go b/api/internal/models/preferences.go
new file mode 100644
index 0000000..cbbd47c
--- /dev/null
+++ b/api/internal/models/preferences.go
@@ -0,0 +1,14 @@
1package models
2
3type Preference struct {
4 ID int64 `json:"-"`
5 Color string `json:"color"`
6 UserID int64 `json:"-"`
7 Size Size `json:"size"`
8}
9
10type Size struct {
11 ID int64 `json:"-"`
12 Size int64 `json:"size"`
13 Unit string `json:"unit"`
14}
diff --git a/api/internal/models/statistics.go b/api/internal/models/statistics.go
new file mode 100644
index 0000000..457e6a0
--- /dev/null
+++ b/api/internal/models/statistics.go
@@ -0,0 +1,26 @@
1package models
2
3import "time"
4
5type Statistic struct {
6 ID int64 `json:"-"`
7 Date time.Time `json:"date"`
8 User User `json:"user"`
9 Quantity int `json:"quantity"`
10}
11
12type StatisticPost struct {
13 Date time.Time `json:"date"`
14 Quantity int64 `json:"quantity"`
15 UserID int64 `json:"user_id"`
16}
17
18type WeeklyStatistic struct {
19 Date string `json:"date"`
20 Total int64 `json:"total"`
21}
22
23type DailyUserTotals struct {
24 Name string `json:"name"`
25 Total int64 `json:"total"`
26}
diff --git a/api/internal/models/user.go b/api/internal/models/user.go
new file mode 100644
index 0000000..2a3e6fd
--- /dev/null
+++ b/api/internal/models/user.go
@@ -0,0 +1,10 @@
1package models
2
3import "github.com/google/uuid"
4
5type User struct {
6 ID int64 `json:"-"`
7 Name string `json:"name"`
8 UUID uuid.UUID `json:"uuid"`
9 Password string `json:"-"`
10}
diff --git a/api/main.go b/api/main.go
deleted file mode 100644
index 17a3c3a..0000000
--- a/api/main.go
+++ /dev/null
@@ -1,301 +0,0 @@
1package main
2
3import (
4 "crypto/rand"
5 "database/sql"
6 "encoding/base64"
7 "errors"
8 "log"
9 "net/http"
10 "strings"
11
12 "github.com/gin-gonic/gin"
13 _ "github.com/mattn/go-sqlite3"
14 "golang.org/x/crypto/bcrypt"
15)
16
17func CORSMiddleware() gin.HandlerFunc {
18 return func(c *gin.Context) {
19 c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
20 c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
21 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")
22 c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
23
24 if c.Request.Method == "OPTIONS" {
25 log.Println(c.Request.Header)
26 c.AbortWithStatus(http.StatusNoContent)
27 return
28 }
29
30 c.Next()
31 }
32}
33
34// generatToken will g
35func generateToken() string {
36 token := make([]byte, 32)
37 _, err := rand.Read(token)
38 if err != nil {
39 return ""
40 }
41 return base64.StdEncoding.EncodeToString(token)
42}
43
44func establishDBConnection() *sql.DB {
45 db, err := sql.Open("sqlite3", "../db/water.sqlite3")
46 if err != nil {
47 panic(err)
48 }
49 return db
50}
51
52func checkForTokenInContext(c *gin.Context) (string, error) {
53 authorizationHeader := c.GetHeader("Authorization")
54 if authorizationHeader == "" {
55 return "", errors.New("authorization header is missing")
56 }
57
58 parts := strings.Split(authorizationHeader, " ")
59
60 if len(parts) != 2 || parts[0] != "Bearer" {
61 return "", errors.New("invalid Authorization header format")
62 }
63
64 return parts[1], nil
65}
66
67func TokenRequired() gin.HandlerFunc {
68 return func(c *gin.Context) {
69 _, err := checkForTokenInContext(c)
70
71 if err != nil {
72 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
73 c.Abort()
74 return
75 }
76
77 c.Next()
78 }
79}
80
81func setupRouter() *gin.Engine {
82 // Disable Console Color
83 // gin.DisableConsoleColor()
84 r := gin.Default()
85 r.Use(CORSMiddleware())
86 r.Use(gin.Logger())
87 r.Use(gin.Recovery())
88
89 api := r.Group("api/v1")
90
91 api.POST("/auth", func(c *gin.Context) {
92 username, password, ok := c.Request.BasicAuth()
93 if !ok {
94 c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`)
95 c.AbortWithStatus(http.StatusUnauthorized)
96 return
97 }
98
99 db := establishDBConnection()
100 defer func(db *sql.DB) {
101 err := db.Close()
102 if err != nil {
103 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
104 return
105 }
106 }(db)
107
108 var user User
109 var preference Preference
110 var size Size
111
112 row := db.QueryRow("SELECT name, uuid, password, color, size, unit FROM Users u INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Sizes s ON p.size_id = s.id WHERE u.name = ?", username)
113 if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil {
114 if errors.Is(err, sql.ErrNoRows) {
115 c.AbortWithStatus(http.StatusUnauthorized)
116 return
117 }
118 }
119
120 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
121 c.AbortWithStatus(http.StatusUnauthorized)
122 return
123 }
124
125 preference.Size = size
126
127 // Generate a simple API token
128 apiToken := generateToken()
129 c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference})
130 })
131
132 stats := api.Group("/stats")
133 stats.Use(TokenRequired())
134 {
135 stats.GET("/", func(c *gin.Context) {
136 db := establishDBConnection()
137 defer func(db *sql.DB) {
138 err := db.Close()
139 if err != nil {
140 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
141 return
142 }
143 }(db)
144
145 rows, err := db.Query("SELECT s.date, s.quantity, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id")
146 if err != nil {
147 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
148 return
149 }
150 defer func(rows *sql.Rows) {
151 err := rows.Close()
152 if err != nil {
153 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
154 return
155 }
156 }(rows)
157
158 var data []Statistic
159
160 for rows.Next() {
161 var stat Statistic
162 var user User
163 if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil {
164 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
165 return
166 }
167 stat.User = user
168 data = append(data, stat)
169 }
170
171 c.JSON(http.StatusOK, data)
172 })
173
174 stats.POST("/", func(c *gin.Context) {
175 var stat StatisticPost
176
177 if err := c.BindJSON(&stat); err != nil {
178 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
179 return
180 }
181
182 db := establishDBConnection()
183 defer func(db *sql.DB) {
184 err := db.Close()
185 if err != nil {
186 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
187 return
188 }
189 }(db)
190
191 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity)
192
193 if err != nil {
194 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
195 }
196
197 id, err := result.LastInsertId()
198 if err != nil {
199 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
200 }
201
202 c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id})
203 })
204
205 stats.GET("weekly/", func(c *gin.Context) {
206 db := establishDBConnection()
207 defer func(db *sql.DB) {
208 err := db.Close()
209 if err != nil {
210 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
211 return
212 }
213 }(db)
214
215 rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`")
216 if err != nil {
217 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
218 return
219 }
220 defer func(rows *sql.Rows) {
221 err := rows.Close()
222 if err != nil {
223 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
224 return
225 }
226 }(rows)
227
228 var data []WeeklyStatistic
229 for rows.Next() {
230 var weeklyStat WeeklyStatistic
231 if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil {
232 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
233 }
234 data = append(data, weeklyStat)
235 }
236
237 c.JSON(http.StatusOK, data)
238 })
239
240 stats.GET("totals/", func(c *gin.Context) {
241 db := establishDBConnection()
242 defer func(db *sql.DB) {
243 err := db.Close()
244 if err != nil {
245 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
246 return
247 }
248 }(db)
249
250 rows, err := db.Query("SELECT name, total FROM DailyUserStatistics")
251
252 if err != nil {
253 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
254 return
255 }
256 defer func(rows *sql.Rows) {
257 err := rows.Close()
258 if err != nil {
259 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
260 return
261 }
262 }(rows)
263
264 var data []DailyUserTotals
265 for rows.Next() {
266 var stat DailyUserTotals
267 if err := rows.Scan(&stat.Name, &stat.Total); err != nil {
268 c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
269 return
270 }
271 data = append(data, stat)
272 }
273
274 c.JSON(http.StatusOK, data)
275
276 })
277
278 stats.GET("user/:uuid", func(c *gin.Context) {
279 c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")})
280 })
281
282 stats.PATCH("user/:uuid", func(c *gin.Context) {
283 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
284 })
285
286 stats.DELETE("user/:uuid", func(c *gin.Context) {
287 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
288 })
289 }
290
291 return r
292}
293
294func main() {
295 r := setupRouter()
296 // Listen and Server in 0.0.0.0:8080
297 err := r.Run(":8080")
298 if err != nil {
299 return
300 }
301}
diff --git a/api/models.go b/api/models.go
deleted file mode 100644
index 0845d1d..0000000
--- a/api/models.go
+++ /dev/null
@@ -1,57 +0,0 @@
1package main
2
3import (
4 "time"
5 "github.com/google/uuid"
6)
7
8type Statistic struct {
9 ID int64 `json:"-"`
10 Date time.Time `json:"date"`
11 User User `json:"user"`
12 Quantity int `json:"quantity"`
13}
14
15type StatisticPost struct {
16 Date time.Time `json:"date"`
17 Quantity int64 `json:"quantity"`
18 UserID int64 `json:"user_id"`
19}
20
21type User struct {
22 ID int64 `json:"-"`
23 Name string `json:"name"`
24 UUID uuid.UUID `json:"uuid"`
25 Password string `json:"-"`
26}
27
28type Token struct {
29 ID int64 `json:"-"`
30 UserID int64 `json:"user_id"`
31 Token string `json:"token"`
32 CreatedAt time.Time `json:"created_at"`
33 ExpiredAt time.Time `json:"expired_at"`
34}
35
36type Preference struct {
37 ID int64 `json:"-"`
38 Color string `json:"color"`
39 UserID int64 `json:"-"`
40 Size Size `json:"size"`
41}
42
43type Size struct {
44 ID int64 `json:"-"`
45 Size int64 `json:"size"`
46 Unit string `json:"unit"`
47}
48
49type WeeklyStatistic struct {
50 Date string `json:"date"`
51 Total int64 `json:"total"`
52}
53
54type DailyUserTotals struct {
55 Name string `json:"name"`
56 Total int64 `json:"total"`
57} \ No newline at end of file