Skip to content

feat(session): serve session expiry from VIPER 2, not the legacy CFM - #304

Open
rlorenzo wants to merge 1 commit into
fix/session-refresh-pathbasefrom
feature/session-timeout-endpoint
Open

feat(session): serve session expiry from VIPER 2, not the legacy CFM#304
rlorenzo wants to merge 1 commit into
fix/session-refresh-pathbasefrom
feature/session-timeout-endpoint

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #302. Review that one first. This branch is based on fix/session-refresh-pathbase, so this diff shows only the endpoint work. #302 is a standalone one-line bug fix and does not depend on anything here. If this PR is declined, #302 is unaffected.

Replaces the cross-origin session-expiry poll to legacy VIPER 1 with a read-only VIPER 2 endpoint.

Why

  • The legacy seconds_until_timeout_v2.cfm takes loginID as an unauthenticated query string parameter, so anyone can read anyone's session expiry. The new endpoint takes no parameters and derives the user from the auth cookie.
  • The CFM carries hardcoded CORS handling for localhost:7157-7159 purely to work in dev. Same-origin removes the need.
  • It removes a runtime dependency on legacy VIPER 1 for Razor pages.

The one thing to check carefully

The endpoint must never extend the session. Both controller base classes in this app write a fresh expiry on every action:

  • ApiController via [ApiSessionUpdateFilter] (ApiSessionUpdateFilter.cs:13)
  • AreaController via its OnActionExecutionAsync override (AreaController.cs:20)

So SessionTimeoutController deliberately derives from plain ControllerBase. If someone "tidies" it onto ApiController later, polling would renew the session every 5 minutes and sessions would never expire. Inheriting ApiController would also wrap the payload in the ApiResponse envelope and break the JSON contract the dialog reads.

That invariant is now pinned by test/Controllers/SessionTimeoutControllerTests.cs, which fails if the base class changes.

Decisions

  • Database error returns ~600 seconds, not 0, matching the legacy <cfcatch>, so a DB blip cannot strand a user behind a warning dialog they cannot dismiss.
  • A missing row is now split two ways, which legacy could not do. Rows are written only by AreaController, ApiSessionUpdateFilter and RefreshSession, so a page served by a plain Controller (CMSController, for instance) leaves a perfectly valid session with no row at all. Legacy had no auth context, could not tell that apart from a dead session, and reported "expired" to a user who was fine. With the cookie we can: an authenticated user with no row gets a full window, an anonymous caller still gets 0. This weakens nothing, because the row drives only this advisory dialog and is not enforced server-side.

The JSON contract (sessionTimeoutDateTime, secondsUntilTimeout) is unchanged, so the client shape is untouched.

Also here

  • Anonymous visitors no longer poll at all. The layout renders this component on public pages, and both before and after this change an anonymous poll returns 0, which told a logged-out visitor on the home page that their session had expired 60 seconds after load. The ViewComponent now renders nothing when unauthenticated, matching the guard the SPA sibling already has.
  • The expiry timestamp is emitted with a UTC offset. The column is a bare datetime written from DateTime.Now, so it is local wall-clock; without an offset a client in another timezone reads it as its own and displays the wrong expiry.
  • no-store on a per-user, time-sensitive GET.
  • The warning now stands down by itself when the session is extended elsewhere. Previously, once secondsUntilTimeout climbed back above 300 the poll skipped the whole branch and never cleared showSessionTimeoutWarning, so a user who kept working in a second tab was left staring at a stale "your session will expire at ..." that would never go away.
  • Midnight renders as "12:05 AM" rather than "0:05 AM". The formatter was duplicated verbatim in both handlers and is now one method, so the two cannot drift.
  • SessionTimeout.cs cleanup: the hardcoded http://localhost/, the double slash, and the loginId in the query string are all gone with the URL. It no longer needs to be async.

Minor behavioural note

Moving the poll same-origin means it now reaches .NET carrying the auth cookie, and cookie auth uses sliding expiration (ExpireTimeSpan 12h at Program.cs:146, SlidingExpiration left unset so it defaults to true). The old cross-origin ColdFusion poll could not touch that cookie.

The practical effect is small. Sliding expiration only reissues the cookie after 6h have elapsed, and polling stops once the session expires, so an abandoned tab goes quiet roughly 30 minutes after the last real activity and never survives to the renewal point. Keeping the poll running to 6h would require activity every ~29.5 minutes to keep the SessionTimeout row alive, and that activity is itself same-origin .NET traffic already renewing the cookie. So the poll can extend cookie life by at most ~30 minutes beyond what ordinary navigation already does. Recording it for the record rather than flagging it as a blocker.

Not done

The SPA (VueApp/src/components/SessionTimeout.vue) still polls the legacy CFM with ?id=<loginId>, so the CFM cannot be decommissioned yet. VueApp/ was out of scope for this work. Migrating it is the natural follow-up and would close the unauthenticated-read hole for the SPA too.

Verification

Command Result
npm run lint -- --fix <changed files> No issues found
npm run verify:build All build verifications passed
npm run test:backend 2710 passed, 0 failed (2708 existing plus 2 new)

Not verifiable locally: local dev has no PathBase, so the /2 behaviour of @Url.Content("~/api/sessionTimeout") needs a TEST deploy to confirm, as does the end-to-end poll against a real database.

@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch from 95891e8 to b893056 Compare August 7, 2026 19:17
@rlorenzo

rlorenzo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds a read-only session-timeout API, exposes its status model, and updates the session-timeout view to poll the API and format expiry times. Reflection tests verify that the controller does not extend sessions through inherited bases or filters.

Changes

Session timeout API and display

Layer / File(s) Summary
Session timeout status contract
web/Classes/SessionTimeoutStatus.cs, web/Classes/Utilities/SessionTimeoutService.cs
Adds the public SessionTimeoutStatus model and exposes SessionTimeoutSeconds.
Session timeout endpoint
web/Controllers/SessionTimeoutController.cs, test/Controllers/SessionTimeoutControllerTests.cs
Adds the non-cached /api/sessionTimeout endpoint. It handles existing and missing records, anonymous requests, database failures, timestamp formatting, and controller session-extension safeguards.
Session timeout view integration
web/Views/Shared/Components/SessionTimeout/SessionTimeout.cs, web/Views/Shared/Components/SessionTimeout/Default.cshtml
Uses synchronous rendering for authenticated users, polls the new endpoint, hides warnings outside the five-minute window, and centralizes expiry formatting.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • ucdavis/VIPER#264: Both changes make the session-timeout view component use synchronous Invoke() rendering.

Suggested labels: .NET

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: serving session expiry from a VIPER 2 endpoint instead of the legacy CFM.
Description check ✅ Passed The description directly explains the endpoint migration, session-renewal safeguards, behavior changes, and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/session-timeout-endpoint

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@web/Controllers/SessionTimeoutController.cs`:
- Around line 66-70: Replace the filtered generic catch in the session-timeout
handling with separate catch blocks for SqlException and
InvalidOperationException. In both blocks, preserve the existing Logger.Error
call and Status fallback response.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a02c865c-16f7-4431-9a5e-4f20891d1ebd

📥 Commits

Reviewing files that changed from the base of the PR and between e05b9b3 and b893056.

📒 Files selected for processing (6)
  • test/Controllers/SessionTimeoutControllerTests.cs
  • web/Classes/SessionTimeoutStatus.cs
  • web/Classes/Utilities/SessionTimeoutService.cs
  • web/Controllers/SessionTimeoutController.cs
  • web/Views/Shared/Components/SessionTimeout/Default.cshtml
  • web/Views/Shared/Components/SessionTimeout/SessionTimeout.cs

Comment thread web/Controllers/SessionTimeoutController.cs Outdated
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch from b893056 to d056d7b Compare August 7, 2026 21:29
@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov-commenter

codecov-commenter commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.85%. Comparing base (ba86fee) to head (f8e26f4).

Files with missing lines Patch % Lines
web/Controllers/SessionTimeoutController.cs 0.00% 35 Missing ⚠️
...Shared/Components/SessionTimeout/SessionTimeout.cs 0.00% 3 Missing ⚠️
web/Classes/SessionTimeoutStatus.cs 0.00% 2 Missing ⚠️
web/Classes/Utilities/SessionTimeoutService.cs 0.00% 2 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##           fix/session-refresh-pathbase     #304      +/-   ##
================================================================
- Coverage                         41.88%   41.85%   -0.03%     
================================================================
  Files                               986      987       +1     
  Lines                             49488    49518      +30     
  Branches                           5843     5843              
================================================================
  Hits                              20726    20726              
- Misses                            27852    27882      +30     
  Partials                            910      910              
Flag Coverage Δ
backend 39.91% <0.00%> (-0.03%) ⬇️
frontend 58.15% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
web/Classes/SessionTimeoutStatus.cs 0.00% <0.00%> (ø)
web/Classes/Utilities/SessionTimeoutService.cs 0.00% <0.00%> (ø)
...Shared/Components/SessionTimeout/SessionTimeout.cs 0.00% <0.00%> (ø)
web/Controllers/SessionTimeoutController.cs 0.00% <0.00%> (ø)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Replaces the legacy ColdFusion session-timeout polling endpoint with a first-party ASP.NET Core API, and updates the session-timeout UI to use it while avoiding polling for anonymous visitors.

Changes:

  • Add /api/sessionTimeout controller returning session timeout status derived from the auth cookie and DB record.
  • Update the session timeout component and client script to poll the new API and improve warning reset/formatting.
  • Remove the old SessionTimeoutCheck model and add a small test to prevent inheriting session-extending controller bases/filters.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
web/Views/Shared/Components/SessionTimeout/SessionTimeout.cs Stops rendering the timeout UI for anonymous users to avoid false “expired” dialogs.
web/Views/Shared/Components/SessionTimeout/Default.cshtml Switches polling to the new API, adds reset behavior, and centralizes time formatting.
web/Models/SessionTimeoutCheck.cs Removes legacy model no longer used by the new polling contract.
web/Controllers/SessionTimeoutController.cs Introduces the new read-only polling endpoint and ensures no-store caching.
web/Classes/Utilities/SessionTimeoutService.cs Exposes timeout duration for reuse by the new controller.
web/Classes/SessionTimeoutStatus.cs Adds the JSON contract returned to the session-timeout dialog.
test/Controllers/SessionTimeoutControllerTests.cs Adds regression tests to ensure the polling controller doesn’t extend sessions via base types/filters.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/Controllers/SessionTimeoutController.cs
Comment thread web/Controllers/SessionTimeoutController.cs Outdated
Comment thread web/Controllers/SessionTimeoutController.cs
Comment thread web/Classes/Utilities/SessionTimeoutService.cs Outdated
Comment thread web/Controllers/SessionTimeoutController.cs
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch 3 times, most recently from 633ad99 to 3ffa268 Compare August 8, 2026 17:35
@rlorenzo
rlorenzo requested a lite review from Copilot August 8, 2026 17:43
@rlorenzo

rlorenzo commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@test/Controllers/SessionTimeoutControllerTests.cs`:
- Around line 14-31: Add execution tests for
SessionTimeoutController.GetSessionTimeout covering a stored expiry, an
authenticated user without a stored row, an anonymous user, and each fallback
exception path. Assert both SecondsUntilTimeout and the returned offset
timestamp contract, using the existing test setup and collaborators rather than
only reflection-based checks.

In `@web/Controllers/SessionTimeoutController.cs`:
- Line 52: Update SessionTimeoutController.GetSessionTimeout() to use a
read-only SessionTimeouts query with AsNoTracking(), filtering by LoginId and
Service, instead of calling SessionTimeoutService.GetSessionTimeout() and its
Find-based path.

In `@web/Views/Shared/Components/SessionTimeout/Default.cshtml`:
- Around line 47-51: Update the secondsUntilTimeout boundary in the session
timeout polling condition to use <= 300 instead of < 300, ensuring the warning
and 15-second pre-expiry polling path also run when the API returns exactly 300
seconds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ad6355cf-63f9-459e-91c1-76b73e3f1971

📥 Commits

Reviewing files that changed from the base of the PR and between b893056 and 3ffa268.

📒 Files selected for processing (6)
  • .review-pr-ignored-304
  • test/Controllers/SessionTimeoutControllerTests.cs
  • web/Classes/Utilities/SessionTimeoutService.cs
  • web/Controllers/SessionTimeoutController.cs
  • web/Models/SessionTimeoutCheck.cs
  • web/Views/Shared/Components/SessionTimeout/Default.cshtml
💤 Files with no reviewable changes (1)
  • web/Models/SessionTimeoutCheck.cs

Comment thread test/Controllers/SessionTimeoutControllerTests.cs
Comment thread web/Controllers/SessionTimeoutController.cs
Comment thread web/Views/Shared/Components/SessionTimeout/Default.cshtml Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (1)

web/Controllers/SessionTimeoutController.cs:56

  • secondsUntilTimeout can become negative if the stored SessionTimeoutDateTime is already in the past (clock skew, delayed poll, stale DB row). Returning a negative value is surprising for the client contract and can lead to inconsistent UI behavior; clamp it to 0 on the server.
                    return Status(record.SessionTimeoutDateTime,
                        (int)(record.SessionTimeoutDateTime - DateTime.Now).TotalSeconds);

@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch from 3ffa268 to f959924 Compare August 8, 2026 18:08
@rlorenzo
rlorenzo requested a lite review from Copilot August 8, 2026 18:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.

@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch from f959924 to 4c01b44 Compare August 10, 2026 03:27
- Keep the endpoint read-only by deriving from ControllerBase, since
  ApiController and AreaController both write a fresh expiry on every
  action and would stop the session ever timing out
- Read the user from the auth cookie, replacing the legacy endpoint's
  unauthenticated loginID query parameter
- Tell an authenticated user with no row apart from a dead session,
  which legacy could not do and so reported as expired
- Skip the poll for anonymous visitors, who would otherwise be told on
  public pages that a session they never had has expired
- Stand the warning down when the session is extended elsewhere, and
  render midnight as 12 AM rather than 0 AM
- Drop the ViewComponent's hardcoded localhost and double slash, now
  that the poll is same-origin
- Delete the unused SessionTimeoutCheck model, which was added as this
  endpoint's DTO in 2023 and never wired up
@rlorenzo
rlorenzo force-pushed the feature/session-timeout-endpoint branch from 4c01b44 to f8e26f4 Compare August 10, 2026 05:56
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.

3 participants