mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(auth): correct callback URL resolution across SSR and hydration (#6217)
* fix(sso): derive the SSO callback URL during render
`callbackUrl` was seeded into `useState('/workspace')` and overwritten from
a `useEffect` that read `searchParams`, so the first painted frame always
carried the default. On any deep link with `?callbackUrl=`, the "Sign in
with email" and "Sign up" links briefly pointed at `/workspace` instead of
the requested destination, and a click landing in that window navigated to
the wrong place.
- derive `callbackUrl` from `searchParams` during render; the validation
gate is unchanged, so an off-origin or malformed value still falls back
to `/workspace`
- keep the warning for a rejected value in an effect, now keyed on the
param itself rather than the `searchParams` object, so it fires once per
actual change instead of once per identity change
- add first-frame tests via `renderToString`, which runs no effects and so
pins exactly the window the old code got wrong
* fix(auth): resolve callback URLs against the app origin server-side
`validateCallbackUrl` compared against a sentinel base
(`https://callback-url-validator.invalid`) when `window` was undefined, so
the server rejected every absolute URL — including the same-origin ones the
function documents as valid. A component deriving a callback URL during
render therefore produced one destination in the SSR markup and a different
one after hydration.
The exposure was not new to the SSO form: `login-form.tsx` and
`signup-form.tsx` already derive their callback URL during render on
`force-dynamic` pages, so both carried the same divergence.
- resolve against the deployment's own origin server-side, so the server
reaches the same verdict the browser will after hydration
- fall back to the sentinel when the app URL is unset or unparseable, which
keeps the server fail-closed: absolute URLs are rejected, as before
- cover the absolute same-origin case and the unset-app-URL fallback in the
existing suite; all 15 open-redirect rejection cases are unchanged
* fix(env): drop the getBaseUrl browser-origin fallback
The fallback was added in #6214 as a safety net while the real cause — the
hosted env script losing its `beforeInteractive` strategy — was fixed in the
same PR. With the injection ordering restored, `window.__ENV` is populated
before hydration, so the fallback is unreachable in any correctly configured
deployment.
Guessing the origin was also unsafe in the one case it could still fire. An
opaque origin — a sandboxed iframe, and `/chat/*` is deliberately embeddable
— serializes to the string `'null'`, which is truthy, so `getBaseUrl()` would
have returned `'null'` and every call site would have silently built
`null/api/...`. A throw surfaces the misconfiguration instead of encoding it
into request URLs.
- restore the unconditional throw when NEXT_PUBLIC_APP_URL is unset or blank
- mirror it back in the shared testing mock
- flip the two fallback tests to assert the throw, keeping whitespace-only
coverage
Server-side behavior is unchanged: there was never a `window` to fall back
to, so callers that already guard `getBaseUrl()` (`getBaseDomain`,
`validateCallbackUrl`) keep their existing fail-closed paths.
This commit is contained in:
@@ -26,18 +26,14 @@ function hasHttpProtocol(url: string): boolean {
|
||||
|
||||
function getBaseUrlImpl(): string {
|
||||
const baseUrl = readEnv('NEXT_PUBLIC_APP_URL')?.trim()
|
||||
if (baseUrl) {
|
||||
// Mirrors the real module: protocol-less values get https:// under isProd.
|
||||
const protocol = envFlagsMock.isProd ? 'https://' : 'http://'
|
||||
return hasHttpProtocol(baseUrl) ? baseUrl : `${protocol}${baseUrl}`
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly'
|
||||
)
|
||||
}
|
||||
// Mirrors the real module: the browser falls back to its own origin, only
|
||||
// server-side (no `window`) callers throw.
|
||||
const browserOrigin = getBrowserOriginImpl()
|
||||
if (browserOrigin) return browserOrigin
|
||||
throw new Error(
|
||||
'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly'
|
||||
)
|
||||
// Mirrors the real module: protocol-less values get https:// under isProd.
|
||||
const protocol = envFlagsMock.isProd ? 'https://' : 'http://'
|
||||
return hasHttpProtocol(baseUrl) ? baseUrl : `${protocol}${baseUrl}`
|
||||
}
|
||||
|
||||
function getInternalApiBaseUrlImpl(): string {
|
||||
|
||||
Reference in New Issue
Block a user