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
10 changes: 6 additions & 4 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
DATABASE_USER: test
DATABASE_PASSWORD: test
DATABASE_NAME: station_test
JWT_SECRET: test-secret-key-for-ci
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
run: pnpm run lint

unit-tests:
Expand Down Expand Up @@ -75,7 +75,8 @@ jobs:
DATABASE_USER: test
DATABASE_PASSWORD: test
DATABASE_NAME: station_test
JWT_SECRET: test-secret-key-for-ci
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
USE_REDIS_CACHE: 'false'
run: pnpm test --coverage --passWithNoTests

- name: Upload coverage reports
Expand Down Expand Up @@ -131,7 +132,8 @@ jobs:
DATABASE_USER: test
DATABASE_PASSWORD: test
DATABASE_NAME: station_test
JWT_SECRET: test-secret-key-for-ci
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
USE_REDIS_CACHE: 'false'
run: pnpm run test:e2e

build:
Expand Down Expand Up @@ -165,7 +167,7 @@ jobs:
DATABASE_USER: test
DATABASE_PASSWORD: test
DATABASE_NAME: station_test
JWT_SECRET: test-secret-key-for-ci
JWT_SECRET: test-secret-key-for-ci-minimum-32-chars
run: pnpm run build

- name: Upload build artifacts
Expand Down
30 changes: 16 additions & 14 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
# Database Configuration
# ─── Application ───────────────────────────────────────────────────────────────
NODE_ENV=development
PORT=3001
APP_NAME=STATION BACKEND

# ─── JWT ────────────────────────────────────────────────────────────────────────
# Required. Minimum 32 characters. Use a strong random value in production.
JWT_SECRET=your-super-secret-jwt-key-minimum-32-chars

# ─── Database ───────────────────────────────────────────────────────────────────
DATABASE_HOST=localhost
DATABASE_PORT=5433
DATABASE_USER=stationDbUser
DATABASE_PASSWORD=stationDbPassword1
DATABASE_NAME=stationDb

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production

# Redis Configuration
# ─── Redis ──────────────────────────────────────────────────────────────────────
REDIS_HOST=localhost
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
# ─── CORS / Frontend ────────────────────────────────────────────────────────────
# Origin allowed by CORS (frontend URL). Must be a valid URI.
ALLOWED_ORIGIN=http://localhost:5173
# Base URL used to construct password reset links sent via email.
FRONTEND_URL=http://localhost:5173

# UEX Sync Configuration
# ─── UEX Sync ───────────────────────────────────────────────────────────────────
UEX_SYNC_ENABLED=true
UEX_CATEGORIES_SYNC_ENABLED=true
UEX_ITEMS_SYNC_ENABLED=true
Expand Down
2 changes: 1 addition & 1 deletion backend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ DATABASE_PORT=5433
DATABASE_USER=stationDbUser
DATABASE_PASSWORD=stationDbPassword1
DATABASE_NAME=stationDb
JWT_SECRET=test-jwt-secret-for-e2e-tests-only
JWT_SECRET=test-jwt-secret-for-e2e-tests-only-32chars
PORT=3000
APP_NAME=STATION BACKEND TEST
USE_REDIS_CACHE=false
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"dotenv": "^16.4.5",
"figlet": "^1.8.0",
"helmet": "^8.0.0",
"joi": "^17.13.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
Expand Down
3 changes: 3 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UserOrganizationRolesModule } from './modules/user-organization-roles/u
import { PermissionsModule } from './modules/permissions/permissions.module';
import { AppService } from './app.service';
import { AppController } from './app.controller';
import { envValidationSchema } from './config/env.validation';
import { DatabaseSeederModule } from './database/seeds/database-seeder.module';
import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
import { GamesModule } from './modules/games/games.module';
Expand All @@ -35,6 +36,8 @@ if (!isTest) {
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: envValidationSchema,
validationOptions: { abortEarly: false },
Comment thread
GitAddRemote marked this conversation as resolved.
}),
...conditionalImports,
CacheModule.registerAsync({
Expand Down
58 changes: 58 additions & 0 deletions backend/src/config/env.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as Joi from 'joi';

export const envValidationSchema = Joi.object({
// Application
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3001),
APP_NAME: Joi.string().default('STATION BACKEND'),

// JWT
JWT_SECRET: Joi.string().min(32).required(),

Comment thread
GitAddRemote marked this conversation as resolved.
Comment thread
GitAddRemote marked this conversation as resolved.
// Database
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().default(5433),
DATABASE_USER: Joi.string().required(),
DATABASE_PASSWORD: Joi.string().required(),
DATABASE_NAME: Joi.string().required(),

// Redis
REDIS_HOST: Joi.string().default('localhost'),
REDIS_PORT: Joi.number().default(6379),
USE_REDIS_CACHE: Joi.string().valid('true', 'false').default('true'),

// CORS / Frontend — required in production; default to localhost in dev/test
ALLOWED_ORIGIN: Joi.string()
.uri()
.when('NODE_ENV', {
is: 'production',
then: Joi.required(),
otherwise: Joi.string().uri().default('http://localhost:5173'),
}),
FRONTEND_URL: Joi.string()
.uri()
.when('NODE_ENV', {
is: 'production',
then: Joi.required(),
otherwise: Joi.string().uri().default('http://localhost:5173'),
}),

// UEX Sync (all optional — service degrades gracefully when disabled)
// Use Joi.boolean() so env strings like 'false' are coerced to actual
// booleans; configService.get<boolean>(...) in the scheduler relies on this.
UEX_SYNC_ENABLED: Joi.boolean().default(false),
UEX_CATEGORIES_SYNC_ENABLED: Joi.boolean().default(false),
UEX_ITEMS_SYNC_ENABLED: Joi.boolean().default(false),
UEX_LOCATIONS_SYNC_ENABLED: Joi.boolean().default(false),
UEX_API_BASE_URL: Joi.string().uri().default('https://uexcorp.space/api/2.0'),
UEX_TIMEOUT_MS: Joi.number().default(60000),
UEX_BATCH_SIZE: Joi.number().default(100),
UEX_CONCURRENT_CATEGORIES: Joi.number().default(3),
UEX_RETRY_ATTEMPTS: Joi.number().default(3),
UEX_BACKOFF_BASE_MS: Joi.number().default(1000),
UEX_RATE_LIMIT_PAUSE_MS: Joi.number().default(2000),
UEX_ENDPOINTS_PAUSE_MS: Joi.number().default(2000),
UEX_API_KEY: Joi.string().allow('').default(''),
});
9 changes: 6 additions & 3 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
// dotenv/config must be the very first import so that process.env is populated
// before any other module is evaluated. Module-level code (e.g. decorator
// arguments and top-level constants) in NestJS modules runs at require() time,
// which is before bootstrap() — moving the load here ensures .env values are
// visible to those expressions during local development.
import 'dotenv/config';
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 cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

dotenv.config();

async function bootstrap() {
const app = await NestFactory.create(AppModule);

Expand Down
7 changes: 7 additions & 0 deletions backend/src/modules/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

describe('AuthController - Password Reset', () => {
let controller: AuthController;
Expand All @@ -25,6 +26,12 @@ describe('AuthController - Password Reset', () => {
provide: AuthService,
useValue: mockAuthService,
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('test'),
},
},
],
}).compile();

Expand Down
8 changes: 6 additions & 2 deletions backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ApiBody,
ApiBearerAuth,
} from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
import { JwtAuthGuard } from './jwt-auth.guard';
Expand All @@ -32,12 +33,15 @@ import {
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
constructor(
private authService: AuthService,
private configService: ConfigService,
) {}

private cookieOptions(maxAge: number) {
return {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
secure: this.configService.get<string>('NODE_ENV') === 'production',
sameSite: 'strict' as const,
path: '/',
maxAge,
Expand Down
2 changes: 1 addition & 1 deletion backend/test/auth-password-reset.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('Auth - Password Reset (e2e)', () => {
isActive: true,
});

// Login to get access token
// Login to get access token cookie
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
Expand Down
46 changes: 45 additions & 1 deletion pnpm-lock.yaml

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

Loading