From ca656d6bffc05431f65560abb25ec86bdec94e86 Mon Sep 17 00:00:00 2001 From: Jonathon Leight Date: Wed, 8 Jul 2026 00:15:58 -0400 Subject: [PATCH] Show display name in audit log --- internal/store/command_log.go | 13 ++++-- internal/store/command_log_paging_test.go | 52 +++++++++++++++++++++++ internal/store/console_sessions.go | 3 +- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/internal/store/command_log.go b/internal/store/command_log.go index c982f67..a28ce67 100644 --- a/internal/store/command_log.go +++ b/internal/store/command_log.go @@ -56,8 +56,11 @@ func (s *Store) MarkCommandReply(ctx context.Context, logID int64, response stri // CommandSession groups command-log entries by the console session they were // sent in. type CommandSession struct { - ID int64 - SenderName string // who held the session + ID int64 + // SenderName is who held the session: their current display name (or username), + // falling back to the username snapshotted at session start if they've since + // been deleted. + SenderName string StartedAt time.Time EndedAt *time.Time Entries []*CommandLogEntry // newest first, matching the newest-first session order @@ -95,8 +98,9 @@ func (s *Store) ListCommandLogSessionsPage(ctx context.Context, repeaterID int64 // behavior of the previous query). headers, err := s.pool.Query(ctx, ` SELECT cs.id, cs.started_at, cs.ended_at, - COALESCE(cs.sender_username, '(deleted)') + COALESCE(NULLIF(u.display_name, ''), u.username, cs.sender_username, '(deleted)') FROM console_sessions cs + LEFT JOIN users u ON u.id = cs.user_id WHERE cs.repeater_id = $1 AND ($2 OR (cs.started_at, cs.id) < ($3, $4)) AND EXISTS (SELECT 1 FROM command_log l WHERE l.session_id = cs.id) @@ -172,10 +176,11 @@ type OwnerCommandLogEntry struct { func (s *Store) ListRecentCommandsForOwner(ctx context.Context, ownerID int64, limit int) ([]OwnerCommandLogEntry, error) { rows, err := s.pool.Query(ctx, ` SELECT l.repeater_id, r.public_id, r.name, - COALESCE(l.sender_username, '(deleted)'), + COALESCE(NULLIF(u.display_name, ''), u.username, l.sender_username, '(deleted)'), l.command_text, l.sent_at, l.ack_received, l.response_text FROM command_log l JOIN repeaters r ON r.id = l.repeater_id + LEFT JOIN users u ON u.id = l.user_id WHERE r.owner_id = $1 ORDER BY l.sent_at DESC LIMIT $2`, ownerID, limit) diff --git a/internal/store/command_log_paging_test.go b/internal/store/command_log_paging_test.go index f3bebe1..d57632e 100644 --- a/internal/store/command_log_paging_test.go +++ b/internal/store/command_log_paging_test.go @@ -6,6 +6,58 @@ import ( "testing" ) +// TestCommandLogSenderName: the log shows the sender's live display name while +// they exist, and falls back to the username snapshot once they're deleted. +func TestCommandLogSenderName(t *testing.T) { + t.Parallel() + st, ctx := orgTestStore(t) + + owner, err := st.CreateUser(ctx, "logowner2", "") + if err != nil { + t.Fatal(err) + } + rep, err := st.CreateRepeater(ctx, &Repeater{ + OwnerID: owner.ID, Name: "R", PublicKeyHex: strings.Repeat("d", 64), + RadioFreqHz: 1, RadioBwHz: 1, RadioSF: 11, RadioCR: 5, + }) + if err != nil { + t.Fatal(err) + } + // A separate sender (not the owner, so deleting them doesn't cascade the repeater). + sender, err := st.CreateUser(ctx, "bobby", "Bob Builder") + if err != nil { + t.Fatal(err) + } + sid, err := st.StartConsoleSession(ctx, rep.ID, sender.ID) + if err != nil { + t.Fatal(err) + } + if _, err := st.LogCommand(ctx, rep.ID, sender.ID, sid, 0, "advert"); err != nil { + t.Fatal(err) + } + + senderName := func() string { + page, _, err := st.ListCommandLogSessionsPage(ctx, rep.ID, nil) + if err != nil || len(page) != 1 { + t.Fatalf("page = %d sessions, %v; want 1", len(page), err) + } + return page[0].SenderName + } + + // While the sender exists: their display name. + if got := senderName(); got != "Bob Builder" { + t.Fatalf("sender name = %q, want the live display name %q", got, "Bob Builder") + } + + // After deletion (user_id → NULL): the username snapshot taken at session start. + if _, err := st.pool.Exec(ctx, `DELETE FROM users WHERE id=$1`, sender.ID); err != nil { + t.Fatalf("delete sender: %v", err) + } + if got := senderName(); got != "bobby" { + t.Fatalf("deleted sender name = %q, want the username snapshot %q", got, "bobby") + } +} + // TestListCommandLogSessionsPage walks the keyset-paginated session log and // checks every session with commands appears once, newest-first, across pages — // and that a session with no commands is omitted. diff --git a/internal/store/console_sessions.go b/internal/store/console_sessions.go index cccf705..7f074a4 100644 --- a/internal/store/console_sessions.go +++ b/internal/store/console_sessions.go @@ -8,7 +8,8 @@ import ( // StartConsoleSession records the start of a console session and returns its id. func (s *Store) StartConsoleSession(ctx context.Context, repeaterID, userID int64) (int64, error) { var id int64 - // Snapshot the username so the log stays a point-in-time record after renames. + // Snapshot the username as a fallback so the log still identifies the sender if + // they're later deleted; while they exist the log shows their live display name. err := s.pool.QueryRow(ctx, `INSERT INTO console_sessions (repeater_id, user_id, sender_username) VALUES ($1, $2, (SELECT username FROM users WHERE id = $2)) RETURNING id`,