Files
jorgecuadros-platform/apps/api/src/main.ts
T
rmancinasandClaude Opus 5 14c6183aa2
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m55s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m35s
feat(deploy): derive the API origin from the page, not from a pinned env var
The browser hard-required API_ORIGIN, so every move of the server — tailnet
today, the 192.168.1.0 office LAN later, a temporary demo domain in between —
meant editing the deploy env and redeploying. Worse, an http:// API origin on a
page served over TLS is blocked outright as mixed active content, which is what
broke the demo on https://jorgecuadros.freakma.com.

The browser now derives the origin from window.location the way a PHP app
would: same host on port 3001 over plain HTTP, or the same-origin /api path
under https (the reverse proxy strips the prefix). API_ORIGIN survives as an
optional override for a deployment that genuinely splits the two hosts, and SSR
still reads process.env because a derived origin is browser-only.

WEB_ORIGIN becomes a comma-separated list to match: one deployment is now
reached under several origins, and a credentialed fetch from an unlisted one
gets no CORS headers and fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:55:32 -07:00

82 lines
3.3 KiB
TypeScript

import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import * as session from "express-session";
import * as passport from "passport";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Every request body is validated and stripped of unknown fields before it
// reaches a controller — this is the structural replacement for the old
// app's complete lack of input validation (src/core/db.php took every
// $_POST field straight into a SQL string).
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret) {
throw new Error("SESSION_SECRET must be set (see .env.example)");
}
// Whether the session cookie carries the Secure flag. This CANNOT simply
// follow NODE_ENV: express-session silently declines to send a Secure cookie
// over a plain-HTTP connection, so a production image served over http://ial
// issues no cookie at all. Login then returns 200 with a user, no session is
// established, every later request 403s, and the UI loops back to /login —
// which is exactly what happened on the first galactus deploy.
//
// Leave it ON wherever the app is reached over TLS. Turn it OFF only for a
// deployment that is HTTP but reached over an already-encrypted transport
// (the galactus install is Tailscale-only, so WireGuard encrypts the wire).
// Behind a TLS-terminating proxy, set trust proxy instead of turning this off.
// An EMPTY value counts as unset, not as "false". Compose interpolation turns
// an absent `${SESSION_COOKIE_SECURE:-}` into the empty string, so testing
// `!== undefined` here would silently drop the Secure flag on any deployment
// that merely passes the variable through without setting it.
const cookieSecureRaw = process.env.SESSION_COOKIE_SECURE;
const cookieSecure = cookieSecureRaw
? cookieSecureRaw === "true"
: process.env.NODE_ENV === "production";
app.use(
session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: cookieSecure,
maxAge: 1000 * 60 * 60 * 8, // 8-hour session, matches a staff workday
},
})
);
app.use(passport.initialize());
app.use(passport.session());
// 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);
}
bootstrap();