diff options
| author | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-02 16:52:55 -0500 |
|---|---|---|
| committer | Zach Berwaldt <zberwaldt@tutamail.com> | 2024-03-02 16:52:55 -0500 |
| commit | cf2113e77edabf8e3a632c7b76c769752039ba88 (patch) | |
| tree | 874872f22aa63df532769de62119816748b167f8 /api | |
| parent | 326f186d67017f87e631a1fbcdf3f184cbc42d7d (diff) | |
feat: Add API logging
Add logging to the API to keep track of specific requests and headers. Also added CORS middleware to handle OPTIONS requests.
---
The commit adds logging functionality to the API and includes a middleware function to handle CORS OPTIONS requests. This will allow us to track specific requests and headers received by the API.
[API/main.go](/api/main.go):
- Added import for the 'log' package
- Added logging statements to print the request headers and "_I am here_" message
- Removed unnecessary newlines and comments
[fe/src/app.css](/fe/src/app.css):
- Added a new style for button hover effects
[fe/src/lib/Card.svelte](/fe/src/lib/Card.svelte):
- Added a new `height` prop to the Card component
[fe/src/lib/Column.svelte](/fe/src/lib/Column.svelte):
- Added a new CSS class for column layout
- Set the width and gap using CSS variables
[fe/src/lib/DataView.svelte](/fe/src/lib/DataView.svelte):
- Updated the 'fetchData' function to also fetch 'totals' and 'userStats' data
- Added canvas references and chart variables for bar and line charts
- Added a new 'getLastSevenDays' function to calculate the labels for the charts
- Updated the 'onMount' function to initialize the bar and line charts using the canvas references and data
- Updated the 'onDestroy' function to destroy the bar and line charts
- Added a new 'addFormOpen' store and imported it
- Added a new 'onClick' handler for the Add button to open the AddForm modal
- Updated the layout and added Card components to display the bar and line charts and the JSON data
- Added a new 'fetchTotals' function to fetch data for the 'totals' section
- Added a new 'fetchStatsForUser' function to fetch data for the 'userStats' section
[fe/src/lib/Layout.svelte](/fe/src/lib/Layout.svelte):
- Added a new 'preferenceFormOpen' variable and initialized it to 'false'
- Added a new 'showPreferencesDialog' function to set 'preferenceFormOpen' to 'true'
- Added a new 'closePreferenceDialog' function to set 'preferenceFormOpen' to 'false'
- Added a new 'showAddDialog' function to open the AddForm modal
Diffstat (limited to 'api')
| -rw-r--r-- | api/main.go | 384 |
1 files changed, 229 insertions, 155 deletions
diff --git a/api/main.go b/api/main.go index 91b7929..57feb09 100644 --- a/api/main.go +++ b/api/main.go | |||
| @@ -2,193 +2,267 @@ package main | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | 4 | "net/http" |
| 5 | "crypto/rand" | 5 | "crypto/rand" |
| 6 | "encoding/base64" | 6 | "encoding/base64" |
| 7 | "database/sql" | 7 | "database/sql" |
| 8 | "strings" | 8 | "strings" |
| 9 | "errors" | 9 | "errors" |
| 10 | "log" | ||
| 10 | 11 | ||
| 11 | "github.com/gin-gonic/gin" | 12 | "github.com/gin-gonic/gin" |
| 12 | _ "github.com/mattn/go-sqlite3" | 13 | _ "github.com/mattn/go-sqlite3" |
| 13 | "golang.org/x/crypto/bcrypt" | 14 | "golang.org/x/crypto/bcrypt" |
| 14 | "water/api/lib" | 15 | "water/api/lib" |
| 15 | ) | 16 | ) |
| 16 | 17 | ||
| 17 | func CORSMiddleware() gin.HandlerFunc { | 18 | func CORSMiddleware() gin.HandlerFunc { |
| 18 | return func(c *gin.Context) { | 19 | return func(c *gin.Context) { |
| 19 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") | 20 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") |
| 20 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") | 21 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") |
| 21 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") | 22 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") |
| 22 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") | 23 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") |
| 23 | 24 | ||
| 24 | if c.Request.Method == "OPTIONS" { | 25 | log.Println("I am here") |
| 25 | c.AbortWithStatus(204) | 26 | |
| 26 | return | 27 | if c.Request.Method == "OPTIONS" { |
| 27 | } | 28 | log.Println(c.Request.Header) |
| 28 | 29 | c.AbortWithStatus(204) | |
| 29 | c.Next() | 30 | return |
| 30 | } | 31 | } |
| 32 | |||
| 33 | log.Println(c.Request.Header) | ||
| 34 | c.Next() | ||
| 35 | } | ||
| 31 | } | 36 | } |
| 32 | 37 | ||
| 33 | // generatToken will g | 38 | // generatToken will g |
| 34 | func generateToken() string { | 39 | func generateToken() string { |
| 35 | token := make([]byte, 32) | 40 | token := make([]byte, 32) |
| 36 | rand.Read(token) | 41 | rand.Read(token) |
| 37 | return base64.StdEncoding.EncodeToString(token) | 42 | return base64.StdEncoding.EncodeToString(token) |
| 38 | } | 43 | } |
| 39 | 44 | ||
| 40 | func establishDBConnection() *sql.DB { | 45 | func establishDBConnection() *sql.DB { |
| 41 | db, err := sql.Open("sqlite3", "../db/water.sqlite3") | 46 | db, err := sql.Open("sqlite3", "../db/water.sqlite3") |
| 42 | if err != nil { | 47 | if err != nil { |
| 43 | panic(err) | 48 | panic(err) |
| 44 | } | 49 | } |
| 45 | return db | 50 | return db |
| 46 | } | 51 | } |
| 47 | 52 | ||
| 48 | |||
| 49 | func checkForTokenInContext(c *gin.Context) (string, error) { | 53 | func checkForTokenInContext(c *gin.Context) (string, error) { |
| 50 | authorizationHeader := c.GetHeader("Authorization") | 54 | authorizationHeader := c.GetHeader("Authorization") |
| 51 | if authorizationHeader == "" { | 55 | if authorizationHeader == "" { |
| 52 | return "", errors.New("Authorization header is missing") | 56 | return "", errors.New("Authorization header is missing") |
| 53 | } | 57 | } |
| 54 | 58 | ||
| 55 | parts := strings.Split(authorizationHeader, " ") | 59 | parts := strings.Split(authorizationHeader, " ") |
| 56 | 60 | ||
| 57 | if len(parts) != 2 || parts[0] != "Bearer" { | 61 | if len(parts) != 2 || parts[0] != "Bearer" { |
| 58 | return "", errors.New("Invalid Authorization header format") | 62 | return "", errors.New("Invalid Authorization header format") |
| 59 | } | 63 | } |
| 60 | 64 | ||
| 61 | 65 | return parts[1], nil | |
| 62 | return parts[1], nil | ||
| 63 | } | 66 | } |
| 64 | 67 | ||
| 65 | |||
| 66 | func TokenRequired() gin.HandlerFunc { | 68 | func TokenRequired() gin.HandlerFunc { |
| 67 | return func(c *gin.Context) { | 69 | return func(c *gin.Context) { |
| 68 | _, err := checkForTokenInContext(c) | 70 | _, err := checkForTokenInContext(c) |
| 69 | 71 | ||
| 70 | if err != nil { | 72 | if err != nil { |
| 71 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) | 73 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) |
| 72 | c.Abort() | 74 | c.Abort() |
| 73 | return | 75 | return |
| 74 | } | 76 | } |
| 75 | 77 | ||
| 76 | c.Next() | 78 | c.Next() |
| 77 | } | 79 | } |
| 78 | } | 80 | } |
| 79 | 81 | ||
| 80 | func setupRouter() *gin.Engine { | 82 | func setupRouter() *gin.Engine { |
| 81 | // Disable Console Color | 83 | // Disable Console Color |
| 82 | // gin.DisableConsoleColor() | 84 | // gin.DisableConsoleColor() |
| 83 | r := gin.Default() | 85 | r := gin.Default() |
| 84 | r.Use(CORSMiddleware()) | 86 | r.Use(CORSMiddleware()) |
| 85 | r.Use(gin.Logger()) | 87 | r.Use(gin.Logger()) |
| 86 | r.Use(gin.Recovery()) | 88 | r.Use(gin.Recovery()) |
| 87 | 89 | ||
| 88 | api := r.Group("api/v1") | 90 | api := r.Group("api/v1") |
| 89 | 91 | ||
| 90 | api.POST("/auth", func(c *gin.Context) { | 92 | api.POST("/auth", func(c *gin.Context) { |
| 91 | username, password, ok := c.Request.BasicAuth() | 93 | username, password, ok := c.Request.BasicAuth() |
| 92 | if !ok { | 94 | if !ok { |
| 93 | c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) | 95 | c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) |
| 94 | c.AbortWithStatus(http.StatusUnauthorized) | 96 | c.AbortWithStatus(http.StatusUnauthorized) |
| 95 | return | 97 | return |
| 96 | } | 98 | } |
| 97 | 99 | ||
| 98 | db := establishDBConnection() | 100 | db := establishDBConnection() |
| 99 | defer db.Close() | 101 | defer db.Close() |
| 100 | 102 | ||
| 101 | var user models.User | 103 | var user models.User |
| 102 | var preference models.Preference | 104 | var preference models.Preference |
| 103 | var size models.Size | 105 | var size models.Size |
| 104 | 106 | ||
| 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) | 107 | 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 { | 108 | if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { |
| 107 | if err == sql.ErrNoRows { | 109 | if err == sql.ErrNoRows { |
| 108 | c.AbortWithStatus(http.StatusUnauthorized) | 110 | c.AbortWithStatus(http.StatusUnauthorized) |
| 109 | return | 111 | return |
| 110 | } | 112 | } |
| 111 | } | 113 | } |
| 112 | 114 | ||
| 113 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { | 115 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { |
| 114 | c.AbortWithStatus(http.StatusUnauthorized) | 116 | c.AbortWithStatus(http.StatusUnauthorized) |
| 115 | return | 117 | return |
| 116 | } | 118 | } |
| 117 | 119 | ||
| 118 | preference.Size = size | 120 | preference.Size = size |
| 119 | 121 | ||
| 120 | // Generate a simple API token | 122 | // Generate a simple API token |
| 121 | apiToken := generateToken() | 123 | apiToken := generateToken() |
| 122 | c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) | 124 | c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) |
| 123 | }) | 125 | }) |
| 124 | 126 | ||
| 125 | stats := api.Group("stats") | 127 | stats := api.Group("/stats") |
| 126 | stats.Use(TokenRequired()) | 128 | stats.Use(TokenRequired()) |
| 127 | { | 129 | { |
| 128 | stats.GET("/", func(c *gin.Context) { | 130 | stats.GET("/", func(c *gin.Context) { |
| 129 | db := establishDBConnection() | 131 | db := establishDBConnection() |
| 130 | defer db.Close() | 132 | defer db.Close() |
| 131 | 133 | ||
| 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"); | 134 | 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") |
| 133 | if err != nil { | 135 | if err != nil { |
| 134 | c.JSON(500, gin.H{"error": err.Error()}) | 136 | c.JSON(500, gin.H{"error": err.Error()}) |
| 135 | return | 137 | return |
| 136 | } | 138 | } |
| 137 | defer rows.Close() | 139 | defer rows.Close() |
| 138 | 140 | ||
| 139 | var data []models.Statistic | 141 | var data []models.Statistic |
| 140 | for rows.Next() { | 142 | |
| 141 | var stat models.Statistic | 143 | for rows.Next() { |
| 142 | var user models.User | 144 | var stat models.Statistic |
| 143 | if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { | 145 | var user models.User |
| 144 | c.JSON(500, gin.H{"error": err.Error()}) | 146 | if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { |
| 145 | return | 147 | c.JSON(500, gin.H{"error": err.Error()}) |
| 146 | } | 148 | return |
| 147 | stat.User = user | 149 | } |
| 148 | data = append(data, stat) | 150 | stat.User = user |
| 149 | } | 151 | data = append(data, stat) |
| 150 | 152 | } | |
| 151 | c.JSON(http.StatusOK, data) | 153 | |
| 152 | }) | 154 | |
| 153 | 155 | // TODO: return to this and figure out how to best collect the data you are looking for for each user (zach and parker) | |
| 154 | stats.POST("/", func(c *gin.Context) { | 156 | rows, err = db.Query("SELECT date(s.date), SUM(s.quantity) as total FROM Statistics s WHERE s.date >= date('now', '-7 days') GROUP BY DATE(s.date)") |
| 155 | var stat models.Statistic | 157 | if err != nil { |
| 156 | 158 | c.JSON(500, gin.H{"error": err.Error()}) | |
| 157 | if err := c.BindJSON(&stat); err != nil { | 159 | return |
| 158 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 160 | } |
| 159 | return | 161 | defer rows.Close() |
| 160 | } | 162 | |
| 161 | 163 | var dailySummaries []models.DailySummary | |
| 162 | db := establishDBConnection() | 164 | for rows.Next() { |
| 163 | defer db.Close() | 165 | var summary models.DailySummary |
| 164 | 166 | if err := rows.Scan(&summary.Date, &summary.Total); err != nil { | |
| 165 | result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) | 167 | c.JSON(500, gin.H{"error": err.Error()}) |
| 166 | 168 | return | |
| 167 | if err != nil { | 169 | } |
| 168 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 170 | dailySummaries = append(dailySummaries, summary) |
| 169 | } | 171 | } |
| 170 | 172 | ||
| 171 | id, err := result.LastInsertId() | 173 | c.JSON(http.StatusOK, gin.H{"stats": data, "totals": dailySummaries}) |
| 172 | if err != nil { | 174 | rows, err = db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") |
| 173 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 175 | |
| 174 | } | 176 | if err != nil { |
| 175 | 177 | c.JSON(500, gin.H{"error": err.Error()}) | |
| 176 | c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) | 178 | return |
| 177 | }) | 179 | } |
| 178 | 180 | defer rows.Close() | |
| 179 | stats.GET("/:uuid", func(c *gin.Context) { | 181 | |
| 180 | c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) | 182 | var totals []interface{} |
| 181 | }) | 183 | for rows.Next() { |
| 182 | 184 | var stat models.Statistic | |
| 183 | stats.PATCH("/:uuid", func(c *gin.Context) { | 185 | var user models.User |
| 184 | c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) | 186 | if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { |
| 185 | }) | 187 | c.JSON(500, gin.H{"error": err.Error()}) |
| 186 | 188 | return | |
| 187 | stats.DELETE("/:uuid", func(c *gin.Context) { | 189 | } |
| 188 | c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) | 190 | stat.User = user |
| 189 | }) | 191 | totals = append(totals, stat) |
| 190 | } | 192 | } |
| 191 | 193 | ||
| 194 | c.JSON(http.StatusOK, gin.H{"stats": data, "totals": totals}) | ||
| 195 | }) | ||
| 196 | |||
| 197 | stats.POST("/", func(c *gin.Context) { | ||
| 198 | var stat models.Statistic | ||
| 199 | |||
| 200 | if err := c.BindJSON(&stat); err != nil { | ||
| 201 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||
| 202 | return | ||
| 203 | } | ||
| 204 | |||
| 205 | db := establishDBConnection() | ||
| 206 | defer db.Close() | ||
| 207 | |||
| 208 | result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) | ||
| 209 | |||
| 210 | if err != nil { | ||
| 211 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||
| 212 | } | ||
| 213 | |||
| 214 | id, err := result.LastInsertId() | ||
| 215 | if err != nil { | ||
| 216 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||
| 217 | } | ||
| 218 | |||
| 219 | c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) | ||
| 220 | }) | ||
| 221 | |||
| 222 | stats.GET("/totals/", func(c *gin.Context) { | ||
| 223 | c.JSON(http.StatusOK, gin.H{"status": "ok"}) | ||
| 224 | }) | ||
| 225 | |||
| 226 | // stats.GET("/totals/", func(c *gin.Context) { | ||
| 227 | // db := establishDBConnection() | ||
| 228 | // defer db.Close() | ||
| 229 | // | ||
| 230 | // rows, err := db.Query("SELECT s.date, SUM(s.quantity) as total, u.uuid, u.name FROM Statistics s INNER JOIN Users u ON u.id = s.user_id WHERE s.date >= date('now', '-7 days') GROUP BY s.date, s.user_id") | ||
| 231 | // | ||
| 232 | // if err != nil { | ||
| 233 | // c.JSON(500, gin.H{"error": err.Error()}) | ||
| 234 | // return | ||
| 235 | // } | ||
| 236 | // defer rows.Close() | ||
| 237 | // | ||
| 238 | // var data []models.Statistic | ||
| 239 | // for rows.Next() { | ||
| 240 | // var stat models.Statistic | ||
| 241 | // var user models.User | ||
| 242 | // if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { | ||
| 243 | // c.JSON(500, gin.H{"error": err.Error()}) | ||
| 244 | // return | ||
| 245 | // } | ||
| 246 | // stat.User = user | ||
| 247 | // data = append(data, stat) | ||
| 248 | // } | ||
| 249 | // | ||
| 250 | // c.JSON(http.StatusOK, data) | ||
| 251 | // | ||
| 252 | // }) | ||
| 253 | |||
| 254 | stats.GET("user/:uuid", func(c *gin.Context) { | ||
| 255 | c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) | ||
| 256 | }) | ||
| 257 | |||
| 258 | stats.PATCH("user/:uuid", func(c *gin.Context) { | ||
| 259 | c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) | ||
| 260 | }) | ||
| 261 | |||
| 262 | stats.DELETE("user/:uuid", func(c *gin.Context) { | ||
| 263 | c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) | ||
| 264 | }) | ||
| 265 | } | ||
| 192 | 266 | ||
| 193 | return r | 267 | return r |
| 194 | } | 268 | } |
