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 | } | ||