From 3eafb413a48cde60dea8a7355ee621c6acca952f Mon Sep 17 00:00:00 2001 From: Doog <157747121+doogongithub@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:07:27 -0500 Subject: first commit --- api/go.mod | 32 + api/go.sum | 80 ++ api/main.go | 99 +++ db/scripts/water_init.sql | 42 ++ db/water.sqlite3 | Bin 0 -> 40960 bytes fe/.gitignore | 24 + fe/.vscode/extensions.json | 3 + fe/README.md | 47 ++ fe/index.html | 13 + fe/package-lock.json | 1772 ++++++++++++++++++++++++++++++++++++++++++++ fe/package.json | 21 + fe/public/vite.svg | 1 + fe/src/App.svelte | 167 +++++ fe/src/app.css | 111 +++ fe/src/assets/svelte.svg | 1 + fe/src/lib/Card.svelte | 22 + fe/src/lib/Counter.svelte | 10 + fe/src/lib/Table.svelte | 41 + fe/src/lib/errors.ts | 7 + fe/src/lib/utils.ts | 9 + fe/src/main.ts | 8 + fe/src/vite-env.d.ts | 2 + fe/svelte.config.js | 7 + fe/tsconfig.json | 20 + fe/tsconfig.node.json | 10 + fe/vite.config.ts | 7 + 26 files changed, 2556 insertions(+) create mode 100644 api/go.mod create mode 100644 api/go.sum create mode 100644 api/main.go create mode 100644 db/scripts/water_init.sql create mode 100644 db/water.sqlite3 create mode 100644 fe/.gitignore create mode 100644 fe/.vscode/extensions.json create mode 100644 fe/README.md create mode 100644 fe/index.html create mode 100644 fe/package-lock.json create mode 100644 fe/package.json create mode 100644 fe/public/vite.svg create mode 100644 fe/src/App.svelte create mode 100644 fe/src/app.css create mode 100644 fe/src/assets/svelte.svg create mode 100644 fe/src/lib/Card.svelte create mode 100644 fe/src/lib/Counter.svelte create mode 100644 fe/src/lib/Table.svelte create mode 100644 fe/src/lib/errors.ts create mode 100644 fe/src/lib/utils.ts create mode 100644 fe/src/main.ts create mode 100644 fe/src/vite-env.d.ts create mode 100644 fe/svelte.config.js create mode 100644 fe/tsconfig.json create mode 100644 fe/tsconfig.node.json create mode 100644 fe/vite.config.ts diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..02f7c09 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,32 @@ +module water/api + +go 1.18 + +require ( + github.com/bytedance/sonic v1.11.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.18.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..eff6af1 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,80 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo= +github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= +github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..ebae5d1 --- /dev/null +++ b/api/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "net/http" + "crypto/rand" + "encoding/base64" + + "github.com/gin-gonic/gin" +) + +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" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} + +func generateToken() string { + token := make([]byte, 32) + rand.Read(token) + return base64.StdEncoding.EncodeToString(token) +} + +type User struct { + Username string + Password string +} + +var users = map[string]User{ + "user1": {"user1", "password1"}, +} + +func setupRouter() *gin.Engine { + // Disable Console Color + // gin.DisableConsoleColor() + r := gin.Default() + r.Use(CORSMiddleware()) + + api := r.Group("api/v1") + + api.POST("/auth", func(c *gin.Context) { + username, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + user, exists := users[username] + + if !exists || user.Password != password { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Generate a simple API token + apiToken := generateToken() + c.JSON(http.StatusOK, gin.H{"token": apiToken}) + }) + + stats := api.Group("stats") + + stats.GET("/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + stats.POST("/", func(c *gin.Context) { + c.JSON(http.StatusCreated, gin.H{"status": "created"}) + }) + + stats.GET("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) + }) + + stats.PATCH("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + + stats.DELETE("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + + return r +} + +func main() { + r := setupRouter() + // Listen and Server in 0.0.0.0:8080 + r.Run(":8080") +} diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql new file mode 100644 index 0000000..d7b912a --- /dev/null +++ b/db/scripts/water_init.sql @@ -0,0 +1,42 @@ +-- user table for users. +CREATE TABLE IF NOT EXISTS Users ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + UNIQUE(name) +); + +-- statistics table for users to log their consumption +CREATE TABLE IF NOT EXISTS Statistics ( + id INT PRIMARY KEY, + date DATETIME NOT NULL, + user_id INT NOT NULL, + quantity INT +); + +-- preferences table for a user. +CREATE TABLE IF NOT EXISTS Preferences ( + id INT PRIMARY KEY, + color TEXT NOT NULL DEFAULT "#000000", + user_id INT NOT NULL, + size_id INT NOT NULL DEFAULT 1, + FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE + FOREIGN KEY(size_id) REFERENCES Sizes(id) +); + +-- lookup table for sizes. +CREATE TABLE IF NOT EXISTS Sizes ( + id INT PRIMARY KEY, + size INT NOT NULL + unit TEXT DEFAULT "oz" +); + +-- create default sizes for sizes lookup table. +INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); + +-- create default users. +INSERT OR IGNORE INTO Users (id, name) VALUES (1, 'Parker'), (2, 'Zach'); + +-- create default preferences. +INSERT OR IGNORE INTO Preferences (id, user_id) VALUES (1, 1), (2, 2); + + diff --git a/db/water.sqlite3 b/db/water.sqlite3 new file mode 100644 index 0000000..97f9214 Binary files /dev/null and b/db/water.sqlite3 differ diff --git a/fe/.gitignore b/fe/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/fe/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/fe/.vscode/extensions.json b/fe/.vscode/extensions.json new file mode 100644 index 0000000..bdef820 --- /dev/null +++ b/fe/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/fe/README.md b/fe/README.md new file mode 100644 index 0000000..e6cd94f --- /dev/null +++ b/fe/README.md @@ -0,0 +1,47 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/fe/index.html b/fe/index.html new file mode 100644 index 0000000..b6c5f0a --- /dev/null +++ b/fe/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + TS + + +
+ + + diff --git a/fe/package-lock.json b/fe/package-lock.json new file mode 100644 index 0000000..2c4146a --- /dev/null +++ b/fe/package-lock.json @@ -0,0 +1,1772 @@ +{ + "name": "fe", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fe", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tsconfig/svelte": "^5.0.2", + "svelte": "^4.2.10", + "svelte-check": "^3.6.3", + "tslib": "^2.6.2", + "typescript": "^5.2.2", + "vite": "^5.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", + "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", + "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.2.tgz", + "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz", + "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-check": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz", + "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.1.0", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", + "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0", + "pnpm": "^8.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + } +} diff --git a/fe/package.json b/fe/package.json new file mode 100644 index 0000000..6bc8619 --- /dev/null +++ b/fe/package.json @@ -0,0 +1,21 @@ +{ + "name": "fe", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tsconfig/svelte": "^5.0.2", + "svelte": "^4.2.10", + "svelte-check": "^3.6.3", + "tslib": "^2.6.2", + "typescript": "^5.2.2", + "vite": "^5.1.0" + } +} diff --git a/fe/public/vite.svg b/fe/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/fe/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fe/src/App.svelte b/fe/src/App.svelte new file mode 100644 index 0000000..cc4e594 --- /dev/null +++ b/fe/src/App.svelte @@ -0,0 +1,167 @@ + + +
+ {#if !authenticated} + +
+
+ + +
+
+ + +
+ {#if error} +

{error}

+ {/if} + +
+
+ {:else} +
+ +
+
+ + + + + + +
+ + + + {#await data} +

...fetching

+ {:then data} + {#if data} +

Status

+

{data.status}

+ +
+
+ {:else} +

No data yet

+ {/if} + {:catch errror} +

{error.message}

+ {/await} + {/if} + + + diff --git a/fe/src/app.css b/fe/src/app.css new file mode 100644 index 0000000..4768cf6 --- /dev/null +++ b/fe/src/app.css @@ -0,0 +1,111 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + --submit: #28a745; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #000; + } +} + +.form { + display: flex; + flex-direction: column; +} + +.form.input.group { + display: flex; + flex-direction: column; + margin-bottom: 1em; +} + +.form.input.group label { + margin-bottom: .5em; +} + +.form.input.group input { + padding: 1em; +} + +.form button[type=submit] { + align-self: flex-end; + background: var(--submit); + color: #fff; +} diff --git a/fe/src/assets/svelte.svg b/fe/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/fe/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte new file mode 100644 index 0000000..feb5bcc --- /dev/null +++ b/fe/src/lib/Card.svelte @@ -0,0 +1,22 @@ + + +
+ {#if title} +

{title}

+ {/if} + +
+ + diff --git a/fe/src/lib/Counter.svelte b/fe/src/lib/Counter.svelte new file mode 100644 index 0000000..979b4df --- /dev/null +++ b/fe/src/lib/Counter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte new file mode 100644 index 0000000..2df9f8c --- /dev/null +++ b/fe/src/lib/Table.svelte @@ -0,0 +1,41 @@ + +
+ {#if title} +

{title}

+ {/if} + {#if !noheader} + + + + + + {/if} + + + + + + {#if !nofooter} + + + + + + + + {/if} +
+ Data Header +
Data
Table Footer
+ diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts new file mode 100644 index 0000000..0663d63 --- /dev/null +++ b/fe/src/lib/errors.ts @@ -0,0 +1,7 @@ +export class UnauthorizedError extends Error { + constructor (message?: string , options?: ErrorOptions) { + super(message, options); + } +} + + diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts new file mode 100644 index 0000000..c5501ae --- /dev/null +++ b/fe/src/lib/utils.ts @@ -0,0 +1,9 @@ +export function processFormInput(form) { + const formData = new FormData(form); + const data = {}; + for (let field of formData) { + const [key, value] = field; + data[key] = value; + } + return data; +} diff --git a/fe/src/main.ts b/fe/src/main.ts new file mode 100644 index 0000000..8a909a1 --- /dev/null +++ b/fe/src/main.ts @@ -0,0 +1,8 @@ +import './app.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app'), +}) + +export default app diff --git a/fe/src/vite-env.d.ts b/fe/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/fe/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/fe/svelte.config.js b/fe/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/fe/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/fe/tsconfig.json b/fe/tsconfig.json new file mode 100644 index 0000000..5fb548f --- /dev/null +++ b/fe/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/fe/tsconfig.node.json b/fe/tsconfig.node.json new file mode 100644 index 0000000..d02c37d --- /dev/null +++ b/fe/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/fe/vite.config.ts b/fe/vite.config.ts new file mode 100644 index 0000000..d701969 --- /dev/null +++ b/fe/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) -- cgit v1.1 From e37c73e33a4aaf7fb8d25b5af03627f20bcda19f Mon Sep 17 00:00:00 2001 From: Doog <157747121+doogongithub@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:08:35 -0500 Subject: add gitignore --- .gitignore | 49 +++++++++++++++ api/go.mod | 6 +- api/go.sum | 9 +++ api/lib/models.go | 23 +++++++ api/main.go | 127 ++++++++++++++++++++++++++++++++------ db/scripts/water_init.sql | 10 +-- db/water.sqlite3 | Bin 40960 -> 24576 bytes fe/src/App.svelte | 147 +++----------------------------------------- fe/src/app.css | 2 +- fe/src/lib/DataView.svelte | 67 ++++++++++++++++++++ fe/src/lib/Layout.svelte | 57 +++++++++++++++++ fe/src/lib/LoginForm.svelte | 64 +++++++++++++++++++ fe/src/lib/Table.svelte | 61 +++++++++++++++--- fe/src/stores/auth.ts | 48 +++++++++++++++ fe/svelte.config.js | 1 + 15 files changed, 500 insertions(+), 171 deletions(-) create mode 100644 .gitignore create mode 100644 api/lib/models.go create mode 100644 fe/src/lib/DataView.svelte create mode 100644 fe/src/lib/Layout.svelte create mode 100644 fe/src/lib/LoginForm.svelte create mode 100644 fe/src/stores/auth.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e424ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Dependency directories +node_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional REPL history +.node_repl_history + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + + diff --git a/api/go.mod b/api/go.mod index 02f7c09..08ad9e1 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,12 +3,16 @@ module water/api go 1.18 require ( + github.com/gin-gonic/gin v1.9.1 + github.com/mattn/go-sqlite3 v1.14.22 +) + +require ( github.com/bytedance/sonic v1.11.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.18.0 // indirect diff --git a/api/go.sum b/api/go.sum index eff6af1..5174feb 100644 --- a/api/go.sum +++ b/api/go.sum @@ -10,6 +10,7 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 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/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= @@ -17,6 +18,7 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -25,6 +27,7 @@ github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtP github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -36,6 +39,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -43,6 +48,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -52,6 +58,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -70,8 +77,10 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= diff --git a/api/lib/models.go b/api/lib/models.go new file mode 100644 index 0000000..92e5703 --- /dev/null +++ b/api/lib/models.go @@ -0,0 +1,23 @@ +package models + +import "time" + +type Statistic struct { + ID int64 `json:"id"` + Date time.Time `json:"date"` + UserID int64 `json:"user_id"` + Quantity int `json:"quantity"` +} + +type User struct { + ID int64 + Name string +} + +type Token struct { + ID int64 + UserID int64 + Token string + CreatedAt time.Time + ExpiredAt time.Time +} diff --git a/api/main.go b/api/main.go index ebae5d1..292a5f9 100644 --- a/api/main.go +++ b/api/main.go @@ -4,8 +4,13 @@ import ( "net/http" "crypto/rand" "encoding/base64" + "database/sql" + "strings" + "errors" "github.com/gin-gonic/gin" + _ "github.com/mattn/go-sqlite3" + "water/api/lib" ) func CORSMiddleware() gin.HandlerFunc { @@ -30,6 +35,44 @@ func generateToken() string { return base64.StdEncoding.EncodeToString(token) } +func establishDBConnection() *sql.DB { + db, err := sql.Open("sqlite3", "../db/water.sqlite3") + if err != nil { + panic(err) + } + return db +} + +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() + } +} + type User struct { Username string Password string @@ -44,6 +87,8 @@ func setupRouter() *gin.Engine { // gin.DisableConsoleColor() r := gin.Default() r.Use(CORSMiddleware()) + r.Use(gin.Logger()) + r.Use(gin.Recovery()) api := r.Group("api/v1") @@ -68,26 +113,70 @@ func setupRouter() *gin.Engine { }) stats := api.Group("stats") + stats.Use(TokenRequired()) + { + stats.GET("/", func(c *gin.Context) { + db := establishDBConnection() + defer db.Close() + + rows, err := db.Query("SELECT * FROM statistics"); + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var data []models.Statistic + for rows.Next() { + var stat models.Statistic + if err := rows.Scan(&stat.ID, &stat.Date, &stat.UserID, &stat.Quantity); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + data = append(data, stat) + } + + c.JSON(http.StatusOK, data) + }) + + stats.POST("/", func(c *gin.Context) { + var stat models.Statistic + + if err := c.BindJSON(&stat); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := establishDBConnection() + defer db.Close() + + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + id, err := result.LastInsertId() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) + }) + + stats.GET("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) + }) + + stats.PATCH("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + + stats.DELETE("/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + } - stats.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - }) - - stats.POST("/", func(c *gin.Context) { - c.JSON(http.StatusCreated, gin.H{"status": "created"}) - }) - - stats.GET("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) - }) - - stats.PATCH("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - - stats.DELETE("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) return r } diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql index d7b912a..0751c41 100644 --- a/db/scripts/water_init.sql +++ b/db/scripts/water_init.sql @@ -1,13 +1,13 @@ -- user table for users. CREATE TABLE IF NOT EXISTS Users ( - id INT PRIMARY KEY, + id INTEGER PRIMARY KEY, name TEXT NOT NULL, UNIQUE(name) ); -- statistics table for users to log their consumption CREATE TABLE IF NOT EXISTS Statistics ( - id INT PRIMARY KEY, + id INTEGER PRIMARY KEY, date DATETIME NOT NULL, user_id INT NOT NULL, quantity INT @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS Statistics ( -- preferences table for a user. CREATE TABLE IF NOT EXISTS Preferences ( - id INT PRIMARY KEY, + id INTEGER PRIMARY KEY, color TEXT NOT NULL DEFAULT "#000000", user_id INT NOT NULL, size_id INT NOT NULL DEFAULT 1, @@ -25,8 +25,8 @@ CREATE TABLE IF NOT EXISTS Preferences ( -- lookup table for sizes. CREATE TABLE IF NOT EXISTS Sizes ( - id INT PRIMARY KEY, - size INT NOT NULL + id INTEGER PRIMARY KEY, + size INT NOT NULL, unit TEXT DEFAULT "oz" ); diff --git a/db/water.sqlite3 b/db/water.sqlite3 index 97f9214..c800708 100644 Binary files a/db/water.sqlite3 and b/db/water.sqlite3 differ diff --git a/fe/src/App.svelte b/fe/src/App.svelte index cc4e594..8811c52 100644 --- a/fe/src/App.svelte +++ b/fe/src/App.svelte @@ -1,146 +1,19 @@
- {#if !authenticated} - -
-
- - -
-
- - -
- {#if error} -

{error}

- {/if} - -
-
+ + {#if !$authenticated} + {:else} -
- -
- - - - - {#await data} -

...fetching

- {:then data} - {#if data} -

Status

-

{data.status}

- -
-
- {:else} -

No data yet

- {/if} - {:catch errror} -

{error.message}

- {/await} + {/if} + diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte new file mode 100644 index 0000000..22c0faf --- /dev/null +++ b/fe/src/lib/LoginForm.svelte @@ -0,0 +1,64 @@ + + + +
+
+ + +
+
+ + +
+ {#if error} +

{error}

+ {/if} + + +
diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 2df9f8c..5572280 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte @@ -1,8 +1,38 @@
{#if title} @@ -11,16 +41,27 @@ {#if !noheader} - + {#each getDataKeys(data) as header} + + {/each} {/if} + {#if data} + {#each data as row} - + {#each getRow(row) as datum} + + + {/each} + {/each} + {:else} + + There is not data. + + {/if} {#if !nofooter} @@ -38,4 +79,8 @@ table { margin: 8px; border: solid 1px black; } + +th { + text-transform: capitalize; +} diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts new file mode 100644 index 0000000..7e70cda --- /dev/null +++ b/fe/src/stores/auth.ts @@ -0,0 +1,48 @@ +import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; +import { writable, derived } from 'svelte/store'; + +type Nullable = T | null; + +interface User { + uuid: string; + username: string; +} + +interface TokenStore { + subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, + authenticate: (newToken: string) => void, + unauthenticate: () => void +} + +function createTokenStore(): TokenStore { + const storedToken = localStorage.getItem("token"); + const { subscribe, set } = writable(storedToken); + + function authenticate(newToken: string): void { + try { + localStorage.setItem("token", newToken); + set(newToken); + } catch (e) { + console.error('error', e); + } + } + + function unauthenticate(): void { + localStorage.removeItem("token"); + set(null); + } + + return { + subscribe, + authenticate, + unauthenticate + }; +} + +function onTokenChange ($token: Nullable): boolean { + return $token ? true : false; +} + +export const token = createTokenStore(); +export const authenticated = derived(token, onTokenChange); +export const user = writable(null); diff --git a/fe/svelte.config.js b/fe/svelte.config.js index b0683fd..b29bf40 100644 --- a/fe/svelte.config.js +++ b/fe/svelte.config.js @@ -4,4 +4,5 @@ export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), + dev: true } -- cgit v1.1 From 9f9a33cbf55d38987a66b709284d2bb4ffea0fe9 Mon Sep 17 00:00:00 2001 From: Doog <157747121+doogongithub@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:13:48 -0500 Subject: modify api, build additional FE components, add types --- api/go.mod | 3 +- api/go.sum | 2 ++ api/lib/models.go | 38 +++++++++++++++------ api/main.go | 48 +++++++++++++++++---------- db/scripts/water_init.sql | 40 ++++++++++++++++++++--- db/water.sqlite3 | Bin 24576 -> 36864 bytes fe/src/lib/DataView.svelte | 67 +++++++++++++++++++++++++++++++++++--- fe/src/lib/LoginForm.svelte | 16 +++++---- fe/src/lib/PreferencesForm.svelte | 45 +++++++++++++++++++++++++ fe/src/lib/Table.svelte | 19 +++++++++++ fe/src/stores/auth.ts | 57 +++++++++++++++++++++++++++++++- fe/src/types.ts | 14 ++++++++ 12 files changed, 303 insertions(+), 46 deletions(-) create mode 100644 fe/src/lib/PreferencesForm.svelte create mode 100644 fe/src/types.ts diff --git a/api/go.mod b/api/go.mod index 08ad9e1..6a326bc 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,7 +4,9 @@ go 1.18 require ( github.com/gin-gonic/gin v1.9.1 + github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.22 + golang.org/x/crypto v0.19.0 ) require ( @@ -27,7 +29,6 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/api/go.sum b/api/go.sum index 5174feb..115a832 100644 --- a/api/go.sum +++ b/api/go.sum @@ -29,6 +29,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/api/lib/models.go b/api/lib/models.go index 92e5703..f959519 100644 --- a/api/lib/models.go +++ b/api/lib/models.go @@ -1,23 +1,41 @@ package models -import "time" +import ( + "time" + "github.com/google/uuid" +) type Statistic struct { - ID int64 `json:"id"` + ID int64 `json:"-"` Date time.Time `json:"date"` - UserID int64 `json:"user_id"` + User User `json:"user"` Quantity int `json:"quantity"` } type User struct { - ID int64 - Name string + ID int64 `json:"-"` + Name string `json:"name"` + UUID uuid.UUID `json:"uuid"` + Password string `json:"-"` } type Token struct { - ID int64 - UserID int64 - Token string - CreatedAt time.Time - ExpiredAt time.Time + ID int64 `json:"-"` + UserID int64 `json:"user_id"` + Token string `json:"token"` + CreatedAt time.Time `json:"created_at"` + ExpiredAt time.Time `json:"expired_at"` +} + +type Preference struct { + ID int64 `json:"-"` + Color string `json:"color"` + UserID int64 `json:"-"` + Size Size `json:"size"` +} + +type Size struct { + ID int64 `json:"-"` + Size int64 `json:"size"` + Unit string `json:"unit"` } diff --git a/api/main.go b/api/main.go index 292a5f9..91b7929 100644 --- a/api/main.go +++ b/api/main.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" "water/api/lib" ) @@ -29,6 +30,7 @@ func CORSMiddleware() gin.HandlerFunc { } } +// generatToken will g func generateToken() string { token := make([]byte, 32) rand.Read(token) @@ -43,6 +45,7 @@ func establishDBConnection() *sql.DB { return db } + func checkForTokenInContext(c *gin.Context) (string, error) { authorizationHeader := c.GetHeader("Authorization") if authorizationHeader == "" { @@ -54,6 +57,7 @@ func checkForTokenInContext(c *gin.Context) (string, error) { if len(parts) != 2 || parts[0] != "Bearer" { return "", errors.New("Invalid Authorization header format") } + return parts[1], nil } @@ -73,15 +77,6 @@ func TokenRequired() gin.HandlerFunc { } } -type User struct { - Username string - Password string -} - -var users = map[string]User{ - "user1": {"user1", "password1"}, -} - func setupRouter() *gin.Engine { // Disable Console Color // gin.DisableConsoleColor() @@ -100,16 +95,31 @@ func setupRouter() *gin.Engine { return } - user, exists := users[username] + db := establishDBConnection() + defer db.Close() + + var user models.User + var preference models.Preference + var size models.Size + + 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) + if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { + if err == sql.ErrNoRows { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } - if !exists || user.Password != password { - c.AbortWithStatus(http.StatusUnauthorized) - return - } + preference.Size = size // Generate a simple API token apiToken := generateToken() - c.JSON(http.StatusOK, gin.H{"token": apiToken}) + c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) }) stats := api.Group("stats") @@ -119,7 +129,7 @@ func setupRouter() *gin.Engine { db := establishDBConnection() defer db.Close() - rows, err := db.Query("SELECT * FROM statistics"); + 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"); if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return @@ -129,10 +139,12 @@ func setupRouter() *gin.Engine { var data []models.Statistic for rows.Next() { var stat models.Statistic - if err := rows.Scan(&stat.ID, &stat.Date, &stat.UserID, &stat.Quantity); err != nil { + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } + stat.User = user data = append(data, stat) } @@ -150,7 +162,7 @@ func setupRouter() *gin.Engine { db := establishDBConnection() defer db.Close() - result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql index 0751c41..6a4de24 100644 --- a/db/scripts/water_init.sql +++ b/db/scripts/water_init.sql @@ -1,8 +1,9 @@ -- user table for users. CREATE TABLE IF NOT EXISTS Users ( id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - UNIQUE(name) + password TEXT UNIQUE NOT NULL, + uuid TEXT UNIQUE NOT NULL, + name TEXT UNIQUE NOT NULL ); -- statistics table for users to log their consumption @@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS Preferences ( color TEXT NOT NULL DEFAULT "#000000", user_id INT NOT NULL, size_id INT NOT NULL DEFAULT 1, - FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE + FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE, FOREIGN KEY(size_id) REFERENCES Sizes(id) ); @@ -30,13 +31,42 @@ CREATE TABLE IF NOT EXISTS Sizes ( unit TEXT DEFAULT "oz" ); +CREATE TABLE IF NOT EXISTS APIToken ( + id INTEGER PRIMARY KEY, + token TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE +); + -- create default sizes for sizes lookup table. INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); -- create default users. -INSERT OR IGNORE INTO Users (id, name) VALUES (1, 'Parker'), (2, 'Zach'); +INSERT OR IGNORE INTO Users (name, password, uuid) VALUES ( + 'parker', + '$2y$10$2UlKrQJQV5cQOo/8VcFlq.ai3MWf7mA4//knEs2xVnHTeB.RnfN.m', + '1aa668f3-7527-4a67-9c24-fdf307542eeb' +), ( + 'zach', + '$2y$10$35UJnLpBj8ulhqN/3G4qKe0GYBOa/YunXit11n7ET6zknZpNeKpRS', + 'be3fd6b7-cf55-4eb8-92d8-1b745b439f34' +); -- create default preferences. -INSERT OR IGNORE INTO Preferences (id, user_id) VALUES (1, 1), (2, 2); +INSERT OR IGNORE INTO Preferences (user_id) VALUES (1), (2); +CREATE TRIGGER IF NOT EXISTS enforce_size_id +BEFORE INSERT ON Preferences +BEGIN + SELECT + CASE + WHEN ( + SELECT COUNT(*) FROM Sizes WHERE id = new.size_id + ) = 0 + THEN RAISE(ABORT, 'Size does not exist') + END; +END; +-- +CREATE VIEW IF NOT EXISTS aggregated_stats AS + SELECT u.uuid, SUM(s.quantity * s.size) from Statistics s INNER JOIN Users u ON u.id = s.user_id INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Size s ON s.id = p.size_id; diff --git a/db/water.sqlite3 b/db/water.sqlite3 index c800708..716c5a4 100644 Binary files a/db/water.sqlite3 and b/db/water.sqlite3 differ diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index cd7b042..dc8acae 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,11 +1,24 @@
+ +

Add Water

- - +
+ + +
+
+ + +
+
@@ -65,3 +112,15 @@ onMount(() => { {/await}
+ + diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte index 22c0faf..499a457 100644 --- a/fe/src/lib/LoginForm.svelte +++ b/fe/src/lib/LoginForm.svelte @@ -1,8 +1,8 @@ + +

User Preferences

+
+
+ + +
+
+ + +
+ + +
+ diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 5572280..4b81800 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte @@ -30,6 +30,11 @@ function formatDatum([key, value]: any[]) { const parsedDate = new Date(value); return formatter.format(parsedDate); } + + if (key === 'user') { + return value['name']; + } + return value; } @@ -78,9 +83,23 @@ table { padding: 16px; margin: 8px; border: solid 1px black; + border-collapse: collapse; } th { text-transform: capitalize; } + +thead tr { + background: rgba(0,0,23, 0.34); +} + +tbody tr:nth-child(odd) { + background: rgba(0,0,23,0.14); +} + +th, td { + padding: 1em; + border: 1px solid rgba(0,0,0, 1); +} diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 7e70cda..10e6bd3 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -1,4 +1,5 @@ import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; +import type { Preference } from '../types'; import { writable, derived } from 'svelte/store'; type Nullable = T | null; @@ -14,6 +15,18 @@ interface TokenStore { unauthenticate: () => void } + +interface UserStore { + subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, + setUser: (user: User) => void, + reset: () => void +} + +interface PreferenceStore { + subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, + set: (this: void, value: Nullable) => void +} + function createTokenStore(): TokenStore { const storedToken = localStorage.getItem("token"); const { subscribe, set } = writable(storedToken); @@ -43,6 +56,48 @@ function onTokenChange ($token: Nullable): boolean { return $token ? true : false; } +function createUserStore(): UserStore { + const user = localStorage.getItem('user'); + const userObj: Nullable = user ? JSON.parse(user) : null; + const { subscribe, set } = writable(userObj); + + const setUser = (user: User) => { + localStorage.setItem('user', JSON.stringify(user)); + set(user); + } + + const reset = () => { + localStorage.removeItem('user'); + set(null); + } + + return { + subscribe, + setUser, + reset + } +} + + +function createPreferenceStore(): PreferenceStore { + const preferences = localStorage.getItem('preferences'); + const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { + color: "#FF0000", + size: { + size: 16, + unit: 'oz' + } + }; + + const { subscribe, set } = writable>(preferenceObj); + + return { + subscribe, + set + } +} + export const token = createTokenStore(); export const authenticated = derived(token, onTokenChange); -export const user = writable(null); +export const user = createUserStore(); +export const preferences = createPreferenceStore(); diff --git a/fe/src/types.ts b/fe/src/types.ts new file mode 100644 index 0000000..03d613d --- /dev/null +++ b/fe/src/types.ts @@ -0,0 +1,14 @@ +export interface Size { + size: number; + unit: string; +} + +export interface Preference { + color: string; + size: Size; +} + +export interface User { + name: string; + uuid: string; +} -- cgit v1.1 From afeffe31bd7d0f8333627a972e1d32e64a325b5b Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 1 Mar 2024 18:17:42 -0500 Subject: reformat fe --- .gitignore | 2 +- db/water.sqlite3 | Bin 36864 -> 0 bytes fe/.prettierrc | 4 + fe/src/App.svelte | 42 ++------- fe/src/lib/Card.svelte | 14 +-- fe/src/lib/Counter.svelte | 10 --- fe/src/lib/DataView.svelte | 177 +++++++++++++++++++------------------- fe/src/lib/Layout.svelte | 77 +++++++++-------- fe/src/lib/LoginForm.svelte | 105 +++++++++++++--------- fe/src/lib/PreferencesForm.svelte | 32 ++++--- fe/src/lib/Table.svelte | 138 ++++++++++++++--------------- fe/src/lib/errors.ts | 6 +- fe/src/lib/utils.ts | 16 ++-- fe/src/main.ts | 2 +- fe/src/stores/auth.ts | 132 ++++++++++++++-------------- fe/src/types.ts | 18 ++-- 16 files changed, 389 insertions(+), 386 deletions(-) delete mode 100644 db/water.sqlite3 create mode 100644 fe/.prettierrc delete mode 100644 fe/src/lib/Counter.svelte diff --git a/.gitignore b/.gitignore index 4e424ad..8a5cdb1 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,4 @@ node_modules/ .env.production.local .env.local - +*.sqlite3 diff --git a/db/water.sqlite3 b/db/water.sqlite3 deleted file mode 100644 index 716c5a4..0000000 Binary files a/db/water.sqlite3 and /dev/null differ diff --git a/fe/.prettierrc b/fe/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/fe/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/fe/src/App.svelte b/fe/src/App.svelte index 8811c52..25d53dc 100644 --- a/fe/src/App.svelte +++ b/fe/src/App.svelte @@ -1,40 +1,16 @@
- + {#if !$authenticated} - + {:else} - - {/if} - + + {/if} +
- - diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index feb5bcc..0835940 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte @@ -1,16 +1,16 @@
- {#if title} -

{title}

- {/if} - + {#if title} +

{title}

+ {/if} +
diff --git a/fe/src/lib/Counter.svelte b/fe/src/lib/Counter.svelte deleted file mode 100644 index 979b4df..0000000 --- a/fe/src/lib/Counter.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index dc8acae..1458c9a 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,126 +1,123 @@ - +
- - - -

Add Water

-
-
- - -
-
- - -
- - - -
- - {#await json then data} -
- Data Header - {header}
Data{formatDatum(datum)}
- {:catch error} -

{error}

- {/await} - + + +

Add Water

+
+
+ + +
+
+ + +
+ + + +
+ + {#await json then data} +
+ {:catch error} +

{error}

+ {/await} + diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte index f349632..94ce84d 100644 --- a/fe/src/lib/Layout.svelte +++ b/fe/src/lib/Layout.svelte @@ -1,57 +1,62 @@
- {#if $authenticated} + {#if $authenticated} - {/if} -
- -
+ + {/if} +
+ +
diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte index 499a457..bf6d9ad 100644 --- a/fe/src/lib/LoginForm.svelte +++ b/fe/src/lib/LoginForm.svelte @@ -1,66 +1,85 @@ - -
-
- - -
-
- - -
- {#if error} -

{error}

- {/if} - - +
+
+ + +
+
+ + +
+ {#if error} +

{error}

+ {/if} + +
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte index 781866c..95e04c1 100644 --- a/fe/src/lib/PreferencesForm.svelte +++ b/fe/src/lib/PreferencesForm.svelte @@ -1,7 +1,8 @@ - +

User Preferences

- - + +
- - + +
@@ -42,4 +43,9 @@ dialog { background: white; color: black; } + +input[type="color"] { + width: 100%; + height: 100%; +} diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 4b81800..3a66e0d 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte @@ -1,105 +1,105 @@ +
- {#if title} + {#if title}

{title}

- {/if} - {#if !noheader} + {/if} + {#if !noheader && data} - - {#each getDataKeys(data) as header} - - {/each} - + + {#each getDataKeys(data) as header} + + {/each} + - {/if} - - {#if data} - {#each data as row} + {/if} + + {#if data} + {#each data as row} - {#each getRow(row) as datum} - + {#each getRow(row) as datum} - {/each} + {/each} - {/each} - {:else} - - There is not data. - - {/if} - - {#if !nofooter} + {/each} + {:else} + There is not data. + {/if} + + {#if !nofooter} - + - + - + - {/if} + {/if}
{header}
{header}
{formatDatum(datum)}
Table FooterTable Footer
+ diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts index 0663d63..d44bec5 100644 --- a/fe/src/lib/errors.ts +++ b/fe/src/lib/errors.ts @@ -1,7 +1,7 @@ export class UnauthorizedError extends Error { - constructor (message?: string , options?: ErrorOptions) { - super(message, options); - } + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + } } diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts index c5501ae..22d4e9a 100644 --- a/fe/src/lib/utils.ts +++ b/fe/src/lib/utils.ts @@ -1,9 +1,9 @@ -export function processFormInput(form) { - const formData = new FormData(form); - const data = {}; - for (let field of formData) { - const [key, value] = field; - data[key] = value; - } - return data; +export function processFormInput(form: HTMLFormElement) { + const formData = new FormData(form); + const data: Record = {}; + for (let field of formData) { + const [key, value] = field; + data[key] = value; + } + return data; } diff --git a/fe/src/main.ts b/fe/src/main.ts index 8a909a1..ff866d0 100644 --- a/fe/src/main.ts +++ b/fe/src/main.ts @@ -2,7 +2,7 @@ import './app.css' import App from './App.svelte' const app = new App({ - target: document.getElementById('app'), + target: document.getElementById('app') as HTMLElement, }) export default app diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 10e6bd3..0efc80b 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -5,96 +5,96 @@ import { writable, derived } from 'svelte/store'; type Nullable = T | null; interface User { - uuid: string; - username: string; + uuid: string; + username: string; } interface TokenStore { - subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, - authenticate: (newToken: string) => void, - unauthenticate: () => void + subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, + authenticate: (newToken: string) => void, + unauthenticate: () => void } interface UserStore { - subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, - setUser: (user: User) => void, - reset: () => void + subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, + setUser: (user: User) => void, + reset: () => void } interface PreferenceStore { - subscribe: (run: Subscriber>, invalidate: Invalidator>) => Unsubscriber, - set: (this: void, value: Nullable) => void + subscribe: (run: Subscriber, invalidate?: Invalidator) => Unsubscriber, + set: (this: void, value: Preference) => void } function createTokenStore(): TokenStore { - const storedToken = localStorage.getItem("token"); - const { subscribe, set } = writable(storedToken); - - function authenticate(newToken: string): void { - try { - localStorage.setItem("token", newToken); - set(newToken); - } catch (e) { - console.error('error', e); - } + const storedToken = localStorage.getItem("token"); + const { subscribe, set } = writable(storedToken); + + function authenticate(newToken: string): void { + try { + localStorage.setItem("token", newToken); + set(newToken); + } catch (e) { + console.error('error', e); } - - function unauthenticate(): void { - localStorage.removeItem("token"); - set(null); - } - - return { - subscribe, - authenticate, - unauthenticate - }; + } + + function unauthenticate(): void { + localStorage.removeItem("token"); + set(null); + } + + return { + subscribe, + authenticate, + unauthenticate + }; } -function onTokenChange ($token: Nullable): boolean { - return $token ? true : false; +function onTokenChange($token: Nullable): boolean { + return $token ? true : false; } function createUserStore(): UserStore { - const user = localStorage.getItem('user'); - const userObj: Nullable = user ? JSON.parse(user) : null; - const { subscribe, set } = writable(userObj); - - const setUser = (user: User) => { - localStorage.setItem('user', JSON.stringify(user)); - set(user); - } + const user = localStorage.getItem('user'); + const userObj: Nullable = user ? JSON.parse(user) : null; + const { subscribe, set } = writable(userObj); + + const setUser = (user: User) => { + localStorage.setItem('user', JSON.stringify(user)); + set(user); + } + + const reset = () => { + localStorage.removeItem('user'); + set(null); + } + + return { + subscribe, + setUser, + reset + } +} - const reset = () => { - localStorage.removeItem('user'); - set(null); - } - return { - subscribe, - setUser, - reset +function createPreferenceStore(): PreferenceStore { + const preferences = localStorage.getItem('preferences'); + const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { + color: "#FF0000", + size: { + size: 16, + unit: 'oz' } -} + }; + const { subscribe, set } = writable(preferenceObj); -function createPreferenceStore(): PreferenceStore { - const preferences = localStorage.getItem('preferences'); - const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { - color: "#FF0000", - size: { - size: 16, - unit: 'oz' - } - }; - - const { subscribe, set } = writable>(preferenceObj); - - return { - subscribe, - set - } + return { + subscribe, + set + } } export const token = createTokenStore(); diff --git a/fe/src/types.ts b/fe/src/types.ts index 03d613d..526e7eb 100644 --- a/fe/src/types.ts +++ b/fe/src/types.ts @@ -1,14 +1,20 @@ export interface Size { - size: number; - unit: string; + size: number; + unit: string; } export interface Preference { - color: string; - size: Size; + color: string; + size: Size; } export interface User { - name: string; - uuid: string; + name: string; + uuid: string; } + +export interface Statistic { + user_id: string; + date: string; + quantity: number; +} \ No newline at end of file -- cgit v1.1 From 74ec025991f6acde6383e448974738e857758ebb Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 1 Mar 2024 18:50:51 -0500 Subject: Add dependencies, refine dataview --- fe/package-lock.json | 58 ++++++++++++++++++++++++++-------------------- fe/package.json | 4 ++++ fe/src/lib/DataView.svelte | 27 ++++++++++++++++++++- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/fe/package-lock.json b/fe/package-lock.json index 2c4146a..f74bf6c 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "fe", "version": "0.0.0", + "dependencies": { + "chart.js": "^4.4.2", + "svelte-chartjs": "^3.1.5" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.2", "@tsconfig/svelte": "^5.0.2", @@ -21,7 +25,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -402,7 +405,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -416,7 +418,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -425,7 +426,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -433,19 +433,22 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -698,8 +701,7 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/pug": { "version": "2.0.10", @@ -711,7 +713,6 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -736,7 +737,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -745,7 +745,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -805,6 +804,17 @@ "node": ">=6" } }, + "node_modules/chart.js": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -833,7 +843,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", @@ -852,7 +861,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" @@ -891,7 +899,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -953,7 +960,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -1131,7 +1137,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dev": true, "dependencies": { "@types/estree": "*" } @@ -1148,14 +1153,12 @@ "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/magic-string": { "version": "0.30.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -1166,8 +1169,7 @@ "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, "node_modules/merge2": { "version": "1.4.1", @@ -1309,7 +1311,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", @@ -1523,7 +1524,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1544,7 +1544,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz", "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -1565,6 +1564,15 @@ "node": ">=16" } }, + "node_modules/svelte-chartjs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz", + "integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==", + "peerDependencies": { + "chart.js": "^3.5.0 || ^4.0.0", + "svelte": "^4.0.0" + } + }, "node_modules/svelte-check": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz", diff --git a/fe/package.json b/fe/package.json index 6bc8619..9c75965 100644 --- a/fe/package.json +++ b/fe/package.json @@ -17,5 +17,9 @@ "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^5.1.0" + }, + "dependencies": { + "chart.js": "^4.4.2", + "svelte-chartjs": "^3.1.5" } } diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 1458c9a..00ee21a 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,12 +1,16 @@ @@ -102,6 +126,7 @@ + {#await json then data} {:catch error} -- cgit v1.1 From d8b0f1335078d53d95a4212b1a4d4b0b28016702 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 1 Mar 2024 20:12:21 -0500 Subject: feat(DataView): Add functionality to add water statistic This commit adds functionality to add water statistics to the DataView component. It includes the following changes: - Remove unused imports and variables - Move the 'handleClick' function logic to a new 'AddForm' component - Create the 'AddForm' component which displays a dialog with input fields for date and quantity and allows the user to add a new water statistic - Dispatch events on form submit and dialog close in the 'AddForm' component - Call the 'fetchData' function on successful submission of a new statistic - Update chart data to display sample data New component: - AddForm.svelte: A form component to add a new water statistic Note: This commit message exceeds the 50-character limit in the subject line, but adheres to the other specified requirements. --- fe/src/lib/DataView.svelte | 206 +++++++++++++++------------------------- fe/src/lib/forms/AddForm.svelte | 74 +++++++++++++++ 2 files changed, 148 insertions(+), 132 deletions(-) create mode 100644 fe/src/lib/forms/AddForm.svelte diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 00ee21a..7f368c6 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,148 +1,90 @@
- - -

Add Water

-
-
- - -
-
- - -
- - - -
- - - {#await json then data} -
- {:catch error} -

{error}

- {/await} - + + + + {#await json then data} +
+ {:catch error} +

{error}

+ {/await} + diff --git a/fe/src/lib/forms/AddForm.svelte b/fe/src/lib/forms/AddForm.svelte new file mode 100644 index 0000000..f22e5f4 --- /dev/null +++ b/fe/src/lib/forms/AddForm.svelte @@ -0,0 +1,74 @@ + + + +

Add Water

+
+
+ + +
+
+ + +
+ + + +
\ No newline at end of file -- cgit v1.1 From 326f186d67017f87e631a1fbcdf3f184cbc42d7d Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 1 Mar 2024 20:26:42 -0500 Subject: feat: Add last seven days labels to chart In the `DataView.svelte` component, the last seven days are now included as labels in the chart. This allows users to easily visualize data for the past week. The `getLastSevenDays` function generates an array of string values representing the dates in ISO format. This array is assigned to the `lastSevenDays` variable, which is then used as the labels in the chart's dataset. --- fe/src/lib/DataView.svelte | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 7f368c6..5182a85 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -12,6 +12,8 @@ let canvasRef: HTMLCanvasElement; let chart: any; + let lastSevenDays: string[]; + async function fetchData() { const res = await fetch("http://localhost:8080/api/v1/stats/", { method: "GET", @@ -26,6 +28,16 @@ } } + function getLastSevenDays() { + const result = []; + for (let i = 0; i < 7; i++) { + let d = new Date(); + d.setDate(d.getDate() - i); + result.push(d.toISOString().substring(0, 10)); + } + return result; + } + function handleClick() { open = true; } @@ -41,14 +53,21 @@ onMount(() => { fetchData(); + lastSevenDays = getLastSevenDays(); chart = new Chart(canvasRef, { type: "bar", data: { - labels: ["one", "two"], + labels: lastSevenDays, datasets: [ { - label: "Water", - data: [1, 2], + label: "Zach", + data: [1, 2, 8, 2, 5, 5, 1], + backgroundColor: "rgba(255, 192, 192, 0.2)", + borderColor: "rgba(75, 192, 192, 1)", + borderWidth: 1 + }, { + label: "Parker", + data: [6, 1, 1, 4, 3, 5, 1], backgroundColor: "rgba(75, 192, 192, 0.2)", borderColor: "rgba(75, 192, 192, 1)", borderWidth: 1 @@ -66,8 +85,8 @@
- + {#await json then data}
{:catch error} -- cgit v1.1 From cf2113e77edabf8e3a632c7b76c769752039ba88 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Sat, 2 Mar 2024 16:52:55 -0500 Subject: feat: Add API logging Add logging to the API to keep track of specific requests and headers. Also added CORS middleware to handle OPTIONS requests. --- The commit adds logging functionality to the API and includes a middleware function to handle CORS OPTIONS requests. This will allow us to track specific requests and headers received by the API. [API/main.go](/api/main.go): - Added import for the 'log' package - Added logging statements to print the request headers and "_I am here_" message - Removed unnecessary newlines and comments [fe/src/app.css](/fe/src/app.css): - Added a new style for button hover effects [fe/src/lib/Card.svelte](/fe/src/lib/Card.svelte): - Added a new `height` prop to the Card component [fe/src/lib/Column.svelte](/fe/src/lib/Column.svelte): - Added a new CSS class for column layout - Set the width and gap using CSS variables [fe/src/lib/DataView.svelte](/fe/src/lib/DataView.svelte): - Updated the 'fetchData' function to also fetch 'totals' and 'userStats' data - Added canvas references and chart variables for bar and line charts - Added a new 'getLastSevenDays' function to calculate the labels for the charts - Updated the 'onMount' function to initialize the bar and line charts using the canvas references and data - Updated the 'onDestroy' function to destroy the bar and line charts - Added a new 'addFormOpen' store and imported it - Added a new 'onClick' handler for the Add button to open the AddForm modal - Updated the layout and added Card components to display the bar and line charts and the JSON data - Added a new 'fetchTotals' function to fetch data for the 'totals' section - Added a new 'fetchStatsForUser' function to fetch data for the 'userStats' section [fe/src/lib/Layout.svelte](/fe/src/lib/Layout.svelte): - Added a new 'preferenceFormOpen' variable and initialized it to 'false' - Added a new 'showPreferencesDialog' function to set 'preferenceFormOpen' to 'true' - Added a new 'closePreferenceDialog' function to set 'preferenceFormOpen' to 'false' - Added a new 'showAddDialog' function to open the AddForm modal --- api/main.go | 384 +++++++++++++++++++++++++++------------------ fe/src/app.css | 101 ++++++------ fe/src/lib/Card.svelte | 30 ++-- fe/src/lib/Column.svelte | 13 ++ fe/src/lib/DataView.svelte | 129 ++++++++++++--- fe/src/lib/Layout.svelte | 116 +++++++------- fe/src/lib/Table.svelte | 172 ++++++++++---------- fe/src/stores/forms.ts | 6 + 8 files changed, 574 insertions(+), 377 deletions(-) create mode 100644 fe/src/lib/Column.svelte create mode 100644 fe/src/stores/forms.ts diff --git a/api/main.go b/api/main.go index 91b7929..57feb09 100644 --- a/api/main.go +++ b/api/main.go @@ -2,193 +2,267 @@ package main import ( "net/http" - "crypto/rand" - "encoding/base64" - "database/sql" - "strings" - "errors" + "crypto/rand" + "encoding/base64" + "database/sql" + "strings" + "errors" + "log" "github.com/gin-gonic/gin" - _ "github.com/mattn/go-sqlite3" - "golang.org/x/crypto/bcrypt" - "water/api/lib" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" + "water/api/lib" ) 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" { - c.AbortWithStatus(204) - return - } - - c.Next() - } + 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") + + log.Println("I am here") + + if c.Request.Method == "OPTIONS" { + log.Println(c.Request.Header) + c.AbortWithStatus(204) + return + } + + log.Println(c.Request.Header) + c.Next() + } } // generatToken will g func generateToken() string { - token := make([]byte, 32) - rand.Read(token) - return base64.StdEncoding.EncodeToString(token) + token := make([]byte, 32) + rand.Read(token) + return base64.StdEncoding.EncodeToString(token) } func establishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../db/water.sqlite3") - if err != nil { - panic(err) - } - return db + db, err := sql.Open("sqlite3", "../db/water.sqlite3") + if err != nil { + panic(err) + } + return db } - func checkForTokenInContext(c *gin.Context) (string, error) { - authorizationHeader := c.GetHeader("Authorization") - if authorizationHeader == "" { - return "", errors.New("Authorization header is missing") - } + authorizationHeader := c.GetHeader("Authorization") + if authorizationHeader == "" { + return "", errors.New("Authorization header is missing") + } - parts := strings.Split(authorizationHeader, " ") + parts := strings.Split(authorizationHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - return "", errors.New("Invalid Authorization header format") - } + if len(parts) != 2 || parts[0] != "Bearer" { + return "", errors.New("Invalid Authorization header format") + } - - return parts[1], nil + return parts[1], nil } - func TokenRequired() gin.HandlerFunc { - return func(c *gin.Context) { - _, err := checkForTokenInContext(c) + return func(c *gin.Context) { + _, err := checkForTokenInContext(c) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } - c.Next() - } + 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", func(c *gin.Context) { - username, password, ok := c.Request.BasicAuth() - if !ok { - c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - db := establishDBConnection() - defer db.Close() - - var user models.User - var preference models.Preference - var size models.Size - - 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) - if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { - if err == sql.ErrNoRows { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - preference.Size = size + r.Use(CORSMiddleware()) + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + api := r.Group("api/v1") + + api.POST("/auth", func(c *gin.Context) { + username, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + db := establishDBConnection() + defer db.Close() + + var user models.User + var preference models.Preference + var size models.Size + + 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) + if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { + if err == sql.ErrNoRows { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + preference.Size = size // Generate a simple API token apiToken := generateToken() - c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) - }) - - stats := api.Group("stats") - stats.Use(TokenRequired()) - { - stats.GET("/", func(c *gin.Context) { - db := establishDBConnection() - defer db.Close() - - 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"); - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - defer rows.Close() - - var data []models.Statistic - for rows.Next() { - var stat models.Statistic - var user models.User - if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - stat.User = user - data = append(data, stat) - } - - c.JSON(http.StatusOK, data) - }) - - stats.POST("/", func(c *gin.Context) { - var stat models.Statistic - - if err := c.BindJSON(&stat); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := establishDBConnection() - defer db.Close() - - result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) - - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - id, err := result.LastInsertId() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) - }) - - stats.GET("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) - }) - - stats.PATCH("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - - stats.DELETE("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - } - + c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) + }) + + stats := api.Group("/stats") + stats.Use(TokenRequired()) + { + stats.GET("/", func(c *gin.Context) { + db := establishDBConnection() + defer db.Close() + + 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") + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var data []models.Statistic + + for rows.Next() { + var stat models.Statistic + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + stat.User = user + data = append(data, stat) + } + + + // TODO: return to this and figure out how to best collect the data you are looking for for each user (zach and parker) + rows, err = db.Query("SELECT date(s.date), SUM(s.quantity) as total FROM Statistics s WHERE s.date >= date('now', '-7 days') GROUP BY DATE(s.date)") + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var dailySummaries []models.DailySummary + for rows.Next() { + var summary models.DailySummary + if err := rows.Scan(&summary.Date, &summary.Total); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + dailySummaries = append(dailySummaries, summary) + } + + c.JSON(http.StatusOK, gin.H{"stats": data, "totals": dailySummaries}) + rows, err = db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") + + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var totals []interface{} + for rows.Next() { + var stat models.Statistic + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + stat.User = user + totals = append(totals, stat) + } + + c.JSON(http.StatusOK, gin.H{"stats": data, "totals": totals}) + }) + + stats.POST("/", func(c *gin.Context) { + var stat models.Statistic + + if err := c.BindJSON(&stat); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := establishDBConnection() + defer db.Close() + + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + id, err := result.LastInsertId() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) + }) + + stats.GET("/totals/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // stats.GET("/totals/", func(c *gin.Context) { + // db := establishDBConnection() + // defer db.Close() + // + // rows, err := db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") + // + // if err != nil { + // c.JSON(500, gin.H{"error": err.Error()}) + // return + // } + // defer rows.Close() + // + // var data []models.Statistic + // for rows.Next() { + // var stat models.Statistic + // var user models.User + // if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + // c.JSON(500, gin.H{"error": err.Error()}) + // return + // } + // stat.User = user + // data = append(data, stat) + // } + // + // c.JSON(http.StatusOK, data) + // + // }) + + stats.GET("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) + }) + + stats.PATCH("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + + stats.DELETE("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + } return r } diff --git a/fe/src/app.css b/fe/src/app.css index 0d5fa90..de19b52 100644 --- a/fe/src/app.css +++ b/fe/src/app.css @@ -1,82 +1,87 @@ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; - --submit: #28a745; + --submit: #28a745; } a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; + font-weight: 500; + color: #646cff; + text-decoration: inherit; } + a:hover { - color: #535bf2; + color: #535bf2; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; } h1 { - font-size: 3.2em; - line-height: 1.1; + font-size: 3.2em; + line-height: 1.1; } .card { - padding: 2em; + padding: 2em; } #app { - flex-grow: 2; - max-width: 1280px; - margin: 0 auto; + flex-grow: 2; + max-width: 1280px; + margin: 0 auto; } button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; } + button:hover { - border-color: #646cff; + border-color: #646cff; } + button:focus, button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } } @media (prefers-color-scheme: dark) { @@ -97,7 +102,7 @@ button:focus-visible { } .form.input.group label { - margin-bottom: .5em; + margin-bottom: .5em; } .form.input.group input { diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index 0835940..d7cd900 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte @@ -1,22 +1,24 @@
- {#if title} -

{title}

- {/if} - + {#if title} +

{title}

+ {/if} +
diff --git a/fe/src/lib/Column.svelte b/fe/src/lib/Column.svelte new file mode 100644 index 0000000..f036073 --- /dev/null +++ b/fe/src/lib/Column.svelte @@ -0,0 +1,13 @@ +
+ +
+ + \ No newline at end of file diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 5182a85..2b1b8b9 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,16 +1,21 @@ -
- - - - {#await json then data} -
- {:catch error} -

{error}

- {/await} - - + + + + + + + + + + + + + {#await json then data} +
+ {:catch error} +

{error}

+ {/await} + + + + {#await totals then data} + {JSON.stringify(data)} + {:catch error} +

{error}

+ {/await} +
+ + + diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index 3a66e0d..d1cd7da 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte @@ -1,105 +1,113 @@
- {#if title} -

{title}

- {/if} - {#if !noheader && data} - - - {#each getDataKeys(data) as header} - - {/each} - - - {/if} - - {#if data} - {#each data as row} + {#if title} +

{title}

+ {/if} + {#if !noheader && data} + - {#each getRow(row) as datum} - - {/each} + {#each getDataKeys(data) as header} + + {/each} - {/each} + + {/if} + + {#if data} + {#each limitedData as row} + + {#each getRow(row) as datum} + + {/each} + + {/each} {:else} - There is not data. + There is not data. + {/if} + + {#if !nofooter} + + + + + + + {/if} - - {#if !nofooter} - - - - - - - - {/if}
{header}
{formatDatum(datum)}{header}
{formatDatum(datum)}
Table Footer
Table Footer
diff --git a/fe/src/stores/forms.ts b/fe/src/stores/forms.ts new file mode 100644 index 0000000..daf9181 --- /dev/null +++ b/fe/src/stores/forms.ts @@ -0,0 +1,6 @@ +import type { Writable } from "svelte/store"; +import { writable } from "svelte/store"; + + +export const preferencesFormOpen: Writable = writable(false); +export const addFormOpen: Writable = writable(false); \ No newline at end of file -- cgit v1.1 From 5fa57845052655883120ba4d19a85d8756fb8d8c Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Wed, 6 Mar 2024 21:53:07 -0500 Subject: [FEAT] Refactor API main file and models This commit refactors the `main.go` file in the API directory, as well as the related models in the `models.go` file. The changes include: - Reordering imports and removing unnecessary imports - Fixing error messages to be more descriptive - Handling database connections more efficiently with deferred closures - Handling errors and returning appropriate error responses - Adding proper JSON bindings for POST requests - Adding new views in the database scripts for aggregated statistics and daily user statistics No changes were made to imports and requires. --- api/lib/models.go | 41 -------- api/main.go | 215 ++++++++++++++++++++++------------------ api/models.go | 57 +++++++++++ db/scripts/water_init.sql | 27 ++++- fe/src/lib/DataView.svelte | 130 ++++++++++++------------ fe/src/lib/forms/AddForm.svelte | 13 ++- 6 files changed, 281 insertions(+), 202 deletions(-) delete mode 100644 api/lib/models.go create mode 100644 api/models.go diff --git a/api/lib/models.go b/api/lib/models.go deleted file mode 100644 index f959519..0000000 --- a/api/lib/models.go +++ /dev/null @@ -1,41 +0,0 @@ -package models - -import ( - "time" - "github.com/google/uuid" -) - -type Statistic struct { - ID int64 `json:"-"` - Date time.Time `json:"date"` - User User `json:"user"` - Quantity int `json:"quantity"` -} - -type User struct { - ID int64 `json:"-"` - Name string `json:"name"` - UUID uuid.UUID `json:"uuid"` - Password string `json:"-"` -} - -type Token struct { - ID int64 `json:"-"` - UserID int64 `json:"user_id"` - Token string `json:"token"` - CreatedAt time.Time `json:"created_at"` - ExpiredAt time.Time `json:"expired_at"` -} - -type Preference struct { - ID int64 `json:"-"` - Color string `json:"color"` - UserID int64 `json:"-"` - Size Size `json:"size"` -} - -type Size struct { - ID int64 `json:"-"` - Size int64 `json:"size"` - Unit string `json:"unit"` -} diff --git a/api/main.go b/api/main.go index 57feb09..17a3c3a 100644 --- a/api/main.go +++ b/api/main.go @@ -1,18 +1,17 @@ package main import ( - "net/http" "crypto/rand" - "encoding/base64" "database/sql" - "strings" + "encoding/base64" "errors" "log" + "net/http" + "strings" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "golang.org/x/crypto/bcrypt" - "water/api/lib" ) func CORSMiddleware() gin.HandlerFunc { @@ -22,15 +21,12 @@ func CORSMiddleware() gin.HandlerFunc { 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") - log.Println("I am here") - if c.Request.Method == "OPTIONS" { log.Println(c.Request.Header) - c.AbortWithStatus(204) + c.AbortWithStatus(http.StatusNoContent) return } - log.Println(c.Request.Header) c.Next() } } @@ -38,7 +34,10 @@ func CORSMiddleware() gin.HandlerFunc { // generatToken will g func generateToken() string { token := make([]byte, 32) - rand.Read(token) + _, err := rand.Read(token) + if err != nil { + return "" + } return base64.StdEncoding.EncodeToString(token) } @@ -53,13 +52,13 @@ func establishDBConnection() *sql.DB { func checkForTokenInContext(c *gin.Context) (string, error) { authorizationHeader := c.GetHeader("Authorization") if authorizationHeader == "" { - return "", errors.New("Authorization header is missing") + 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 "", errors.New("invalid Authorization header format") } return parts[1], nil @@ -98,15 +97,21 @@ func setupRouter() *gin.Engine { } db := establishDBConnection() - defer db.Close() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) - var user models.User - var preference models.Preference - var size models.Size + var user User + var preference Preference + var size Size 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) if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { c.AbortWithStatus(http.StatusUnauthorized) return } @@ -129,128 +134,147 @@ func setupRouter() *gin.Engine { { stats.GET("/", func(c *gin.Context) { db := establishDBConnection() - defer db.Close() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) 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") if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - defer rows.Close() + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) - var data []models.Statistic + var data []Statistic for rows.Next() { - var stat models.Statistic - var user models.User + var stat Statistic + var user User if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } stat.User = user data = append(data, stat) } + c.JSON(http.StatusOK, data) + }) - // TODO: return to this and figure out how to best collect the data you are looking for for each user (zach and parker) - rows, err = db.Query("SELECT date(s.date), SUM(s.quantity) as total FROM Statistics s WHERE s.date >= date('now', '-7 days') GROUP BY DATE(s.date)") - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + stats.POST("/", func(c *gin.Context) { + var stat StatisticPost + + if err := c.BindJSON(&stat); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - defer rows.Close() - var dailySummaries []models.DailySummary - for rows.Next() { - var summary models.DailySummary - if err := rows.Scan(&summary.Date, &summary.Total); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + db := establishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - dailySummaries = append(dailySummaries, summary) - } + }(db) - c.JSON(http.StatusOK, gin.H{"stats": data, "totals": dailySummaries}) - rows, err = db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } - defer rows.Close() - var totals []interface{} - for rows.Next() { - var stat models.Statistic - var user models.User - if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - stat.User = user - totals = append(totals, stat) + id, err := result.LastInsertId() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } - c.JSON(http.StatusOK, gin.H{"stats": data, "totals": totals}) + c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) }) - stats.POST("/", func(c *gin.Context) { - var stat models.Statistic + stats.GET("weekly/", func(c *gin.Context) { + db := establishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) - if err := c.BindJSON(&stat); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) + + var data []WeeklyStatistic + for rows.Next() { + var weeklyStat WeeklyStatistic + if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + data = append(data, weeklyStat) + } + + c.JSON(http.StatusOK, data) + }) + stats.GET("totals/", func(c *gin.Context) { db := establishDBConnection() - defer db.Close() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) - result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) + rows, err := db.Query("SELECT name, total FROM DailyUserStatistics") if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) - id, err := result.LastInsertId() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + var data []DailyUserTotals + for rows.Next() { + var stat DailyUserTotals + if err := rows.Scan(&stat.Name, &stat.Total); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + data = append(data, stat) } - c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) - }) + c.JSON(http.StatusOK, data) - stats.GET("/totals/", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) - // stats.GET("/totals/", func(c *gin.Context) { - // db := establishDBConnection() - // defer db.Close() - // - // rows, err := db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") - // - // if err != nil { - // c.JSON(500, gin.H{"error": err.Error()}) - // return - // } - // defer rows.Close() - // - // var data []models.Statistic - // for rows.Next() { - // var stat models.Statistic - // var user models.User - // if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - // c.JSON(500, gin.H{"error": err.Error()}) - // return - // } - // stat.User = user - // data = append(data, stat) - // } - // - // c.JSON(http.StatusOK, data) - // - // }) - stats.GET("user/:uuid", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) }) @@ -270,5 +294,8 @@ func setupRouter() *gin.Engine { func main() { r := setupRouter() // Listen and Server in 0.0.0.0:8080 - r.Run(":8080") + err := r.Run(":8080") + if err != nil { + return + } } diff --git a/api/models.go b/api/models.go new file mode 100644 index 0000000..0845d1d --- /dev/null +++ b/api/models.go @@ -0,0 +1,57 @@ +package main + +import ( + "time" + "github.com/google/uuid" +) + +type Statistic struct { + ID int64 `json:"-"` + Date time.Time `json:"date"` + User User `json:"user"` + Quantity int `json:"quantity"` +} + +type StatisticPost struct { + Date time.Time `json:"date"` + Quantity int64 `json:"quantity"` + UserID int64 `json:"user_id"` +} + +type User struct { + ID int64 `json:"-"` + Name string `json:"name"` + UUID uuid.UUID `json:"uuid"` + Password string `json:"-"` +} + +type Token struct { + ID int64 `json:"-"` + UserID int64 `json:"user_id"` + Token string `json:"token"` + CreatedAt time.Time `json:"created_at"` + ExpiredAt time.Time `json:"expired_at"` +} + +type Preference struct { + ID int64 `json:"-"` + Color string `json:"color"` + UserID int64 `json:"-"` + Size Size `json:"size"` +} + +type Size struct { + ID int64 `json:"-"` + Size int64 `json:"size"` + Unit string `json:"unit"` +} + +type WeeklyStatistic struct { + Date string `json:"date"` + Total int64 `json:"total"` +} + +type DailyUserTotals struct { + Name string `json:"name"` + Total int64 `json:"total"` +} \ No newline at end of file diff --git a/db/scripts/water_init.sql b/db/scripts/water_init.sql index 6a4de24..3b79ed5 100644 --- a/db/scripts/water_init.sql +++ b/db/scripts/water_init.sql @@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS Statistics ( CREATE TABLE IF NOT EXISTS Preferences ( id INTEGER PRIMARY KEY, color TEXT NOT NULL DEFAULT "#000000", - user_id INT NOT NULL, + user_id INT UNIQUE NOT NULL, size_id INT NOT NULL DEFAULT 1, FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE, FOREIGN KEY(size_id) REFERENCES Sizes(id) @@ -70,3 +70,28 @@ END; -- CREATE VIEW IF NOT EXISTS aggregated_stats AS SELECT u.uuid, SUM(s.quantity * s.size) from Statistics s INNER JOIN Users u ON u.id = s.user_id INNER JOIN Preferences p ON p.user_id = u.id INNER JOIN Size s ON s.id = p.size_id; + +CREATE VIEW IF NOT EXISTS `DailyUserStatistics` AS +SELECT users.name, IFNULL(SUM(statistics.quantity), 0) as total, preferences.color as color +FROM users +LEFT JOIN statistics ON users.id = statistics.user_id AND DATE(statistics.date) = DATE('now', '-1 day') +LEFT JOIN preferences ON users.id = preferences.user_id +GROUP BY users.name; + + +CREATE VIEW IF NOT EXISTS `WeeklyStatisticsView` AS + WITH DateSequence(Dates) AS + ( + SELECT Date(CURRENT_DATE, '-7 day') + UNION ALL + SELECT Date(Dates, '+1 day') + FROM DateSequence + WHERE Date(Dates, '+1 day') < Date(CURRENT_DATE) + ) +SELECT DateSequence.Dates as 'date', + IFNULL(SUM(statistics.quantity), 0) AS 'total' +FROM DateSequence +LEFT JOIN statistics +ON Date(statistics.date) = DateSequence.Dates +GROUP BY DateSequence.Dates +ORDER BY DateSequence.Dates; \ No newline at end of file diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 2b1b8b9..7d62a43 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -9,8 +9,6 @@ import AddForm from "./forms/AddForm.svelte"; let json: Promise; - let totals: Promise; - let userStats: Promise; let barCanvasRef: HTMLCanvasElement; let lineCanvasRef: HTMLCanvasElement; @@ -18,6 +16,10 @@ let lineChart: any; let lastSevenDays: string[]; + let lastSevenDaysData: number[]; + + let userTotalsLabels: string[]; + let userTotalsData: number[]; async function fetchData() { const res = await fetch("http://localhost:8080/api/v1/stats/", { @@ -33,24 +35,27 @@ } } - async function fetchTotals() { + async function fetchDailyUserStatistics() { const res = await fetch("http://localhost:8080/api/v1/stats/totals/", { - method: 'GET', - mode: 'no-cors', + method: "GET", headers: { Authorization: `Bearer ${$token}` } }); if (res.ok) { - totals = res.json(); + const json = await res.json(); + let labels = json.map(d => d.name); + let data = json.map(d => d.total); + return [labels, data]; } else { throw new Error("There was a problem with your request"); } + } - async function fetchStatsForUser() { - const res = await fetch("http://localhost:8080/api/v1/stats/user/1aa668f3-7527-4a67-9c24-fdf307542eeb", { + async function fetchWeeklyTotals() { + const res = await fetch("http://localhost:8080/api/v1/stats/weekly/", { method: "GET", headers: { Authorization: `Bearer ${$token}` @@ -58,22 +63,15 @@ }); if (res.ok) { - userStats = res.json(); + const json = await res.json(); + let labels = json.map(d => d.date); + let data = json.map(d => d.total); + return [labels, data]; } else { throw new Error("There was a problem with your request"); } } - function getLastSevenDays() { - const result = []; - for (let i = 0; i < 7; i++) { - let d = new Date(); - d.setDate(d.getDate() - i); - result.push(d.toISOString().substring(0, 10)); - } - return result; - } - function closeDialog() { addFormOpen.set(false); } @@ -81,61 +79,79 @@ function onStatisticAdd() { closeDialog(); fetchData(); + fetchWeeklyTotals().then(updateWeeklyTotalsChart).catch(err => console.error(err)); + fetchDailyUserStatistics().then(updateDailyUserTotalsChart).catch(err => console.error(err)); } - onMount(() => { - fetchData(); -// fetchTotals(); - fetchStatsForUser(); - lastSevenDays = getLastSevenDays(); - barChart = new Chart(barCanvasRef, { - type: "bar", + function setupWeeklyTotalsChart(result) { + [lastSevenDays, lastSevenDaysData] = result; + lineChart = new Chart(lineCanvasRef, { + type: "line", data: { labels: lastSevenDays, datasets: [ { - label: "Zach", - data: [1, 2, 8, 2, 5, 5, 1], - backgroundColor: "rgba(255, 192, 192, 0.2)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1 - }, { - label: "Parker", - data: [6, 1, 1, 4, 3, 5, 1], - backgroundColor: "rgba(75, 192, 192, 0.2)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1 + label: "Totals", + data: lastSevenDaysData, + backgroundColor: "rgba(255, 192, 192, 0.2)" } ] }, options: { - responsive: true + responsive: true, + plugins: { + legend: { + display: false + } + } } }); - lineChart = new Chart(lineCanvasRef, { - type: "line", + } + + function setupDailyUserTotalsChart(result) { + [userTotalsLabels, userTotalsData] = result; + + barChart = new Chart(barCanvasRef, { + type: "bar", data: { - labels: lastSevenDays, + labels: userTotalsLabels, datasets: [ { - label: "Zach", - data: [1, 2, 8, 2, 5, 5, 1], - backgroundColor: "rgba(255, 192, 192, 0.2)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1 - }, { - label: "Parker", - data: [6, 1, 1, 4, 3, 5, 1], - backgroundColor: "rgba(75, 192, 192, 0.2)", - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 1 + data: userTotalsData, + backgroundColor: [ + "#330000", + "rgba(100, 200, 192, 0.2)" + ] } ] }, options: { - responsive: true + responsive: true, + plugins: { + legend: { + display: false + } + } } }); + } + + function updateWeeklyTotalsChart(result) { + [,lastSevenDaysData] = result; + lineChart.data.datasets[0].data = lastSevenDaysData; + lineChart.update(); + } + + function updateDailyUserTotalsChart(result) { + [,userTotalsData] = result; + barChart.data.datasets[0].data = userTotalsData; + barChart.update(); + } + + onMount(() => { + fetchData(); + fetchWeeklyTotals().then(setupWeeklyTotalsChart); + fetchDailyUserStatistics().then(setupDailyUserTotalsChart); }); onDestroy(() => { @@ -164,14 +180,6 @@

{error}

{/await} - - - {#await totals then data} - {JSON.stringify(data)} - {:catch error} -

{error}

- {/await} -
diff --git a/fe/src/lib/forms/AddForm.svelte b/fe/src/lib/forms/AddForm.svelte index f22e5f4..4520b1b 100644 --- a/fe/src/lib/forms/AddForm.svelte +++ b/fe/src/lib/forms/AddForm.svelte @@ -34,20 +34,23 @@ dispatch("close"); } - async function handleSubmitStat() { - const response = await fetch("http://localhost:8080/api/v1/stats/", { + async function handleSubmitStat() + { + const { date, quantity } = statistic; + await fetch("http://localhost:8080/api/v1/stats/", { method: "POST", headers: { Authorization: `Bearer ${$token}` }, body: JSON.stringify({ - date: new Date(), - user_id: 1, - quantity: 3 + date: new Date(date), + user_id: 2, + quantity }) }); dispatch("submit"); } + -- cgit v1.1 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 +++++++ api/go.mod | 19 +++ api/go.sum | 47 +++++- api/internal/controllers/auth.go | 66 ++++++++ api/internal/controllers/stats.go | 164 ++++++++++++++++++++ api/internal/controllers/user.go | 17 +++ api/internal/database/database.go | 22 +++ api/internal/models/auth.go | 11 ++ api/internal/models/preferences.go | 14 ++ api/internal/models/statistics.go | 26 ++++ api/internal/models/user.go | 10 ++ api/main.go | 301 ------------------------------------- api/models.go | 57 ------- 14 files changed, 551 insertions(+), 363 deletions(-) create mode 100644 api/cmd/main.go create mode 100644 api/cmd/main_test.go create mode 100644 api/internal/controllers/auth.go create mode 100644 api/internal/controllers/stats.go create mode 100644 api/internal/controllers/user.go create mode 100644 api/internal/database/database.go create mode 100644 api/internal/models/auth.go create mode 100644 api/internal/models/preferences.go create mode 100644 api/internal/models/statistics.go create mode 100644 api/internal/models/user.go delete mode 100644 api/main.go delete mode 100644 api/models.go 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 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 ( github.com/gin-gonic/gin v1.9.1 github.com/google/uuid v1.6.0 github.com/mattn/go-sqlite3 v1.14.22 + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.19.0 ) @@ -13,25 +15,42 @@ require ( github.com/bytedance/sonic v1.11.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.18.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) 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 github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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= @@ -27,22 +31,30 @@ github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtP github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.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 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.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 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.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= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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/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 @@ +package controllers + +import ( + "encoding/base64" + "net/http" + "github.com/gin-gonic/gin" + "water/api/database" + "errors" + "crypto/rand" + "database/sql" + + "water/api/models" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" +) + +func AuthHandler (c *gin.Context) { + username, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + var user models.User + var preference models.Preference + var size models.Size + + 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) + if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + preference.Size = size + + // Generate a simple API token + apiToken := generateToken() + c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) +} + +// generatToken will g +func generateToken() string { + token := make([]byte, 32) + _, err := rand.Read(token) + if err != nil { + return "" + } + return base64.StdEncoding.EncodeToString(token) +} \ 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 @@ +package controllers + +import ( + "database/sql" + "github.com/gin-gonic/gin" + "net/http" + "water/api/database" + "water/api/models" +) + +func GetAllStatistics(c *gin.Context) { + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + 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") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) + + var data []models.Statistic + + for rows.Next() { + var stat models.Statistic + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + stat.User = user + data = append(data, stat) + } + + c.JSON(http.StatusOK, data) +} + +func PostNewStatistic(c *gin.Context) { + var stat models.StatisticPost + + if err := c.BindJSON(&stat); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + id, err := result.LastInsertId() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) +} + +func GetWeeklyStatistics (c *gin.Context) { + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) + + var data []models.WeeklyStatistic + for rows.Next() { + var weeklyStat models.WeeklyStatistic + if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + data = append(data, weeklyStat) + } + + c.JSON(http.StatusOK, data) +} + +func GetDailyUserStatistics(c *gin.Context) { + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + rows, err := db.Query("SELECT name, total FROM DailyUserStatistics") + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) + + var data []models.DailyUserTotals + for rows.Next() { + var stat models.DailyUserTotals + if err := rows.Scan(&stat.Name, &stat.Total); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + data = append(data, stat) + } + + c.JSON(http.StatusOK, data) + +} + +func GetUserStatistics(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) +} + +func UpdateUserStatistic(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) +} + +func DeleteUserStatistic(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) +} 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 @@ +package controllers + +import ( + "net/http" + "github.com/gin-gonic/gin" +) + +func GetUser(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "User found"}) +} +func GetUserPreferences(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Preferences fetched successfully"}) +} + +func UpdateUserPreferences(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "Preferences updated successfully"}) +} \ 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 @@ +package database + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "log" +) + +func SetupDatabase() { + _, err := sql.Open("sqlite3", "water.db") + if err != nil { + log.Fatal(err) + } +} + +func EstablishDBConnection() *sql.DB { + db, err := sql.Open("sqlite3", "../db/water.sqlite3") + if err != nil { + panic(err) + } + return db +} \ 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 @@ +package models + +import "time" + +type Token struct { + ID int64 `json:"-"` + UserID int64 `json:"user_id"` + Token string `json:"token"` + CreatedAt time.Time `json:"created_at"` + ExpiredAt time.Time `json:"expired_at"` +} 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 @@ +package models + +type Preference struct { + ID int64 `json:"-"` + Color string `json:"color"` + UserID int64 `json:"-"` + Size Size `json:"size"` +} + +type Size struct { + ID int64 `json:"-"` + Size int64 `json:"size"` + Unit string `json:"unit"` +} 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 @@ +package models + +import "time" + +type Statistic struct { + ID int64 `json:"-"` + Date time.Time `json:"date"` + User User `json:"user"` + Quantity int `json:"quantity"` +} + +type StatisticPost struct { + Date time.Time `json:"date"` + Quantity int64 `json:"quantity"` + UserID int64 `json:"user_id"` +} + +type WeeklyStatistic struct { + Date string `json:"date"` + Total int64 `json:"total"` +} + +type DailyUserTotals struct { + Name string `json:"name"` + Total int64 `json:"total"` +} 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 @@ +package models + +import "github.com/google/uuid" + +type User struct { + ID int64 `json:"-"` + Name string `json:"name"` + UUID uuid.UUID `json:"uuid"` + Password string `json:"-"` +} 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 @@ -package main - -import ( - "crypto/rand" - "database/sql" - "encoding/base64" - "errors" - "log" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - _ "github.com/mattn/go-sqlite3" - "golang.org/x/crypto/bcrypt" -) - -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() - } -} - -// generatToken will g -func generateToken() string { - token := make([]byte, 32) - _, err := rand.Read(token) - if err != nil { - return "" - } - return base64.StdEncoding.EncodeToString(token) -} - -func establishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../db/water.sqlite3") - if err != nil { - panic(err) - } - return db -} - -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", func(c *gin.Context) { - username, password, ok := c.Request.BasicAuth() - if !ok { - c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - db := establishDBConnection() - defer func(db *sql.DB) { - err := db.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(db) - - var user User - var preference Preference - var size Size - - 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) - if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { - if errors.Is(err, sql.ErrNoRows) { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - preference.Size = size - - // Generate a simple API token - apiToken := generateToken() - c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) - }) - - stats := api.Group("/stats") - stats.Use(TokenRequired()) - { - stats.GET("/", func(c *gin.Context) { - db := establishDBConnection() - defer func(db *sql.DB) { - err := db.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(db) - - 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") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(rows) - - var data []Statistic - - for rows.Next() { - var stat Statistic - var user User - if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - stat.User = user - data = append(data, stat) - } - - c.JSON(http.StatusOK, data) - }) - - stats.POST("/", func(c *gin.Context) { - var stat StatisticPost - - if err := c.BindJSON(&stat); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := establishDBConnection() - defer func(db *sql.DB) { - err := db.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(db) - - result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) - - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - id, err := result.LastInsertId() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) - }) - - stats.GET("weekly/", func(c *gin.Context) { - db := establishDBConnection() - defer func(db *sql.DB) { - err := db.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(db) - - rows, err := db.Query("SELECT date, total FROM `WeeklyStatisticsView`") - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(rows) - - var data []WeeklyStatistic - for rows.Next() { - var weeklyStat WeeklyStatistic - if err := rows.Scan(&weeklyStat.Date, &weeklyStat.Total); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } - data = append(data, weeklyStat) - } - - c.JSON(http.StatusOK, data) - }) - - stats.GET("totals/", func(c *gin.Context) { - db := establishDBConnection() - defer func(db *sql.DB) { - err := db.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(db) - - rows, err := db.Query("SELECT name, total FROM DailyUserStatistics") - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - defer func(rows *sql.Rows) { - err := rows.Close() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - }(rows) - - var data []DailyUserTotals - for rows.Next() { - var stat DailyUserTotals - if err := rows.Scan(&stat.Name, &stat.Total); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - data = append(data, stat) - } - - c.JSON(http.StatusOK, data) - - }) - - stats.GET("user/:uuid", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) - }) - - stats.PATCH("user/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - - stats.DELETE("user/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - } - - return r -} - -func main() { - r := setupRouter() - // Listen and Server in 0.0.0.0:8080 - err := r.Run(":8080") - if err != nil { - return - } -} 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 @@ -package main - -import ( - "time" - "github.com/google/uuid" -) - -type Statistic struct { - ID int64 `json:"-"` - Date time.Time `json:"date"` - User User `json:"user"` - Quantity int `json:"quantity"` -} - -type StatisticPost struct { - Date time.Time `json:"date"` - Quantity int64 `json:"quantity"` - UserID int64 `json:"user_id"` -} - -type User struct { - ID int64 `json:"-"` - Name string `json:"name"` - UUID uuid.UUID `json:"uuid"` - Password string `json:"-"` -} - -type Token struct { - ID int64 `json:"-"` - UserID int64 `json:"user_id"` - Token string `json:"token"` - CreatedAt time.Time `json:"created_at"` - ExpiredAt time.Time `json:"expired_at"` -} - -type Preference struct { - ID int64 `json:"-"` - Color string `json:"color"` - UserID int64 `json:"-"` - Size Size `json:"size"` -} - -type Size struct { - ID int64 `json:"-"` - Size int64 `json:"size"` - Unit string `json:"unit"` -} - -type WeeklyStatistic struct { - Date string `json:"date"` - Total int64 `json:"total"` -} - -type DailyUserTotals struct { - Name string `json:"name"` - Total int64 `json:"total"` -} \ 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 ++++++++++++++++----- api/internal/controllers/auth.go | 14 +++++++------- api/internal/controllers/stats.go | 4 ++-- api/internal/controllers/user.go | 2 +- api/internal/database/database.go | 2 +- 5 files changed, 27 insertions(+), 16 deletions(-) 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 diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go index 744a884..de9ed05 100644 --- a/api/internal/controllers/auth.go +++ b/api/internal/controllers/auth.go @@ -1,17 +1,17 @@ package controllers import ( - "encoding/base64" - "net/http" - "github.com/gin-gonic/gin" - "water/api/database" - "errors" "crypto/rand" "database/sql" - - "water/api/models" + "encoding/base64" + "errors" + "github.com/gin-gonic/gin" + "net/http" + "water/api/internal/models" + _ "github.com/mattn/go-sqlite3" "golang.org/x/crypto/bcrypt" + "water/api/internal/database" ) func AuthHandler (c *gin.Context) { diff --git a/api/internal/controllers/stats.go b/api/internal/controllers/stats.go index 9808ace..d8ed434 100644 --- a/api/internal/controllers/stats.go +++ b/api/internal/controllers/stats.go @@ -4,8 +4,8 @@ import ( "database/sql" "github.com/gin-gonic/gin" "net/http" - "water/api/database" - "water/api/models" + "water/api/internal/database" + "water/api/internal/models" ) func GetAllStatistics(c *gin.Context) { diff --git a/api/internal/controllers/user.go b/api/internal/controllers/user.go index 1f3f813..76dedc8 100644 --- a/api/internal/controllers/user.go +++ b/api/internal/controllers/user.go @@ -1,8 +1,8 @@ package controllers import ( - "net/http" "github.com/gin-gonic/gin" + "net/http" ) func GetUser(c *gin.Context) { diff --git a/api/internal/database/database.go b/api/internal/database/database.go index e313af5..19ae818 100644 --- a/api/internal/database/database.go +++ b/api/internal/database/database.go @@ -14,7 +14,7 @@ func SetupDatabase() { } func EstablishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../db/water.sqlite3") + db, err := sql.Open("sqlite3", "../../db/water.sqlite3") if err != nil { panic(err) } -- 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 +---------------------------------- api/internal/middleware/middleware.go | 55 +++++++++++++++++++++ api/internal/router/router.go | 42 ++++++++++++++++ 3 files changed, 99 insertions(+), 90 deletions(-) create mode 100644 api/internal/middleware/middleware.go create mode 100644 api/internal/router/router.go 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 { diff --git a/api/internal/middleware/middleware.go b/api/internal/middleware/middleware.go new file mode 100644 index 0000000..819f1e5 --- /dev/null +++ b/api/internal/middleware/middleware.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "errors" + "github.com/gin-gonic/gin" + "log" + "net/http" + "strings" +) + +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 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 +} \ No newline at end of file diff --git a/api/internal/router/router.go b/api/internal/router/router.go new file mode 100644 index 0000000..adf96d0 --- /dev/null +++ b/api/internal/router/router.go @@ -0,0 +1,42 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "water/api/internal/controllers" + "water/api/internal/middleware" +) + +func SetupRouter() *gin.Engine { + // Disable Console Color + // gin.DisableConsoleColor() + r := gin.Default() + r.Use(middleware.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(middleware.TokenRequired()) + { + user.GET("", controllers.GetUser) + user.GET("preferences", controllers.GetUserPreferences) + user.PATCH("preferences", controllers.UpdateUserPreferences) + } + + stats := api.Group("/stats") + stats.Use(middleware.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 +} \ No newline at end of file -- 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 +++--- api/internal/controllers/auth.go | 11 ++++++- api/internal/controllers/stats.go | 6 +++- api/internal/database/database.go | 4 +-- fe/src/http.ts | 60 +++++++++++++++++++++++++++++++++++++++ fe/src/lib/DataView.svelte | 53 ++++++++++++++++++++++++++++++---- fe/src/lib/Table.svelte | 13 +++++---- 7 files changed, 136 insertions(+), 20 deletions(-) create mode 100644 fe/src/http.ts 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") } diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go index de9ed05..58653d0 100644 --- a/api/internal/controllers/auth.go +++ b/api/internal/controllers/auth.go @@ -14,6 +14,11 @@ import ( "water/api/internal/database" ) + + +// AuthHandler is a function that handles users' authentication. It checks if the request +// has valid credentials, authenticates the user and sets the user's session. +// If the authentication is successful, it will allow the user to access protected routes. func AuthHandler (c *gin.Context) { username, password, ok := c.Request.BasicAuth() if !ok { @@ -55,7 +60,11 @@ func AuthHandler (c *gin.Context) { c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) } -// generatToken will g + +// generateToken is a helper function used in the AuthHandler. It generates a random token for API authentication. +// This function creates an empty byte slice of length 32 and fills it with cryptographic random data using the rand.Read function. +// If an error occurs during the generation, it will return an empty string. +// The generated cryptographic random data is then encoded into a base64 string and returned. func generateToken() string { token := make([]byte, 32) _, err := rand.Read(token) diff --git a/api/internal/controllers/stats.go b/api/internal/controllers/stats.go index d8ed434..2234787 100644 --- a/api/internal/controllers/stats.go +++ b/api/internal/controllers/stats.go @@ -8,6 +8,10 @@ import ( "water/api/internal/models" ) +// TODO: add comments to all exported members of package. + +// GetAllStatistics connects to the database and queries for all statistics in the database. +// If none have been found it will return an error, otherwise a 200 code is sent along with the list of statistics. func GetAllStatistics(c *gin.Context) { db := database.EstablishDBConnection() defer func(db *sql.DB) { @@ -78,7 +82,7 @@ func PostNewStatistic(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) } -func GetWeeklyStatistics (c *gin.Context) { +func GetWeeklyStatistics(c *gin.Context) { db := database.EstablishDBConnection() defer func(db *sql.DB) { err := db.Close() diff --git a/api/internal/database/database.go b/api/internal/database/database.go index 19ae818..7af9780 100644 --- a/api/internal/database/database.go +++ b/api/internal/database/database.go @@ -7,14 +7,14 @@ import ( ) func SetupDatabase() { - _, err := sql.Open("sqlite3", "water.db") + _, err := sql.Open("sqlite3", "water.sqlite3") if err != nil { log.Fatal(err) } } func EstablishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../../db/water.sqlite3") + db, err := sql.Open("sqlite3", "../db/water.sqlite3") if err != nil { panic(err) } diff --git a/fe/src/http.ts b/fe/src/http.ts new file mode 100644 index 0000000..cc5a906 --- /dev/null +++ b/fe/src/http.ts @@ -0,0 +1,60 @@ +export default class HttpClient { + private static instance: HttpClient; + baseURL: string; + + private constructor(baseURL: string) { + this.baseURL = baseURL; + } + + private getURL(endpoint: string): URL { + return new URL(endpoint, this.baseURL) + } + + public static getInstance(): HttpClient { + if (!HttpClient.instance) { + const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? 'http://localhost:8080/api/v1'; + HttpClient.instance = new HttpClient(baseUrl); + } + + return HttpClient.instance; + } + + async get({ endpoint }: IHttpParameters): Promise { + const url = this.getURL(endpoint); + const response = await fetch(url, { + method: 'GET', + headers: headers, + }); + return response.json(); + } + + async post({ endpoint }: IHttpParameters): Promise { + const url = this.getURL(endpoint); + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: headers, + }); + return response.json(); + } + + async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise { + const url = this.getURL(endpoint); + if (authenticated) { + + } + const response: Response = await fetch(url) + } + + async delete({ endpoint, authenticated }: IHttpParameters): Promise { + const url = this.getURL(endpoint); + if (authenticated) { } + const response = await fetch() + } +} + +interface IHttpParameters { + endpoint: string; + authenticated: boolean; + headers: Headers +} diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 7d62a43..0a6b81b 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,5 +1,6 @@ - - + + - + diff --git a/fe/src/lib/Table.svelte b/fe/src/lib/Table.svelte index d1cd7da..621157e 100644 --- a/fe/src/lib/Table.svelte +++ b/fe/src/lib/Table.svelte @@ -5,6 +5,10 @@ export let omit: string[] = ["id"]; export let title: string | undefined = undefined; + export let sortBy: string = 'date'; + + type SortComparator = (a, b) => number + function getDataKeys(data: any[]): string[] { if (!data || data.length === 0) return []; return Object.keys(data[0]) @@ -16,11 +20,8 @@ return Object.entries(row).filter((r) => !omit.includes(r[0])); } - - let limitedData: Array = []; - - if (data && (data as any[]).length > 0) { - limitedData = (data as any[]).slice(0, 4); + function sort(arr: Array>, fn: SortComparator = (a , b) => new Date(b[sortBy]) - new Date(a[sortBy])) { + return arr.sort(fn) } const formatter = new Intl.DateTimeFormat("en", { @@ -62,7 +63,7 @@ {/if} {#if data} - {#each limitedData as row} + {#each sort(data) as row} {#each getRow(row) as datum} {formatDatum(datum)} -- cgit v1.1 From 9cae9c1d2a0b4f7fa72f3075541b9ffafe1a7275 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 15 Mar 2024 18:49:43 -0400 Subject: Add routes for preference, clean up and add types --- api/internal/controllers/auth.go | 18 ++-- api/internal/controllers/preferences.go | 45 ++++++++++ api/internal/controllers/user.go | 59 +++++++++++- api/internal/middleware/middleware.go | 7 +- api/internal/models/auth.go | 2 +- api/internal/models/preferences.go | 8 +- api/internal/models/statistics.go | 2 +- api/internal/models/user.go | 2 +- api/internal/router/router.go | 5 +- fe/src/app.css | 4 + fe/src/http.ts | 96 +++++++++++++------- fe/src/lib/Card.svelte | 1 - fe/src/lib/Chart.svelte | 63 +++++++++++++ fe/src/lib/DataView.svelte | 40 +++------ fe/src/lib/Layout.svelte | 8 +- fe/src/lib/LoginForm.svelte | 2 +- fe/src/lib/PreferencesForm.svelte | 145 ++++++++++++++++++++++-------- fe/src/lib/errors.ts | 6 +- fe/src/lib/utils.ts | 2 +- fe/src/stores/auth.ts | 153 +++++++++++++++----------------- fe/src/types.ts | 45 ++++++++-- 21 files changed, 495 insertions(+), 218 deletions(-) create mode 100644 api/internal/controllers/preferences.go create mode 100644 fe/src/lib/Chart.svelte diff --git a/api/internal/controllers/auth.go b/api/internal/controllers/auth.go index 58653d0..ab2fbbb 100644 --- a/api/internal/controllers/auth.go +++ b/api/internal/controllers/auth.go @@ -38,23 +38,27 @@ func AuthHandler (c *gin.Context) { var user models.User var preference models.Preference - var size models.Size - 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) - if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { + row := db.QueryRow("SELECT id as 'id', name, uuid, password FROM Users WHERE name = ?", username) + if err := row.Scan(&user.ID, &user.Name, &user.UUID, &user.Password); err != nil { if errors.Is(err, sql.ErrNoRows) { - c.AbortWithStatus(http.StatusUnauthorized) + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } } + row = db.QueryRow("SELECT id, color, size_id, user_id FROM Preferences where user_id = ?", user.ID) + if err := row.Scan(&preference.ID, &preference.Color, &preference.SizeID, &preference.UserID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + } + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - c.AbortWithStatus(http.StatusUnauthorized) + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } - preference.Size = size - // Generate a simple API token apiToken := generateToken() c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) diff --git a/api/internal/controllers/preferences.go b/api/internal/controllers/preferences.go new file mode 100644 index 0000000..a1bcf4f --- /dev/null +++ b/api/internal/controllers/preferences.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "database/sql" + "water/api/internal/database" + "water/api/internal/models" +) + +func GetSizes(c *gin.Context) { + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + }(db) + + rows, err := db.Query("SELECT id, size, unit FROM Sizes") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(rows) + + var data []models.Size + + for rows.Next() { + var size models.Size + if err := rows.Scan(&size.ID, &size.Size, &size.Unit); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + data = append(data, size) + } + + c.JSON(http.StatusOK, data) +} diff --git a/api/internal/controllers/user.go b/api/internal/controllers/user.go index 76dedc8..dbb09cf 100644 --- a/api/internal/controllers/user.go +++ b/api/internal/controllers/user.go @@ -1,17 +1,68 @@ package controllers import ( - "github.com/gin-gonic/gin" + "database/sql" + "errors" + "log" "net/http" + "water/api/internal/database" + "water/api/internal/models" + + "github.com/gin-gonic/gin" ) func GetUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "User found"}) } func GetUserPreferences(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "Preferences fetched successfully"}) + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + var preference models.Preference + + row := db.QueryRow("SELECT id, user_id, color, size_id FROM Preferences WHERE user_id = ?", c.Param("id")) + if err := row.Scan(&preference.ID, &preference.UserID, &preference.Color, &preference.SizeID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + + c.JSON(http.StatusOK, preference) } func UpdateUserPreferences(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"message": "Preferences updated successfully"}) -} \ No newline at end of file + db := database.EstablishDBConnection() + defer func(db *sql.DB) { + err := db.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + }(db) + + var newPreferences models.Preference + if err := c.BindJSON(&newPreferences); err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) + return + } + + log.Printf("newPreferences: %v", newPreferences) + + _, err := db.Exec("UPDATE Preferences SET color = ?, size_id = ? WHERE id = ?", newPreferences.Color, newPreferences.SizeID, newPreferences.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/api/internal/middleware/middleware.go b/api/internal/middleware/middleware.go index 819f1e5..aa27fb8 100644 --- a/api/internal/middleware/middleware.go +++ b/api/internal/middleware/middleware.go @@ -2,10 +2,11 @@ package middleware import ( "errors" - "github.com/gin-gonic/gin" "log" "net/http" "strings" + + "github.com/gin-gonic/gin" ) func TokenRequired() gin.HandlerFunc { @@ -27,7 +28,7 @@ func CORSMiddleware() gin.HandlerFunc { 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") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH") if c.Request.Method == "OPTIONS" { log.Println(c.Request.Header) @@ -52,4 +53,4 @@ func checkForTokenInContext(c *gin.Context) (string, error) { } return parts[1], nil -} \ No newline at end of file +} diff --git a/api/internal/models/auth.go b/api/internal/models/auth.go index 41344d5..fa7dbe4 100644 --- a/api/internal/models/auth.go +++ b/api/internal/models/auth.go @@ -3,7 +3,7 @@ package models import "time" type Token struct { - ID int64 `json:"-"` + ID int64 `json:"id"` UserID int64 `json:"user_id"` Token string `json:"token"` CreatedAt time.Time `json:"created_at"` diff --git a/api/internal/models/preferences.go b/api/internal/models/preferences.go index cbbd47c..8022099 100644 --- a/api/internal/models/preferences.go +++ b/api/internal/models/preferences.go @@ -1,14 +1,14 @@ package models type Preference struct { - ID int64 `json:"-"` + ID int64 `json:"id"` Color string `json:"color"` - UserID int64 `json:"-"` - Size Size `json:"size"` + UserID int64 `json:"user_id"` + SizeID int64 `json:"size_id"` } type Size struct { - ID int64 `json:"-"` + ID int64 `json:"id"` Size int64 `json:"size"` Unit string `json:"unit"` } diff --git a/api/internal/models/statistics.go b/api/internal/models/statistics.go index 457e6a0..7dceb3a 100644 --- a/api/internal/models/statistics.go +++ b/api/internal/models/statistics.go @@ -3,7 +3,7 @@ package models import "time" type Statistic struct { - ID int64 `json:"-"` + ID int64 `json:"id"` Date time.Time `json:"date"` User User `json:"user"` Quantity int `json:"quantity"` diff --git a/api/internal/models/user.go b/api/internal/models/user.go index 2a3e6fd..ca5daa4 100644 --- a/api/internal/models/user.go +++ b/api/internal/models/user.go @@ -3,7 +3,7 @@ package models import "github.com/google/uuid" type User struct { - ID int64 `json:"-"` + ID int64 `json:"id"` Name string `json:"name"` UUID uuid.UUID `json:"uuid"` Password string `json:"-"` diff --git a/api/internal/router/router.go b/api/internal/router/router.go index adf96d0..3c86b8c 100644 --- a/api/internal/router/router.go +++ b/api/internal/router/router.go @@ -17,13 +17,14 @@ func SetupRouter() *gin.Engine { api := r.Group("api/v1") api.POST("/auth", controllers.AuthHandler) + api.GET("/sizes", middleware.TokenRequired(), controllers.GetSizes) + api.PATCH("/user/preferences", controllers.UpdateUserPreferences) - user := api.Group("/user/:uuid") + user := api.Group("/user/:id") user.Use(middleware.TokenRequired()) { user.GET("", controllers.GetUser) user.GET("preferences", controllers.GetUserPreferences) - user.PATCH("preferences", controllers.UpdateUserPreferences) } stats := api.Group("/stats") diff --git a/fe/src/app.css b/fe/src/app.css index de19b52..c24c713 100644 --- a/fe/src/app.css +++ b/fe/src/app.css @@ -109,6 +109,10 @@ button:focus-visible { padding: 1em; } +.form.input.group input[type=color] { + padding: 0; +} + .form button[type=submit] { align-self: flex-end; background: var(--submit); diff --git a/fe/src/http.ts b/fe/src/http.ts index cc5a906..3b2a4f0 100644 --- a/fe/src/http.ts +++ b/fe/src/http.ts @@ -1,60 +1,92 @@ -export default class HttpClient { - private static instance: HttpClient; +let instance; +const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1"; + +class HttpClient { baseURL: string; - - private constructor(baseURL: string) { + commonHeaders: Headers; + + constructor(baseURL: string) { this.baseURL = baseURL; + this.commonHeaders = new Headers({ + "Content-Type": "application/json" + }) + if (instance) { + throw new Error("New instance cannot be created!"); + } + + instance = this; } - + private getURL(endpoint: string): URL { - return new URL(endpoint, this.baseURL) + return new URL(endpoint, this.baseURL); } - public static getInstance(): HttpClient { - if (!HttpClient.instance) { - const baseUrl = import.meta.env?.VITE_API_BASE_URL ?? 'http://localhost:8080/api/v1'; - HttpClient.instance = new HttpClient(baseUrl); - } + private token(): string | null { + return localStorage.getItem('token'); + } - return HttpClient.instance; + private async makeRequest(request: Request): Promise { + return fetch(request) } - async get({ endpoint }: IHttpParameters): Promise { - const url = this.getURL(endpoint); - const response = await fetch(url, { - method: 'GET', - headers: headers, + async get({ endpoint, headers }: IHttpParameters): Promise { + const url: URL = this.getURL(endpoint); + headers = Object.assign(headers, this.commonHeaders); + const request: Request = new Request(url, { + method: "GET", + headers }); - return response.json(); + + return this.makeRequest(request); } - async post({ endpoint }: IHttpParameters): Promise { + async post({ endpoint, authenticated, body, headers }: IHttpParameters): Promise { const url = this.getURL(endpoint); - const response = await fetch(url, { - method: 'POST', + + if (authenticated) { + const token: string | null = this.token(); + headers.append('Authorization', `Bearer ${token}`); + } + + const request: Request = new Request(url, { + method: "POST", body: JSON.stringify(body), - headers: headers, - }); - return response.json(); + headers + }) + + return this.makeRequest(request); } - + async patch({ endpoint, authenticated, headers }: IHttpParameters): Promise { const url = this.getURL(endpoint); if (authenticated) { - + } - const response: Response = await fetch(url) + const response: Response = await fetch(url, { + method: "PATCH", + headers + }); } - - async delete({ endpoint, authenticated }: IHttpParameters): Promise { + + async delete({ endpoint, authenticated, headers }: IHttpParameters): Promise { const url = this.getURL(endpoint); - if (authenticated) { } - const response = await fetch() + if (authenticated) { + + } + const response: Response = await fetch(url, { + method: "DELETE", + headers + }) } } interface IHttpParameters { endpoint: string; + body: Record; authenticated: boolean; - headers: Headers + headers: Headers; } + +let http: Readonly = Object.freeze(new HttpClient(baseUrl)); + +export default http; diff --git a/fe/src/lib/Card.svelte b/fe/src/lib/Card.svelte index d7cd900..cd1e02c 100644 --- a/fe/src/lib/Card.svelte +++ b/fe/src/lib/Card.svelte @@ -1,6 +1,5 @@
diff --git a/fe/src/lib/Chart.svelte b/fe/src/lib/Chart.svelte new file mode 100644 index 0000000..b19d932 --- /dev/null +++ b/fe/src/lib/Chart.svelte @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 0a6b81b..5e81a5a 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -1,10 +1,11 @@ + -

User Preferences

-
-
- - -
-
- - -
- -
+

User Preferences

+
+
+ + +
+
+ + +
+ + +
+ diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts index d44bec5..81f7145 100644 --- a/fe/src/lib/errors.ts +++ b/fe/src/lib/errors.ts @@ -1,7 +1,5 @@ export class UnauthorizedError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options); + constructor(message?: string) { + super(message); } } - - diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts index 22d4e9a..e78556c 100644 --- a/fe/src/lib/utils.ts +++ b/fe/src/lib/utils.ts @@ -1,5 +1,5 @@ export function processFormInput(form: HTMLFormElement) { - const formData = new FormData(form); + const formData: FormData = new FormData(form); const data: Record = {}; for (let field of formData) { const [key, value] = field; diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 0efc80b..63f027e 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -1,100 +1,87 @@ -import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; -import type { Preference } from '../types'; -import { writable, derived } from 'svelte/store'; +import type { Preference, TokenStore, Nullable, UserStore, User, PreferenceStore } from "../types"; +import { writable, derived } from "svelte/store"; -type Nullable = T | null; - -interface User { - uuid: string; - username: string; -} - -interface TokenStore { - subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, - authenticate: (newToken: string) => void, - unauthenticate: () => void -} - - -interface UserStore { - subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, - setUser: (user: User) => void, - reset: () => void -} - -interface PreferenceStore { - subscribe: (run: Subscriber, invalidate?: Invalidator) => Unsubscriber, - set: (this: void, value: Preference) => void -} function createTokenStore(): TokenStore { - const storedToken = localStorage.getItem("token"); - const { subscribe, set } = writable(storedToken); - - function authenticate(newToken: string): void { - try { - localStorage.setItem("token", newToken); - set(newToken); - } catch (e) { - console.error('error', e); + const storedToken = localStorage.getItem("token"); + const { subscribe, set } = writable(storedToken); + + function authenticate(newToken: string): void { + try { + localStorage.setItem("token", newToken); + set(newToken); + } catch (e) { + console.error("error", e); + } + } + + function unauthenticate(): void { + localStorage.removeItem("token"); + set(null); } - } - - function unauthenticate(): void { - localStorage.removeItem("token"); - set(null); - } - - return { - subscribe, - authenticate, - unauthenticate - }; + + return { + subscribe, + authenticate, + unauthenticate + }; } function onTokenChange($token: Nullable): boolean { - return $token ? true : false; + return $token ? true : false; } function createUserStore(): UserStore { - const user = localStorage.getItem('user'); - const userObj: Nullable = user ? JSON.parse(user) : null; - const { subscribe, set } = writable(userObj); - - const setUser = (user: User) => { - localStorage.setItem('user', JSON.stringify(user)); - set(user); - } - - const reset = () => { - localStorage.removeItem('user'); - set(null); - } - - return { - subscribe, - setUser, - reset - } + const user = localStorage.getItem("user"); + const userObj: Nullable = user ? JSON.parse(user) : null; + const { subscribe, set } = writable(userObj); + + const setUser = (user: User) => { + localStorage.setItem("user", JSON.stringify(user)); + set(user); + }; + + const reset = () => { + localStorage.removeItem("user"); + set(null); + }; + + return { + subscribe, + setUser, + reset + }; } function createPreferenceStore(): PreferenceStore { - const preferences = localStorage.getItem('preferences'); - const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { - color: "#FF0000", - size: { - size: 16, - unit: 'oz' - } - }; - - const { subscribe, set } = writable(preferenceObj); - - return { - subscribe, - set - } + const preferences = localStorage.getItem("preferences"); + const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { + id: 0, + color: "#FF0000", + size_id: 0, + user_id: 0 + }; + + const { subscribe, set, update } = writable>(preferenceObj); + + const setPreference = (preference: Preference) => { + localStorage.setItem("preference", JSON.stringify(preference)); + set(preference); + }; + + const reset = () => { + localStorage.removeItem("preference"); + set(null); + }; + + return { + set, + subscribe, + reset, + update, + setPreference, + }; } export const token = createTokenStore(); diff --git a/fe/src/types.ts b/fe/src/types.ts index 526e7eb..c8f2f00 100644 --- a/fe/src/types.ts +++ b/fe/src/types.ts @@ -1,14 +1,19 @@ -export interface Size { - size: number; - unit: string; -} +import type { Invalidator, Subscriber, Unsubscriber, Updater } from "svelte/store"; export interface Preference { + id: number; color: string; - size: Size; + size_id: number; + user_id: number; +} + +export interface Size { + size: number; + unit: string; } export interface User { + id: number; name: string; uuid: string; } @@ -17,4 +22,32 @@ export interface Statistic { user_id: string; date: string; quantity: number; -} \ No newline at end of file +} + +export type Nullable = T | null; + +export interface User { + uuid: string; + username: string; +} + +export interface TokenStore { + subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, + authenticate: (newToken: string) => void, + unauthenticate: () => void +} + + +export interface UserStore { + subscribe: (run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber, + setUser: (user: User) => void, + reset: () => void +} + +export interface PreferenceStore { + set: (this: void, value: Preference) => void; + subscribe: (this: void, run: Subscriber>, invalidate?: Invalidator>) => Unsubscriber; + reset: () => void; + update: (this: void, updater: Updater>) => void; + setPreference: (user: Preference) => void; +} -- 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/.env.sample | 10 ++++++++++ api/cmd/main.go | 13 +++++++++---- api/internal/config/config.go | 17 +++++++++++++++++ api/internal/database/database.go | 16 +++++++++------- fe/.env.sample | 1 + fe/src/errors.ts | 5 +++++ fe/src/lib/errors.ts | 5 ----- fe/src/lib/utils.ts | 9 --------- fe/src/utils.ts | 9 +++++++++ 9 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 api/.env.sample create mode 100644 api/internal/config/config.go create mode 100644 fe/.env.sample create mode 100644 fe/src/errors.ts delete mode 100644 fe/src/lib/errors.ts delete mode 100644 fe/src/lib/utils.ts create mode 100644 fe/src/utils.ts diff --git a/api/.env.sample b/api/.env.sample new file mode 100644 index 0000000..6e25893 --- /dev/null +++ b/api/.env.sample @@ -0,0 +1,10 @@ +# user for test +TEST_USER=user1 +# test user password +TEST_PASS=12345 +# database path +DB_PATH="path/to/database/file" +# database driver +DB_DRIVER="sqlite3" +# port +PORT=":8080" \ No newline at end of file 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) } } diff --git a/api/internal/config/config.go b/api/internal/config/config.go new file mode 100644 index 0000000..1892696 --- /dev/null +++ b/api/internal/config/config.go @@ -0,0 +1,17 @@ +package config + +import ( + "fmt" + "github.com/spf13/viper" +) + +func Load() (*viper.Viper, error) { + v := viper.New() + v.SetConfigFile(".env") + v.AddConfigPath(".") + err := v.ReadInConfig() + if err != nil { + return nil, fmt.Errorf("error reading .env file: %s", err) + } + return v, nil +} \ No newline at end of file diff --git a/api/internal/database/database.go b/api/internal/database/database.go index 7af9780..1866655 100644 --- a/api/internal/database/database.go +++ b/api/internal/database/database.go @@ -4,17 +4,19 @@ import ( "database/sql" _ "github.com/mattn/go-sqlite3" "log" + "path/filepath" + "water/api/internal/config" ) -func SetupDatabase() { - _, err := sql.Open("sqlite3", "water.sqlite3") +func EstablishDBConnection() *sql.DB { + c, err := config.Load() + + driver := c.GetString("DB_DRIVER") + path, err := filepath.Abs(c.GetString("DB_PATH")) if err != nil { - log.Fatal(err) + log.Fatal("There was and error getting the absolute path of the database.") } -} - -func EstablishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../db/water.sqlite3") + db, err := sql.Open(driver, path) if err != nil { panic(err) } diff --git a/fe/.env.sample b/fe/.env.sample new file mode 100644 index 0000000..60c383f --- /dev/null +++ b/fe/.env.sample @@ -0,0 +1 @@ +VITE_API_BASE_URL="https://www.example.org" \ No newline at end of file diff --git a/fe/src/errors.ts b/fe/src/errors.ts new file mode 100644 index 0000000..81f7145 --- /dev/null +++ b/fe/src/errors.ts @@ -0,0 +1,5 @@ +export class UnauthorizedError extends Error { + constructor(message?: string) { + super(message); + } +} diff --git a/fe/src/lib/errors.ts b/fe/src/lib/errors.ts deleted file mode 100644 index 81f7145..0000000 --- a/fe/src/lib/errors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class UnauthorizedError extends Error { - constructor(message?: string) { - super(message); - } -} diff --git a/fe/src/lib/utils.ts b/fe/src/lib/utils.ts deleted file mode 100644 index e78556c..0000000 --- a/fe/src/lib/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function processFormInput(form: HTMLFormElement) { - const formData: FormData = new FormData(form); - const data: Record = {}; - for (let field of formData) { - const [key, value] = field; - data[key] = value; - } - return data; -} diff --git a/fe/src/utils.ts b/fe/src/utils.ts new file mode 100644 index 0000000..e78556c --- /dev/null +++ b/fe/src/utils.ts @@ -0,0 +1,9 @@ +export function processFormInput(form: HTMLFormElement) { + const formData: FormData = new FormData(form); + const data: Record = {}; + for (let field of formData) { + const [key, value] = field; + data[key] = value; + } + return data; +} -- 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 ++-------- api/internal/config/config.go | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) 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 { diff --git a/api/internal/config/config.go b/api/internal/config/config.go index 1892696..d54e40e 100644 --- a/api/internal/config/config.go +++ b/api/internal/config/config.go @@ -14,4 +14,4 @@ func Load() (*viper.Viper, error) { return nil, fmt.Errorf("error reading .env file: %s", err) } return v, nil -} \ No newline at end of file +} -- cgit v1.1 From fd1332a3df191577e91c6d846a8b5db1747099fd Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Fri, 15 Mar 2024 22:00:10 -0400 Subject: cleanup --- api/internal/router/router.go | 8 ++++---- fe/src/lib/DataView.svelte | 7 ++++--- fe/src/lib/LoginForm.svelte | 3 ++- fe/src/lib/PreferencesForm.svelte | 15 ++++++++------- fe/src/lib/forms/AddForm.svelte | 3 ++- fe/src/utils.ts | 5 +++++ 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/api/internal/router/router.go b/api/internal/router/router.go index 3c86b8c..a71c3e6 100644 --- a/api/internal/router/router.go +++ b/api/internal/router/router.go @@ -30,10 +30,10 @@ func SetupRouter() *gin.Engine { stats := api.Group("/stats") stats.Use(middleware.TokenRequired()) { - stats.GET("/", controllers.GetAllStatistics) - stats.POST("/", controllers.PostNewStatistic) - stats.GET("weekly/", controllers.GetWeeklyStatistics) - stats.GET("daily/", controllers.GetDailyUserStatistics) + 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) diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte index 5e81a5a..ffc2fe8 100644 --- a/fe/src/lib/DataView.svelte +++ b/fe/src/lib/DataView.svelte @@ -9,6 +9,7 @@ import Card from "./Card.svelte"; import Column from "./Column.svelte"; import AddForm from "./forms/AddForm.svelte"; + import { apiURL } from "../utils"; let json: Promise; @@ -24,7 +25,7 @@ let userTotalsData: number[]; async function fetchData() { - const res = await fetch("http://localhost:8080/api/v1/stats/", { + const res = await fetch(apiURL("stats"), { method: "GET", headers: { Authorization: `Bearer ${$token}` @@ -38,7 +39,7 @@ } async function fetchDailyUserStatistics() { - const res = await fetch("http://localhost:8080/api/v1/stats/daily/", { + const res = await fetch(apiURL("stats/daily"), { method: "GET", headers: { Authorization: `Bearer ${$token}` @@ -57,7 +58,7 @@ } async function fetchWeeklyTotals() { - const res = await fetch("http://localhost:8080/api/v1/stats/weekly/", { + const res = await fetch(apiURL("stats/weekly"), { method: "GET", headers: { Authorization: `Bearer ${$token}` diff --git a/fe/src/lib/LoginForm.svelte b/fe/src/lib/LoginForm.svelte index 8c3c288..cf5febf 100644 --- a/fe/src/lib/LoginForm.svelte +++ b/fe/src/lib/LoginForm.svelte @@ -1,6 +1,7 @@