- 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]>
112 lines
3.1 KiB
TypeScript
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);
|
|
});
|