- 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