Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ REDIS_PORT=6379
USE_REDIS_CACHE=true

# Application Configuration
# Allowed values: development | production | test
NODE_ENV=development
PORT=3001
APP_NAME=STATION BACKEND

# CORS Configuration
# Production: https://station.drdnt.org
ALLOWED_ORIGIN=http://localhost:5173

Comment thread
GitAddRemote marked this conversation as resolved.
# UEX Sync Configuration
UEX_SYNC_ENABLED=true
UEX_CATEGORIES_SYNC_ENABLED=true
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"class-validator": "^0.14.2",
"dotenv": "^16.4.5",
"figlet": "^1.8.0",
"helmet": "^8.0.0",
"passport": "^0.7.0",
"passport-http-bearer": "^1.0.1",
"passport-jwt": "^4.0.1",
Expand Down
60 changes: 54 additions & 6 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as figlet from 'figlet';
import * as dotenv from 'dotenv';
import helmet from 'helmet';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

dotenv.config();
Expand All @@ -13,14 +15,60 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Application configuration
const port = process.env.PORT || 3001;
const appName = process.env.APP_NAME || 'STATION BACKEND';
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') ?? 3001;
const appName = configService.get<string>('APP_NAME') ?? 'STATION BACKEND';

const nodeEnv = configService.get<string>('NODE_ENV');
const validNodeEnvs = ['production', 'development', 'test'] as const;
if (
!nodeEnv ||
!validNodeEnvs.includes(nodeEnv as (typeof validNodeEnvs)[number])
) {
throw new Error(
`Invalid NODE_ENV value: ${nodeEnv ?? 'undefined'}. Expected one of: ${validNodeEnvs.join(', ')}`,
);
}
const isProduction = nodeEnv === 'production';

Comment thread
GitAddRemote marked this conversation as resolved.
// ASCII Art for Application Name
console.log(figlet.textSync(appName, { horizontalLayout: 'full' }));

// Enable CORS (if needed for APIs)
app.enableCors();
// Security headers — Swagger UI requires 'unsafe-inline' for scripts/styles,
// but Swagger is disabled in production so production uses a strict CSP.
// frameguard and hsts are set explicitly to meet security requirements.
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: [`'self'`],
baseUri: [`'self'`],
Comment thread
GitAddRemote marked this conversation as resolved.
objectSrc: [`'none'`],
scriptSrc: isProduction ? [`'self'`] : [`'self'`, `'unsafe-inline'`],
styleSrc: isProduction ? [`'self'`] : [`'self'`, `'unsafe-inline'`],
imgSrc: [`'self'`, 'data:', 'https:'],
fontSrc: [`'self'`, 'https:', 'data:'],
},
},
frameguard: { action: 'deny' },
hsts: isProduction
? { maxAge: 31536000, includeSubDomains: true }
: false,
}),
);

// CORS — require ALLOWED_ORIGIN in production; fall back to localhost in dev.
// Use || (not ??) so a whitespace-only value is treated the same as unset.
const allowedOrigin =
configService.get<string>('ALLOWED_ORIGIN')?.trim() || undefined;
if (!allowedOrigin && isProduction) {
throw new Error('Missing required environment variable: ALLOWED_ORIGIN');
}
app.enableCors({
origin: allowedOrigin ?? 'http://localhost:5173',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
});

// Global Validation Pipe
app.useGlobalPipes(
Expand All @@ -35,7 +83,7 @@ async function bootstrap() {
app.useGlobalFilters(new HttpExceptionFilter());

// Swagger/OpenAPI Documentation — development only
if (process.env.NODE_ENV !== 'production') {
if (!isProduction) {
const config = new DocumentBuilder()
.setTitle('Station API')
.setDescription(
Expand Down Expand Up @@ -84,7 +132,7 @@ async function bootstrap() {
`🚀 Application '${appName}' is running on: http://localhost:${port}`,
'Bootstrap',
);
if (process.env.NODE_ENV !== 'production') {
if (!isProduction) {
Logger.log(
`📚 Swagger documentation available at: http://localhost:${port}/api/docs`,
'Bootstrap',
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading