diff --git a/.env.example b/.env.example index 3c899d9d..f60b315a 100644 --- a/.env.example +++ b/.env.example @@ -66,6 +66,9 @@ TRUST_PROXY=true # Application URL (for tracking links) APP_URL=http://localhost:3000 +# CORS Configuration (comma-separated list of allowed origins) +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000 + # GraphQL Complexity Analysis GRAPHQL_MAX_DEPTH=10 GRAPHQL_MAX_COMPLEXITY=1000 diff --git a/src/config/cors.config.ts b/src/config/cors.config.ts new file mode 100644 index 00000000..18cd10ab --- /dev/null +++ b/src/config/cors.config.ts @@ -0,0 +1,64 @@ +import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; + +/** + * CORS configuration for the TeachLink API. + * This configuration restricts access to specific origins, methods, and headers + * to enhance security, especially in production environments. + */ +export const corsConfig: CorsOptions = { + /** + * Defines the allowed origins for CORS requests. + * In production, this should be restricted to specific frontend domains. + * If CORS_ALLOWED_ORIGINS is not set, it defaults to common local development ports. + */ + origin: (origin, callback) => { + const rawOrigins = process.env.CORS_ALLOWED_ORIGINS; + + // Default allowed origins if none are configured + const defaultOrigins = ['http://localhost:3000', 'http://localhost:4000']; + + const allowedOrigins = rawOrigins + ? rawOrigins.split(',').map((o) => o.trim()).filter((o) => o !== '') + : defaultOrigins; + + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin) { + return callback(null, true); + } + + if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + + /** + * Restricts allowed HTTP methods. + */ + methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'], + + /** + * Restricts allowed headers. + * Includes common headers and custom correlation ID headers. + */ + allowedHeaders: [ + 'Content-Type', + 'Accept', + 'Authorization', + 'X-Requested-With', + 'X-HTTP-Method-Override', + 'x-request-id', + 'x-correlation-id', + ], + + /** + * Allows credentials (cookies, authorization headers) to be sent. + */ + credentials: true, + + /** + * Configures how long the results of a preflight request can be cached. + */ + maxAge: 3600, +}; diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts index 7ca3611d..e521d90c 100644 --- a/src/config/env.validation.ts +++ b/src/config/env.validation.ts @@ -128,14 +128,6 @@ export const envValidationSchema = Joi.object({ // Application URL APP_URL: Joi.string().uri().default('http://localhost:3000'), - // API Versioning - API_VERSION_HEADER_NAME: Joi.string().default('X-API-Version'), - API_DEFAULT_VERSION: Joi.string() - .pattern(/^\d+(?:\.0+)?$/) - .default('1'), - API_SUPPORTED_VERSIONS: Joi.string().default('1'), - - // Malware scanning - CLAMAV_URL: Joi.string().uri().optional(), - VIRUSTOTAL_API_KEY: Joi.string().optional(), + // CORS Configuration + CORS_ALLOWED_ORIGINS: Joi.string().default('http://localhost:3000,http://localhost:4000'), }); diff --git a/src/main.ts b/src/main.ts index 96cd87aa..d4138328 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,12 +14,7 @@ import { correlationMiddleware } from './common/utils/correlation.utils'; import { sessionConfig } from './config/cache.config'; import { SESSION_REDIS_CLIENT } from './session/session.constants'; import helmet from 'helmet'; -import { API_VERSIONING_DOCUMENTATION } from './common/modules/api-versioning.module'; -import { - API_VERSION_HEADER, - DEFAULT_API_VERSION, - SUPPORTED_API_VERSIONS, -} from './common/interceptors/api-version.interceptor'; +import { corsConfig } from './config/cors.config'; async function bootstrapWorker() { const logger = new Logger('Bootstrap'); @@ -97,7 +92,7 @@ async function bootstrapWorker() { // TimeoutInterceptor is now provided globally via APP_INTERCEPTOR in AppModule // ─── CORS ───────────────────────────────────────────────────────────────── - app.enableCors(); + app.enableCors(corsConfig); // ─── Validation ────────────────────────────────────────────────────────── app.useGlobalPipes(