diff options
| author | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-24 20:08:35 -0500 |
|---|---|---|
| committer | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-24 20:08:35 -0500 |
| commit | e37c73e33a4aaf7fb8d25b5af03627f20bcda19f (patch) | |
| tree | 277a534e826325e25f881e61e322b4e2e7ec94f9 | |
| parent | 3eafb413a48cde60dea8a7355ee621c6acca952f (diff) | |
add gitignore
| -rw-r--r-- | .gitignore | 49 | ||||
| -rw-r--r-- | api/go.mod | 6 | ||||
| -rw-r--r-- | api/go.sum | 9 | ||||
| -rw-r--r-- | api/lib/models.go | 23 | ||||
| -rw-r--r-- | api/main.go | 127 | ||||
| -rw-r--r-- | db/scripts/water_init.sql | 10 | ||||
| -rw-r--r-- | db/water.sqlite3 | bin | 40960 -> 24576 bytes | |||
| -rw-r--r-- | fe/src/App.svelte | 147 | ||||
| -rw-r--r-- | fe/src/app.css | 2 | ||||
| -rw-r--r-- | fe/src/lib/DataView.svelte | 67 | ||||
| -rw-r--r-- | fe/src/lib/Layout.svelte | 57 | ||||
| -rw-r--r-- | fe/src/lib/LoginForm.svelte | 64 | ||||
| -rw-r--r-- | fe/src/lib/Table.svelte | 61 | ||||
| -rw-r--r-- | fe/src/stores/auth.ts | 48 | ||||
| -rw-r--r-- | fe/svelte.config.js | 1 |
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) | ||
| 18 | vendor/ | ||
| 19 | |||
| 20 | # Go workspace file | ||
| 21 | go.work | ||
| 22 | |||
| 23 | |||
| 24 | # Logs | ||
| 25 | logs | ||
| 26 | *.log | ||
| 27 | npm-debug.log* | ||
| 28 | yarn-debug.log* | ||
| 29 | yarn-error.log* | ||
| 30 | lerna-debug.log* | ||
| 31 | .pnpm-debug.log* | ||
| 32 | |||
| 33 | # Dependency directories | ||
| 34 | node_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 | |||
| @@ -3,12 +3,16 @@ module water/api | |||
| 3 | go 1.18 | 3 | go 1.18 |
| 4 | 4 | ||
| 5 | require ( | 5 | require ( |
| 6 | github.com/gin-gonic/gin v1.9.1 | ||
| 7 | github.com/mattn/go-sqlite3 v1.14.22 | ||
| 8 | ) | ||
| 9 | |||
| 10 | require ( | ||
| 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 |
| @@ -10,6 +10,7 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI | |||
| 10 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= | 10 | github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= |
| 11 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= | 11 | github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= |
| 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
| 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
| 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
| 14 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= | 15 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= |
| 15 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= | 16 | 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 | |||
| 17 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | 18 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= |
| 18 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | 19 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= |
| 19 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | 20 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= |
| 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||
| 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= |
| 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= |
| 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | 24 | 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 | |||
| 25 | github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | 27 | github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= |
| 26 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | 28 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= |
| 27 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= | 29 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= |
| 30 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||
| 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
| 29 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= |
| 30 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | 33 | 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= | |||
| 36 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | 39 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= |
| 37 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | 40 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= |
| 38 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | 41 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |
| 42 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||
| 43 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||
| 39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | 44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
| 40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= |
| 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | 46 | 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 | |||
| 43 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | 48 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= |
| 44 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= | 49 | github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= |
| 45 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | 50 | github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= |
| 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
| 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
| 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
| 48 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | 54 | 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/ | |||
| 52 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | 58 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
| 53 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | 59 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= |
| 54 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | 60 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= |
| 61 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
| 55 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | 62 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= |
| 56 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | 63 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= |
| 57 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | 64 | 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= | |||
| 70 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | 77 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
| 71 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | 78 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= |
| 72 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | 79 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= |
| 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||
| 73 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= | 81 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= |
| 74 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | 82 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= |
| 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
| 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
| 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | 85 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
| 77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | 86 | 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 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import "time" | ||
| 4 | |||
| 5 | type 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 | |||
| 12 | type User struct { | ||
| 13 | ID int64 | ||
| 14 | Name string | ||
| 15 | } | ||
| 16 | |||
| 17 | type 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 | ||
| 11 | func CORSMiddleware() gin.HandlerFunc { | 16 | func 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 | ||
| 38 | func 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 | |||
| 46 | func 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 | |||
| 62 | func 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 | |||
| 33 | type User struct { | 76 | type 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. |
| 2 | CREATE TABLE IF NOT EXISTS Users ( | 2 | CREATE 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 |
| 9 | CREATE TABLE IF NOT EXISTS Statistics ( | 9 | CREATE 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. |
| 17 | CREATE TABLE IF NOT EXISTS Preferences ( | 17 | CREATE 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. |
| 27 | CREATE TABLE IF NOT EXISTS Sizes ( | 27 | CREATE 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 | ||
| 50 | button { | 50 | button { |
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'> | ||
| 2 | import { onMount } from 'svelte'; | ||
| 3 | import { token } from '../stores/auth' | ||
| 4 | import Table from './Table.svelte'; | ||
| 5 | |||
| 6 | let json; | ||
| 7 | let showAddForm: boolean = false; | ||
| 8 | |||
| 9 | async 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 | |||
| 23 | async 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 | |||
| 38 | function handleClick() { | ||
| 39 | showAddForm = true; | ||
| 40 | } | ||
| 41 | |||
| 42 | function handleAddDialogSubmit (e) { | ||
| 43 | console.log(e.keyCode) | ||
| 44 | } | ||
| 45 | |||
| 46 | onMount(() => { | ||
| 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> | ||
| 2 | import { authenticated, token } from '../stores/auth'; | ||
| 3 | |||
| 4 | const logout = () => token.unauthenticate(); | ||
| 5 | |||
| 6 | function 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 | } | ||
| 33 | nav { | ||
| 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 | |||
| 42 | nav div { | ||
| 43 | width: fit-content; | ||
| 44 | } | ||
| 45 | |||
| 46 | nav 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'> | ||
| 2 | import { token } from '../stores/auth'; | ||
| 3 | import Card from './Card.svelte'; | ||
| 4 | |||
| 5 | let user = { | ||
| 6 | username: '', | ||
| 7 | password: '' | ||
| 8 | } | ||
| 9 | |||
| 10 | let error; | ||
| 11 | |||
| 12 | interface CredentialObject { | ||
| 13 | username: string; | ||
| 14 | password: string; | ||
| 15 | } | ||
| 16 | |||
| 17 | function prepareCredentials ({ username, password }: CredentialObject): string { | ||
| 18 | return btoa(`${username}:${password}`); | ||
| 19 | } | ||
| 20 | |||
| 21 | async 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; | 2 | import {afterUpdate} from 'svelte'; |
| 3 | export let nofooter: boolean = false; | 3 | export let data: Array<any> | undefined = undefined; |
| 4 | export let noheader: boolean = false; | 4 | export let nofooter: boolean = false; |
| 5 | export let title: string; | 5 | export let noheader: boolean = false; |
| 6 | export let omit: string[] = ['id']; | ||
| 7 | export let title: string | undefined = undefined; | ||
| 8 | |||
| 9 | function 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 | |||
| 14 | function getRow(row: Record<string, any>): Array<any> { | ||
| 15 | return Object.entries(row).filter(r => !omit.includes(r[0])); | ||
| 16 | } | ||
| 17 | |||
| 18 | const 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 | |||
| 28 | function 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 | |||
| 83 | th { | ||
| 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 @@ | |||
| 1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; | ||
| 2 | import { writable, derived } from 'svelte/store'; | ||
| 3 | |||
| 4 | type Nullable<T> = T | null; | ||
| 5 | |||
| 6 | interface User { | ||
| 7 | uuid: string; | ||
| 8 | username: string; | ||
| 9 | } | ||
| 10 | |||
| 11 | interface TokenStore { | ||
| 12 | subscribe: (run: Subscriber<Nullable<string>>, invalidate: Invalidator<Nullable<string>>) => Unsubscriber, | ||
| 13 | authenticate: (newToken: string) => void, | ||
| 14 | unauthenticate: () => void | ||
| 15 | } | ||
| 16 | |||
| 17 | function 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 | |||
| 42 | function onTokenChange ($token: Nullable<string>): boolean { | ||
| 43 | return $token ? true : false; | ||
| 44 | } | ||
| 45 | |||
| 46 | export const token = createTokenStore(); | ||
| 47 | export const authenticated = derived(token, onTokenChange); | ||
| 48 | export 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 | } |
