feat(settings): paginate the sessions and trusted-device lists (BUNYIP-177) #200
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-177-paginate-sessions-devices"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Adds offset pagination to the Active Sessions and Trusted Devices lists on
/settings, end to end (BUNYIP-177).Why
/settingsfetched and rendered every active session and trusted device with no limit (find_user_refresh_tokens/find_user_deviceswere unboundedSELECTs; the cards looped every row). An account that signs in from many devices without revoking accumulates non-expired tokens, so the page cost grew without bound - a latent DoS for high-activity accounts and an unbounded DOM.How
Follows the existing
PaginatedResponse/paginated()convention (page,per_pagedefault 20 max 100, separateCOUNT(*)).*_paginated+count_*repo fns (LIMIT/OFFSET+COUNT). The original unbounded fns stay - revoke-others still needs the full token set.GET /v1/users/me/sessionsand/trusted-devicestakepage/per_pageand return aPaginatedResponse. Response shape changes from{ sessions: [...] }/{ devices: [...] }to the standard{ items, total, page, per_page, total_pages }under the data envelope. Only bunyip-web consumes these.PaginatedResponse<T>;/settingsrenders one page of each list with plain prev/next links (no htmx-pagination precedent exists, so this mirrors the admin pager). The two lists page independently - each pager preserves the other's page param (session_page/device_page). "Log out all other devices" now keys offtotal > 1.Guard against breaking BUNYIP-176
GET /v1/users/me/sessionsnow returnsdata.items(wasdata.sessions), so the BUNYIP-176sessions.specassertion is updated to match in this same PR.profile.specis unaffected (it checks the profile form, not the lists). CI will re-run both.Verification
cargo check --workspace,cargo clippy --workspace --all-targets -- -D warnings, andcargo fmt --all --checkall pass (via the rust-builder image);e2etsc clean. No schema change (existing tables). The e2e suite runs on this PR.🤖 Generated with Claude Code
/settings fetched and rendered ALL of a user's active sessions and trusted devices with no limit (find_user_refresh_tokens / find_user_devices were unbounded SELECTs, and the cards looped every row). A user who signs in from many devices without revoking accumulates non-expired tokens, so the page cost grew without bound - a latent DoS for high-activity accounts and an unbounded DOM. This adds offset pagination end to end, following the existing PaginatedResponse / paginated() convention (page, per_page default 20 max 100, separate COUNT). Domain (crates/bunyip-domain): add find_user_refresh_tokens_paginated + count_user_refresh_tokens and find_user_devices_paginated + count_user_devices (LIMIT/OFFSET + COUNT). The original unbounded fns are kept - revoke-others still needs the full token set. API (bunyip-api): GET /v1/users/me/sessions and /trusted-devices take page/per_page and return a PaginatedResponse via paginated(). Response shape changes from { sessions: [...] } / { devices: [...] } to the standard { items, total, page, per_page, total_pages } under the data envelope. Only bunyip-web consumes these. Web (bunyip-web): the api client returns PaginatedResponse<T>; /settings renders one page of each list with plain prev/next links (no htmx precedent exists; mirrors the admin pager). The two lists page independently - each pager preserves the other list's page param (session_page / device_page). "Log out all other devices" now keys off total > 1 so it shows even when the other sessions are on a later page. e2e: GET /v1/users/me/sessions now returns data.items (was data.sessions), so the BUNYIP-176 sessions.spec assertion is updated to match. profile.spec is unaffected (it checks the profile form, not the lists). #BUNYIP-177 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>