113 lines
3.7 KiB
Plaintext
113 lines
3.7 KiB
Plaintext
@implements IDisposable
|
|
@inject NavigationManager Navigation
|
|
@inject IJSRuntime JS
|
|
|
|
<AntiforgeryToken />
|
|
|
|
@code {
|
|
// Passive HTTP probes observe expiry but never advance activity. The JS module
|
|
// separately reports trusted browser input through an antiforgery-protected POST.
|
|
|
|
/// <summary>Server endpoint that reports live session validity.</summary>
|
|
internal const string PingUrl = "/auth/ping";
|
|
|
|
/// <summary>HTTP status returned by <see cref="PingUrl"/> once the cookie has lapsed.</summary>
|
|
private const int Unauthorized = 401;
|
|
|
|
private const string ModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/session-expiry.js";
|
|
|
|
/// <summary>How often the session validity is re-checked.</summary>
|
|
internal static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(1);
|
|
|
|
private CancellationTokenSource? _cts;
|
|
private IJSObjectReference? _module;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
// The login page uses the same layout, so this component renders there
|
|
// too. Polling/redirecting on /login → /login would loop.
|
|
if (IsOnLoginPage) return;
|
|
|
|
_cts = new CancellationTokenSource();
|
|
_ = PollSessionAsync(_cts.Token);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender || IsOnLoginPage) return;
|
|
try { _module ??= await JS.InvokeAsync<IJSObjectReference>("import", ModulePath); }
|
|
catch (JSDisconnectedException) { }
|
|
catch (JSException) { }
|
|
}
|
|
|
|
private bool IsOnLoginPage =>
|
|
Navigation.ToBaseRelativePath(Navigation.Uri)
|
|
.StartsWith("login", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private async Task PollSessionAsync(CancellationToken token)
|
|
{
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
try { await Task.Delay(PollInterval, token); }
|
|
catch (TaskCanceledException) { return; }
|
|
|
|
if (token.IsCancellationRequested) return;
|
|
await CheckSessionAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs one liveness check: pings the server and, if the session has lapsed
|
|
/// server-side (HTTP 401), redirects to the login page. Exposed for tests
|
|
/// (CentralUI-025) so the redirect path can be exercised without waiting on
|
|
/// the poll interval.
|
|
/// </summary>
|
|
internal async Task CheckSessionAsync()
|
|
{
|
|
if (IsOnLoginPage) return;
|
|
|
|
int status;
|
|
try
|
|
{
|
|
_module ??= await JS.InvokeAsync<IJSObjectReference>("import", ModulePath);
|
|
status = await _module.InvokeAsync<int>("ping", PingUrl);
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit gone — nothing to redirect.
|
|
return;
|
|
}
|
|
catch (JSException)
|
|
{
|
|
// Network blip or fetch failure: treat as inconclusive and retry on
|
|
// the next poll rather than logging an authenticated user out on a
|
|
// transient error.
|
|
return;
|
|
}
|
|
|
|
if (status == Unauthorized)
|
|
{
|
|
await InvokeAsync(() => Navigation.NavigateTo("/login", forceLoad: true));
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
// The module reference is owned by the circuit's JS runtime; once the
|
|
// circuit is disposed disposing it would throw — fire-and-forget and
|
|
// swallow the expected disconnect.
|
|
if (_module is not null)
|
|
{
|
|
_ = DisposeModuleAsync(_module);
|
|
}
|
|
}
|
|
|
|
private static async Task DisposeModuleAsync(IJSObjectReference module)
|
|
{
|
|
try { await module.DisposeAsync(); }
|
|
catch (JSDisconnectedException) { /* circuit already gone */ }
|
|
}
|
|
}
|