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