diff options
| author | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-29 20:13:48 -0500 |
|---|---|---|
| committer | Doog <157747121+doogongithub@users.noreply.github.com> | 2024-02-29 20:13:48 -0500 |
| commit | 9f9a33cbf55d38987a66b709284d2bb4ffea0fe9 (patch) | |
| tree | 1e0539e708983ca05bb4e07d22b9ec10b95d2473 | |
| parent | e37c73e33a4aaf7fb8d25b5af03627f20bcda19f (diff) | |
modify api, build additional FE components, add types
| -rw-r--r-- | api/go.mod | 3 | ||||
| -rw-r--r-- | api/go.sum | 2 | ||||
| -rw-r--r-- | api/lib/models.go | 38 | ||||
| -rw-r--r-- | api/main.go | 48 | ||||
| -rw-r--r-- | db/scripts/water_init.sql | 40 | ||||
| -rw-r--r-- | db/water.sqlite3 | bin | 24576 -> 36864 bytes | |||
| -rw-r--r-- | fe/src/lib/DataView.svelte | 67 | ||||
| -rw-r--r-- | fe/src/lib/LoginForm.svelte | 16 | ||||
| -rw-r--r-- | fe/src/lib/PreferencesForm.svelte | 45 | ||||
| -rw-r--r-- | fe/src/lib/Table.svelte | 19 | ||||
| -rw-r--r-- | fe/src/stores/auth.ts | 57 | ||||
| -rw-r--r-- | fe/src/types.ts | 14 |
12 files changed, 303 insertions, 46 deletions
| @@ -4,7 +4,9 @@ go 1.18 | |||
| 4 | 4 | ||
| 5 | require ( | 5 | require ( |
| 6 | github.com/gin-gonic/gin v1.9.1 | 6 | github.com/gin-gonic/gin v1.9.1 |
| 7 | github.com/google/uuid v1.6.0 | ||
| 7 | github.com/mattn/go-sqlite3 v1.14.22 | 8 | github.com/mattn/go-sqlite3 v1.14.22 |
| 9 | golang.org/x/crypto v0.19.0 | ||
| 8 | ) | 10 | ) |
| 9 | 11 | ||
| 10 | require ( | 12 | require ( |
| @@ -27,7 +29,6 @@ require ( | |||
| 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect |
| 28 | github.com/ugorji/go/codec v1.2.12 // indirect | 30 | github.com/ugorji/go/codec v1.2.12 // indirect |
| 29 | golang.org/x/arch v0.7.0 // indirect | 31 | golang.org/x/arch v0.7.0 // indirect |
| 30 | golang.org/x/crypto v0.19.0 // indirect | ||
| 31 | golang.org/x/net v0.21.0 // indirect | 32 | golang.org/x/net v0.21.0 // indirect |
| 32 | golang.org/x/sys v0.17.0 // indirect | 33 | golang.org/x/sys v0.17.0 // indirect |
| 33 | golang.org/x/text v0.14.0 // indirect | 34 | golang.org/x/text v0.14.0 // indirect |
| @@ -29,6 +29,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | |||
| 29 | 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= | 30 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= |
| 31 | 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= |
| 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
| 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
| 32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | 34 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= |
| 33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | 35 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= |
| 34 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | 36 | 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 @@ | |||
| 1 | package models | 1 | package models |
| 2 | 2 | ||
| 3 | import "time" | 3 | import ( |
| 4 | "time" | ||
| 5 | "github.com/google/uuid" | ||
| 6 | ) | ||
| 4 | 7 | ||
| 5 | type Statistic struct { | 8 | type Statistic struct { |
| 6 | ID int64 `json:"id"` | 9 | ID int64 `json:"-"` |
| 7 | Date time.Time `json:"date"` | 10 | Date time.Time `json:"date"` |
| 8 | UserID int64 `json:"user_id"` | 11 | User User `json:"user"` |
| 9 | Quantity int `json:"quantity"` | 12 | Quantity int `json:"quantity"` |
| 10 | } | 13 | } |
| 11 | 14 | ||
| 12 | type User struct { | 15 | type User struct { |
| 13 | ID int64 | 16 | ID int64 `json:"-"` |
| 14 | Name string | 17 | Name string `json:"name"` |
| 18 | UUID uuid.UUID `json:"uuid"` | ||
| 19 | Password string `json:"-"` | ||
| 15 | } | 20 | } |
| 16 | 21 | ||
| 17 | type Token struct { | 22 | type Token struct { |
| 18 | ID int64 | 23 | ID int64 `json:"-"` |
| 19 | UserID int64 | 24 | UserID int64 `json:"user_id"` |
| 20 | Token string | 25 | Token string `json:"token"` |
| 21 | CreatedAt time.Time | 26 | CreatedAt time.Time `json:"created_at"` |
| 22 | ExpiredAt time.Time | 27 | ExpiredAt time.Time `json:"expired_at"` |
| 28 | } | ||
| 29 | |||
| 30 | type Preference struct { | ||
| 31 | ID int64 `json:"-"` | ||
| 32 | Color string `json:"color"` | ||
| 33 | UserID int64 `json:"-"` | ||
| 34 | Size Size `json:"size"` | ||
| 35 | } | ||
| 36 | |||
| 37 | type Size struct { | ||
| 38 | ID int64 `json:"-"` | ||
| 39 | Size int64 `json:"size"` | ||
| 40 | Unit string `json:"unit"` | ||
| 23 | } | 41 | } |
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 ( | |||
| 10 | 10 | ||
| 11 | "github.com/gin-gonic/gin" | 11 | "github.com/gin-gonic/gin" |
| 12 | _ "github.com/mattn/go-sqlite3" | 12 | _ "github.com/mattn/go-sqlite3" |
| 13 | "golang.org/x/crypto/bcrypt" | ||
| 13 | "water/api/lib" | 14 | "water/api/lib" |
| 14 | ) | 15 | ) |
| 15 | 16 | ||
| @@ -29,6 +30,7 @@ func CORSMiddleware() gin.HandlerFunc { | |||
| 29 | } | 30 | } |
| 30 | } | 31 | } |
| 31 | 32 | ||
| 33 | // generatToken will g | ||
| 32 | func generateToken() string { | 34 | func generateToken() string { |
| 33 | token := make([]byte, 32) | 35 | token := make([]byte, 32) |
| 34 | rand.Read(token) | 36 | rand.Read(token) |
| @@ -43,6 +45,7 @@ func establishDBConnection() *sql.DB { | |||
| 43 | return db | 45 | return db |
| 44 | } | 46 | } |
| 45 | 47 | ||
| 48 | |||
| 46 | func checkForTokenInContext(c *gin.Context) (string, error) { | 49 | func checkForTokenInContext(c *gin.Context) (string, error) { |
| 47 | authorizationHeader := c.GetHeader("Authorization") | 50 | authorizationHeader := c.GetHeader("Authorization") |
| 48 | if authorizationHeader == "" { | 51 | if authorizationHeader == "" { |
| @@ -54,6 +57,7 @@ func checkForTokenInContext(c *gin.Context) (string, error) { | |||
| 54 | if len(parts) != 2 || parts[0] != "Bearer" { | 57 | if len(parts) != 2 || parts[0] != "Bearer" { |
| 55 | return "", errors.New("Invalid Authorization header format") | 58 | return "", errors.New("Invalid Authorization header format") |
| 56 | } | 59 | } |
| 60 | |||
| 57 | 61 | ||
| 58 | return parts[1], nil | 62 | return parts[1], nil |
| 59 | } | 63 | } |
| @@ -73,15 +77,6 @@ func TokenRequired() gin.HandlerFunc { | |||
| 73 | } | 77 | } |
| 74 | } | 78 | } |
| 75 | 79 | ||
| 76 | type User struct { | ||
| 77 | Username string | ||
| 78 | Password string | ||
| 79 | } | ||
| 80 | |||
| 81 | var users = map[string]User{ | ||
| 82 | "user1": {"user1", "password1"}, | ||
| 83 | } | ||
| 84 | |||
| 85 | func setupRouter() *gin.Engine { | 80 | func setupRouter() *gin.Engine { |
| 86 | // Disable Console Color | 81 | // Disable Console Color |
| 87 | // gin.DisableConsoleColor() | 82 | // gin.DisableConsoleColor() |
| @@ -100,16 +95,31 @@ func setupRouter() *gin.Engine { | |||
| 100 | return | 95 | return |
| 101 | } | 96 | } |
| 102 | 97 | ||
| 103 | user, exists := users[username] | 98 | db := establishDBConnection() |
| 99 | defer db.Close() | ||
| 100 | |||
| 101 | var user models.User | ||
| 102 | var preference models.Preference | ||
| 103 | var size models.Size | ||
| 104 | |||
| 105 | 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) | ||
| 106 | if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { | ||
| 107 | if err == sql.ErrNoRows { | ||
| 108 | c.AbortWithStatus(http.StatusUnauthorized) | ||
| 109 | return | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { | ||
| 114 | c.AbortWithStatus(http.StatusUnauthorized) | ||
| 115 | return | ||
| 116 | } | ||
| 104 | 117 | ||
| 105 | if !exists || user.Password != password { | 118 | preference.Size = size |
| 106 | c.AbortWithStatus(http.StatusUnauthorized) | ||
| 107 | return | ||
| 108 | } | ||
| 109 | 119 | ||
| 110 | // Generate a simple API token | 120 | // Generate a simple API token |
| 111 | apiToken := generateToken() | 121 | apiToken := generateToken() |
| 112 | c.JSON(http.StatusOK, gin.H{"token": apiToken}) | 122 | c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) |
| 113 | }) | 123 | }) |
| 114 | 124 | ||
| 115 | stats := api.Group("stats") | 125 | stats := api.Group("stats") |
| @@ -119,7 +129,7 @@ func setupRouter() *gin.Engine { | |||
| 119 | db := establishDBConnection() | 129 | db := establishDBConnection() |
| 120 | defer db.Close() | 130 | defer db.Close() |
| 121 | 131 | ||
| 122 | rows, err := db.Query("SELECT * FROM statistics"); | 132 | 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"); |
| 123 | if err != nil { | 133 | if err != nil { |
| 124 | c.JSON(500, gin.H{"error": err.Error()}) | 134 | c.JSON(500, gin.H{"error": err.Error()}) |
| 125 | return | 135 | return |
| @@ -129,10 +139,12 @@ func setupRouter() *gin.Engine { | |||
| 129 | var data []models.Statistic | 139 | var data []models.Statistic |
| 130 | for rows.Next() { | 140 | for rows.Next() { |
| 131 | var stat models.Statistic | 141 | var stat models.Statistic |
| 132 | if err := rows.Scan(&stat.ID, &stat.Date, &stat.UserID, &stat.Quantity); err != nil { | 142 | var user models.User |
| 143 | if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { | ||
| 133 | c.JSON(500, gin.H{"error": err.Error()}) | 144 | c.JSON(500, gin.H{"error": err.Error()}) |
| 134 | return | 145 | return |
| 135 | } | 146 | } |
| 147 | stat.User = user | ||
| 136 | data = append(data, stat) | 148 | data = append(data, stat) |
| 137 | } | 149 | } |
| 138 | 150 | ||
| @@ -150,7 +162,7 @@ func setupRouter() *gin.Engine { | |||
| 150 | db := establishDBConnection() | 162 | db := establishDBConnection() |
| 151 | defer db.Close() | 163 | defer db.Close() |
| 152 | 164 | ||
| 153 | result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, stat.UserID, stat.Quantity) | 165 | result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) |
| 154 | 166 | ||
| 155 | if err != nil { | 167 | if err != nil { |
| 156 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 168 | 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 @@ | |||
| 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 INTEGER PRIMARY KEY, | 3 | id INTEGER PRIMARY KEY, |
| 4 | name TEXT NOT NULL, | 4 | password TEXT UNIQUE NOT NULL, |
| 5 | UNIQUE(name) | 5 | uuid TEXT UNIQUE NOT NULL, |
| 6 | name TEXT UNIQUE NOT NULL | ||
| 6 | ); | 7 | ); |
| 7 | 8 | ||
| 8 | -- statistics table for users to log their consumption | 9 | -- statistics table for users to log their consumption |
| @@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS Preferences ( | |||
| 19 | color TEXT NOT NULL DEFAULT "#000000", | 20 | color TEXT NOT NULL DEFAULT "#000000", |
| 20 | user_id INT NOT NULL, | 21 | user_id INT NOT NULL, |
| 21 | size_id INT NOT NULL DEFAULT 1, | 22 | size_id INT NOT NULL DEFAULT 1, |
| 22 | FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE | 23 | FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE, |
| 23 | FOREIGN KEY(size_id) REFERENCES Sizes(id) | 24 | FOREIGN KEY(size_id) REFERENCES Sizes(id) |
| 24 | ); | 25 | ); |
| 25 | 26 | ||
| @@ -30,13 +31,42 @@ CREATE TABLE IF NOT EXISTS Sizes ( | |||
| 30 | unit TEXT DEFAULT "oz" | 31 | unit TEXT DEFAULT "oz" |
| 31 | ); | 32 | ); |
| 32 | 33 | ||
| 34 | CREATE TABLE IF NOT EXISTS APIToken ( | ||
| 35 | id INTEGER PRIMARY KEY, | ||
| 36 | token TEXT NOT NULL, | ||
| 37 | user_id INTEGER NOT NULL, | ||
| 38 | FOREIGN KEY(user_id) REFERENCES Users(id) ON DELETE CASCADE | ||
| 39 | ); | ||
| 40 | |||
| 33 | -- create default sizes for sizes lookup table. | 41 | -- create default sizes for sizes lookup table. |
| 34 | INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); | 42 | INSERT OR IGNORE INTO Sizes (id, size) VALUES (1, 8), (2, 16), (3, 24), (4, 32), (5, 40), (6, 48); |
| 35 | 43 | ||
| 36 | -- create default users. | 44 | -- create default users. |
| 37 | INSERT OR IGNORE INTO Users (id, name) VALUES (1, 'Parker'), (2, 'Zach'); | 45 | INSERT OR IGNORE INTO Users (name, password, uuid) VALUES ( |
| 46 | 'parker', | ||
| 47 | '$2y$10$2UlKrQJQV5cQOo/8VcFlq.ai3MWf7mA4//knEs2xVnHTeB.RnfN.m', | ||
| 48 | '1aa668f3-7527-4a67-9c24-fdf307542eeb' | ||
| 49 | ), ( | ||
| 50 | 'zach', | ||
| 51 | '$2y$10$35UJnLpBj8ulhqN/3G4qKe0GYBOa/YunXit11n7ET6zknZpNeKpRS', | ||
| 52 | 'be3fd6b7-cf55-4eb8-92d8-1b745b439f34' | ||
| 53 | ); | ||
| 38 | 54 | ||
| 39 | -- create default preferences. | 55 | -- create default preferences. |
| 40 | INSERT OR IGNORE INTO Preferences (id, user_id) VALUES (1, 1), (2, 2); | 56 | INSERT OR IGNORE INTO Preferences (user_id) VALUES (1), (2); |
| 41 | 57 | ||
| 58 | CREATE TRIGGER IF NOT EXISTS enforce_size_id | ||
| 59 | BEFORE INSERT ON Preferences | ||
| 60 | BEGIN | ||
| 61 | SELECT | ||
| 62 | CASE | ||
| 63 | WHEN ( | ||
| 64 | SELECT COUNT(*) FROM Sizes WHERE id = new.size_id | ||
| 65 | ) = 0 | ||
| 66 | THEN RAISE(ABORT, 'Size does not exist') | ||
| 67 | END; | ||
| 68 | END; | ||
| 42 | 69 | ||
| 70 | -- | ||
| 71 | CREATE VIEW IF NOT EXISTS aggregated_stats AS | ||
| 72 | 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 --- a/db/water.sqlite3 +++ b/db/water.sqlite3 | |||
| Binary files 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 @@ | |||
| 1 | <script lang='ts'> | 1 | <script lang='ts'> |
| 2 | import { onMount } from 'svelte'; | 2 | import { onMount } from 'svelte'; |
| 3 | import { token } from '../stores/auth' | 3 | import type { Preference } from '../types'; |
| 4 | import { token, user, preferences } from '../stores/auth' | ||
| 4 | import Table from './Table.svelte'; | 5 | import Table from './Table.svelte'; |
| 6 | import PreferencesForm from './PreferencesForm.svelte'; | ||
| 7 | |||
| 8 | const formatter = new Intl.DateTimeFormat( | ||
| 9 | 'en', | ||
| 10 | { | ||
| 11 | year: 'numeric', | ||
| 12 | month: '2-digit', | ||
| 13 | day: '2-digit' | ||
| 14 | } | ||
| 15 | ); | ||
| 5 | 16 | ||
| 6 | let json; | 17 | let json; |
| 7 | let showAddForm: boolean = false; | 18 | let showAddForm: boolean = false; |
| 8 | 19 | ||
| 20 | let statistic: Statistic = newStatistic(); | ||
| 21 | |||
| 9 | async function fetchData() { | 22 | async function fetchData() { |
| 10 | const res = await fetch('http://localhost:8080/api/v1/stats/', { | 23 | const res = await fetch('http://localhost:8080/api/v1/stats/', { |
| 11 | method: "GET", | 24 | method: "GET", |
| @@ -40,7 +53,32 @@ function handleClick() { | |||
| 40 | } | 53 | } |
| 41 | 54 | ||
| 42 | function handleAddDialogSubmit (e) { | 55 | function handleAddDialogSubmit (e) { |
| 43 | console.log(e.keyCode) | 56 | console.log(statistic); |
| 57 | showAddForm = false; | ||
| 58 | } | ||
| 59 | |||
| 60 | function closeDialog () { | ||
| 61 | showAddForm = false; | ||
| 62 | } | ||
| 63 | |||
| 64 | function newStatistic (): Statistic { | ||
| 65 | let now = new Date(), month, day, year; | ||
| 66 | |||
| 67 | month = `${now.getMonth() + 1}`; | ||
| 68 | day = `${now.getDate()}`; | ||
| 69 | year = now.getFullYear(); | ||
| 70 | if (month.length < 2) | ||
| 71 | month = '0' + month; | ||
| 72 | if (day.length < 2) | ||
| 73 | day = '0' + day; | ||
| 74 | |||
| 75 | const date = [year, month, day].join('-'); | ||
| 76 | |||
| 77 | return { | ||
| 78 | user_id: $user.uuid, | ||
| 79 | date, | ||
| 80 | quantity: 1 | ||
| 81 | } | ||
| 44 | } | 82 | } |
| 45 | 83 | ||
| 46 | onMount(() => { | 84 | onMount(() => { |
| @@ -50,10 +88,19 @@ onMount(() => { | |||
| 50 | </script> | 88 | </script> |
| 51 | <div> | 89 | <div> |
| 52 | <button on:click={submitStat}>Add Stat Test</button> | 90 | <button on:click={submitStat}>Add Stat Test</button> |
| 91 | <PreferencesForm /> | ||
| 53 | <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> | 92 | <dialog open={showAddForm} on:submit={handleAddDialogSubmit}> |
| 93 | <h2>Add Water</h2> | ||
| 54 | <form method="dialog"> | 94 | <form method="dialog"> |
| 55 | <input name="date" type="date" /> | 95 | <div class='form input group'> |
| 56 | <input name="quantity" type="number" min="0" autocomplete="off"/> | 96 | <label for="date">Date:</label> |
| 97 | <input bind:value={statistic.date} id="date" name="date" type="date" /> | ||
| 98 | </div> | ||
| 99 | <div class='form input group'> | ||
| 100 | <label for="quantity">Quantity:</label> | ||
| 101 | <input bind:value={statistic.quantity} id="quantity" name="quantity" type="number" min="0" autocomplete="off"/> | ||
| 102 | </div> | ||
| 103 | <button on:click={closeDialog}>Cancel</button> | ||
| 57 | <button type="submit">Submit</button> | 104 | <button type="submit">Submit</button> |
| 58 | </form> | 105 | </form> |
| 59 | </dialog> | 106 | </dialog> |
| @@ -65,3 +112,15 @@ onMount(() => { | |||
| 65 | {/await} | 112 | {/await} |
| 66 | <!-- <Chart /> --> | 113 | <!-- <Chart /> --> |
| 67 | </div> | 114 | </div> |
| 115 | |||
| 116 | <style> | ||
| 117 | dialog { | ||
| 118 | background: red; | ||
| 119 | box-shadow: 0 20px 5em 10px rgba(0,0,0,0.8); | ||
| 120 | } | ||
| 121 | dialog::backdrop { | ||
| 122 | padding: 20px; | ||
| 123 | box-shadow: 20px 20px rgba(0,0,0,0.8); | ||
| 124 | background-color: red; | ||
| 125 | } | ||
| 126 | </style> | ||
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 @@ | |||
| 1 | <script lang='ts'> | 1 | <script lang='ts'> |
| 2 | import { token } from '../stores/auth'; | 2 | import { token, user, preferences } from '../stores/auth'; |
| 3 | import Card from './Card.svelte'; | 3 | import Card from './Card.svelte'; |
| 4 | 4 | ||
| 5 | let user = { | 5 | let credentials: CredentialObject = { |
| 6 | username: '', | 6 | username: '', |
| 7 | password: '' | 7 | password: '' |
| 8 | } | 8 | } |
| @@ -19,11 +19,11 @@ function prepareCredentials ({ username, password }: CredentialObject): string { | |||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | async function onSubmit (e) { | 21 | async function onSubmit (e) { |
| 22 | if (!user.username || !user.password) { | 22 | if (!credentials.username || !credentials.password) { |
| 23 | error = 'please enter your username and password'; | 23 | error = 'please enter your username and password'; |
| 24 | return; | 24 | return; |
| 25 | } | 25 | } |
| 26 | const auth = prepareCredentials(user); | 26 | const auth = prepareCredentials(credentials); |
| 27 | 27 | ||
| 28 | const response = await fetch('http://localhost:8080/api/v1/auth', { | 28 | const response = await fetch('http://localhost:8080/api/v1/auth', { |
| 29 | method: 'POST', | 29 | method: 'POST', |
| @@ -38,7 +38,9 @@ async function onSubmit (e) { | |||
| 38 | } | 38 | } |
| 39 | 39 | ||
| 40 | if (response.ok) { | 40 | if (response.ok) { |
| 41 | const { token: apiToken } = await response.json(); | 41 | const { token: apiToken, user: userData, preferences: userPreferences } = await response.json(); |
| 42 | user.setUser(userData); | ||
| 43 | preferences.set(userPreferences); | ||
| 42 | token.authenticate(apiToken); | 44 | token.authenticate(apiToken); |
| 43 | } | 45 | } |
| 44 | 46 | ||
| @@ -50,11 +52,11 @@ async function onSubmit (e) { | |||
| 50 | <form class="form" on:submit|preventDefault={onSubmit}> | 52 | <form class="form" on:submit|preventDefault={onSubmit}> |
| 51 | <div class='form input group'> | 53 | <div class='form input group'> |
| 52 | <label for="username">Username</label> | 54 | <label for="username">Username</label> |
| 53 | <input bind:value={user.username} id="username" name='username' type="text" autocomplete="username" /> | 55 | <input bind:value={credentials.username} id="username" name='username' type="text" autocomplete="username" /> |
| 54 | </div> | 56 | </div> |
| 55 | <div class='form input group'> | 57 | <div class='form input group'> |
| 56 | <label for="password">Password</label> | 58 | <label for="password">Password</label> |
| 57 | <input bind:value={user.password} id="password" name='password' type="password" autocomplete="current-password"/> | 59 | <input bind:value={credentials.password} id="password" name='password' type="password" autocomplete="current-password"/> |
| 58 | </div> | 60 | </div> |
| 59 | {#if error} | 61 | {#if error} |
| 60 | <p class="error">{error}</p> | 62 | <p class="error">{error}</p> |
diff --git a/fe/src/lib/PreferencesForm.svelte b/fe/src/lib/PreferencesForm.svelte new file mode 100644 index 0000000..781866c --- /dev/null +++ b/fe/src/lib/PreferencesForm.svelte | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | <script lang="ts"> | ||
| 2 | import { preferences } from '../stores/auth'; | ||
| 3 | import type { Size, Preference } from '../types'; | ||
| 4 | export let open: boolean = true; | ||
| 5 | |||
| 6 | let preference: Preference = { | ||
| 7 | color: "#00FF00", | ||
| 8 | size: { | ||
| 9 | size: 8, | ||
| 10 | unit: 'oz' | ||
| 11 | } | ||
| 12 | } | ||
| 13 | |||
| 14 | preferences.subscribe((value) => { | ||
| 15 | preference = value; | ||
| 16 | }); | ||
| 17 | |||
| 18 | function onPreferencesSave(): void { | ||
| 19 | preferences.set(preferences); | ||
| 20 | } | ||
| 21 | </script> | ||
| 22 | <dialog {open}> | ||
| 23 | <h2>User Preferences</h2> | ||
| 24 | <form method="dialog"> | ||
| 25 | <div class="form input group"> | ||
| 26 | <label>Color</label> | ||
| 27 | <input type="color" bind:value={preference.color}/> | ||
| 28 | </div> | ||
| 29 | <div class="form input group"> | ||
| 30 | <label>Bottle Size</label> | ||
| 31 | <select bind:value={preference.size.size}> | ||
| 32 | {#each [8,16,24,32,40,48] as size} | ||
| 33 | <option>{ size }</option> | ||
| 34 | {/each} | ||
| 35 | </select> | ||
| 36 | </div> | ||
| 37 | <button type="submit">Save</button> | ||
| 38 | </form> | ||
| 39 | </dialog> | ||
| 40 | <style> | ||
| 41 | dialog { | ||
| 42 | background: white; | ||
| 43 | color: black; | ||
| 44 | } | ||
| 45 | </style> | ||
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[]) { | |||
| 30 | const parsedDate = new Date(value); | 30 | const parsedDate = new Date(value); |
| 31 | return formatter.format(parsedDate); | 31 | return formatter.format(parsedDate); |
| 32 | } | 32 | } |
| 33 | |||
| 34 | if (key === 'user') { | ||
| 35 | return value['name']; | ||
| 36 | } | ||
| 37 | |||
| 33 | return value; | 38 | return value; |
| 34 | } | 39 | } |
| 35 | 40 | ||
| @@ -78,9 +83,23 @@ table { | |||
| 78 | padding: 16px; | 83 | padding: 16px; |
| 79 | margin: 8px; | 84 | margin: 8px; |
| 80 | border: solid 1px black; | 85 | border: solid 1px black; |
| 86 | border-collapse: collapse; | ||
| 81 | } | 87 | } |
| 82 | 88 | ||
| 83 | th { | 89 | th { |
| 84 | text-transform: capitalize; | 90 | text-transform: capitalize; |
| 85 | } | 91 | } |
| 92 | |||
| 93 | thead tr { | ||
| 94 | background: rgba(0,0,23, 0.34); | ||
| 95 | } | ||
| 96 | |||
| 97 | tbody tr:nth-child(odd) { | ||
| 98 | background: rgba(0,0,23,0.14); | ||
| 99 | } | ||
| 100 | |||
| 101 | th, td { | ||
| 102 | padding: 1em; | ||
| 103 | border: 1px solid rgba(0,0,0, 1); | ||
| 104 | } | ||
| 86 | </style> | 105 | </style> |
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 @@ | |||
| 1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; | 1 | import type { Invalidator, Subscriber, Unsubscriber } from 'svelte/store'; |
| 2 | import type { Preference } from '../types'; | ||
| 2 | import { writable, derived } from 'svelte/store'; | 3 | import { writable, derived } from 'svelte/store'; |
| 3 | 4 | ||
| 4 | type Nullable<T> = T | null; | 5 | type Nullable<T> = T | null; |
| @@ -14,6 +15,18 @@ interface TokenStore { | |||
| 14 | unauthenticate: () => void | 15 | unauthenticate: () => void |
| 15 | } | 16 | } |
| 16 | 17 | ||
| 18 | |||
| 19 | interface UserStore { | ||
| 20 | subscribe: (run: Subscriber<Nullable<User>>, invalidate: Invalidator<Nullable<User>>) => Unsubscriber, | ||
| 21 | setUser: (user: User) => void, | ||
| 22 | reset: () => void | ||
| 23 | } | ||
| 24 | |||
| 25 | interface PreferenceStore { | ||
| 26 | subscribe: (run: Subscriber<Nullable<Preference>>, invalidate: Invalidator<Nullable<Preference>>) => Unsubscriber, | ||
| 27 | set: (this: void, value: Nullable<Preference>) => void | ||
| 28 | } | ||
| 29 | |||
| 17 | function createTokenStore(): TokenStore { | 30 | function createTokenStore(): TokenStore { |
| 18 | const storedToken = localStorage.getItem("token"); | 31 | const storedToken = localStorage.getItem("token"); |
| 19 | const { subscribe, set } = writable<string | null>(storedToken); | 32 | const { subscribe, set } = writable<string | null>(storedToken); |
| @@ -43,6 +56,48 @@ function onTokenChange ($token: Nullable<string>): boolean { | |||
| 43 | return $token ? true : false; | 56 | return $token ? true : false; |
| 44 | } | 57 | } |
| 45 | 58 | ||
| 59 | function createUserStore(): UserStore { | ||
| 60 | const user = localStorage.getItem('user'); | ||
| 61 | const userObj: Nullable<User> = user ? JSON.parse(user) : null; | ||
| 62 | const { subscribe, set } = writable<User | null>(userObj); | ||
| 63 | |||
| 64 | const setUser = (user: User) => { | ||
| 65 | localStorage.setItem('user', JSON.stringify(user)); | ||
| 66 | set(user); | ||
| 67 | } | ||
| 68 | |||
| 69 | const reset = () => { | ||
| 70 | localStorage.removeItem('user'); | ||
| 71 | set(null); | ||
| 72 | } | ||
| 73 | |||
| 74 | return { | ||
| 75 | subscribe, | ||
| 76 | setUser, | ||
| 77 | reset | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | |||
| 82 | function createPreferenceStore(): PreferenceStore { | ||
| 83 | const preferences = localStorage.getItem('preferences'); | ||
| 84 | const preferenceObj: Preference = preferences ? JSON.parse(preferences) : { | ||
| 85 | color: "#FF0000", | ||
| 86 | size: { | ||
| 87 | size: 16, | ||
| 88 | unit: 'oz' | ||
| 89 | } | ||
| 90 | }; | ||
| 91 | |||
| 92 | const { subscribe, set } = writable<Nullable<Preference>>(preferenceObj); | ||
| 93 | |||
| 94 | return { | ||
| 95 | subscribe, | ||
| 96 | set | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 46 | export const token = createTokenStore(); | 100 | export const token = createTokenStore(); |
| 47 | export const authenticated = derived(token, onTokenChange); | 101 | export const authenticated = derived(token, onTokenChange); |
| 48 | export const user = writable<User | null>(null); | 102 | export const user = createUserStore(); |
| 103 | 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 @@ | |||
| 1 | export interface Size { | ||
| 2 | size: number; | ||
| 3 | unit: string; | ||
| 4 | } | ||
| 5 | |||
| 6 | export interface Preference { | ||
| 7 | color: string; | ||
| 8 | size: Size; | ||
| 9 | } | ||
| 10 | |||
| 11 | export interface User { | ||
| 12 | name: string; | ||
| 13 | uuid: string; | ||
| 14 | } | ||
