11import { createLogger } from "@obp/shared/utils" ;
22const logger = createLogger ( "LayoutServer" ) ;
3- import type { RequestEvent } from "@sveltejs/kit" ;
3+ import type { ServerLoadEvent } from "@sveltejs/kit" ;
4+ import type { Session } from "svelte-kit-sessions" ;
45import { obpIntegrationService } from "$lib/server/opey/OBPIntegrationService" ;
6+ import { OBP_API_URL } from "$lib/config" ;
57import type { OBPConsentInfo } from "$lib/obp/types" ;
68// import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
79// import { storePopup } from '@skeletonlabs/skeleton';
@@ -22,10 +24,61 @@ export interface RootLayoutData {
2224 jitEnabled : boolean ;
2325}
2426
25- export async function load ( event : RequestEvent ) {
27+ // Session user (incl. entitlements) is a login-time snapshot. Refresh it from
28+ // /users/current when older than this, so granted roles (manual or JIT) show up
29+ // without logout/login. The client layout re-polls via invalidate() every 5 min;
30+ // keeping the base threshold below the poll interval guarantees each poll refreshes.
31+ // Users with very large entitlement lists (JIT accumulation can reach thousands of
32+ // rows) get a several-hundred-KB /users/current response, so back off for them —
33+ // see security_TODO.md "Prune JIT-accumulated entitlements".
34+ const USER_REFRESH_STALE_MS = 4 * 60 * 1000 ;
35+ const USER_REFRESH_STALE_LARGE_MS = 15 * 60 * 1000 ;
36+ const LARGE_ENTITLEMENT_COUNT = 500 ;
37+
38+ async function refreshSessionUser ( session : Session ) {
39+ const lastRefresh = session . data . userRefreshedAt ?? 0 ;
40+ const entitlementCount =
41+ ( session . data . user as any ) ?. entitlements ?. list ?. length ?? 0 ;
42+ const staleMs =
43+ entitlementCount > LARGE_ENTITLEMENT_COUNT
44+ ? USER_REFRESH_STALE_LARGE_MS
45+ : USER_REFRESH_STALE_MS ;
46+ if ( Date . now ( ) - lastRefresh < staleMs ) return ;
47+
48+ try {
49+ const response = await fetch ( `${ OBP_API_URL } /users/current` , {
50+ headers : { Authorization : `Bearer ${ session . data . oauth ! . access_token } ` } ,
51+ } ) ;
52+ if ( ! response . ok ) {
53+ logger . warn ( `Failed to refresh /users/current: ${ response . status } ` ) ;
54+ return ;
55+ }
56+ const freshUser = await response . json ( ) ;
57+ if ( ! freshUser . user_id ) {
58+ logger . warn ( "Refresh of /users/current returned no user_id, keeping session user" ) ;
59+ return ;
60+ }
61+ await session . setData ( {
62+ ...session . data ,
63+ user : freshUser ,
64+ userRefreshedAt : Date . now ( ) ,
65+ } ) ;
66+ await session . save ( ) ;
67+ logger . info (
68+ `Refreshed session user from /users/current (${ freshUser . entitlements ?. list ?. length ?? 0 } entitlements)` ,
69+ ) ;
70+ } catch ( error ) {
71+ logger . warn ( "Failed to refresh /users/current, keeping session user:" , error ) ;
72+ }
73+ }
74+
75+ export async function load ( event : ServerLoadEvent ) {
2676 const startTime = performance . now ( ) ;
2777 logger . info ( "🚀 Layout server load started" ) ;
2878
79+ // Rerun this load (and dependants) when the client calls invalidate("app:session-user")
80+ event . depends ( "app:session-user" ) ;
81+
2982 const { session } = event . locals ;
3083
3184 let data : Partial < RootLayoutData > = { } ;
@@ -59,6 +112,7 @@ export async function load(event: RequestEvent) {
59112 // User is only considered logged in if they have both user data AND a valid access token
60113 logger . info ( "👤 Checking user session" ) ;
61114 if ( session ?. data ?. user && session ?. data ?. oauth ?. access_token ) {
115+ await refreshSessionUser ( session ) ;
62116 data . userId = session . data . user . user_id ;
63117 data . email = session . data . user . email ;
64118 data . username = session . data . user . username ;
0 commit comments