## Summary - trust the new Railway Go backend in the standalone Auth UI - preserve exact-origin validation for untrusted redirects - document the root cause, delivery plan, and production validation path ## Root cause The Auth UI rejected `api_server_url=https://airi-server-next.up.railway.app`, fell back to the legacy API, and surfaced `Load failed` at the browser CORS boundary. ## Test plan - focused bootstrap-context tests: 6/6 - complete Auth UI suite: 27/27 - `vue-tsc --noEmit` - production Vite build - emitted bundle contains the exact new backend origin
119 lines
3.2 KiB
TypeScript
119 lines
3.2 KiB
TypeScript
import { SERVER_URL } from '@proj-airi/stage-ui/libs/server'
|
|
|
|
export interface ServerAuthBootstrapContext {
|
|
apiServerUrl: string
|
|
currentUrl: string
|
|
oidcCallback?: {
|
|
code: string
|
|
error: string
|
|
errorDescription: string
|
|
state: string
|
|
}
|
|
}
|
|
|
|
const SCRIPT_ID = 'airi-server-auth-context'
|
|
const API_SERVER_URL_QUERY_PARAM = 'api_server_url'
|
|
|
|
const TRUSTED_STANDALONE_API_SERVER_ORIGINS = [
|
|
'https://api.airi.build',
|
|
'https://airi-server-dev.up.railway.app',
|
|
'https://airi-server-next.up.railway.app',
|
|
]
|
|
|
|
const TRUSTED_HTTPS_API_SERVER_HOSTS = new Map(
|
|
TRUSTED_STANDALONE_API_SERVER_ORIGINS.map((origin) => {
|
|
const url = new URL(origin)
|
|
return [url.hostname, origin]
|
|
}),
|
|
)
|
|
|
|
const TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS = [
|
|
/^http:\/\/localhost(:\d+)?$/,
|
|
/^http:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
/^https:\/\/localhost(:\d+)?$/,
|
|
/^https:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
]
|
|
|
|
let cachedContext: ServerAuthBootstrapContext | null | undefined
|
|
|
|
export function getServerAuthBootstrapContext(): ServerAuthBootstrapContext | null {
|
|
if (cachedContext !== undefined)
|
|
return cachedContext
|
|
|
|
const element = document.getElementById(SCRIPT_ID)
|
|
if (!element) {
|
|
cachedContext = resolveStandaloneServerAuthContext(window.location.href, SERVER_URL)
|
|
return cachedContext
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(element.textContent ?? '') as Partial<ServerAuthBootstrapContext>
|
|
cachedContext = {
|
|
apiServerUrl: parsed.apiServerUrl ?? SERVER_URL,
|
|
currentUrl: parsed.currentUrl ?? window.location.href,
|
|
oidcCallback: parsed.oidcCallback,
|
|
}
|
|
return cachedContext
|
|
}
|
|
catch {
|
|
cachedContext = resolveStandaloneServerAuthContext(window.location.href, SERVER_URL)
|
|
return cachedContext
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves API-server context carried by server redirects into static auth UI.
|
|
*
|
|
* Use when:
|
|
* - The standalone auth UI serves more than one AIRI environment from the same
|
|
* Pages deployment, such as production `accounts.airi.build` handling server-dev
|
|
* OIDC redirects.
|
|
*
|
|
* Expects:
|
|
* - The server-owned `/auth/*` redirect sets `api_server_url`.
|
|
* - Only known AIRI API origins and localhost development origins are accepted.
|
|
*
|
|
* Returns:
|
|
* - A bootstrap context using the trusted API origin, or null when no trusted
|
|
* override is present.
|
|
*/
|
|
export function resolveStandaloneServerAuthContext(currentUrl: string, fallbackApiServerUrl: string): ServerAuthBootstrapContext | null {
|
|
const url = new URL(currentUrl)
|
|
const apiServerUrl = normalizeTrustedApiServerUrl(
|
|
url.searchParams.get(API_SERVER_URL_QUERY_PARAM),
|
|
)
|
|
|
|
if (!apiServerUrl)
|
|
return null
|
|
|
|
return {
|
|
apiServerUrl: apiServerUrl ?? fallbackApiServerUrl,
|
|
currentUrl,
|
|
}
|
|
}
|
|
|
|
function normalizeTrustedApiServerUrl(value: string | null): string | null {
|
|
if (!value)
|
|
return null
|
|
|
|
try {
|
|
const url = new URL(value)
|
|
const normalizedHttpsOrigin = TRUSTED_HTTPS_API_SERVER_HOSTS.get(url.hostname)
|
|
if (normalizedHttpsOrigin)
|
|
return normalizedHttpsOrigin
|
|
|
|
const origin = url.origin
|
|
|
|
if (TRUSTED_STANDALONE_API_SERVER_ORIGINS.includes(origin))
|
|
return origin
|
|
|
|
if (TRUSTED_LOCAL_API_SERVER_ORIGIN_PATTERNS.some(pattern => pattern.test(origin)))
|
|
return origin
|
|
|
|
return null
|
|
}
|
|
catch {
|
|
return null
|
|
}
|
|
}
|