Skip to content

feat(console): SOC2 audit log + account activity alerts#1288

Merged
absorbb merged 22 commits into
newjitsufrom
feat/soc2-audit-log-account-alerts
May 8, 2026
Merged

feat(console): SOC2 audit log + account activity alerts#1288
absorbb merged 22 commits into
newjitsufrom
feat/soc2-audit-log-account-alerts

Conversation

@vklimontovich
Copy link
Copy Markdown
Contributor

Summary

Implements the SOC2 audit log + Account Activity Alerts task needed to satisfy two failing Vanta tests:

  • customer-facing-logs — customers can now view a workspace-scoped activity log under Settings > Audit Log (owner-only).
  • customer-account-activity-alerts — customers can subscribe to security events via the existing notification channels (new account event type).

What it does

  • Extends AuditLog with a severity column (info / warning / security) and a composite (workspaceId, timestamp) index.
  • New helpers authAuditLog() and membershipAuditLog() in lib/server/audit-log.ts, gated on the existing CONSOLE_ENABLE_AUDIT_LOG env var.
  • Auth touchpoints: NextAuth jwt callback (login) + new events.signOut; Firebase create-session and revoke-session; OIDC callback (login) and logout.
  • Membership touchpoints: invite (POST users), remove (DELETE users), accept (/api/user/accept), role change (users/[userId]/role).
  • New read API at GET /api/[workspaceId]/audit-log with cursor pagination + filters (type, severity, from, to). Owner-only via verifyAccessWithRole(user, ws, "manageUsers").
  • New settings page /[workspaceId]/settings/audit-log with filterable, expandable Antd table.
  • New "account" value in NotificationChannel.events + label entry.
  • New immediate email dispatcher in lib/server/account-alerts.ts (fire-and-forget after the DB write — no cron round-trip). Fires on all security severity events.
  • New React-Email template emails/account-alert.tsx for the alert mail.

Severity / dispatch matrix

Event Severity Triggers email?
auth-login, auth-logout info no
member-{invited,joined,removed,role-changed} security yes
workspace-deleted security yes
config-object-* info no

Test plan

  • CONSOLE_ENABLE_AUDIT_LOG=true; run pnpm console:dev.
  • Apply schema with pnpm db:update-schema (adds severity + composite index).
  • Log out and back in via Firebase → row in AuditLog with type=auth-login, authType=firebase. Logout → auth-logout.
  • Log in via NextAuth (GitHub) → authType=nextauth-github.
  • Log in via OIDC → authType=oidc.
  • Invite a user → member-invited. Accept the invite → member-joined. Change their role → member-role-changed. Remove them → member-removed. Verify all four severity=security rows.
  • Open Settings > Audit Log as an owner → events visible, paginated, filterable.
  • Open as an editor/analyst → access-denied panel.
  • Create an email notification channel with the account event type, trigger a member-invite from another browser → email delivered.
  • Vanta evidence to attach: screenshots of the audit log, the notification subscriber dropdown, and a delivered alert email.

Notes

  • prisma/schema/ is gitignored (auto-generated by zod-prisma); prisma generate regenerates it.
  • This project uses prisma db push, not migrations — no migration SQL file checked in.
  • Tests not added: the webapps/console package has no existing Jest infrastructure. Verification is manual + typecheck.

🤖 Generated with Claude Code

- Add Audit Log settings page (owner-only) with filter + cursor pagination
- Log auth events (NextAuth login/logout, Firebase login/logout, OIDC login/logout)
- Log membership events (invite, accept, role-change, remove)
- New "account" event type in notification channels
- Immediate email dispatcher for security-severity events (no cron)
- Extend AuditLog with severity column + (workspaceId, timestamp) index
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two correctness issues that will cause misleading or missing audit information in production.

Comment thread webapps/console/lib/server/audit-log.ts
Comment thread webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts Outdated
Security:
- Stop persisting raw config object versions (prevVersion/newVersion) to
  AuditLog.changes — these can contain API keys, passwords and OAuth tokens.
  Only objectType + objectName are stored now.
- API allow-lists safe fields in the response, so any pre-existing rows with
  raw config payloads are also redacted on read.

UX:
- Audit log table is full-width
- Time column shows relative time (with absolute UTC tooltip)
- Drop the auth-method chip from the actor cell
- Drop the expandable raw-changes JSON row
- Render config-object events as 'Updated <type> <name>' with a link to the
  entity edit page (no link for delete)
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness issues that should be fixed before merge.

  1. Auth login/logout events are saved without workspaceId, so they cannot appear in the new workspace-scoped audit log view.
  2. Canceling a pending invitation currently emits member-removed audit events (and account alerts), which creates false security notifications for actions that did not remove a member.

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting previously pending review shell.

Reuse the per-type outputFilter (the same one used by the config editor) to
mask sensitive fields, so prevVersion / newVersion can stay in the audit log:

- destinations: secrets at credentialsUi paths replaced with MASKED_SECRET
- services: airbyte_secret-marked fields masked
- streams: privateKey/publicKey plaintext + hash stripped
- types without a registered outputFilter (link, profilebuilder, function code):
  fall back to a generic name-based scrubber that masks fields whose key matches
  password / secret / token / apiKey / etc.

Rows written through this helper are tagged `_redacted: true`. The read API only
exposes prevVersion / newVersion when that flag is present, so any rows already
in the DB from before this fix continue to be redacted on read (objectType +
objectName only).

UI: expandable row shows the masked Before / After diff side-by-side.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found three correctness issues that will cause missing or misleading audit signals: (1) auth events are written without workspace scope and won’t appear in workspace audit log, (2) workspace audit helper is never wired to workspace update/delete paths, so workspace events/alerts are never emitted, and (3) member removal logs are emitted even for no-op deletes. I left inline comments with concrete fixes.

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting pending review shell (no additional comments).

@jitsu-code-review
Copy link
Copy Markdown

Review findings (could not attach inline due GitHub API thread errors in this environment):

  1. Auth events are not workspace-scoped

    • authAuditLog writes workspaceId: workspaceId ?? null in webapps/console/lib/server/audit-log.ts.
    • Current call sites pass only 3 args (e.g. webapps/console/lib/nextauth.config.ts, webapps/console/pages/api/fb-auth/create-session.ts, webapps/console/pages/api/auth/dynamic-oidc/callback.ts), so rows are saved with workspaceId = null.
    • GET /api/[workspaceId]/audit-log filters by where: { workspaceId }, so auth-login/auth-logout rows never appear in the workspace audit log.
  2. Workspace events are never emitted

    • workspaceAuditLog is defined in webapps/console/lib/server/audit-log.ts but not called from workspace mutation endpoints.
    • webapps/console/pages/api/workspace/[workspaceIdOrSlug]/index.ts (PUT) and webapps/console/pages/api/workspace/index.ts (DELETE) perform updates/deletes without calling this helper.
    • Result: workspace-updated/workspace-deleted log rows (and workspace-deleted alert dispatch) never happen.
  3. member-removed can be logged on no-op deletes

    • webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts calls membershipAuditLog(..., "removed", ...) unconditionally after optional delete branches.
    • If no target is provided or deleteMany affects 0 rows, it still emits a security audit event and may send misleading account-alert emails.
    • Recommend requiring exactly one of email/userId and logging only when a deletion actually occurred.

audit-log.ts imported config-objects.ts at module load, which transitively
pulls pages/api/[workspaceId]/domain-check.ts -> lib/api.ts -> nextauth.config
-> back to audit-log.ts. The cycle surfaced as 'Cannot access httpMethods
before initialization' on the first /api request.

Lazy-require config-objects on first call, and inline the MASKED_SECRET
constant so we don't need to pull lib/schema/destinations at load time.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness regressions that should be fixed before merge:\n\n1. events are written without , so they never appear in the new workspace-scoped audit log API/UI.\n2. The member-remove endpoint now emits audit/security alerts even when nothing was actually removed.\n\nPlease address these and I can re-review quickly.

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarification of requested fixes: (1) auth login/logout rows are currently inserted without workspace scope, so they do not appear in the workspace audit-log API/UI, and (2) member-removal audit/alerts are emitted even for no-op delete requests. Inline comments include exact locations and suggested guard conditions.

- API computes a flat diff [{ field, description }] from the masked
  prevVersion/newVersion server-side and returns it as `diff`. The raw
  prev/next blobs are no longer sent to the client. Masked secrets are
  surfaced as 'changed (secret value)' so the sentinel doesn't leak.
- API enriches connection (link) entries with a synthesized
  'from.name → to.name' display name (links have no name field of their own).
- UI Time column shows absolute UTC + relative ('ago') stacked.
- Entity name is the visible label; the underlying ID lives in a tooltip.
- Expandable row renders the diff as a small Field / Change table.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a few correctness issues that should be fixed before merge:

  1. webapps/console/pages/api/[workspaceId]/audit-log.ts:61
    flattenDiff returns early on shallowEqual(prev, next), so when both sides are MASKED_SECRET it returns no diff entry. This hides secret rotations instead of showing changed (secret value). Handle masked-secret cases before the early return.

  2. webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts:153
    DELETE logs member-removed unconditionally even when both email and userId are omitted (both params are optional). In that case no deletion happens, but a security audit event and account alert are emitted. Validate that one target is provided and only log when a row is actually removed.

  3. webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/[userId]/role.ts:61
    No-op role updates (prevRole === body.role) still emit member-role-changed and send account alerts, creating false-positive security notifications. Short-circuit or skip audit/alerts for no-op updates.

- API derives objectName from prevVersion/newVersion server-side when the
  stored 'objectName' is empty. Pre-fix rows still showed entity IDs because
  they don't have the dedicated field — this fallback fixes that without
  exposing the raw versions to the client.
- Expand toggle uses an explicit +/- icon (lucide Plus/Minus) so the
  expandable rows are obvious. Rows without a diff render a fixed-width
  spacer so columns stay aligned.
- Drop the custom +/- expand icon. Use Antd's default chevron — it's the
  same widget shown elsewhere in the app and avoids a fragile manual layout.
- Compute the diff for non-redacted rows too. Re-run a name-based scrubber
  on the way out so credentials in legacy rows never reach the client.
  Without this, only post-fix rows showed an expand handle.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness issues that can affect audit-log accuracy and visibility; adding inline comments with details.

@jitsu-code-review
Copy link
Copy Markdown

Review findings:

  1. webapps/console/lib/server/audit-log.ts:143 writes auth events with workspaceId: workspaceId ?? null. All new authAuditLog(...) call sites omit workspaceId, while the new read API scopes strictly by workspace (webapps/console/pages/api/[workspaceId]/audit-log.ts:162). Result: auth-login/auth-logout rows are written but never visible in the workspace audit log UI despite being exposed in filters.

  2. webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts:153 logs member-removed unconditionally. If no rows are actually deleted (unknown email/userId, or empty request), this still writes a security audit event and can trigger account-alert emails for a non-event. Consider validating input and logging only when deletion count > 0.

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting pending review shell (no additional inline comments).

- API strips id, workspaceId, type, cloneId from prev/new before diffing.
  These are identity / metadata fields that the write path strips before
  saving (legacy audit-log payloads sometimes captured them inline, which
  surfaced as confusing 'id removed' rows).
- Replace the nested Antd Table in the expandable row with a plain
  two-column grid — same field/change information, no header bar, no row
  borders or stripes. Field name in monospace, description plain text.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review test

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness gaps that block the intended account-alert behavior:

  1. webapps/console/lib/server/account-alerts.ts:63
    dispatchAccountAlert only delivers to channel === "email" rows with explicit emails. In this PR, notification channels configured from the workspace Settings UI are still forced to channel: "slack" (webapps/console/pages/[workspaceId]/settings/notifications.tsx:146-148) while adding the new account event option there. Result: users can select “Account Activity” in Slack notifications, but no alert is actually delivered.

  2. webapps/console/lib/server/audit-log.ts:217
    workspaceAuditLog is introduced (including workspace-deleted alert dispatch), but it is not wired into workspace update/delete mutation handlers. As a result, workspace-updated / workspace-deleted audit events and workspace-deletion account alerts are never emitted.

Please wire these flows before merge so the new audit/alert UI doesn’t advertise behavior that currently never triggers.

Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: requesting changes for correctness issues found in this patch.

@jitsu-code-review
Copy link
Copy Markdown

Review findings:

  1. webapps/console/lib/server/audit-log.ts:143 + new auth callers (webapps/console/lib/nextauth.config.ts:158,180, webapps/console/pages/api/auth/dynamic-oidc/callback.ts:267, webapps/console/pages/api/fb-auth/create-session.ts:50, etc.) write auth-* rows with workspaceId = null. The read API (webapps/console/pages/api/[workspaceId]/audit-log.ts:203) filters strictly by workspace ID, so login/logout events never appear in the workspace audit log despite being exposed in the UI filter.

  2. workspaceAuditLog is added (webapps/console/lib/server/audit-log.ts:217) but not called from workspace update/delete handlers (webapps/console/pages/api/workspace/[workspaceIdOrSlug]/index.ts PUT and webapps/console/pages/api/workspace/index.ts DELETE). This means workspace-updated / workspace-deleted events and workspace-deleted account alerts are never emitted.

  3. webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts:153 logs member-removed unconditionally, even when no invitation/access row was actually deleted (including requests with neither email nor userId). This creates false-positive security audit entries/alerts; logging should be gated on actual deletion count.

- New components/AuditLogDiff/AuditLogDiff.tsx — self-contained, full-width,
  responsive (stacks field/change on narrow widths). Card with header
  ('Changes' + N fields summary), thin row separators, no per-cell borders.
- Expanded row indents the card to align with the Time column (past Antd's
  expand-chevron gutter), with a subtle neutral-50 backdrop.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a few correctness/security issues that should be fixed before merge (details inline). The main ones are: legacy audit rows can still leak sensitive values through diff, auth events are currently not visible in the workspace audit log because they are stored with workspaceId = null, and workspace audit events/alerts are defined but not wired to update/delete handlers.

Comment thread webapps/console/pages/api/[workspaceId]/audit-log.ts Outdated
Comment thread webapps/console/lib/server/audit-log.ts
Comment thread webapps/console/lib/server/audit-log.ts
Recurse into plain objects even when one side is undefined, so creating an
entity produces:

  data.mode          added: "batch"
  data.batchSize     added: 10000
  data.dataLayout    added: "segment-single-table"
  …

instead of one '(root) → added: {…}' blob. Same for deletes — every leaf is
listed as 'removed (was …)'. Arrays stay atomic, so 'foo.bar → added: []'.
- Add ssh_key, ssl_key, signing_key, encryption_key to the redaction patterns
  on both the write side (lib/server/audit-log.ts) and the read-side scrubber
  (pages/api/[workspaceId]/audit-log.ts). credentials.tunnel_method.ssh_key
  on Airbyte connectors is now masked even on legacy rows that didn't go
  through outputFilter at write time.
- Server: relax the per-value cap from 80 to 2000 chars so the client gets
  the full string to display.
- AuditLogDiff: middle-truncate long descriptions to ~100 chars and show the
  full text in an Antd Tooltip on hover. Truncated values get a 'help'
  cursor as a hover affordance.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this through. I found two correctness issues that should be fixed before merge:

  1. Auth login/logout events are written without workspaceId, but the new read API only queries by workspace. Those events will not be visible in the workspace audit log UI.
  2. Secret-field diff rendering has an unreachable branch, so updates where both sides are masked get dropped from the diff instead of surfacing as a secret change.

Comment thread webapps/console/lib/server/audit-log.ts
Comment thread webapps/console/pages/api/audit-log.ts
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for shipping this. I found a few blocking correctness issues that prevent key parts of the feature from working as described:

  1. Auth login/logout rows are written with workspaceId = null, so they never appear in the workspace-scoped audit log API/UI.
  2. Account-activity alerts currently filter to persisted notification config rows with channel === "email", but the product flow only creates Slack notification rows; this leaves no real subscribers for the new account alerts.
  3. workspace-deleted / workspace-updated types are exposed in the UI, but no route currently calls workspaceAuditLog(), so those events are never produced.

Please address these before merge so the SOC2-facing behavior is actually reachable end-to-end.

Comment thread webapps/console/lib/server/audit-log.ts
Comment thread webapps/console/lib/server/account-alerts.ts
Comment thread webapps/console/lib/server/audit-log.ts
- Revert the regex extension (ssh_key etc). Use the per-type outputFilter on
  the read side instead — same masking the editor UI does. Pulls
  config-objects via lazy require so the leaf API route doesn't reintroduce
  the api.ts cycle. credentials.tunnel_method.ssh_key on Airbyte sources is
  now masked via airbyte_secret on legacy rows.
- API returns structured diff entries: { field, kind, prev?, next? } where
  kind is added | removed | changed | secret-changed | noop.
- AuditLogDiff renders each kind with an icon (+, −, →, key, no-change).
  changed rows show prev → next, both middle-truncated with full-text tooltip.
- For 'config-object-update' rows whose prev and next end up byte-identical
  (the user clicked Save without editing — produces a real audit row but
  empty diff), show a single 'no field-level changes' noop entry so the row
  is still expandable and the situation is explicit.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found two correctness regressions that should be addressed before merge.

  1. Auth login/logout events are written without workspaceId, but the new audit-log API is workspace-scoped (where: { workspaceId }). As implemented, the UI exposes Login/Logout filters but those events never appear in workspace audit logs.
  2. The workspace users DELETE endpoint now always emits a member-removed audit entry (and account-alert side effects), including invitation cancellation flows and even requests with neither email nor userId, which can generate misleading security alerts.

Comment thread webapps/console/lib/nextauth.config.ts
Comment thread webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts Outdated
…oading

- Replace ALWAYS_SAFE_FIELDS allow-list with a small deny-list:
  prevVersion, newVersion (rendered server-side via 'diff' instead) and the
  internal _redacted marker. Everything else our audit-log helpers write is
  safe summary metadata; we shouldn't have to enumerate it here.
- Add isMasked() that recognizes both the local display sentinel
  ('*********') and the canonical sentinel ('__MASKED_BY_JITSU__') emitted
  by lib/schema/secrets#maskSecrets via outputFilter. Without this the
  secret-changed kind never fires for legacy rows masked through the
  per-type filter.
- UI: while a Load-more fetch is in flight, show a disabled loading
  button. The previous logic briefly fell through to 'End of log' because
  query.data is undefined during the in-flight request.
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the implementation. I found two correctness issues that block key parts of the feature set:

  1. auth-* events are persisted without workspaceId in all new call sites, while the new read API hard-filters by workspaceId; as a result, login/logout events won’t appear in the workspace audit log.
  2. workspaceAuditLog() is introduced but not wired into workspace update/delete flows, so workspace-updated / workspace-deleted rows (and the workspace-deleted account alert email path) are never emitted.

Please address these before merge.

Comment thread webapps/console/lib/server/audit-log.ts
Comment thread webapps/console/lib/server/audit-log.ts
The audit-log API never sent prev/new on the wire, but the previous code
expressed that as 'pass r.changes through, strip prev/new on the way out',
which inverted the intent. Switch to an explicit allow-list of summary
fields (objectType, objectName, actorEmail, …) — the raw config blobs
simply aren't enumerated, so they can't accidentally leak. The diff is the
only place the per-field changes show up.
@vklimontovich vklimontovich marked this pull request as draft May 6, 2026 21:54
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the substantial audit-log work. I found a few correctness issues that should be addressed before merge:

  1. member-removed audit events (and account-alert emails) can be emitted even when no membership/invitation was actually removed.
  2. Role updates emit member-role-changed even when the requested role equals the current role.
  3. Auth login/logout rows are written without workspaceId, but the new audit-log API is workspace-scoped, so those auth events are not retrievable from the new UI/API.

Comment thread webapps/console/pages/api/workspace/[workspaceIdOrSlug]/users/index.ts Outdated
Comment thread webapps/console/pages/api/[workspaceId]/audit-log.ts Outdated
- Move /api/[workspaceId]/audit-log -> /api/audit-log with workspaceId as
  an optional query param. With workspaceId: workspace-scoped, requires
  manageUsers in that workspace. Without: cross-workspace, requires admin
  (verifyAdmin). API also bulk-fetches workspace name+slug per row and
  returns it as 'workspace' so the admin view can render a workspace
  column.
- Extract the table into components/AuditLog/AuditLog.tsx. When workspaceId
  is undefined the component runs in admin mode: adds a Workspace column
  with name + link to /<slug-or-id>, and uses each row's per-workspace
  slug to build entity edit links.
- /admin/audit-log: new page that renders <AuditLog /> in admin mode.
- /[workspaceId]/settings/audit-log: now a thin wrapper around <AuditLog
  workspaceId={...} workspaceSlug={...} />.
- Add 'Admin Audit Log' entry next to 'Admin Workspaces' in the workspace
  selector menu (admin users only).
/api/fb-auth/create-session is called every time Firebase mints or
refreshes a session cookie — every short-lived ID-token rotation, every
reload after a long idle — not just on actual sign-in. That flooded the
audit log with dozens of 'Logged in' rows per user per day.

Suppress duplicate auth-login rows for the same (userId, authType)
inside a 30-minute window. Logouts still fire on every signout — they
are explicit user actions, not implicit refreshes.
The previous in-memory throttle didn't work across replicas. Switch to a
DB-backed check keyed on the auth event's actual timestamp:

- authAuditLog gains an opts.authTime parameter. When provided, it queries
  for any auth-login row for this user with timestamp >= authTime; if
  found, it skips the write. Firebase rotates ID tokens on a schedule but
  auth_time only changes on actual re-authentication, so refresh calls
  collapse onto the original row instead of creating duplicates.
- create-session decodes the ID token (already does, for the user
  identity), pulls auth_time, and passes it through.
- NextAuth and OIDC don't need this — their callbacks only fire on actual
  auth flows, not on token refreshes.
- Drop the in-memory map and the per-(userId, authType) 30-min throttle.
Drop the dedup logic entirely. Replace it with explicit instrumentation
on the two client entry points users actually hit:

- New endpoint /api/fb-auth/audit-login that takes an idToken in the body,
  verifies it via Firebase Admin, and writes the auth-login row.
- firebase-client signIn (email/password) and signInWith (popup OAuth)
  both call audit-login right after Firebase reports success.
- create-session no longer logs — it's the cookie-mint endpoint and runs
  on every ID-token rotation, which is what was flooding the audit log.
- authAuditLog reverts to the simple (user, op, authType, workspaceId?)
  signature; no opts, no auth_time, no DB lookup. The signOut path was
  already correct (revoke-session is only hit at sign-out), so logout
  needs no changes.

Net: one audit row per actual user-driven sign-in, zero per token
refresh / session re-establish.
- show auth events in workspace-scoped audit-log via member-id OR clause
- gate member-removed audit on confirmed deletion + reject empty target
- skip role-changed audit when role is unchanged
- wire workspace-deleted (DELETE) and workspace-updated (PUT) audit rows
- account-alerts: also fan out to workspace members with notifications.account on
- detect secret rotations at write time (_rotatedSecrets) and surface them as secret-changed on read
- prettier reformat across the 9 files CI flagged
@vklimontovich vklimontovich marked this pull request as ready for review May 6, 2026 22:54
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one correctness regression that should be fixed before merge.

workspace-deleted alerts currently skip member recipients because the recipient query filters out deleted workspaces. Since the workspace is marked deleted before this alert is dispatched, the members list comes back empty in the common case, and no email is sent unless an explicit notification channel exists.

Please adjust recipient selection for workspace-deleted events so workspace members can still receive this alert.

Comment thread webapps/console/lib/server/account-alerts.ts Outdated
…ed alert

The recipient query had `w.deleted = false`, which silently dropped all
member recipients exactly for workspace-deleted events (the workspace
is flagged deleted before the alert dispatches).
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the substantial audit-log work here. I found a few correctness issues in the new API path that should be fixed before merge.

Comment thread webapps/console/pages/api/audit-log.ts Outdated
Comment thread webapps/console/pages/api/audit-log.ts
Comment thread webapps/console/pages/api/audit-log.ts Outdated
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one blocking issue in the new audit-log API pagination logic.

The first workspace-scoped page is filtered correctly, but the cursor branch overwrites the existing workspace OR-clause. That causes subsequent pages to drop workspace scoping and potentially return audit rows from other workspaces. This is a data exposure bug, so this should be fixed before merge.

Comment thread webapps/console/pages/api/audit-log.ts Outdated
Copy link
Copy Markdown

@jitsu-code-review jitsu-code-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff in this PR (audit log API/UI, auth/membership/workspace event emitters, and account-alert delivery wiring).

I did not find actionable bugs, security issues, or correctness regressions in the submitted changes. The access controls on the new audit API routes are in place, secret redaction paths are handled defensively, and event emission is best-effort without breaking user flows.

@absorbb absorbb merged commit 33cb29c into newjitsu May 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants