feat(admin): what has been done on this server, across every project
The other half of the error log. That one is what broke; this is what was done, and nobody could ask it: `ListActivity` takes one project id, and an administrator is not necessarily a member of the project they need to look into. Paged with a keyset cursor rather than an offset. The log is written to constantly and read rarely, so a page fetched a minute after the one before it would be shifted by everything written in between — one row shown twice and the row behind it skipped, which is the single failure an audit log must not have. Both halves of the cursor matter: the log is written in whole seconds, so a timestamp alone cannot separate two rows written in the same one. Migration 0009 makes `activity.user_id` SET NULL for the same reason 0008 did it to `created_by`. A log whose rows vanish with the account they describe answers "what has happened here" with everything except the part somebody had a reason to remove. `project_id` keeps cascading on purpose: the trash is the undo, and past it the deletion was deliberate. The event names moved to `web/src/lib/events.js`, because two surfaces read the same log now and two copies of nineteen strings is two places for them to drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ebdc8eacf2
commit
85f7b2488d
@@ -144,6 +144,7 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
r.Use(s.requireAuth)
|
||||
r.Get("/me", s.handleMe)
|
||||
r.Patch("/me", s.handleUpdateProfile)
|
||||
r.Put("/sidebar-sections", s.handleSetSidebarSections)
|
||||
r.Post("/logout", s.handleLogout)
|
||||
r.Post("/password/change", s.handleChangePassword)
|
||||
r.Post("/totp/setup", s.handleTOTPSetup)
|
||||
@@ -236,6 +237,17 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
r.Delete("/", s.handleDisconnectGmail)
|
||||
r.Post("/authorize", s.handleGmailAuthorize)
|
||||
r.Post("/sync", s.handleGmailSyncNow)
|
||||
|
||||
// The OAuth client itself is the instance's registration with
|
||||
// Google, not anybody's mailbox — so unlike everything above it, it
|
||||
// is administrators only, and behind a session as well: a leaked
|
||||
// token must not be able to read or replace this instance's
|
||||
// identity to Google.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireSession, s.requireAdmin)
|
||||
r.Get("/client", s.handleGetGmailClient)
|
||||
r.Put("/client", s.handleSetGmailClient)
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/import", func(r chi.Router) {
|
||||
@@ -306,6 +318,17 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireSession, s.requireAdmin)
|
||||
r.Get("/errors", s.handleListErrors)
|
||||
// A backup file is a complete copy of the database, so this whole
|
||||
// group is sessions-only as well as administrators-only: a leaked
|
||||
// token must not be able to download everybody's data.
|
||||
r.Get("/backups", s.handleListBackups)
|
||||
r.Post("/backups", s.handleRunBackup)
|
||||
r.Get("/backups/{backupID}", s.handleDownloadBackup)
|
||||
// The instance-wide history. Above the per-project one in every
|
||||
// sense: it crosses projects the caller is not a member of, which
|
||||
// is the whole reason it is here and not there.
|
||||
r.Get("/activity", s.handleAuditLog)
|
||||
r.Get("/activity/events", s.handleAuditEvents)
|
||||
r.Get("/users", s.handleListUsers)
|
||||
r.Post("/users", s.handleCreateUser)
|
||||
r.Patch("/users/{userID}", s.handleUpdateUser)
|
||||
|
||||
Reference in New Issue
Block a user