Skip to content

#645 Add A Last Sign-In Column To The Users Page - #777

Merged
cielbellerose merged 4 commits into
devfrom
645-add-a-last-sign-in-column-to-the-users-page
Sep 25, 2026
Merged

cielbellerose merged 4 commits into
devfrom
645-add-a-last-sign-in-column-to-the-users-page

Conversation

@cielbellerose

@cielbellerose cielbellerose commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #645

Summary

  • Adds a stored User.lastLoginAt DateTime? column, stamped from Better Auth's session-create after hook so a blocked (deactivated) sign-in never records a login.
  • Adds a "Last sign-in" column to /users, sorted with never-signed-in users last in both directions, plus the same info on the mobile card and a footnote explaining what the dash means.
  • No value is backfilled — everyone shows a dash until their next sign-in, per the ticket's explicit accept-the-gap decision.

Changes

  • prisma/schema.prisma + prisma/migrations/20260924232626_add_user_last_login_at/ — User.lastLoginAt DateTime?, ADD COLUMN only, no backfill.
  • lib/auth/session-hooks.ts (new) — assertSessionUserActive(userId) (the existing deactivated-account check, moved verbatim) and recordSignIn(userId), both plain functions so tests/db can exercise them directly.
  • lib/auth/config.ts — session.create.before calls assertSessionUserActive; new session.create.after calls recordSignIn. A before throw skips after, so a refused sign-in is never stamped.
  • prisma/data/users.ts / lib/types.ts — getUsersForAdmin selects lastLoginAt; AdminUserListItem includes it.
  • lib/data-table.ts — extracted the DataTable row comparator into a pure, exported sortRows(rows, column, direction) so the nulls-last rule is unit-testable; components/ui/data-table.tsx now calls it (behavior unchanged).
  • components/features/users-table.tsx — new "Last sign-in" column (LocalTime relative time, — + sr-only "No sign-in recorded" for null), the same line in the mobile card, and a permanent footnote explaining the dash.
  • docs/WORKFLOWS.md — AD-10 (column, sort, dash meaning) and AN-4 (end state now notes User.lastLoginAt is stamped).
  • tests/db/last-sign-in.test.ts (new) and tests/unit/data-table.test.ts (extended) — see Testing.

Testing plan

  • npm run prettier:check, eslint:check, tsc:check, test all pass locally.
  • Run npm run prisma:migrate, then sign in as an admin via the OTP flow → /users shows your row's Last sign-in as "Just now"; everyone else shows —, and the footnote is visible.
  • Sign out, sign back in as another test user, reload /users as admin → that user's cell updates; hover shows the full local datetime.
  • Deactivate a test user, then try to sign in as them → sign-in is refused, their row disappears from /users, and in the DB lastLoginAt is unchanged from before the attempt (happy-path vs. blocked-path distinction).
  • Click Last sign-in → dated rows go oldest→newest with — rows at the bottom. Click again → newest→oldest, — rows still last (edge: null handling both directions).
  • Narrow the viewport to mobile → each card shows "Last sign-in …" or "No sign-in recorded", plus the footnote under the list.
  • Screen reader / accessibility tree → a null cell reads "No sign-in recorded", not "dash" (a11y check).
  • Non-admin visiting /users → still 404s (requireAdminOr404, unchanged by this PR — auth/roles check).

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test (unit + db projects) — pass (843 tests)

Notes

  • No backfill: all sessions were cleared in the 2026-08-24 incident, and synthesizing a value from createdAt/updatedAt was explicitly ruled out by the ticket — a wrong timestamp is worse than an honest blank here.
  • #733/#734 (the SGAuth migration) will need to re-wire recordSignIn's call site once Better Auth's databaseHooks go away; the ticket flags this as a follow-up for the human on that sibling issue, and this PR doesn't touch it.
  • Field is named lastLoginAt (matching the ticket and #554, which will consume it as an inactivity signal) even though the UI says "Last sign-in".
  • Risk: recordSignIn runs post-commit in session.create.after; if its updateMany throws, the sign-in errors out but the already-committed Session row is left behind as an orphaned session the client never got a cookie for. Narrow window, consistent with the plan's tradeoff, no code change needed.

🤖 Generated with Claude Code

cielbellerose and others added 3 commits September 24, 2026 19:29
Stored on User because sessions are hard-deleted on sign-out, expiry,
and deactivation, so nothing derived from Session survives long enough
to answer "who has gone quiet". The write moves into the session
create `after` hook so a blocked (deactivated) sign-in never stamps it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Makes the nulls-last sort rule unit-testable independently of the
DataTable component; behavior is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sorts nulls (never-signed-in users) last in both directions; dash cells
carry an sr-only "No sign-in recorded" label, and a footnote explains
what the dash means since nothing is backfilled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cielbellerose cielbellerose added the claude Will be worked on by Claude label Sep 24, 2026
@cielbellerose cielbellerose self-assigned this Sep 24, 2026
@vercel

vercel Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aplio Ready Ready Preview Sep 24, 2026 11:49pm UTC

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · needs revision

1 open — 1 🟡 Low (see inline)

Comment thread lib/auth/config.ts
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 24, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-L1 (acknowledged in PR description Notes, no code change)

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 2 · approved

0 open — R1-L1 resolved (acknowledged in PR Notes), no regressions found

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 24, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 2 · operator addendum · needs revision

1 open (operator feedback from the preview)

R2-M1 (Medium, UX, operator request): remove the footnote under the Users table.
Delete the permanent footnote "“—” means no sign-in has been recorded since sign-in tracking began." from components/features/users-table.tsx, and any constant it uses.

  • Keep the rest as it is: the "—" cell, its screen-reader text "No sign-in recorded", and the mobile card's "No sign-in recorded" wording.
  • Update docs/WORKFLOWS.md (AD-10) and any test that asserts the footnote exists.

@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Sep 24, 2026
Remove the permanent footnote under the users table per operator
request; the "—" cell and its screen-reader text still explain the
missing sign-in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 2

fixed R2-M1 · 8d45b25

@cielbellerose cielbellerose added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 24, 2026
@cielbellerose cielbellerose added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 24, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 4 · approved

0 open — R2-M1 resolved (footnote removed cleanly), no regressions found

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 24, 2026
@cielbellerose
cielbellerose merged commit a678c07 into dev Sep 25, 2026
14 of 19 checks passed
@cielbellerose
cielbellerose deleted the 645-add-a-last-sign-in-column-to-the-users-page branch September 25, 2026 00:04

This branch was successfully deployed

1 active deployment
Preview — 8d45b25d Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add A Last Sign-In Column To The Users Page

1 participant