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>
82 lines
3.3 KiB
TypeScript
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();
|