@@ -12,12 +12,13 @@ import { logger } from "./logger.server";
1212 * The deadline is written at login (and any time the effective duration is
1313 * recomputed — see `commitAuthenticatedSession`) and shortened in bulk when
1414 * an admin lowers an org cap (see the admin `session-duration` route).
15- * Reading here is a free piggyback on the User row that `requireUser`/
16- * `getUser` already fetches — there is no per-request DB query added by this
17- * check. `requireUserId`/`getUserId` deliberately do NOT enforce: enforcement
18- * happens at the next page navigation (root.tsx loader calls `getUser`),
19- * which matches HIPAA auto-logoff semantics — terminate sessions at the
20- * navigation boundary, not on every polling fetch.
15+ * Non-impersonation `requireUserId`/`getUserId` paths do NOT enforce —
16+ * enforcement happens at the next page navigation (root.tsx calls `getUser`),
17+ * which matches HIPAA auto-logoff semantics: terminate at the navigation
18+ * boundary, not on every polling fetch. Impersonation paths DO enforce in
19+ * `getUserId` (against the admin's deadline) so a `requireUserId`-only route
20+ * can't keep operating as the impersonation target after the admin's session
21+ * has expired or their admin role was revoked.
2122 *
2223 * `nextSessionEnd === null` means "no enforced deadline" — applies to legacy
2324 * sessions from before this feature shipped. The default `User.sessionDuration`
@@ -52,41 +53,43 @@ function maybeAutoLogout(
5253}
5354
5455export async function getUserId ( request : Request ) : Promise < string | undefined > {
55- // Cookie-only fast path: zero DB queries. Impersonation admin-verification
56- // and auto-logout enforcement happen in `getUser`/`requireUser`, where we
57- // already pay for a User row fetch.
5856 const impersonatedUserId = await getImpersonationId ( request ) ;
59- if ( impersonatedUserId ) return impersonatedUserId ;
6057
61- const authUser = await authenticator . isAuthenticated ( request ) ;
62- return authUser ?. userId ;
63- }
64-
65- export async function getUser ( request : Request ) {
66- const impersonatedUserId = await getImpersonationId ( request ) ;
67- const authUser = await authenticator . isAuthenticated ( request ) ;
68-
69- if ( impersonatedUserId && authUser ?. userId ) {
70- // Impersonating: verify the real user is still an admin and enforce the
71- // *admin's* deadline (the cap belongs to the cookie, not the
72- // impersonation target). If the admin is no longer admin, fall back to
73- // operating as the admin themselves — same defense-in-depth as before.
58+ if ( impersonatedUserId ) {
59+ // Impersonating: verify the real user (the admin) is still an admin and
60+ // enforce against their deadline (the cap belongs to the cookie, not the
61+ // target). If admin status was revoked, fall through to operate as the
62+ // admin themselves — otherwise a `requireUserId`-only route would keep
63+ // letting them act as the impersonation target after losing the role.
64+ const authUser = await authenticator . isAuthenticated ( request ) ;
65+ if ( ! authUser ?. userId ) return undefined ;
7466 const realUser = await getUserById ( authUser . userId ) ;
75- if ( ! realUser ) throw await logout ( request ) ;
67+ if ( ! realUser ) return authUser . userId ;
7668 if ( realUser . admin ) {
7769 maybeAutoLogout ( request , realUser , impersonatedUserId ) ;
78- const target = await getUserById ( impersonatedUserId ) ;
79- if ( ! target ) throw await logout ( request ) ;
80- return target ;
70+ return impersonatedUserId ;
8171 }
8272 maybeAutoLogout ( request , realUser ) ;
83- return realUser ;
73+ return authUser . userId ;
8474 }
8575
86- if ( ! authUser ?. userId ) return null ;
87- const user = await getUserById ( authUser . userId ) ;
76+ // Non-impersonation fast path: zero DB queries. Auto-logout enforcement
77+ // for this path happens in `getUser`, where we already pay for the User
78+ // row fetch. `requireUserId` callers stay cookie-only.
79+ const authUser = await authenticator . isAuthenticated ( request ) ;
80+ return authUser ?. userId ;
81+ }
82+
83+ export async function getUser ( request : Request ) {
84+ const userId = await getUserId ( request ) ;
85+ if ( userId === undefined ) return null ;
86+ const user = await getUserById ( userId ) ;
8887 if ( ! user ) throw await logout ( request ) ;
89- maybeAutoLogout ( request , user ) ;
88+ // Auto-logout for the non-impersonation path. The impersonation path was
89+ // already enforced inside `getUserId` against the admin's deadline, so
90+ // skip re-checking against the (impersonation target's) row here.
91+ const impersonationId = await getImpersonationId ( request ) ;
92+ if ( ! impersonationId ) maybeAutoLogout ( request , user ) ;
9093 return user ;
9194}
9295
0 commit comments