diff --git a/README.md b/README.md index 0d1e1be..4a19ef8 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,5 @@ Counters also has support for Docker Compose, a extended version of docker that - [] Add API Integration - [] Add Logging (similar to Votes "Actions") - [] Time Zone support + + \ No newline at end of file diff --git a/internal/database/counter.go b/internal/database/counter.go index 941f2fd..5bce14c 100644 --- a/internal/database/counter.go +++ b/internal/database/counter.go @@ -2,12 +2,16 @@ package database import ( "context" + "fmt" + "strings" "time" "github.com/ComputerScienceHouse/counters/internal/models" "github.com/jackc/pgx/v5" ) +const MAX_COUNTERS_IN_QUERY = 10 + func CreateCounterWithReset(ctx context.Context, userId string, username string, cReq *models.CreateCounterInput, rReq *models.CreateResetInput) (string, error) { tx, err := db.Begin(ctx) if err != nil { @@ -45,42 +49,61 @@ func CreateCounterWithReset(ctx context.Context, userId string, username string, return counterId, nil } -func GetCounters(ctx context.Context) ([]*models.CounterListPart, error) { - rows, err := db.Query(ctx, ` - SELECT c.counter_id, c.user_id, c.title, c.description, r.description, r.username, r.occured_at FROM counters c +func GetCounters(ctx context.Context, filter *models.GetCountersFilter) ([]*models.GetCountersCounterPart, error) { + args := make([]any, 0) + + postQuery := ` + SELECT c.counter_id, c.title, r.description, r.username, r.occured_at FROM counters c JOIN resets r ON c.last_reset = r.reset_id - ORDER BY r.occured_at DESC - `) + ` + if filter.Timestamp != nil { + args = append(args, time.Unix(*filter.Timestamp, 0)) + + if filter.SortDescending { + postQuery = fmt.Sprintf("%s AND r.occured_at < $%d", postQuery, len(args)) + } else { + postQuery = fmt.Sprintf("%s AND r.occured_at > $%d", postQuery, len(args)) + } + } + + if filter.Name != nil && strings.TrimSpace(*filter.Name) != "" { + args = append(args, "%"+*filter.Name+"%") + postQuery = fmt.Sprintf("%s AND c.title ILIKE $%d", postQuery, len(args)) + } + + if filter.SortDescending { + postQuery = fmt.Sprintf("%s ORDER BY r.occured_at DESC LIMIT %d", postQuery, MAX_COUNTERS_IN_QUERY) + } else { + postQuery = fmt.Sprintf("%s ORDER BY r.occured_at ASC LIMIT %d", postQuery, MAX_COUNTERS_IN_QUERY) + } + + rows, err := db.Query(ctx, postQuery, args...) if err != nil { return nil, err } defer rows.Close() - counters := make([]*models.CounterListPart, 0) + counters := make([]*models.GetCountersCounterPart, 0) for rows.Next() { var ( - counterId string - counterOwnerID string - counterTitle string - counterDescription string - resetDescription string - resetUsername string - resetCreatedAt time.Time + counterId string + counterTitle string + resetDescription string + resetUsername string + resetCreatedAt time.Time ) - if err := rows.Scan(&counterId, &counterOwnerID, &counterTitle, &counterDescription, &resetDescription, &resetUsername, &resetCreatedAt); err != nil { + if err := rows.Scan(&counterId, &counterTitle, &resetDescription, &resetUsername, &resetCreatedAt); err != nil { return nil, err } - counters = append(counters, &models.CounterListPart{ - CounterID: counterId, - CounterOwner: counterOwnerID, - CounterTitle: counterTitle, - CounterDescription: counterDescription, - ResetDescription: resetDescription, - ResetUsername: resetUsername, - ResetOccuredAt: resetCreatedAt.Unix(), + counters = append(counters, &models.GetCountersCounterPart{ + CounterID: counterId, + CounterTitle: counterTitle, + ResetDescription: resetDescription, + ResetUsername: resetUsername, + ResetTimestamp: resetCreatedAt.Unix(), }) } diff --git a/internal/database/reset.go b/internal/database/reset.go index afff888..06ee4db 100644 --- a/internal/database/reset.go +++ b/internal/database/reset.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" "time" "github.com/ComputerScienceHouse/counters/internal/models" @@ -9,6 +10,8 @@ import ( "github.com/jackc/pgx/v5" ) +const MAX_RESETS_IN_QUERY = 10 + func CreateReset(ctx context.Context, userID string, username string, req *models.CreateResetInput) (string, error) { resetID, err := GenerateUUID() if err != nil { @@ -73,12 +76,31 @@ func GetCounterFromReset(ctx context.Context, resetID string) (string, error) { return counterID, nil } -func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.ResetListPart, error) { - rows, err := db.Query(ctx, ` +func GetResetsFromCounterId(ctx context.Context, counterID string, filter *models.GetResetsFilter) ([]*models.ResetListPart, error) { + args := make([]any, 0) + args = append(args, counterID) + + postQuery := ` SELECT r.reset_id, r.description, r.user_id, r.username, r.occured_at FROM resets r WHERE r.counter_id = $1 - ORDER BY r.occured_at DESC - `, counterID) + ` + if filter.Timestamp != nil { + args = append(args, time.Unix(*filter.Timestamp, 0)) + + if filter.SortDescending { + postQuery = fmt.Sprintf("%s AND r.occured_at < $%d", postQuery, len(args)) + } else { + postQuery = fmt.Sprintf("%s AND r.occured_at > $%d", postQuery, len(args)) + } + } + + if filter.SortDescending { + postQuery = fmt.Sprintf("%s ORDER BY r.occured_at DESC LIMIT %d", postQuery, MAX_RESETS_IN_QUERY) + } else { + postQuery = fmt.Sprintf("%s ORDER BY r.occured_at ASC LIMIT %d", postQuery, MAX_RESETS_IN_QUERY) + } + + rows, err := db.Query(ctx, postQuery, args...) if err != nil { return nil, err } @@ -92,10 +114,10 @@ func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.Re resetDescription string resetOwner string resetUsername string - resetOccuredAt time.Time + resetOccurredAt time.Time ) - if err := rows.Scan(&resetId, &resetDescription, &resetOwner, &resetUsername, &resetOccuredAt); err != nil { + if err := rows.Scan(&resetId, &resetDescription, &resetOwner, &resetUsername, &resetOccurredAt); err != nil { return nil, err } @@ -104,7 +126,7 @@ func GetResetsFromCounterId(ctx context.Context, counterID string) ([]*models.Re ResetDescription: resetDescription, ResetOwner: resetOwner, ResetUsername: resetUsername, - ResetOccuredAt: resetOccuredAt.Unix(), + ResetOccurredAt: resetOccurredAt.Unix(), }) } diff --git a/internal/models/api_counter.go b/internal/models/api_counter.go index f729074..66e83e9 100644 --- a/internal/models/api_counter.go +++ b/internal/models/api_counter.go @@ -1,5 +1,30 @@ package models +type GetCountersFilter struct { + Timestamp *int64 `json:"timestamp"` + SortDescending bool `json:"sort_descending"` + Name *string `json:"name"` +} + +type GetCountersCounterPart struct { + CounterID string `json:"counter_id"` + CounterTitle string `json:"counter_title"` + ResetDescription string `json:"reset_description"` + ResetUsername string `json:"reset_username"` + ResetTimestamp int64 `json:"reset_timestamp"` +} + +type GetCountersInput struct { + CounterID string `json:"counter_id"` + Filter GetCountersFilter `json:"filter"` +} + +type GetCountersOutput struct { + Counters []*GetCountersCounterPart `json:"counters"` + HasMore bool `json:"has_more"` + Timestamp *int64 `json:"timestamp"` +} + type CreateCounterWithResetInput struct { Counter CreateCounterInput `json:"counter"` Reset CreateResetInput `json:"reset"` diff --git a/internal/models/api_reset.go b/internal/models/api_reset.go index 56d014e..5cd04ec 100644 --- a/internal/models/api_reset.go +++ b/internal/models/api_reset.go @@ -1,5 +1,29 @@ package models +type GetResetsFilter struct { + Timestamp *int64 `json:"timestamp"` + SortDescending bool `json:"sort_descending"` +} + +type GetResetsResetPart struct { + Id string `json:"id"` + Username string `json:"username"` + Description string `json:"description"` + Timestamp int64 `json:"timestamp"` + CanDelete bool `json:"can_delete"` +} + +type GetResetsInput struct { + CounterID string `json:"counter_id"` + Filter GetResetsFilter `json:"filter"` +} + +type GetResetsOutput struct { + Resets []*GetResetsResetPart `json:"resets"` + HasMore bool `json:"has_more"` + Timestamp *int64 `json:"timestamp"` +} + type CreateResetInput struct { CounterID string `json:"counter_id"` Description string `json:"description"` diff --git a/internal/models/reset.go b/internal/models/reset.go index cf90581..b6b67f8 100644 --- a/internal/models/reset.go +++ b/internal/models/reset.go @@ -13,5 +13,5 @@ type ResetListPart struct { ResetDescription string `json:"reset_description"` ResetOwner string `json:"reset_owner"` ResetUsername string `json:"reset_username"` - ResetOccuredAt int64 `json:"reset_occured_at"` + ResetOccurredAt int64 `json:"reset_occurred_at"` } diff --git a/internal/routes/api/v1/counters/counter.go b/internal/routes/api/v1/counters/counter.go index fc88b0f..6a8f21d 100644 --- a/internal/routes/api/v1/counters/counter.go +++ b/internal/routes/api/v1/counters/counter.go @@ -1,7 +1,9 @@ package counters import ( + "context" "net/http" + "time" "github.com/ComputerScienceHouse/counters/internal/database" "github.com/ComputerScienceHouse/counters/internal/logging" @@ -12,38 +14,42 @@ import ( "github.com/sirupsen/logrus" ) -func GetCounter(c *gin.Context) { - user, err := users.GetCSHAuth(c) +func GetCounters(c *gin.Context) { + _, err := users.GetCSHAuth(c) if err != nil { c.Status(http.StatusUnauthorized) return } - idToUse := c.Param("id") - counter, err := database.GetCounterFromId(c.Request.Context(), idToUse) - if err != nil { + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + + var req models.GetCountersInput + + if err := c.ShouldBindJSON(&req); err != nil { + logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error binding req") c.Status(http.StatusBadRequest) return } - resets, err := database.GetResetsFromCounterId(c.Request.Context(), idToUse) + counters, err := database.GetCounters(ctx, &req.Filter) if err != nil { + logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("error getting counters!") c.Status(http.StatusBadRequest) return } - c.HTML(200, "counter.tmpl", gin.H{ - "Id": counter.CounterID, - "CounterID": counter.CounterOwner, - "Title": counter.CounterTitle, - "Description": counter.CounterDescription, - "Timestamp": counter.ResetOccuredAt, - "History": resets, - "Username": user.Username, - "FullName": user.FullName, - "UserID": user.Uuid, - "EBoard": users.IsEboard(user), - "RTP": users.IsActiveRTP(user), + var hasMore bool = false + var timestamp int64 = 0 + if len(counters) > 1 { + hasMore = (len(counters) > (database.MAX_COUNTERS_IN_QUERY)-1) + timestamp = counters[len(counters)-1].ResetTimestamp + } + + c.JSON(http.StatusOK, models.GetCountersOutput{ + Counters: counters, + HasMore: hasMore, + Timestamp: ×tamp, }) } @@ -55,6 +61,9 @@ func DeleteCounter(c *gin.Context) { return } + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + var req models.DeleteCounterInput if err := c.ShouldBindJSON(&req); err != nil { logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error casting request") @@ -62,7 +71,7 @@ func DeleteCounter(c *gin.Context) { return } - counterOwner, err := database.GetCounterOwner(c.Request.Context(), req.RowID) + counterOwner, err := database.GetCounterOwner(ctx, req.RowID) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error getting counter owner") c.Status(http.StatusBadRequest) @@ -76,11 +85,12 @@ func DeleteCounter(c *gin.Context) { return } - if err := database.DeleteCounter(c.Request.Context(), req.RowID); err != nil { + if err := database.DeleteCounter(ctx, req.RowID); err != nil { logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error deleting counter") c.Status(http.StatusInternalServerError) return } + c.Status(http.StatusNoContent) } @@ -91,6 +101,9 @@ func CreateCounter(c *gin.Context) { return } + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + var req models.CreateCounterWithResetInput if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil { @@ -105,7 +118,7 @@ func CreateCounter(c *gin.Context) { return } - counterID, err := database.CreateCounterWithReset(c.Request.Context(), user.Uuid, user.Username, &req.Counter, &req.Reset) + counterID, err := database.CreateCounterWithReset(ctx, user.Uuid, user.Username, &req.Counter, &req.Reset) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "counters", "error": err}).Warning("Error creating new database") c.Status(http.StatusBadRequest) @@ -121,4 +134,6 @@ func Routes(r *gin.RouterGroup) { cGroup.GET("/:id") cGroup.POST("", CreateCounter) cGroup.DELETE("", DeleteCounter) + + cGroup.Handle("QUERY", "", GetCounters) } diff --git a/internal/routes/api/v1/reset/reset.go b/internal/routes/api/v1/reset/reset.go index beb59b3..b4ea626 100644 --- a/internal/routes/api/v1/reset/reset.go +++ b/internal/routes/api/v1/reset/reset.go @@ -1,7 +1,9 @@ package reset import ( + "context" "net/http" + "time" "github.com/ComputerScienceHouse/counters/internal/database" "github.com/ComputerScienceHouse/counters/internal/logging" @@ -11,6 +13,62 @@ import ( "github.com/sirupsen/logrus" ) +func GetResets(c *gin.Context) { + user, err := users.GetCSHAuth(c) + if err != nil { + c.Status(http.StatusUnauthorized) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + + var req models.GetResetsInput + + if err := c.ShouldBindJSON(&req); err != nil { + logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error binding req") + c.Status(http.StatusBadRequest) + return + } + + resets, err := database.GetResetsFromCounterId(ctx, req.CounterID, &req.Filter) + if err != nil { + logging.Logger.WithFields(logrus.Fields{"module": "counter", "error": err}).Warning("Error getting resets for counter id") + c.Status(http.StatusBadRequest) + return + } + + finalResets := make([]*models.GetResetsResetPart, len(resets)) + + isCounterOwner := req.CounterID == user.Uuid + + for index, reset := range resets { + isResetOwner := reset.ResetID == user.Uuid + + finalResets[index] = &models.GetResetsResetPart{ + Id: reset.ResetID, + Username: reset.ResetUsername, + Description: reset.ResetDescription, + Timestamp: reset.ResetOccurredAt, + CanDelete: (users.IsEboard(user) || users.IsActiveRTP(user) || isCounterOwner || isResetOwner), + } + } + + var hasMore bool = false + var timestamp int64 = 0 + + if len(finalResets) > 1 { + hasMore = (len(finalResets) > (database.MAX_RESETS_IN_QUERY)-1) + timestamp = finalResets[len(finalResets)-1].Timestamp + } + + c.JSON(http.StatusOK, models.GetResetsOutput{ + Resets: finalResets, + HasMore: hasMore, + Timestamp: ×tamp, + }) +} + func CreateReset(c *gin.Context) { user, err := users.GetCSHAuth(c) if err != nil { @@ -18,6 +76,9 @@ func CreateReset(c *gin.Context) { return } + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + var req models.CreateResetInput if err := c.ShouldBindJSON(&req); err != nil { logging.Logger.Warnf("failed to cast error %s", err) @@ -31,13 +92,13 @@ func CreateReset(c *gin.Context) { return } - if _, err := database.CreateReset(c.Request.Context(), user.Uuid, user.Username, &req); err != nil { + if _, err := database.CreateReset(ctx, user.Uuid, user.Username, &req); err != nil { logging.Logger.Warnf("failed to create error %s", err) c.Status(http.StatusBadRequest) return } - if _, err := database.UpdateCounterLastReset(c.Request.Context(), req.CounterID); err != nil { + if _, err := database.UpdateCounterLastReset(ctx, req.CounterID); err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error updating last reset") c.Status(http.StatusInternalServerError) return @@ -54,6 +115,9 @@ func DeleteReset(c *gin.Context) { return } + ctx, cancel := context.WithTimeout(c.Request.Context(), time.Second*15) + defer cancel() + var req models.DeleteResetInput if err := c.ShouldBindJSON(&req); err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error compiling reset") @@ -61,21 +125,21 @@ func DeleteReset(c *gin.Context) { return } - counterID, err := database.GetCounterFromReset(c.Request.Context(), req.RowID) + counterID, err := database.GetCounterFromReset(ctx, req.RowID) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting counter from reset") c.Status(http.StatusBadRequest) return } - counterOwner, err := database.GetCounterOwner(c.Request.Context(), counterID) + counterOwner, err := database.GetCounterOwner(ctx, counterID) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting counter owner") c.Status(http.StatusBadRequest) return } - resetOwner, err := database.GetResetOwner(c.Request.Context(), req.RowID) + resetOwner, err := database.GetResetOwner(ctx, req.RowID) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error deleting reset") c.Status(http.StatusBadRequest) @@ -90,13 +154,13 @@ func DeleteReset(c *gin.Context) { return } - if err := database.DeleteReset(c.Request.Context(), counterID, req.RowID); err != nil { + if err := database.DeleteReset(ctx, counterID, req.RowID); err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error deleting reset") c.Status(http.StatusInternalServerError) return } - counterExists, err := database.UpdateCounterLastReset(c.Request.Context(), counterID) + counterExists, err := database.UpdateCounterLastReset(ctx, counterID) if err != nil { logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error updating last reset") c.Status(http.StatusInternalServerError) @@ -110,6 +174,8 @@ func Routes(r *gin.RouterGroup) { rGroup := r.Group("/reset") rGroup.GET("/:id") - rGroup.POST("", CreateReset) // create reset + rGroup.POST("", CreateReset) rGroup.DELETE("", DeleteReset) + + rGroup.Handle("QUERY", "", GetResets) } diff --git a/internal/routes/web/pages/counter.go b/internal/routes/web/pages/counter.go index e736f22..6c37edf 100644 --- a/internal/routes/web/pages/counter.go +++ b/internal/routes/web/pages/counter.go @@ -32,23 +32,13 @@ func GetCounterPage(c *gin.Context) { return } - history, err := database.GetResetsFromCounterId(c.Request.Context(), idToUse) - if err != nil { - logging.Logger.WithFields(logrus.Fields{"module": "reset", "error": err}).Warning("Error getting history for counter") - c.JSON(http.StatusBadRequest, models.NewErrorResponse()) - return - } - - logging.Logger.Info(counter.CounterDescription) - c.HTML(http.StatusOK, "counter.tmpl", gin.H{ - "Id": counter.CounterID, - "CounterID": counter.CounterOwner, - "Title": counter.CounterTitle, - "Description": counter.CounterDescription, - "Timestamp": counter.ResetOccuredAt, + "CounterID": counter.CounterID, + "CounterOwnerUUID": counter.CounterOwner, + "CounterTitle": counter.CounterTitle, + "CounterDescription": counter.CounterDescription, + "MostRecentTimestamp": counter.ResetOccuredAt, - "History": history, "Username": user.Username, "FullName": user.FullName, "UserID": user.Uuid, diff --git a/internal/routes/web/pages/home.go b/internal/routes/web/pages/home.go index 1c22429..3344dc6 100644 --- a/internal/routes/web/pages/home.go +++ b/internal/routes/web/pages/home.go @@ -2,13 +2,9 @@ package pages import ( "net/http" - "sort" - "github.com/ComputerScienceHouse/counters/internal/database" - "github.com/ComputerScienceHouse/counters/internal/logging" "github.com/ComputerScienceHouse/counters/internal/users" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" ) func GetHomePage(c *gin.Context) { @@ -18,20 +14,7 @@ func GetHomePage(c *gin.Context) { return } - counters, err := database.GetCounters(c.Request.Context()) - - if err != nil { - logging.Logger.WithFields(logrus.Fields{"error": err, "module": "api", "method": "GetHomePage"}).Warn("Unable to load counters!") - c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to load counters!"}) - return - } - - sort.Slice(counters, func(i, j int) bool { - return counters[i].ResetOccuredAt > counters[j].ResetOccuredAt - }) - - c.HTML(http.StatusOK, "index.html", gin.H{ - "Counters": counters, + c.HTML(http.StatusOK, "index.tmpl", gin.H{ "Username": user.Username, "FullName": user.FullName, "EBoard": users.IsEboard(user), diff --git a/internal/routes/web/pages/reset.go b/internal/routes/web/pages/reset.go index 79aadec..f128874 100644 --- a/internal/routes/web/pages/reset.go +++ b/internal/routes/web/pages/reset.go @@ -15,6 +15,7 @@ func GetResetPage(c *gin.Context) { } idToUse := c.Param("id") + c.HTML(http.StatusOK, "reset.tmpl", gin.H{ "Id": idToUse, "Username": user.Username, diff --git a/templates/counter.tmpl b/templates/counter.tmpl index 49ff42c..94e57a5 100644 --- a/templates/counter.tmpl +++ b/templates/counter.tmpl @@ -1,5 +1,34 @@ - {{ define "counter.tmpl"}} - {{ template "header.tmpl" . }} +{{ define "counter.tmpl"}} +{{ template "header.tmpl" . }} + +