From df1581d48e9c4433cdf71396ff2d2c9d44260035 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Thu, 19 Oct 2023 22:10:42 +0100 Subject: [PATCH] activity: route to show activity activity log filterable by type, sortable by time, and paginated. --- api-activities.go | 141 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 3 + models.go | 22 ++++++++ router.go | 2 + storage.go | 3 + 5 files changed, 171 insertions(+) create mode 100644 api-activities.go diff --git a/api-activities.go b/api-activities.go new file mode 100644 index 0000000..d6fdd60 --- /dev/null +++ b/api-activities.go @@ -0,0 +1,141 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/timshannon/badgerhold/v4" +) + +func stringToActivityType(v string) ActivityType { + switch v { + case "creation": + return ActivityCreation + case "deletion": + return ActivityDeletion + case "disabled": + return ActivityDisabled + case "enabled": + return ActivityEnabled + case "contactLinked": + return ActivityContactLinked + case "contactUnlinked": + return ActivityContactUnlinked + case "changePassword": + return ActivityChangePassword + case "resetPassword": + return ActivityResetPassword + case "createInvite": + return ActivityCreateInvite + case "deleteInvite": + return ActivityDeleteInvite + } + return ActivityUnknown +} + +func activityTypeToString(v ActivityType) string { + switch v { + case ActivityCreation: + return "creation" + case ActivityDeletion: + return "deletion" + case ActivityDisabled: + return "disabled" + case ActivityEnabled: + return "enabled" + case ActivityContactLinked: + return "contactLinked" + case ActivityContactUnlinked: + return "contactUnlinked" + case ActivityChangePassword: + return "changePassword" + case ActivityResetPassword: + return "resetPassword" + case ActivityCreateInvite: + return "createInvite" + case ActivityDeleteInvite: + return "deleteInvite" + } + return "unknown" +} + +func stringToActivitySource(v string) ActivitySource { + switch v { + case "user": + return ActivityUser + case "admin": + return ActivityAdmin + case "anon": + return ActivityAnon + case "daemon": + return ActivityDaemon + } + return ActivityAnon +} + +func activitySourceToString(v ActivitySource) string { + switch v { + case ActivityUser: + return "user" + case ActivityAdmin: + return "admin" + case ActivityAnon: + return "anon" + case ActivityDaemon: + return "daemon" + } + return "anon" +} + +// @Summary Get the requested set of activities, Paginated, filtered and sorted. +// @Produce json +// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters" +// @Success 200 {object} GetActivitiesRespDTO +// @Router /activity [get] +// @Security Bearer +// @tags Activity +func (app *appContext) GetActivities(gc *gin.Context) { + req := GetActivitiesDTO{} + gc.BindJSON(&req) + query := &badgerhold.Query{} + activityType := stringToActivityType(req.Type) + if activityType != ActivityUnknown { + query = badgerhold.Where("Type").Eq(activityType) + } + + if req.Ascending { + query = query.Reverse() + } + + query = query.SortBy("Time") + + if req.Limit == 0 { + req.Limit = 10 + } + + query = query.Skip(req.Page * req.Limit).Limit(req.Limit) + + var results []Activity + err := app.storage.db.Find(&results, query) + + if err != nil { + app.err.Printf("Failed to read activities from DB: %v\n", err) + } + + resp := GetActivitiesRespDTO{ + Activities: make([]ActivityDTO, len(results)), + } + + for i, act := range results { + resp.Activities[i] = ActivityDTO{ + ID: act.ID, + Type: activityTypeToString(act.Type), + UserID: act.UserID, + SourceType: activitySourceToString(act.SourceType), + Source: act.Source, + InviteCode: act.InviteCode, + Value: act.Value, + Time: act.Time.Unix(), + } + } + + gc.JSON(200, resp) +} diff --git a/main.go b/main.go index 4b6c211..1249d29 100644 --- a/main.go +++ b/main.go @@ -638,6 +638,9 @@ func flagPassed(name string) (found bool) { // @tag.name Profiles & Settings // @tag.description Profile and settings related operations. +// @tag.name Activity +// @tag.description Routes related to the activity log. + // @tag.name Configuration // @tag.description jfa-go settings. diff --git a/models.go b/models.go index d4f841e..aa77f2f 100644 --- a/models.go +++ b/models.go @@ -430,3 +430,25 @@ type GetMyReferralRespDTO struct { type EnableDisableReferralDTO struct { Users []string `json:"users"` } + +type ActivityDTO struct { + ID string `json:"id"` + Type string `json:"type"` + UserID string `json:"user_id"` + SourceType string `json:"source_type"` + Source string `json:"source"` + InviteCode string `json:"invite_code"` + Value string `json:"value"` + Time int64 `json:"time"` +} + +type GetActivitiesDTO struct { + Type string `json:"type"` // Type of activity to get. Leave blank for all. + Limit int `json:"limit"` + Page int `json:"page"` // zero-indexed + Ascending bool `json:"ascending"` +} + +type GetActivitiesRespDTO struct { + Activities []ActivityDTO `json:"activities"` +} diff --git a/router.go b/router.go index 875ad20..edcbfdb 100644 --- a/router.go +++ b/router.go @@ -232,6 +232,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.DELETE(p+"/profiles/referral/:profile", app.DisableReferralForProfile) } + api.GET(p+"/activity", app.GetActivities) + if userPageEnabled { user.GET("/details", app.MyDetails) user.POST("/contact", app.SetMyContactMethods) diff --git a/storage.go b/storage.go index 39f31c6..5d5af8d 100644 --- a/storage.go +++ b/storage.go @@ -34,6 +34,7 @@ const ( ActivityResetPassword ActivityCreateInvite ActivityDeleteInvite + ActivityUnknown ) type ActivitySource int @@ -46,6 +47,7 @@ const ( ) type Activity struct { + ID string `badgerhold:"key"` Type ActivityType `badgerhold:"index"` UserID string // ID of target user. For account creation, this will be the newly created account SourceType ActivitySource @@ -562,6 +564,7 @@ func (st *Storage) GetActivityKey(k string) (Activity, bool) { // SetActivityKey stores value v in key k. func (st *Storage) SetActivityKey(k string, v Activity) { + v.ID = k err := st.db.Upsert(k, v) if err != nil { // fmt.Printf("Failed to set custom content: %v\n", err)