Update CBS Mobile App codebase
This commit is contained in:
@@ -1,17 +1,162 @@
|
||||
import { appConfig } from '../../config/appConfig';
|
||||
|
||||
let lastActivityAt = Date.now();
|
||||
/**
|
||||
* Two independent limits, and the session dies at whichever comes first:
|
||||
* an idle window the server resets on every authenticated call, and an
|
||||
* absolute ceiling it never extends however active the user is.
|
||||
*/
|
||||
interface SessionWindow {
|
||||
idleTimeoutMs: number;
|
||||
/** Epoch ms of the server's absolute expiry, or null when unknown. */
|
||||
absoluteExpiryAt: number | null;
|
||||
}
|
||||
|
||||
export type SessionExpiryReason = 'idle' | 'absolute';
|
||||
|
||||
/**
|
||||
* When we last exchanged an authenticated request with the server. This — not
|
||||
* anything the user did — is what the server's idle window is measured from,
|
||||
* so it is what expiry has to be judged on.
|
||||
*/
|
||||
let lastServerContactAt = Date.now();
|
||||
|
||||
/**
|
||||
* When the user last touched the screen. Tracked separately because most of
|
||||
* this app reads from fixtures: someone can tap around for five minutes without
|
||||
* a single authenticated call, and treating that as idle would sign them out
|
||||
* mid-use. Interaction cannot reset the server's timer by itself — only a call
|
||||
* can — so this drives a keep-alive rather than the expiry clock.
|
||||
*/
|
||||
let lastInteractionAt = Date.now();
|
||||
|
||||
let window: SessionWindow = {
|
||||
idleTimeoutMs: appConfig.security.sessionTimeoutMs,
|
||||
absoluteExpiryAt: null,
|
||||
};
|
||||
|
||||
export const sessionManager = {
|
||||
/**
|
||||
* Adopt the limits the server reported at sign-in. `idleTimeoutSeconds` and
|
||||
* `expiresAt` come straight off the login response, so the app never has to
|
||||
* guess and never drifts from what the backend will enforce.
|
||||
*/
|
||||
configure({
|
||||
idleTimeoutSeconds,
|
||||
expiresAt,
|
||||
}: {
|
||||
idleTimeoutSeconds?: number;
|
||||
expiresAt?: string;
|
||||
}) {
|
||||
const absolute = expiresAt ? Date.parse(expiresAt) : NaN;
|
||||
|
||||
window = {
|
||||
idleTimeoutMs:
|
||||
typeof idleTimeoutSeconds === 'number' && idleTimeoutSeconds > 0
|
||||
? idleTimeoutSeconds * 1000
|
||||
: appConfig.security.sessionTimeoutMs,
|
||||
absoluteExpiryAt: Number.isFinite(absolute) ? absolute : null,
|
||||
};
|
||||
|
||||
lastServerContactAt = Date.now();
|
||||
lastInteractionAt = Date.now();
|
||||
},
|
||||
|
||||
clear() {
|
||||
window = {
|
||||
idleTimeoutMs: appConfig.security.sessionTimeoutMs,
|
||||
absoluteExpiryAt: null,
|
||||
};
|
||||
lastServerContactAt = Date.now();
|
||||
lastInteractionAt = Date.now();
|
||||
},
|
||||
|
||||
/** An authenticated call completed, so the server's idle window just reset. */
|
||||
touch() {
|
||||
lastActivityAt = Date.now();
|
||||
lastServerContactAt = Date.now();
|
||||
lastInteractionAt = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* The user touched the screen. Deliberately does not reset the expiry clock:
|
||||
* the server has not heard from us, so pretending otherwise would leave the
|
||||
* app believing it is signed in while the next real request 401s. It only
|
||||
* makes `shouldKeepAlive()` true, and the keep-alive call is what actually
|
||||
* moves the server's window.
|
||||
*/
|
||||
noteInteraction() {
|
||||
lastInteractionAt = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to spend a keep-alive call now.
|
||||
*
|
||||
* True only when the user has done something since we last spoke to the
|
||||
* server and the idle window is over halfway gone. The halfway threshold is
|
||||
* what keeps this to roughly one request per two and a half minutes instead
|
||||
* of one per tap.
|
||||
*/
|
||||
shouldKeepAlive() {
|
||||
if (lastInteractionAt <= lastServerContactAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nothing to preserve if the session is already gone, and no point
|
||||
// extending the idle window when the absolute cap is what will bite.
|
||||
if (this.hasExpired() || this.getAbsoluteRemainingMs() <= window.idleTimeoutMs / 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.getIdleDurationMs() >= window.idleTimeoutMs / 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* Which limit will end the session first. The warning copy depends on it:
|
||||
* against the idle window carrying on using the app helps, and against the
|
||||
* absolute cap nothing does, so telling the user to "tap to stay signed in"
|
||||
* would be a lie.
|
||||
*/
|
||||
nearestLimit(): SessionExpiryReason {
|
||||
return this.getAbsoluteRemainingMs() <= this.getIdleRemainingMs() ? 'absolute' : 'idle';
|
||||
},
|
||||
|
||||
getIdleTimeoutMs() {
|
||||
return window.idleTimeoutMs;
|
||||
},
|
||||
|
||||
getIdleDurationMs() {
|
||||
return Date.now() - lastActivityAt;
|
||||
return Date.now() - lastServerContactAt;
|
||||
},
|
||||
|
||||
getIdleRemainingMs() {
|
||||
return Math.max(0, window.idleTimeoutMs - this.getIdleDurationMs());
|
||||
},
|
||||
|
||||
getAbsoluteRemainingMs() {
|
||||
if (window.absoluteExpiryAt === null) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
return Math.max(0, window.absoluteExpiryAt - Date.now());
|
||||
},
|
||||
|
||||
/** Milliseconds until the session dies, whichever limit bites first. */
|
||||
getRemainingMs() {
|
||||
return Math.min(this.getIdleRemainingMs(), this.getAbsoluteRemainingMs());
|
||||
},
|
||||
|
||||
expiryReason(): SessionExpiryReason | null {
|
||||
if (this.getAbsoluteRemainingMs() <= 0) {
|
||||
return 'absolute';
|
||||
}
|
||||
|
||||
if (this.getIdleRemainingMs() <= 0) {
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
hasExpired() {
|
||||
return this.getIdleDurationMs() >= appConfig.security.sessionTimeoutMs;
|
||||
return this.expiryReason() !== null;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user