aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoog <157747121+doogongithub@users.noreply.github.com>2024-02-24 20:08:35 -0500
committerDoog <157747121+doogongithub@users.noreply.github.com>2024-02-24 20:08:35 -0500
commite37c73e33a4aaf7fb8d25b5af03627f20bcda19f (patch)
tree277a534e826325e25f881e61e322b4e2e7ec94f9
parent3eafb413a48cde60dea8a7355ee621c6acca952f (diff)
add gitignore
-rw-r--r--.gitignore49
-rw-r--r--api/go.mod6
-rw-r--r--api/go.sum9
-rw-r--r--api/lib/models.go23
-rw-r--r--api/main.go127
-rw-r--r--db/scripts/water_init.sql10
-rw-r--r--db/water.sqlite3bin40960 -> 24576 bytes
-rw-r--r--fe/src/App.svelte147
-rw-r--r--fe/src/app.css2
-rw-r--r--fe/src/lib/DataView.svelte67
-rw-r--r--fe/src/lib/Layout.svelte57
-rw-r--r--fe/src/lib/LoginForm.svelte64
-rw-r--r--fe/src/lib/Table.svelte61
-rw-r--r--fe/src/stores/auth.ts48
-rw-r--r--fe/svelte.config.js1
15 files changed, 500 insertions, 171 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4e424ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
1# If you prefer the allow list template instead of the deny list, see community template:
2# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3#
4# Binaries for programs and plugins
5*.exe
6*.exe~
7*.dll
8*.so
9*.dylib
10
11# Test binary, built with `go test -c`
12*.test
13
14# Output of the go coverage tool, specifically when used with LiteIDE
15*.out
16
17# Dependency directories (remove the comment below to include it)
18vendor/
19
20# Go workspace file
21go.work
22
23
24# Logs
25logs
26*.log
27npm-debug.log*
28yarn-debug.log*
29yarn-error.log*
30lerna-debug.log*
31.pnpm-debug.log*
32
33# Dependency directories
34node_modules/
35
36# TypeScript cache
37*.tsbuildinfo
38
39# Optional REPL history
40.node_repl_history
41
42# dotenv environment variable files
43.env
44.env.development.local
45.env.test.local
46.env.production.local
47.env.local
48
49
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
3go 1.18 3go 1.18
4 4
5require ( 5require (
6 github.com/gin-gonic/gin v1.9.1
7 github.com/mattn/go-sqlite3 v1.14.22
8)
9
10require (
6 github.com/bytedance/sonic v1.11.0 // indirect 11 github.com/bytedance/sonic v1.11.0 // indirect
7 github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 12 github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
8 github.com/chenzhuoyu/iasm v0.9.1 // indirect 13 github.com/chenzhuoyu/iasm v0.9.1 // indirect
9 github.com/gabriel-vasile/mimetype v1.4.3 // indirect 14 github.com/gabriel-vasile/mimetype v1.4.3 // indirect
10 github.com/gin-contrib/sse v0.1.0 // indirect 15 github.com/gin-contrib/sse v0.1.0 // indirect
11 github.com/gin-gonic/gin v1.9.1 // indirect
12 github.com/go-playground/locales v0.14.1 // indirect 16 github.com/go-playground/locales v0.14.1 // indirect
13 github.com/go-playground/universal-translator v0.18.1 // indirect 17 github.com/go-playground/universal-translator v0.18.1 // indirect
14 github.com/go-playground/validator/v10 v10.18.0 // indirect 18 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
10github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= 10github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
11github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 11github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
12github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 15github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
15github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 16github.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
17github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 18github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
18github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 19github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
19github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 20github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
21github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
20github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 22github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
21github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 23github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
22github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 24github.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
25github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 27github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
26github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 28github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
27github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 29github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
30github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
28github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
29github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
30github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33github.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=
36github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
37github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
38github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
42github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
43github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
39github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
40github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 45github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
41github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46github.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
43github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 48github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
44github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= 49github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
45github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 50github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
51github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
47github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 53github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
48github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 54github.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/
52github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 58github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
53github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 59github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
54github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 60github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
61github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
55github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 62github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
56github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 63github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
57github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 64github.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=
70golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 77golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
71golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 78golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
72golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 79golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
80golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
73google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 81google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
74google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 82google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
83gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
75gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
76gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
77gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 86gopkg.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 @@
1package models
2
3import "time"
4
5type Statistic struct {
6 ID int64 `json:"id"`
7 Date time.Time `json:"date"`
8 UserID int64 `json:"user_id"`
9 Quantity int `json:"quantity"`
10}
11
12type User struct {
13 ID int64
14 Name string
15}
16
17type Token struct {
18 ID int64
19 UserID int64
20 Token string
21 CreatedAt time.Time
22 ExpiredAt time.Time
23}
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 (
4 "net/http" 4 "net/http"
5 "crypto/rand" 5 "crypto/rand"
6 "encoding/base64" 6 "encoding/base64"
7 "database/sql"
8 "strings"
9 "errors"
7 10
8 "github.com/gin-gonic/gin" 11 "github.com/gin-gonic/gin"
12 _ "github.com/mattn/go-sqlite3"
13 "water/api/lib"
9) 14)
10 15
11func CORSMiddleware() gin.HandlerFunc { 16func CORSMiddleware() gin.HandlerFunc {
@@ -30,6 +35,44 @@ func generateToken() string {
30 return base64.StdEncoding.EncodeToString(token) 35 return base64.StdEncoding.EncodeToString(token)
31} 36}
32 37
38func establishDBConnection() *sql.DB {
39 db, err := sql.Open("sqlite3", "../db/water.sqlite3")
40 if err != nil {
41 panic(err)
42 }
43 return db
44}
45
46func checkForTokenInContext(c *gin.Context) (string, error) {
47 authorizationHeader := c.GetHeader("Authorization")
48 if authorizationHeader == "" {
49 return "", errors.New("Authorization header is missing")
50 }
51
52 parts := strings.Split(authorizationHeader, " ")
53
54 if len(parts) != 2 || parts[0] != "Bearer" {
55 return "", errors.New("Invalid Authorization header format")
56 }
57
58 return parts[1], nil
59}
60
61
62func TokenRequired() gin.HandlerFunc {
63 return func(c *gin.Context) {
64 _, err := checkForTokenInContext(c)
65
66 if err != nil {
67 c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
68 c.Abort()
69 return
70 }
71
72 c.Next()
73 }
74}
75
33type User struct { 76type User struct {
34 Username string 77 Username string
35 Password string 78 Password string
@@ -44,6 +87,8 @@ func setupRouter() *gin.Engine {
44 // gin.DisableConsoleColor() 87 // gin.DisableConsoleColor()
45 r := gin.Default() 88 r := gin.Default()
46 r.Use(CORSMiddleware()) 89 r.Use(CORSMiddleware())
90 r.Use(gin.Logger())
91 r.Use(gin.Recovery())
47 92
48 api := r.Group("api/v1") 93 api := r.Group("api/v1")
49 94
@@ -68,26 +113,70 @@ func setupRouter() *gin.Engine {
68 }) 113 })
69 114
70 stats := api.Group("stats") 115 stats := api.Group("stats")
116 stats.Use(TokenRequired())
117 {
118 stats.GET("/", func(c *gin.Context) {
119 db := establishDBConnection()
120 defer db.Close()
121
122 rows, err := db.Query("SELECT * FROM statistics");
123 if err != nil {
124 c.JSON(500, gin.H{"error": err.Error()})
125 return
126 }
127 defer rows.Close()
128
129 var data []models.Statistic
130 for rows.Next() {
131 var stat models.Statistic
132 if err := rows.Scan(&stat.ID, &stat.Date, &stat.UserID, &stat.Quantity); err != nil {
133 c.JSON(500, gin.H{"error": err.Error()})
134 return
135 }
136 data = append(data, stat)
137 }
138
139 c.JSON(http.StatusOK, data)
140 })
141
142 stats.POST("/", func(c *gin.Context) {
143 var stat models.Statistic
144
145 if err := c.BindJSON(&stat); err != nil {
146 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
147 return
148 }
149
150 db := establishDBConnection()
151 defer db.Close()
152
153 result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity)
154
155 if err != nil {
156 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
157 }
158
159 id, err := result.LastInsertId()
160 if err != nil {
161 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
162 }
163
164 c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id})
165 })
166
167 stats.GET("/:uuid", func(c *gin.Context) {
168 c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")})
169 })
170
171 stats.PATCH("/:uuid", func(c *gin.Context) {
172 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
173 })
174
175 stats.DELETE("/:uuid", func(c *gin.Context) {
176 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
177 })
178 }
71 179
72 stats.GET("/", func(c *gin.Context) {
73 c.JSON(http.StatusOK, gin.H{"status": "ok"})
74 })
75
76 stats.POST("/", func(c *gin.Context) {
77 c.JSON(http.StatusCreated, gin.H{"status": "created"})
78 })
79
80 stats.GET("/:uuid", func(c *gin.Context) {
81 c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")})
82 })
83
84 stats.PATCH("/:uuid", func(c *gin.Context) {
85 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
86 })
87
88 stats.DELETE("/:uuid", func(c *gin.Context) {
89 c.JSON(http.StatusNoContent, gin.H{"status": "No Content"})
90 })
91 180
92 return r 181 return r
93} 182}
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 @@
1-- user table for users. 1-- user table for users.
2CREATE TABLE IF NOT EXISTS Users ( 2CREATE TABLE IF NOT EXISTS Users (
3 id INT PRIMARY KEY, 3 id INTEGER PRIMARY KEY,
4 name TEXT NOT NULL, 4 name TEXT NOT NULL,
5 UNIQUE(name) 5 UNIQUE(name)
6); 6);
7 7
8-- statistics table for users to log their consumption 8-- statistics table for users to log their consumption
9CREATE TABLE IF NOT EXISTS Statistics ( 9CREATE TABLE IF NOT EXISTS Statistics (
10 id INT PRIMARY KEY, 10 id INTEGER PRIMARY KEY,
11 date DATETIME NOT NULL, 11 date DATETIME NOT NULL,
12 user_id INT NOT NULL, 12 user_id INT NOT NULL,
13 quantity INT 13 quantity INT
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS Statistics (
15 15
16-- preferences table for a user. 16-- preferences table for a user.
17CREATE TABLE IF NOT EXISTS Preferences ( 17CREATE TABLE IF NOT EXISTS Preferences (
18 id INT PRIMARY KEY, 18 id INTEGER PRIMARY KEY,
19 color TEXT NOT NULL DEFAULT "#000000", 19 color TEXT NOT NULL DEFAULT "#000000",
20 user_id INT NOT NULL, 20 user_id INT NOT NULL,
21 size_id INT NOT NULL DEFAULT 1, 21 size_id INT NOT NULL DEFAULT 1,
@@ -25,8 +25,8 @@ CREATE TABLE IF NOT EXISTS Preferences (
25 25
26-- lookup table for sizes. 26-- lookup table for sizes.
27CREATE TABLE IF NOT EXISTS Sizes ( 27CREATE TABLE IF NOT EXISTS Sizes (
28 id INT PRIMARY KEY, 28 id INTEGER PRIMARY KEY,
29 size INT NOT NULL 29 size INT NOT NULL,
30 unit TEXT DEFAULT "oz" 30 unit TEXT DEFAULT "oz"
31); 31);
32 32
diff --git a/db/water.sqlite3 b/db/water.sqlite3
index 97f9214..c800708 100644
--- a/db/water.sqlite3
+++ b/db/water.sqlite3
Binary files 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 @@
1<script lang="ts"> 1<script lang="ts">
2 import {onMount} from 'svelte'; 2 import { onMount, onDestroy } from 'svelte';
3 import svelteLogo from './assets/svelte.svg' 3 import Layout from './lib/Layout.svelte'
4 import viteLogo from '/vite.svg' 4 import LoginForm from './lib/LoginForm.svelte';
5 import Counter from './lib/Counter.svelte' 5 import DataView from './lib/DataView.svelte';
6 import Table from './lib/Table.svelte' 6 import { authenticated } from './stores/auth';
7 import Card from './lib/Card.svelte'
8 import { UnauthorizedError } from './lib/errors';
9
10 let data;
11 let error;
12
13 let user = {
14 username: '',
15 password: ''
16 }
17
18 interface CredentialObject {
19 username: string;
20 password: string;
21 }
22
23 function sleep(ms) {
24 return new Promise(resolve => setTimeout(resolve, ms));
25 }
26
27 async function getData() {
28 const res = await fetch('http://localhost:8080/api/v1/stats/');
29 if (res.ok) {
30 await sleep(3000);
31 return await res.json();
32 } else {
33 throw new Error('There was a problem with your request');
34 }
35 }
36
37 function handleClick () {
38 data = getData();
39 }
40
41 let authenticated: boolean = false;
42
43 function prepareCredentials ({ username, password }: CredentialObject): string {
44 return btoa(`${username}:${password}`);
45 }
46
47
48 async function onSubmit(e) {
49 if (!user.username || !user.password) {
50 error = 'please enter your username and password';
51 return;
52 }
53 const auth = prepareCredentials(user);
54
55 const response = await fetch('http://localhost:8080/api/v1/auth', {
56 method: 'POST',
57 headers: {
58 'Authorization': `Basic ${auth}`,
59 },
60 });
61
62 if (response.status === 401) {
63 error = "Your username or password is wrong";
64 return;
65 }
66
67 if (response.ok) {
68 const { token } = await response.json();
69 console.log(token);
70 localStorage.user = JSON.stringify(user);
71 localStorage.token = token;
72 authenticated = true;
73 }
74
75
76 error = null;
77 }
78
79 function logout() {
80 localStorage.removeItem("user");
81 localStorage.removeItem("token");
82 authenticated = false;
83 }
84
85
86 onMount(() => {
87 if (localStorage.token) {
88 authenticated = true;
89 }
90 });
91</script> 7</script>
92 8
93<main> 9<main>
94 {#if !authenticated} 10 <Layout>
95 <Card> 11 {#if !$authenticated}
96 <form class="form" on:submit|preventDefault={onSubmit}> 12 <LoginForm />
97 <div class='form input group'>
98 <label for="username">Username</label>
99 <input bind:value={user.username} id="username" name='username' type="text" />
100 </div>
101 <div class='form input group'>
102 <label for="password">Password</label>
103 <input bind:value={user.password} id="password" name='password' type="password" />
104 </div>
105 {#if error}
106 <p class="error">{error}</p>
107 {/if}
108 <button type="submit">Log in</button>
109 </form>
110 </Card>
111 {:else} 13 {:else}
112 <div> 14 <DataView />
113 <button on:click={logout}>Logout</button>
114 </div>
115 <div>
116 <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
117 <img src={viteLogo} class="logo" alt="Vite Logo" />
118 </a>
119 <a href="https://svelte.dev" target="_blank" rel="noreferrer">
120 <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
121 </a>
122 </div>
123
124 <button on:click={handleClick}>
125 Get Data
126 </button>
127
128 {#await data}
129 <p>...fetching</p>
130 {:then data}
131 {#if data}
132 <p>Status</p>
133 <p>{data.status}</p>
134 <Table />
135 <Table nofooter title="No Footer"/>
136 <Table noheader title="No Header"/>
137 {:else}
138 <p>No data yet</p>
139 {/if}
140 {:catch errror}
141 <p>{error.message}</p>
142 {/await}
143 {/if} 15 {/if}
16 </Layout>
144</main> 17</main>
145 18
146<style> 19<style>
diff --git a/fe/src/app.css b/fe/src/app.css
index 4768cf6..0d5fa90 100644
--- a/fe/src/app.css
+++ b/fe/src/app.css
@@ -42,9 +42,9 @@ h1 {
42} 42}
43 43
44#app { 44#app {
45 flex-grow: 2;
45 max-width: 1280px; 46 max-width: 1280px;
46 margin: 0 auto; 47 margin: 0 auto;
47 padding: 2rem;
48} 48}
49 49
50button { 50button {
diff --git a/fe/src/lib/DataView.svelte b/fe/src/lib/DataView.svelte
new file mode 100644
index 0000000..cd7b042
--- /dev/null
+++ b/fe/src/lib/DataView.svelte
@@ -0,0 +1,67 @@
1<script lang='ts'>
2import { onMount } from 'svelte';
3import { token } from '../stores/auth'
4import Table from './Table.svelte';
5
6let json;
7let showAddForm: boolean = false;
8
9async function fetchData() {
10 const res = await fetch('http://localhost:8080/api/v1/stats/', {
11 method: "GET",
12 headers: {
13 'Authorization': `Bearer ${$token}`
14 }
15 });
16 if (res.ok) {
17 json = res.json();
18 } else {
19 throw new Error('There was a problem with your request');
20 }
21}
22
23async function submitStat() {
24 const response = await fetch('http://localhost:8080/api/v1/stats/', {
25 method: "POST",
26 headers: {
27 'Authorization': `Bearer ${$token}`
28 },
29 body: JSON.stringify({
30 date: new Date,
31 user_id: 1,
32 quantity: 3
33 })
34 });
35 fetchData();
36}
37
38function handleClick() {
39 showAddForm = true;
40}
41
42function handleAddDialogSubmit (e) {
43 console.log(e.keyCode)
44}
45
46onMount(() => {
47 fetchData();
48});
49
50</script>
51<div>
52 <button on:click={submitStat}>Add Stat Test</button>
53 <dialog open={showAddForm} on:submit={handleAddDialogSubmit}>
54 <form method="dialog">
55 <input name="date" type="date" />
56 <input name="quantity" type="number" min="0" autocomplete="off"/>
57 <button type="submit">Submit</button>
58 </form>
59 </dialog>
60 <button on:click={handleClick}>Add</button>
61 {#await json then data}
62 <Table {data} nofooter />
63 {:catch error}
64 <p>{error}</p>
65 {/await}
66 <!-- <Chart /> -->
67</div>
diff --git a/fe/src/lib/Layout.svelte b/fe/src/lib/Layout.svelte
new file mode 100644
index 0000000..f349632
--- /dev/null
+++ b/fe/src/lib/Layout.svelte
@@ -0,0 +1,57 @@
1<script>
2import { authenticated, token } from '../stores/auth';
3
4const logout = () => token.unauthenticate();
5
6function showSettingsDialog() {
7 console.log('show settings');
8}
9
10</script>
11
12<div class="layout">
13 {#if $authenticated}
14 <nav>
15 <div>
16 <h1>Water</h1>
17 </div>
18 <div>
19 <button on:click={showSettingsDialog}>Settings</button>
20 <button on:click={logout}>Logout</button>
21 </div>
22 </nav>
23 {/if}
24 <div id="content">
25 <slot />
26 </div>
27</div>
28
29<style>
30.layout {
31 height: 100vh;
32}
33nav {
34 display: flex;
35 flex-direction: row;
36 align-items: center;
37 justify-content: space-between;
38 height: 64px;
39 padding: 0 2em;
40}
41
42nav div {
43 width: fit-content;
44}
45
46nav div h1 {
47 font-size: 1.75em;
48}
49
50#content {
51 display: flex;
52 flex-direction: column;
53 justify-content: center;
54 align-items: center;
55 padding: 3em 0;
56}
57</style>
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 @@
1<script lang='ts'>
2import { token } from '../stores/auth';
3import Card from './Card.svelte';
4
5let user = {
6 username: '',
7 password: ''
8}
9
10let error;
11
12interface CredentialObject {
13 username: string;
14 password: string;
15}
16
17function prepareCredentials ({ username, password }: CredentialObject): string {
18 return btoa(`${username}:${password}`);
19}
20
21async function onSubmit (e) {
22 if (!user.username || !user.password) {
23 error = 'please enter your username and password';
24 return;
25 }
26 const auth = prepareCredentials(user);
27
28 const response = await fetch('http://localhost:8080/api/v1/auth', {
29 method: 'POST',
30 headers: {
31 'Authorization': `Basic ${auth}`,
32 },
33 });
34
35 if (response.status === 401) {
36 error = "Your username or password is wrong";
37 return;
38 }
39
40 if (response.ok) {
41 const { token: apiToken } = await response.json();
42 token.authenticate(apiToken);
43 }
44
45 error = null;
46}
47</script>
48
49<Card>
50 <form class="form" on:submit|preventDefault={onSubmit}>
51 <div class='form input group'>
52 <label for="username">Username</label>
53 <input bind:value={user.username} id="username" name='username' type="text" autocomplete="username" />
54 </div>
55 <div class='form input group'>
56 <label for="password">Password</label>
57 <input bind:value={user.password} id="password" name='password' type="password" autocomplete="current-password"/>
58 </div>
59 {#if error}
60 <p class="error">{error}</p>
61 {/if}
62 <button type="submit">Log in</button>
63 </form>
64</Card>
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 @@
1<script lang="ts"> 1<script lang="ts">
2 export let data; 2import {afterUpdate} from 'svelte';
3 export let nofooter: boolean = false; 3export let data: Array<any> | undefined = undefined;
4 export let noheader: boolean = false; 4export let nofooter: boolean = false;
5 export let title: string; 5export let noheader: boolean = false;
6export let omit: string[] = ['id'];
7export let title: string | undefined = undefined;
8
9function getDataKeys(data: any[]): string[] {
10 if (!data || data.length === 0) return [];
11 return Object.keys(data[0]).map(k => k.split('_').join(' ')).filter(k => !omit.includes(k));
12}
13
14function getRow(row: Record<string, any>): Array<any> {
15 return Object.entries(row).filter(r => !omit.includes(r[0]));
16}
17
18const formatter = new Intl.DateTimeFormat('en', {
19 year: 'numeric',
20 month: 'numeric',
21 day: 'numeric',
22 hour: 'numeric',
23 minute: '2-digit',
24 second: '2-digit',
25 timeZone: "America/New_York"
26});
27
28function formatDatum([key, value]: any[]) {
29 if (key === 'date') {
30 const parsedDate = new Date(value);
31 return formatter.format(parsedDate);
32 }
33 return value;
34}
35
6</script> 36</script>
7<table> 37<table>
8 {#if title} 38 {#if title}
@@ -11,16 +41,27 @@
11 {#if !noheader} 41 {#if !noheader}
12 <thead> 42 <thead>
13 <tr> 43 <tr>
14 <th> 44 {#each getDataKeys(data) as header}
15 Data Header 45 <th>{header}</th>
16 </th> 46 {/each}
17 </tr> 47 </tr>
18 </thead> 48 </thead>
19 {/if} 49 {/if}
20 <tbody> 50 <tbody>
51 {#if data}
52 {#each data as row}
21 <tr> 53 <tr>
22 <td>Data</td> 54 {#each getRow(row) as datum}
55
56 <td>{formatDatum(datum)}</td>
57 {/each}
23 </tr> 58 </tr>
59 {/each}
60 {:else}
61 <tr>
62 There is not data.
63 </tr>
64 {/if}
24 </tbody> 65 </tbody>
25 {#if !nofooter} 66 {#if !nofooter}
26 <slot name="footer"> 67 <slot name="footer">
@@ -38,4 +79,8 @@ table {
38 margin: 8px; 79 margin: 8px;
39 border: solid 1px black; 80 border: solid 1px black;
40} 81}
82
83th {
84 text-transform: capitalize;
85}
41</style> 86</style>
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 @@
1import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store';
2import { writable, derived } from 'svelte/store';
3
4type Nullable<T> = T | null;
5
6interface User {
7 uuid: string;
8 username: string;
9}
10
11interface TokenStore {
12 subscribe: (run: Subscriber<Nullable<string>>, invalidate: Invalidator<Nullable<string>>) => Unsubscriber,
13 authenticate: (newToken: string) => void,
14 unauthenticate: () => void
15}
16
17function createTokenStore(): TokenStore {
18 const storedToken = localStorage.getItem("token");
19 const { subscribe, set } = writable<string | null>(storedToken);
20
21 function authenticate(newToken: string): void {
22 try {
23 localStorage.setItem("token", newToken);
24 set(newToken);
25 } catch (e) {
26 console.error('error', e);
27 }
28 }
29
30 function unauthenticate(): void {
31 localStorage.removeItem("token");
32 set(null);
33 }
34
35 return {
36 subscribe,
37 authenticate,
38 unauthenticate
39 };
40}
41
42function onTokenChange ($token: Nullable<string>): boolean {
43 return $token ? true : false;
44}
45
46export const token = createTokenStore();
47export const authenticated = derived(token, onTokenChange);
48export const user = writable<User | null>(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 {
4 // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 4 // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5 // for more information about preprocessors 5 // for more information about preprocessors
6 preprocess: vitePreprocess(), 6 preprocess: vitePreprocess(),
7 dev: true
7} 8}