#645 Add A Last Sign-In Column To The Users Page - #777
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 1 · needs revision
1 open — 1 🟡 Low (see inline)
Revision — Cycle 1fixed R1-L1 (acknowledged in PR description Notes, no code change) |
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 2 · approved
0 open — R1-L1 resolved (acknowledged in PR Notes), no regressions found
cielbellerose
left a comment
There was a problem hiding this comment.
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.
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>
Revision — Cycle 2fixed R2-M1 · 8d45b25 |
cielbellerose
left a comment
There was a problem hiding this comment.
Code Review — Cycle 4 · approved
0 open — R2-M1 resolved (footnote removed cleanly), no regressions found
Closes #645
Summary
User.lastLoginAt DateTime?column, stamped from Better Auth's session-createafterhook so a blocked (deactivated) sign-in never records a login./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.Changes
prisma/schema.prisma+prisma/migrations/20260924232626_add_user_last_login_at/—User.lastLoginAt DateTime?,ADD COLUMNonly, no backfill.lib/auth/session-hooks.ts(new) —assertSessionUserActive(userId)(the existing deactivated-account check, moved verbatim) andrecordSignIn(userId), both plain functions sotests/dbcan exercise them directly.lib/auth/config.ts—session.create.beforecallsassertSessionUserActive; newsession.create.aftercallsrecordSignIn. Abeforethrow skipsafter, so a refused sign-in is never stamped.prisma/data/users.ts/lib/types.ts—getUsersForAdminselectslastLoginAt;AdminUserListItemincludes it.lib/data-table.ts— extracted the DataTable row comparator into a pure, exportedsortRows(rows, column, direction)so the nulls-last rule is unit-testable;components/ui/data-table.tsxnow calls it (behavior unchanged).components/features/users-table.tsx— new "Last sign-in" column (LocalTimerelative 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 notesUser.lastLoginAtis stamped).tests/db/last-sign-in.test.ts(new) andtests/unit/data-table.test.ts(extended) — see Testing.Testing plan
npm run prettier:check,eslint:check,tsc:check,testall pass locally.npm run prisma:migrate, then sign in as an admin via the OTP flow →/usersshows your row's Last sign-in as "Just now"; everyone else shows—, and the footnote is visible./usersas admin → that user's cell updates; hover shows the full local datetime./users, and in the DBlastLoginAtis unchanged from before the attempt (happy-path vs. blocked-path distinction).—rows at the bottom. Click again → newest→oldest,—rows still last (edge: null handling both directions)./users→ still 404s (requireAdminOr404, unchanged by this PR — auth/roles check).Automated checks
npm run prettier:check— passnpm run eslint:check— passnpm run tsc:check— passnpm run test(unit + db projects) — pass (843 tests)Notes
createdAt/updatedAtwas 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-wirerecordSignIn's call site once Better Auth'sdatabaseHooksgo away; the ticket flags this as a follow-up for the human on that sibling issue, and this PR doesn't touch it.lastLoginAt(matching the ticket and#554, which will consume it as an inactivity signal) even though the UI says "Last sign-in".recordSignInruns post-commit insession.create.after; if itsupdateManythrows, the sign-in errors out but the already-committedSessionrow 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