feat(admin): extend session on activity, expire on 15 min idle
All checks were successful
build-and-release / build (push) Successful in 1m33s
All checks were successful
build-and-release / build (push) Successful in 1m33s
Pairs with the backend's new POST /api/admin/auth/refresh (nxtgauge-backend-rust). While the admin is active (mouse/keyboard/scroll/touch), silently exchange the refresh-token cookie for a new access token every 3 minutes so the 15-minute access token never actually expires mid-work. Once idle for 15 minutes, the refresh loop stops on its own and the token expires naturally, triggering the existing session-expired dialog - "logged out on inactivity, not on a fixed timer" as intended. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ca2c5a4ef6
commit
0e08cf9d6b
2 changed files with 53 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ import { isExternalIdentity } from "~/lib/admin-auth";
|
|||
import { clearAdminSession, hasAdminSession, setAdminSession } from "~/lib/admin-session";
|
||||
import { normalizeAllowedModules } from "~/lib/admin/module-access";
|
||||
import { sessionExpired, clearSessionExpired, installSessionExpiryWatcher } from "~/lib/session-expired";
|
||||
import { installSessionRefreshLoop } from "~/lib/session-refresh";
|
||||
|
||||
type Tab = { href: string; label: string; exact?: boolean };
|
||||
type SearchResult = { id: string; title: string; subtitle: string; href: string };
|
||||
|
|
@ -491,6 +492,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
|
||||
onMount(() => {
|
||||
installSessionExpiryWatcher();
|
||||
installSessionRefreshLoop();
|
||||
|
||||
const savedTheme = (
|
||||
typeof localStorage !== "undefined" ? localStorage.getItem("nxtgauge_admin_theme") : null
|
||||
|
|
|
|||
51
src/lib/session-refresh.ts
Normal file
51
src/lib/session-refresh.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Sliding session window for the admin panel: the access token is only valid for
|
||||
// 15 minutes (see crates/auth/src/jwt.rs), matching the backend's employee_sessions
|
||||
// expiry policy. While the admin is active we silently exchange the refresh-token
|
||||
// cookie for a new access token before the old one expires; once idle for 15
|
||||
// minutes we stop refreshing and let the token expire naturally, so the existing
|
||||
// session-expired dialog (src/lib/session-expired.ts) kicks in on the next request.
|
||||
const IDLE_LIMIT_MS = 15 * 60 * 1000;
|
||||
const CHECK_INTERVAL_MS = 3 * 60 * 1000;
|
||||
const ACTIVITY_EVENTS = ['mousedown', 'keydown', 'scroll', 'touchstart'] as const;
|
||||
|
||||
let lastActivityAt = Date.now();
|
||||
let installed = false;
|
||||
|
||||
function markActivity(): void {
|
||||
lastActivityAt = Date.now();
|
||||
}
|
||||
|
||||
async function refreshAccessToken(): Promise<void> {
|
||||
try {
|
||||
const res = await fetch('/api/admin/auth/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json().catch(() => null);
|
||||
if (data?.access_token && typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.setItem('nxtgauge_admin_access_token', data.access_token);
|
||||
}
|
||||
} catch {
|
||||
// Network hiccup — the next successful check will catch up. If the token
|
||||
// does expire in the meantime, the session-expired dialog handles it.
|
||||
}
|
||||
}
|
||||
|
||||
/** Idempotent — safe to call from every AdminShell mount. */
|
||||
export function installSessionRefreshLoop(): void {
|
||||
if (installed || typeof window === 'undefined') return;
|
||||
installed = true;
|
||||
|
||||
markActivity();
|
||||
for (const evt of ACTIVITY_EVENTS) {
|
||||
window.addEventListener(evt, markActivity, { passive: true });
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (Date.now() - lastActivityAt < IDLE_LIMIT_MS) {
|
||||
void refreshAccessToken();
|
||||
}
|
||||
}, CHECK_INTERVAL_MS);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue