Peter Sanchez: 1 Adding audit log views for the following: 13 files changed, 292 insertions(+), 6 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/114/mbox | git am -3Learn more about email & git
- Personal account level - Organization level - Link listing level Changelog-added: Handlers to view audit logs at various levels. --- accounts/routes.go | 14 +++ api/graph/schema.resolvers.go | 57 +++++++++- cmd/migrations.go | 7 ++ core/import.go | 2 + core/routes.go | 24 ++++ helpers.go | 103 ++++++++++++++++++ list/routes.go | 29 +++++ .../0006_update_auditlog_metadata.down.sql | 2 + .../0006_update_auditlog_metadata.up.sql | 10 ++ templates/auditlog.html | 47 ++++++++ templates/listing_list.html | 1 + templates/org_list.html | 1 + templates/settings.html | 1 + 13 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 migrations/0006_update_auditlog_metadata.down.sql create mode 100644 migrations/0006_update_auditlog_metadata.up.sql create mode 100644 templates/auditlog.html diff --git a/accounts/routes.go b/accounts/routes.go index d9350ce..9bb2beb 100644 --- a/accounts/routes.go +++ b/accounts/routes.go @@ -57,6 +57,7 @@ func (s *Service) RegisterRoutes() { gservice.RegisterRoutes() s.Group.Use(auth.AuthRequired()) s.Group.GET("/settings", s.Settings).Name = s.RouteName("settings") + s.Group.GET("/settings/log", s.UserLog).Name = s.RouteName("settings_log") s.Group.GET("/profile", s.EditProfile).Name = s.RouteName("profile_edit") s.Group.POST("/profile", s.EditProfile).Name = s.RouteName("profile_edit_post") } @@ -229,6 +230,7 @@ func (s *Service) Settings(c echo.Context) error { pd.Data["edit_profile"] = lt.Translate("Edit profile") pd.Data["manage"] = lt.Translate("Manage Your Organizations") pd.Data["upgrade"] = lt.Translate("Upgrade Org") + pd.Data["logs"] = lt.Translate("View Audit Logs") user := gctx.User.(*models.User) langTrans := map[string]string{ @@ -278,6 +280,18 @@ func (s *Service) Settings(c echo.Context) error { return s.Render(c, http.StatusOK, "settings.html", gmap) } +// UserLog will show the user their auditlog history (for all orgs) +func (s *Service) UserLog(c echo.Context) error { + gctx := c.(*server.Context) + user := gctx.User.(*models.User) + + gmap, err := links.FetchAuditLogs(c, int(user.ID), "", 0, 0) + if err != nil { + return err + } + return s.Render(c, http.StatusOK, "auditlog.html", gmap) +} + // CompleteRegister is used when a user has been invited to an organization but does not // yet have an account. For normal user registration "completion", see // gobwebs/accounts/routes.go diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 6a3ae3b..a00cc26 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -190,6 +190,7 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -384,6 +385,7 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model. c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -566,6 +568,7 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, userID, @@ -772,6 +775,7 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug ltype := models.LOG_BOOKMARK_UPDATED ldet := "bookmark" if orgLink.Type == models.NoteType { @@ -877,6 +881,7 @@ func (r *mutationResolver) DeleteLink(ctx context.Context, hash string) (*model. c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug ltype := models.LOG_BOOKMARK_DELETED ldet := "bookmark" if link.Type == models.NoteType { @@ -1199,6 +1204,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["user_id"] = user.ID err = models.RecordAuditLog( ctx, @@ -1302,6 +1308,7 @@ func (r *mutationResolver) DeleteMember(ctx context.Context, orgSlug string, ema mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["user_id"] = duser.ID err = models.RecordAuditLog( ctx, @@ -1430,6 +1437,7 @@ func (r *mutationResolver) ConfirmMember(ctx context.Context, key string) (*mode c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -1904,6 +1912,7 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input *model.Profi c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = personalOrg.ID + mdata["org_slug"] = personalOrg.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -2057,6 +2066,7 @@ func (r *mutationResolver) AddDomain(ctx context.Context, input model.DomainInpu c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["domain_id"] = domain.ID err = models.RecordAuditLog( ctx, @@ -2141,6 +2151,7 @@ func (r *mutationResolver) DeleteDomain(ctx context.Context, id int) (*model.Del c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = domain.OrgID + mdata["org_slug"] = domain.OrgSlug mdata["domain_id"] = domain.ID err = models.RecordAuditLog( ctx, @@ -2357,6 +2368,7 @@ func (r *mutationResolver) AddLinkShort(ctx context.Context, input *model.LinkSh mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -2552,6 +2564,7 @@ func (r *mutationResolver) UpdateLinkShort(ctx context.Context, input *model.Upd mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -2619,6 +2632,7 @@ func (r *mutationResolver) DeleteLinkShort(ctx context.Context, id int) (*model. mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -2935,7 +2949,9 @@ func (r *mutationResolver) AddListing(ctx context.Context, input *model.AddListi c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["list_id"] = listing.ID + mdata["list_slug"] = listing.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -3037,7 +3053,9 @@ func (r *mutationResolver) AddListingLink(ctx context.Context, input *model.AddL c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["list_id"] = listing.ID + mdata["list_slug"] = listing.Slug mdata["list_link_id"] = listingLink.ID err = models.RecordAuditLog( ctx, @@ -3382,7 +3400,9 @@ func (r *mutationResolver) UpdateListing(ctx context.Context, input *model.Updat c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["list_id"] = listing.ID + mdata["list_slug"] = listing.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -3458,6 +3478,12 @@ func (r *mutationResolver) UpdateListingLink(ctx context.Context, input *model.U return nil, nil } + listing := &models.Listing{ID: listingLink.ListingID} + err = listing.Load(ctx) + if err != nil { + return nil, err + } + listingLink.Title = input.Title listingLink.LinkOrder = input.LinkOrder listingLink.URL = input.URL @@ -3472,7 +3498,8 @@ func (r *mutationResolver) UpdateListingLink(ctx context.Context, input *model.U c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID - mdata["list_id"] = listingLink.ListingID + mdata["org_slug"] = org.Slug + mdata["list_slug"] = listing.Slug mdata["list_link_id"] = listingLink.ID err = models.RecordAuditLog( ctx, @@ -3542,7 +3569,9 @@ func (r *mutationResolver) DeleteListing(ctx context.Context, id int) (*model.De c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["list_id"] = listing.ID + mdata["list_slug"] = listing.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -3604,6 +3633,12 @@ func (r *mutationResolver) DeleteListingLink(ctx context.Context, id int) (*mode return nil, nil } + listing := &models.Listing{ID: listingLink.ID} + err = listing.Load(ctx) + if err != nil { + return nil, err + } + deletedID := strconv.Itoa(listingLink.ID) err = listingLink.Delete(ctx) if err != nil { @@ -3613,14 +3648,16 @@ func (r *mutationResolver) DeleteListingLink(ctx context.Context, id int) (*mode c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["list_id"] = listingLink.ListingID + mdata["list_slug"] = listing.Slug mdata["list_link_id"] = listingLink.ID err = models.RecordAuditLog( ctx, int(user.ID), c.RealIP(), models.LOG_LIST_LINK_DELETED, - fmt.Sprintf("Updated listing entry '%s' (%d)", listingLink.Title, listingLink.ID), + fmt.Sprintf("Deleted listing entry '%s' (%d)", listingLink.Title, listingLink.ID), mdata, ) if err != nil { @@ -3905,6 +3942,7 @@ func (r *mutationResolver) AddQRCode(ctx context.Context, input model.AddQRCodeI mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["qrcode_id"] = qr.ID mdata["qrcode_type"] = qr.CodeType err = models.RecordAuditLog( @@ -3975,6 +4013,7 @@ func (r *mutationResolver) DeleteQRCode(ctx context.Context, id int) (*model.Del c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug mdata["qrcode_id"] = qrCode.ID mdata["qrcode_type"] = qrCode.CodeType err = models.RecordAuditLog( @@ -4048,6 +4087,7 @@ func (r *mutationResolver) Follow(ctx context.Context, orgSlug string) (*model.F c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -4125,6 +4165,7 @@ func (r *mutationResolver) Unfollow(ctx context.Context, orgSlug string) (*model c := server.EchoForContext(ctx) mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err = models.RecordAuditLog( ctx, int(user.ID), @@ -6652,10 +6693,13 @@ func (r *queryResolver) GetAuditLogs(ctx context.Context, input *model.AuditLogI return nil, nil } - var org *models.Organization - var err error + var ( + org *models.Organization + err error + ) opts := &database.FilterOptions{ - Filter: sq.And{}, + Filter: sq.And{}, + OrderBy: "al.id DESC", } if input.OrgSlug != nil && *input.OrgSlug != "" { @@ -6781,8 +6825,9 @@ func (r *queryResolver) GetAuditLogs(ctx context.Context, input *model.AuditLogI } else if input.Before != nil { opts.Filter = sq.And{ opts.Filter, - sq.Expr("ol.id >= ?", input.Before.Before), + sq.Expr("al.id >= ?", input.Before.Before), } + opts.OrderBy = "al.id ASC" numElements = input.Before.Limit } diff --git a/cmd/migrations.go b/cmd/migrations.go index 05c7bc5..43f3de1 100644 --- a/cmd/migrations.go +++ b/cmd/migrations.go @@ -59,5 +59,12 @@ func GetMigrations() []migrate.Migration { 0, links.MigrateFS, ), + migrate.FSFileMigration( + "0006_update_auditlog_metadata", + "migrations/0006_update_auditlog_metadata.up.sql", + "migrations/0006_update_auditlog_metadata.down.sql", + 0, + links.MigrateFS, + ), } } diff --git a/core/import.go b/core/import.go index 27c844d..b1a1129 100644 --- a/core/import.go +++ b/core/import.go @@ -478,6 +478,7 @@ func ImportFromPinBoard(ctx context.Context, path string, if totalCount > 0 { mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err := models.RecordAuditLog( ctx, int(user.ID), @@ -625,6 +626,7 @@ func ImportFromHTML(ctx context.Context, path string, if listlen > 0 { mdata := make(map[string]any) mdata["org_id"] = org.ID + mdata["org_slug"] = org.Slug err := models.RecordAuditLog( ctx, int(user.ID), diff --git a/core/routes.go b/core/routes.go index 3afa8f6..661c1d9 100644 --- a/core/routes.go +++ b/core/routes.go @@ -83,6 +83,7 @@ func (s *Service) RegisterRoutes() { s.Group.GET("/:slug/import", s.ImportData).Name = s.RouteName("import_data") s.Group.POST("/:slug/import", s.ImportData).Name = s.RouteName("import_data") s.Group.GET("/:slug/integrations", s.Integrations).Name = s.RouteName("integrations") + s.Group.GET("/:slug/log", s.OrgLog).Name = s.RouteName("org_log") s.Group.GET("/home", s.OrgLinksList).Name = s.RouteName("home_link_list") s.Group.GET("/add", s.OrgLinksCreate).Name = s.RouteName("link_create") @@ -368,6 +369,28 @@ func (s *Service) FeatureTour(c echo.Context) error { return s.Render(c, http.StatusOK, "feature_tour.html", gmap) } +func (s *Service) OrgLog(c echo.Context) error { + gctx := c.(*server.Context) + user := gctx.User.(*models.User) + slug := c.Param("slug") + userID := int(user.ID) + + org, err := user.GetOrgsSlug(c.Request().Context(), models.OrgUserPermissionAdminWrite, slug) + if err != nil { + return err + } + if org != nil { + userID = 0 // Admin privileges can view all audit logs for org + } + + gmap, err := links.FetchAuditLogs(c, userID, slug, 0, 0) + if err != nil { + return err + } + + return s.Render(c, http.StatusOK, "auditlog.html", gmap) +} + func (s *Service) DomainList(c echo.Context) error { gctx := c.(*server.Context) user := gctx.User.(*models.User) @@ -686,6 +709,7 @@ func (s *Service) OrgList(c echo.Context) error { pd.Data["import"] = lt.Translate("Import") pd.Data["integrations"] = lt.Translate("Integrations") pd.Data["payment_history"] = lt.Translate("Payment History") + pd.Data["logs"] = lt.Translate("View Audit Logs") // If we want to highlight an org based on a given domain var dOrgSlug string diff --git a/helpers.go b/helpers.go index fd41437..e1da322 100644 --- a/helpers.go +++ b/helpers.go @@ -38,6 +38,7 @@ import ( "golang.org/x/text/language" "golang.org/x/time/rate" "netlandish.com/x/gobwebs" + auditlog "netlandish.com/x/gobwebs-auditlog" "netlandish.com/x/gobwebs/config" "netlandish.com/x/gobwebs/core" "netlandish.com/x/gobwebs/crypto" @@ -1242,3 +1243,105 @@ func TagAbuseRedirect(c echo.Context) error { } return nil } + +// AuditLogResponse is a struct for auditlog gql query response storage +type AuditLogResponse struct { + AuditLogs struct { + Result []auditlog.AuditLog `json:"result"` + PageInfo struct { + Cursor string + HasNextPage bool + HasPrevPage bool + } `json:"pageInfo"` + } `json:"getAuditLogs"` +} + +// FetchAuditLogs is helper to run a query fetching audit logs depending on various +// conditions. +func FetchAuditLogs(c echo.Context, userID int, + orgSlug string, listID int, limit int) (gobwebs.Map, error) { + op := gqlclient.NewOperation( + `query GetAuditLogs($userId: Int, $slug: String, $listingId: Int, $after: Cursor, + $before: Cursor, $limit: Int) { + getAuditLogs(input: { + userId: $userId, + orgSlug: $slug, + listingId: $listingId, + after: $after, + before: $before, + limit: $limit, + }) { + result { + userId + ipAddress + eventType + details + metadata + createdOn + } + pageInfo { + cursor + hasPrevPage + hasNextPage + } + } + }`) + + if userID > 0 { + op.Var("userId", userID) + } + if orgSlug != "" { + op.Var("slug", orgSlug) + } + if listID > 0 { + op.Var("listingId", listID) + } + if limit > 0 { + op.Var("limit", limit) + } + if c.QueryParam("next") != "" { + op.Var("after", c.QueryParam("next")) + } else if c.QueryParam("prev") != "" { + op.Var("before", c.QueryParam("prev")) + } + + var result AuditLogResponse + err := Execute(LangContext(c), op, &result) + if err != nil { + return nil, err + } + + gctx := c.(*server.Context) + user := gctx.User.(*models.User) + if c.QueryParam("prev") != "" { + slices.Reverse(result.AuditLogs.Result) + } + + lt := localizer.GetSessionLocalizer(c) + pd := localizer.NewPageData(lt.Translate("Audit Log")) + pd.Data["ip_address"] = lt.Translate("IP Address") + pd.Data["org"] = lt.Translate("Organization") + pd.Data["listing"] = lt.Translate("Link Listing") + pd.Data["event_type"] = lt.Translate("Action") + pd.Data["details"] = lt.Translate("Details") + pd.Data["timestamp"] = lt.Translate("Timestamp") + pd.Data["no_logs"] = lt.Translate("No audit logs to display") + pd.Data["next"] = lt.Translate("Next") + pd.Data["prev"] = lt.Translate("Prev") + gmap := gobwebs.Map{ + "pd": pd, + "user": user, + "logs": result.AuditLogs.Result, + "orgSlug": orgSlug, + "listId": listID, + } + if result.AuditLogs.PageInfo.HasPrevPage { + gmap["prevURL"] = GetPaginationParams("prev", "", "", result.AuditLogs.PageInfo.Cursor) + } + + if result.AuditLogs.PageInfo.HasNextPage { + gmap["nextURL"] = GetPaginationParams("next", "", "", result.AuditLogs.PageInfo.Cursor) + } + + return gmap, nil +} diff --git a/list/routes.go b/list/routes.go index 6af5cc6..09af3f9 100644 --- a/list/routes.go +++ b/list/routes.go @@ -39,6 +39,7 @@ func (s *Service) RegisterRoutes() { s.Group.GET("/:id/delete", s.ListingDelete).Name = s.RouteName("listing_delete") s.Group.POST("/:id/delete", s.ListingDelete).Name = s.RouteName("listing_delete") s.Group.GET("/:id/links", s.ListingLinksManage).Name = s.RouteName("listing_links") + s.Group.GET("/:id/log", s.ListingLog).Name = s.RouteName("listing_log") s.Group.GET("/:id/links/add", s.ListingLinksCreate).Name = s.RouteName("listing_link_create") s.Group.POST("/:id/links/add", s.ListingLinksCreate).Name = s.RouteName("listing_link_create_post") s.Group.GET("/:id/links/:lid/edit", s.ListingLinksUpdate).Name = s.RouteName("listing_link_update") @@ -51,6 +52,33 @@ func (s *Service) RegisterRoutes() { s.Group.GET("", s.ListingList).Name = s.RouteName("listing_list") } +func (s *Service) ListingLog(c echo.Context) error { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + return echo.NotFoundHandler(c) + } + slug := c.Param("slug") + + gctx := c.(*server.Context) + user := gctx.User.(*models.User) + userID := int(user.ID) + + org, err := user.GetOrgsSlug(c.Request().Context(), models.OrgUserPermissionAdminWrite, slug) + if err != nil { + return err + } + if org != nil { + userID = 0 // Admin privileges can view all audit logs for org + } + + gmap, err := links.FetchAuditLogs(c, userID, slug, id, 0) + if err != nil { + return err + } + + return s.Render(c, http.StatusOK, "auditlog.html", gmap) +} + func (s *Service) ListingLinksUpdate(c echo.Context) error { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -1009,6 +1037,7 @@ func (s *Service) ListingList(c echo.Context) error { pd.Data["apply"] = lt.Translate("Apply") pd.Data["clear"] = lt.Translate("Clear") pd.Data["is_default"] = lt.Translate("Is Default") + pd.Data["logs"] = lt.Translate("View Audit Logs") type GraphQLResponse struct { Listings struct { diff --git a/migrations/0006_update_auditlog_metadata.down.sql b/migrations/0006_update_auditlog_metadata.down.sql new file mode 100644 index 0000000..281a6d2 --- /dev/null +++ b/migrations/0006_update_auditlog_metadata.down.sql @@ -0,0 +1,2 @@ +UPDATE audit_log SET metadata = metadata - 'org_slug' WHERE metadata ? 'org_slug'; +UPDATE audit_log SET metadata = metadata - 'list_slug' WHERE metadata ? 'list_slug'; diff --git a/migrations/0006_update_auditlog_metadata.up.sql b/migrations/0006_update_auditlog_metadata.up.sql new file mode 100644 index 0000000..66173cc --- /dev/null +++ b/migrations/0006_update_auditlog_metadata.up.sql @@ -0,0 +1,10 @@ +UPDATE audit_log a +SET metadata = jsonb_set(metadata, '{org_slug}', to_jsonb(o.slug)) +FROM organizations o +WHERE (metadata->>'org_id')::INTEGER = o.id; + +UPDATE audit_log a +SET metadata = jsonb_set(a.metadata, '{list_slug}', to_jsonb(l.slug)) +FROM listings l +WHERE (a.metadata->>'list_id')::INTEGER = l.id; + diff --git a/templates/auditlog.html b/templates/auditlog.html new file mode 100644 index 0000000..62220bc --- /dev/null +++ b/templates/auditlog.html @@ -0,0 +1,47 @@ +{{template "base" .}} +{{ define "title" }}{{ if gt .listId 0 }}{{ .pd.Data.listing }} {{ else if ne .orgSlug "" }}{{ .pd.Data.org }} {{ .orgSlug }} {{ end }}{{ .pd.Title }}{{ end }} +<section class="app-header"> + <h1 class="app-header__title">{{ if gt .listId 0 }}{{ .pd.Data.listing }} {{ else if ne .orgSlug "" }}{{ .pd.Data.org }} {{ .orgSlug }} {{ end }}{{ .pd.Title }}</h1> +</section> + +<section class="card shadow-card"> + <table class="striped mt-1"> + <thead> + <tr> + <th class="text-center">{{.pd.Data.timestamp}}</th> + <th class="text-center">{{.pd.Data.org}}</th> + <th class="text-center">{{.pd.Data.event_type}}</th> + <th class="text-center">{{.pd.Data.ip_address}}</th> + <th class="text-center">{{.pd.Data.details}}</th> + </tr> + </thead> + <tbody> + {{ if .logs }} + {{range .logs}} + <tr> + <td class="text-center">{{ formatDate .CreatedOn }}</td> + <td class="text-center">{{ .Metadata.org_slug }}</td> + <td class="text-center">{{ .EventType }}</td> + <td class="text-center">{{ .IPAddress }}</td> + <td class="text-center">{{ .Details }}</td> + </tr> + {{end}} + {{else}} + <tr> + <td colspan="6"><p class="text-center">{{.pd.Data.no_logs}}</p></td> + </tr> + {{end}} + </tbody> + </table> + {{if or .prevURL .nextURL}} + <footer class="is-right"> + {{if .prevURL}} + <a href="?{{.prevURL}}" class="button secondary">{{.pd.Data.prev}}</a> + {{end}} + {{if .nextURL}} + <a href="?{{.nextURL}}" class="button secondary">{{.pd.Data.next}}</a> + {{end}} + </footer> + {{end}} +</section> +{{template "base_footer" .}} diff --git a/templates/listing_list.html b/templates/listing_list.html index da23de4..838e598 100644 --- a/templates/listing_list.html +++ b/templates/listing_list.html @@ -86,6 +86,7 @@ <p><a class="mr-1" href="{{reverse "list:listing_update" $.org.Slug .ID}}">{{$.pd.Data.edit}}</a></p> <p><a class="mr-1" href="{{reverse "list:listing_delete" $.org.Slug .ID}}">{{$.pd.Data.delete}}</a></p> <p><a class="mr-1" href="{{reverse "analytics:detail" $.org.Slug "lists" .ID}}">{{$.pd.Data.analytics}}</a></p> + <p><a class="mr-1" href="{{reverse "list:listing_log" $.org.Slug .ID}}">{{$.pd.Data.logs}}</a></p> {{end}} </div> </details> diff --git a/templates/org_list.html b/templates/org_list.html index df27a2e..227d14f 100644 --- a/templates/org_list.html +++ b/templates/org_list.html @@ -58,6 +58,7 @@ <p><a class="mr-1" href="{{reverse "core:domain_list" .Slug}}">{{$.pd.Data.domains}}</a></p> <p><a class="mr-1" href="{{reverse "core:integrations" .Slug}}">{{$.pd.Data.integrations}}</a></p> <p><a class="mr-1" href="{{reverse "core:org_member_list" .Slug}}">{{$.pd.Data.members}}</a></p> + <p><a class="mr-1" href="{{reverse "core:org_log" .Slug}}">{{$.pd.Data.logs}}</a></p> {{end}} </div> </details> diff --git a/templates/settings.html b/templates/settings.html index b7e3598..43b0cb5 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -16,6 +16,7 @@ <p><a class="mr-1" href="{{reverse "accounts:profile_edit"}}">{{.pd.Data.edit_profile}}</a></p> <p><a class="mr-1" href="{{reverse "accounts:update_email"}}">{{.pd.Data.update_email}}</a></p> <p><a class="mr-1" href="{{reverse "accounts:change_password"}}">{{.pd.Data.change_password}}</a></p> + <p><a class="mr-1" href="{{reverse "accounts:settings_log"}}">{{.pd.Data.logs}}</a></p> </div> </details> </td> -- 2.47.2
Applied. To git@git.code.netlandish.com:~netlandish/links 9ac75f7..47a5324 master -> master