From cf2113e77edabf8e3a632c7b76c769752039ba88 Mon Sep 17 00:00:00 2001 From: Zach Berwaldt Date: Sat, 2 Mar 2024 16:52:55 -0500 Subject: 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 --- api/main.go | 384 ++++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 229 insertions(+), 155 deletions(-) (limited to 'api/main.go') 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 import ( "net/http" - "crypto/rand" - "encoding/base64" - "database/sql" - "strings" - "errors" + "crypto/rand" + "encoding/base64" + "database/sql" + "strings" + "errors" + "log" "github.com/gin-gonic/gin" - _ "github.com/mattn/go-sqlite3" - "golang.org/x/crypto/bcrypt" - "water/api/lib" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" + "water/api/lib" ) func CORSMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - 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") - c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + 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") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") + + log.Println("I am here") + + if c.Request.Method == "OPTIONS" { + log.Println(c.Request.Header) + c.AbortWithStatus(204) + return + } + + log.Println(c.Request.Header) + c.Next() + } } // generatToken will g func generateToken() string { - token := make([]byte, 32) - rand.Read(token) - return base64.StdEncoding.EncodeToString(token) + token := make([]byte, 32) + rand.Read(token) + return base64.StdEncoding.EncodeToString(token) } func establishDBConnection() *sql.DB { - db, err := sql.Open("sqlite3", "../db/water.sqlite3") - if err != nil { - panic(err) - } - return db + db, err := sql.Open("sqlite3", "../db/water.sqlite3") + if err != nil { + panic(err) + } + return db } - func checkForTokenInContext(c *gin.Context) (string, error) { - authorizationHeader := c.GetHeader("Authorization") - if authorizationHeader == "" { - return "", errors.New("Authorization header is missing") - } + authorizationHeader := c.GetHeader("Authorization") + if authorizationHeader == "" { + return "", errors.New("Authorization header is missing") + } - parts := strings.Split(authorizationHeader, " ") + parts := strings.Split(authorizationHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - return "", errors.New("Invalid Authorization header format") - } + if len(parts) != 2 || parts[0] != "Bearer" { + return "", errors.New("Invalid Authorization header format") + } - - return parts[1], nil + return parts[1], nil } - func TokenRequired() gin.HandlerFunc { - return func(c *gin.Context) { - _, err := checkForTokenInContext(c) + return func(c *gin.Context) { + _, err := checkForTokenInContext(c) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) + c.Abort() + return + } - c.Next() - } + c.Next() + } } func setupRouter() *gin.Engine { // Disable Console Color // gin.DisableConsoleColor() r := gin.Default() - r.Use(CORSMiddleware()) - r.Use(gin.Logger()) - r.Use(gin.Recovery()) - - api := r.Group("api/v1") - - api.POST("/auth", func(c *gin.Context) { - username, password, ok := c.Request.BasicAuth() - if !ok { - c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - db := establishDBConnection() - defer db.Close() - - var user models.User - var preference models.Preference - var size models.Size - - 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) - if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { - if err == sql.ErrNoRows { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { - c.AbortWithStatus(http.StatusUnauthorized) - return - } - - preference.Size = size + r.Use(CORSMiddleware()) + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + api := r.Group("api/v1") + + api.POST("/auth", func(c *gin.Context) { + username, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", `Basic realm="Please enter your username and password."`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + db := establishDBConnection() + defer db.Close() + + var user models.User + var preference models.Preference + var size models.Size + + 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) + if err := row.Scan(&user.Name, &user.UUID, &user.Password, &preference.Color, &size.Size, &size.Unit); err != nil { + if err == sql.ErrNoRows { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + preference.Size = size // Generate a simple API token apiToken := generateToken() - c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) - }) - - stats := api.Group("stats") - stats.Use(TokenRequired()) - { - stats.GET("/", func(c *gin.Context) { - db := establishDBConnection() - defer db.Close() - - 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"); - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - defer rows.Close() - - var data []models.Statistic - for rows.Next() { - var stat models.Statistic - var user models.User - if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - stat.User = user - data = append(data, stat) - } - - c.JSON(http.StatusOK, data) - }) - - stats.POST("/", func(c *gin.Context) { - var stat models.Statistic - - if err := c.BindJSON(&stat); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - db := establishDBConnection() - defer db.Close() - - result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) - - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - id, err := result.LastInsertId() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) - }) - - stats.GET("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) - }) - - stats.PATCH("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - - stats.DELETE("/:uuid", func(c *gin.Context) { - c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) - }) - } - + c.JSON(http.StatusOK, gin.H{"token": apiToken, "user": user, "preferences": preference}) + }) + + stats := api.Group("/stats") + stats.Use(TokenRequired()) + { + stats.GET("/", func(c *gin.Context) { + db := establishDBConnection() + defer db.Close() + + 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") + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var data []models.Statistic + + for rows.Next() { + var stat models.Statistic + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + stat.User = user + data = append(data, stat) + } + + + // TODO: return to this and figure out how to best collect the data you are looking for for each user (zach and parker) + 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)") + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var dailySummaries []models.DailySummary + for rows.Next() { + var summary models.DailySummary + if err := rows.Scan(&summary.Date, &summary.Total); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + dailySummaries = append(dailySummaries, summary) + } + + c.JSON(http.StatusOK, gin.H{"stats": data, "totals": dailySummaries}) + 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") + + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var totals []interface{} + for rows.Next() { + var stat models.Statistic + var user models.User + if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + stat.User = user + totals = append(totals, stat) + } + + c.JSON(http.StatusOK, gin.H{"stats": data, "totals": totals}) + }) + + stats.POST("/", func(c *gin.Context) { + var stat models.Statistic + + if err := c.BindJSON(&stat); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + db := establishDBConnection() + defer db.Close() + + result, err := db.Exec("INSERT INTO statistics (date, user_id, quantity) values (?, ?, ?)", stat.Date, 1, stat.Quantity) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + id, err := result.LastInsertId() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + c.JSON(http.StatusCreated, gin.H{"status": "created", "id": id}) + }) + + stats.GET("/totals/", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // stats.GET("/totals/", func(c *gin.Context) { + // db := establishDBConnection() + // defer db.Close() + // + // 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") + // + // if err != nil { + // c.JSON(500, gin.H{"error": err.Error()}) + // return + // } + // defer rows.Close() + // + // var data []models.Statistic + // for rows.Next() { + // var stat models.Statistic + // var user models.User + // if err := rows.Scan(&stat.Date, &stat.Quantity, &user.UUID, &user.Name); err != nil { + // c.JSON(500, gin.H{"error": err.Error()}) + // return + // } + // stat.User = user + // data = append(data, stat) + // } + // + // c.JSON(http.StatusOK, data) + // + // }) + + stats.GET("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok", "uuid": c.Param("uuid")}) + }) + + stats.PATCH("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + + stats.DELETE("user/:uuid", func(c *gin.Context) { + c.JSON(http.StatusNoContent, gin.H{"status": "No Content"}) + }) + } return r } -- cgit v1.1