({ ssrOrigin }: { ssrOrigin: string | null })
| 214 | } |
| 215 | |
| 216 | function AuthGate({ ssrOrigin }: { ssrOrigin: string | null }) { |
| 217 | const auth = useAuth(); |
| 218 | const location = useLocation(); |
| 219 | const navigate = useNavigate(); |
| 220 | const isOnboardingRoute = ONBOARDING_PATHS.has(location.pathname); |
| 221 | const isPublicRoute = PUBLIC_PATHS.has(location.pathname); |
| 222 | // The org the URL names (the `{-$orgSlug}` segment), if any. `/account/me` |
| 223 | // is scoped to it, so `auth.organization` IS this org when the caller is a |
| 224 | // member — and `null` when the URL names an org they can't access. |
| 225 | const urlOrgSlug = (useParams({ strict: false }) as { orgSlug?: string }).orgSlug; |
| 226 | // The same slug derived from the PATHNAME instead of the route params: the |
| 227 | // params resolve asynchronously (a fresh load renders once with no orgSlug, |
| 228 | // then again with it), and anything keyed on them remounts on that flap. |
| 229 | // The pathname is synchronously correct on the very first render, and it is |
| 230 | // exactly what the request header derives from (getActiveOrgSlug), so the |
| 231 | // registry scope below can never disagree with the header scope. |
| 232 | const firstSegment = location.pathname.split("/")[1] ?? ""; |
| 233 | const pathnameOrgSlug = isValidOrgSlug(firstSegment) ? firstSegment : null; |
| 234 | |
| 235 | // The SSR gate already bounced fresh org-less document requests to |
| 236 | // /create-org; this catches the MID-SESSION transitions (org deleted, |
| 237 | // membership revoked → /account/me now reports no org). Only for BARE paths: |
| 238 | // an org-less result on a slugged URL is a wrong address (404 below), not a |
| 239 | // reason to send the user to onboarding. |
| 240 | const needsOrgRedirect = |
| 241 | auth.status === "authenticated" && |
| 242 | auth.organization == null && |
| 243 | !urlOrgSlug && |
| 244 | !isOnboardingRoute && |
| 245 | !isPublicRoute; |
| 246 | |
| 247 | React.useEffect(() => { |
| 248 | if (needsOrgRedirect) { |
| 249 | void navigate({ to: "/create-org", replace: true }); |
| 250 | } |
| 251 | }, [needsOrgRedirect, navigate]); |
| 252 | |
| 253 | // The signed-out safety net behind the SSR gate: if a session dies while |
| 254 | // the SPA is already loaded (logout elsewhere, expiry), go to /login the |
| 255 | // same way a fresh document request would — keeping where they were. |
| 256 | const needsLoginRedirect = auth.status === "unauthenticated" && !isPublicRoute; |
| 257 | React.useEffect(() => { |
| 258 | if (needsLoginRedirect) { |
| 259 | window.location.assign(loginPath(`${location.pathname}${location.searchStr}`)); |
| 260 | } |
| 261 | }, [needsLoginRedirect, location.pathname, location.searchStr]); |
| 262 | |
| 263 | if (isPublicRoute) { |
| 264 | return <Outlet />; |
| 265 | } |
| 266 | |
| 267 | // Every state that isn't "authenticated with an org, on a page that wants |
| 268 | // the shell" is a moment between redirects or an edge the gates make |
| 269 | // near-impossible (a verified user whose hint hasn't seeded yet). Neutral |
| 270 | // blank — the one placeholder that's correct whatever happens next. The |
| 271 | // app-shell skeleton this file used to render here is exactly the |
| 272 | // wrong-UI flash the SSR gate + hint exist to prevent. |
| 273 | if (auth.status === "loading" || auth.status === "unauthenticated") { |
nothing calls this directly
no test coverage detected