diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 6298d51..532deb9 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -60,7 +60,19 @@ async function bootstrap() { app.use(passport.initialize()); app.use(passport.session()); - app.enableCors({ credentials: true, origin: process.env.WEB_ORIGIN ?? "http://localhost:3000" }); + // The same deployment is reached under several origins — the office LAN IP, + // the tailnet name, the demo domain — and the browser derives the API origin + // from whichever one served the page (apps/web/src/lib/api.ts). So WEB_ORIGIN + // is a comma-separated LIST, not a single value. A request whose Origin is + // not listed gets no CORS headers and the credentialed fetch fails, so add an + // entry when a new way of reaching the app is introduced. Same-origin setups + // (web and API behind one proxy) never hit CORS at all. + const webOrigins = (process.env.WEB_ORIGIN ?? "http://localhost:3000") + .split(",") + .map((o) => o.trim()) + .filter(Boolean); + + app.enableCors({ credentials: true, origin: webOrigins }); const port = process.env.PORT ? Number(process.env.PORT) : 3001; await app.listen(port); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 4c98cd8..158b917 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -8,19 +8,18 @@ export const metadata = { "Plataforma interna unificada de clientes, servicios y seguros.", }; -// The browser talks to the API cross-origin, so it needs the API URL at -// runtime. NEXT_PUBLIC_* would bake it at build time (one URL per image); we -// want the URL to come from the deploy .env instead. So read it here on the -// server per request and inject it as window.__API_ORIGIN__ (see lib/api.ts). -// force-dynamic guarantees process.env is read at request time, never baked -// into a static prerender. +// API_ORIGIN is an OPTIONAL override, read here on the server per request and +// injected as window.__API_ORIGIN__ (see lib/api.ts). NEXT_PUBLIC_* would bake +// it at build time (one URL per image); reading it here keeps one image usable +// anywhere. Left unset — the normal case — this injects the empty string and +// lib/api.ts derives the origin from window.location instead, so the app +// follows the server when it moves without an env edit. force-dynamic +// guarantees process.env is read at request time, never baked into a static +// prerender. export const dynamic = "force-dynamic"; export default function RootLayout({ children }: { children: ReactNode }) { - const apiOrigin = - process.env.API_ORIGIN ?? - process.env.NEXT_PUBLIC_API_ORIGIN ?? - "http://localhost:3001"; + const apiOrigin = process.env.API_ORIGIN ?? ""; // Same reason as the API origin: read on the server per request so the built // image is not pinned to one build identity in its client bundle. const build = readBuildInfoFromEnv(); diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 5450f06..9b5293f 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -82,15 +82,26 @@ import type { UserRow, } from "./types"; -// Resolve the API origin at runtime, not build time. In the browser it comes -// from window.__API_ORIGIN__, injected server-side by the root layout from the -// deploy .env (API_ORIGIN) — so one built image serves any deployment. On the -// server (SSR) read process.env directly. NEXT_PUBLIC_API_ORIGIN stays as the -// dev/build fallback. +// Resolve the API origin at runtime, not build time — so one built image serves +// any deployment and the app follows the box when it moves (tailnet today, +// 192.168.1.x office LAN later) with no config change. +// +// In the browser, derive the origin from the page's own location, the way a PHP +// app would. An explicit API_ORIGIN (injected as window.__API_ORIGIN__ by the +// root layout) still wins when a deployment genuinely splits the two hosts. +// On the server (SSR) read process.env directly — a derived origin is +// browser-only, and "/api" is not fetchable server-side. function resolveApiOrigin(): string { if (typeof window !== "undefined") { const injected = (window as { __API_ORIGIN__?: string }).__API_ORIGIN__; if (injected) return injected; + const { protocol, hostname } = window.location; + // Over TLS the API must share the page's origin or the browser blocks the + // call as mixed active content. The reverse proxy maps /api to the API. + if (protocol === "https:") return "/api"; + // Plain HTTP: same host, API port. 3001 is the port the API container + // publishes everywhere (deploy/galactus/jorgecuadros-app.compose.yml). + return `http://${hostname}:3001`; } return ( process.env.API_ORIGIN ?? diff --git a/deploy/galactus/jorgecuadros-app.compose.yml b/deploy/galactus/jorgecuadros-app.compose.yml index 3a5c3a1..7d4ef5c 100644 --- a/deploy/galactus/jorgecuadros-app.compose.yml +++ b/deploy/galactus/jorgecuadros-app.compose.yml @@ -132,8 +132,12 @@ services: dns_search: - ${TAILNET_SUFFIX:-tail01aa2.ts.net} environment: - # Public API URL the browser calls (injected at runtime, see layout.tsx). - API_ORIGIN: ${API_ORIGIN:?API_ORIGIN must be set} + # OPTIONAL override of the API URL the browser calls (injected at runtime, + # see layout.tsx). Leave it unset: the browser then derives the origin + # from the page it loaded — same host on port 3001 over plain HTTP, or + # /api behind a TLS-terminating proxy. Set it only when the API really + # lives on a different host than the web app. + API_ORIGIN: ${API_ORIGIN:-} ports: - "${WEB_PORT:-3000}:3000" depends_on: