Files
stack/apps/api/src/main.ts
T
jason.woltjeandClaude Sonnet 4.5 a2b61d2bff feat(#193): Align authentication mechanism between API and web client
- Update AuthUser type in @mosaic/shared to include workspace fields
- Update AuthGuard to support both cookie-based and Bearer token authentication
- Add /auth/session endpoint for session validation
- Install and configure cookie-parser middleware
- Update CurrentUser decorator to use shared AuthUser type
- Update tests for cookie and token authentication (20 tests passing)

This ensures consistent authentication handling across API and web client,
with proper type safety and support for both web browsers (cookies) and
API clients (Bearer tokens).

Fixes #193

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-03 22:29:42 -06:00

112 lines
3.1 KiB
TypeScript

import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import cookieParser from "cookie-parser";
import { AppModule } from "./app.module";
import { GlobalExceptionFilter } from "./filters/global-exception.filter";
function getPort(): number {
const portEnv = process.env.PORT;
if (portEnv === undefined || portEnv === "") {
return 3001;
}
const port = parseInt(portEnv, 10);
if (isNaN(port)) {
throw new Error(`Invalid PORT environment variable: "${portEnv}". PORT must be a number.`);
}
if (port < 1 || port > 65535) {
throw new Error(
`Invalid PORT environment variable: ${String(port)}. PORT must be between 1 and 65535.`
);
}
return port;
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable cookie parser for session handling
app.use(cookieParser());
// Enable global validation pipe with transformation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: false,
transformOptions: {
enableImplicitConversion: false,
},
})
);
app.useGlobalFilters(new GlobalExceptionFilter());
// Configure CORS for cookie-based authentication
// SECURITY: Cannot use wildcard (*) with credentials: true
const allowedOrigins = [
process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
"http://localhost:3001", // API origin (dev)
"https://app.mosaicstack.dev", // Production web
"https://api.mosaicstack.dev", // Production API
];
app.enableCors({
origin: (
origin: string | undefined,
callback: (err: Error | null, allow?: boolean) => void
): void => {
// Allow requests with no origin (e.g., mobile apps, Postman)
if (!origin) {
callback(null, true);
return;
}
// Check if origin is in allowed list
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true, // Required for cookie-based authentication
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
exposedHeaders: ["Set-Cookie"],
maxAge: 86400, // 24 hours - cache preflight requests
});
const port = getPort();
await app.listen(port);
console.log(`API running on http://localhost:${String(port)}`);
}
bootstrap().catch((err: unknown) => {
const isProduction = process.env.NODE_ENV === "production";
const errorMessage = err instanceof Error ? err.message : String(err);
const errorStack = err instanceof Error ? err.stack : undefined;
if (isProduction) {
console.error(
JSON.stringify({
level: "error",
message: "Failed to start application",
error: errorMessage,
timestamp: new Date().toISOString(),
})
);
} else {
console.error("Failed to start application:", errorMessage);
if (errorStack) {
console.error(errorStack);
}
}
process.exit(1);
});