Skip to content

Keep a person's last sign-in when their sessions go away - #466

Open
zopeVaibhav wants to merge 1 commit into
CopilotKit:mainfrom
zopeVaibhav:fix/record-last-sign-in
Open

Keep a person's last sign-in when their sessions go away#466
zopeVaibhav wants to merge 1 commit into
CopilotKit:mainfrom
zopeVaibhav:fix/record-last-sign-in

Conversation

@zopeVaibhav

Copy link
Copy Markdown
Contributor

Fixes #465

What this changes

A person's last sign-in is now stored on the person, instead of being recomputed from whichever
sessions happen to still exist. Sessions expire, get cleaned up, and are deleted outright when
somebody is removed, so the old value disappeared exactly when an administrator most wanted it.

  • users.last_signed_in_at (server/drizzle/0029_last_signed_in_at.sql), backfilled from the
    sessions that still exist at upgrade time (server/drizzle/0030_backfill_last_signed_in_at.sql).
  • stampSignIn writes it in the session-create after hook (server/src/auth/index.ts:46-66,
    called at :309). It is greatest(coalesce(existing, new), new), so the stamp can never move
    backwards, and the whole write is wrapped — a failure logs sign-in-stamp-write-failed and the
    sign-in still succeeds.
  • The People query reads the column and drops the join to sessions
    (server/src/people/store.ts:199), so the keyset comparison moves from a HAVING on the aggregate
    to a WHERE on the column (:170-181).
  • A removed person's row now carries their last sign-in too
    (app/src/routes/_authed/admin/people.tsx:57-58); it previously showed only "Access removed" and
    the provider, which was the case where the date was most missing.

What this cannot fix: anybody whose sessions were already gone before the migration runs stays
blank. The backfill can only read evidence that still exists.

Where it runs

  • New state that outlives a request? One column on an existing row, in Postgres.
  • What happens on the second replica? Every replica writes the same person's stamp on
    sign-in. greatest makes the write order-independent: two processes stamping the same person
    converge on the later time rather than the last writer's.
  • Anything serialised? Nothing needs to be. The update is a single statement whose new value
    is computed from the existing column inside the same statement, not read-then-written.
  • Anything fanned out to a browser? No. The People screen reads it on its existing query.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway. Nothing here acts; this is bookkeeping on a
    sign-in that has already been decided.
  • New refusals and new failures each write a row: a failed stamp writes no audit row on purpose
    — it is not a refusal, and it must not be able to stop somebody signing in. It is logged.
  • Nothing new is trusted from the client. The user id and the timestamp both come from the
    session the library just created.

No permission changes. The value was already on this administrator-only screen; it is now correct
there.

Changelog

  • Under Unreleased: "The People screen keeps a person's last sign-in when their sessions go
    away".

Proof

The bug, on origin/main 1c7bd92, through main's own API. A person with one session row, read
through PeopleStore.find, then removed through PeopleStore.revoke:

expect(after?.lastSignedInAt).toBe("2026-08-31T09:00:00.000Z")

Expected: "2026-08-31T09:00:00.000Z"
Received: null

Written against the surface main already has, so it fails on the behaviour rather than on a
missing export.

Live, before and after, against the same database. On this branch a person showing
"last signed in 31/08/2026" keeps that line after every one of their session rows is deleted. On
main, with exactly the same rows, the same person reads "never signed in" and drops to the bottom
of the list beside somebody who has genuinely never arrived — the API answers "lastSignedInAt": null
for them.

server/tests/people-last-sign-in.integration.test.ts adds six passing tests: the removal, the
sign-out, the ordering across a removal and a restore, the stamp landing on one person only, an
out-of-order stamp, and a stamp that cannot be written not refusing the sign-in.

Gates. Typecheck clean across app, server and worker; lint 605 files, format 601, no diff. Both
migrations applied to a live database, and drizzle-kit generate afterwards reports
No schema changes, nothing to migrate, so the snapshot and the schema agree.

Migration numbering. This work predates 0028_audit_initiator landing on main. The column and
the backfill were regenerated as 0029 and 0030 on top of it rather than renumbered by hand, and
the generated SQL is identical to what it replaced. main still tops out at 0028, so the numbering
is current as of this rebase.

Suite, rebased onto 1c7bd92 and run in one session against a same-session control, with the two
agent-handoff-*.integration files excluded because they fail most of their tests on bare main and
make any comparison meaningless:

pass skip fail tests files
origin/main @ 1c7bd92 2609 25 8 2642 218
this branch 2615 25 8 2648 219

The failing test names are byte-identical on both sides, as is the @langchain/core/messages
resolution error, so none of them belong to this change. The delta is exactly this PR's one new file
and its six tests. The paging file's test count is unchanged and covers the keyset move.

@zopeVaibhav
zopeVaibhav force-pushed the fix/record-last-sign-in branch from 18fa020 to 200cc04 Compare September 11, 2026 17:21
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.

The People screen forgets when somebody last signed in

1 participant