Add Inactive user status for users who haven't logged in for 30+ days#49211
Add Inactive user status for users who haven't logged in for 30+ days#49211allenhouchins wants to merge 4 commits into
Conversation
Users who haven't logged in for 30+ days now show an "Inactive" status (with an explanatory tooltip) in the Users table. - Add users.last_login_at column, backfilled from live sessions' created_at (login time), preserving updated_at - Stamp last_login_at whenever a session is created (password, SSO, MFA) - Wrap NewSession's insert + stamp in a transaction - Return last_login_at from the users API - Frontend derives Inactive from last_login_at (created_at fallback); API-only users are never marked inactive
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #49211 +/- ##
========================================
Coverage 67.99% 67.99%
========================================
Files 3779 3781 +2
Lines 239082 239227 +145
Branches 12610 12646 +36
========================================
+ Hits 162563 162665 +102
- Misses 61756 61793 +37
- Partials 14763 14769 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Pull request overview
This PR introduces backend tracking of a user’s most recent login time (users.last_login_at) and uses it in the admin Users table to surface an Inactive status for users who haven’t logged in for 30+ days (with a tooltip explaining the status).
Changes:
- Add
users.last_login_at(schema + migration with best-effort backfill from live sessions) and expose it onfleet.User. - Stamp
last_login_atwhen creating a session (now done within a transaction) and add datastore tests to ensureupdated_atis preserved. - Update the frontend Users table to derive “Inactive” status (with tooltip) and add unit tests + stubs/mocks updates.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/fleet/users.go | Adds LastLoginAt to the User API model. |
| server/datastore/mysql/users.go | Includes last_login_at in standard user SELECT columns. |
| server/datastore/mysql/sessions.go | Wraps session creation in a transaction and stamps users.last_login_at. |
| server/datastore/mysql/sessions_test.go | Adds test coverage for last_login_at stamping and updated_at preservation. |
| server/datastore/mysql/schema.sql | Updates schema snapshot for users.last_login_at and migration status table. |
| server/datastore/mysql/migrations/tables/20260713150609_AddLastLoginAtToUsers.go | Adds the column + backfills from sessions.created_at. |
| server/datastore/mysql/migrations/tables/20260713150609_AddLastLoginAtToUsers_test.go | Tests migration backfill + updated_at preservation. |
| frontend/interfaces/user.ts | Adds last_login_at to IUser. |
| frontend/test/stubs.ts | Updates frontend userStub to include last_login_at. |
| frontend/mocks/userMock.ts | Updates default mock user to include last_login_at. |
| frontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tsx | Implements Inactive status logic + tooltip in the Users table. |
| frontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tests.tsx | Adds unit tests for Active/Inactive/No access/Invite pending cases. |
| frontend/components/StatusIndicator/_styles.scss | Adds styling hook for the “inactive” status indicator. |
| docs/REST API/rest-api.md | Updated (content excluded from review by policy). |
| changes/inactive-user-status.md | Added/updated (content excluded from review by policy). |
Files excluded by content exclusion policy (2)
- changes/inactive-user-status.md
- docs/REST API/rest-api.md
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if _, err := tx.ExecContext(ctx, | ||
| `UPDATE users SET last_login_at = ?, updated_at = updated_at WHERE id = ?`, | ||
| ds.clock.Now(), userID, | ||
| ); err != nil { |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds a nullable 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tests.tsx (1)
74-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for the "No access" invite path.
generateInviteStatuscan return"No access"when an invite hasglobal_role: nulland emptyteams, but no test covers this branch. Adding it would complete coverage for both return paths of the function.🧪 Suggested test addition
it("returns 'Invite pending' for invites", () => { const invites = [createMockInvite()]; const [row] = combineDataSets([], invites, 99); expect(row.status).toBe("Invite pending"); }); + + it("returns 'No access' for an invite without a role", () => { + const invites = [createMockInvite({ global_role: null, teams: [] })]; + const [row] = combineDataSets([], invites, 99); + expect(row.status).toBe("No access"); + }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tests.tsx` around lines 74 - 78, Add a test alongside the existing invite-status test in UsersTableConfig tests that creates an invite with global_role set to null and an empty teams array, passes it through combineDataSets, and asserts the resulting row status is "No access".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@frontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tests.tsx`:
- Around line 74-78: Add a test alongside the existing invite-status test in
UsersTableConfig tests that creates an invite with global_role set to null and
an empty teams array, passes it through combineDataSets, and asserts the
resulting row status is "No access".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a9ef56ee-676b-4f43-b008-6341295ace31
⛔ Files ignored due to path filters (2)
changes/inactive-user-status.mdis excluded by!**/*.mddocs/REST API/rest-api.mdis excluded by!**/*.md
📒 Files selected for processing (13)
frontend/__mocks__/userMock.tsfrontend/components/StatusIndicator/_styles.scssfrontend/interfaces/user.tsfrontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tests.tsxfrontend/pages/admin/ManageUsersPage/components/UsersTable/UsersTableConfig.tsxfrontend/test/stubs.tsserver/datastore/mysql/migrations/tables/20260713150609_AddLastLoginAtToUsers.goserver/datastore/mysql/migrations/tables/20260713150609_AddLastLoginAtToUsers_test.goserver/datastore/mysql/schema.sqlserver/datastore/mysql/sessions.goserver/datastore/mysql/sessions_test.goserver/datastore/mysql/users.goserver/fleet/users.go
Surface last_activity_at (MAX of live sessions' accessed_at, bumped on every authenticated request) on user API responses, and use the most recent of last activity / last login in the Inactive check so API-only accounts are covered too, with an API-specific tooltip. Adds a user_id index on sessions for the aggregate.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>


Adds an "Inactive" status to the Users table for accounts that haven't been used for 30+ days — both human users and API-only accounts — with a tooltip explaining what it means, and builds the underlying last-login / last-activity tracking to surface that info.
Details
Backend
users.last_login_atcolumn (nullable timestamp). Backfilled from live sessions'created_at(session creation = login); users with no live session keepNULL. The backfill explicitly preservesupdated_at(which hasON UPDATE CURRENT_TIMESTAMP).last_login_atis stamped whenever a session is created (makeSessionInTransaction), covering password, SSO, and MFA logins.NewSessionnow wraps the session insert + stamp in a transaction.last_activity_atfield:MAX(accessed_at)over the user's live sessions (Fleet already bumpsaccessed_aton every authenticated request). This is the inactivity signal for API-only accounts, whose long-lived token session is created once but used constantly. A newidx_sessions_user_idindex supports the aggregate.nullwhen never logged in / no live session).Frontend
last_activity_atandlast_login_at, falling back tocreated_atfor accounts that have never logged in.Checklist for submitter
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Testing
Database migrations
users.updated_athasON UPDATE CURRENT_TIMESTAMP; both the migration backfill and the per-login stamp explicitly preserve it (updated_at = updated_at), covered by tests.COLLATE utf8mb4_unicode_ci).TIMESTAMP, no character columns added.Summary by CodeRabbit