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
55 changes: 48 additions & 7 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Audited environment source of truth
# Audited against the backend source (including auth, database, payments and
# payment adapters, Soroban/Stellar, wallets, credits, retention, common
# infrastructure, and backend/scripts). Sections group variables by the
# subsystem that reads them.
# Values marked [RESERVED/UNUSED] are retained for compatibility but are not
# read by the current backend source.

# Runtime, HTTP security, and orchestrator probes
# PORT and NODE_ENV are read by src/main.ts and the auth cookie settings.
PORT=6000
NODE_ENV=development
# Exact, comma-separated browser origins. Whitespace is ignored. In development
# and test, an empty value falls back to localhost:3000 and localhost:3001; in
# production an unset or empty value is an empty allowlist and denies all
# cross-origin requests. Read by src/main.ts.
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# Bounded TypeORM readiness ping timeout in milliseconds. Read by
# src/health/health.controller.ts; invalid values use 3000 and values are capped
# at 30000.
HEALTH_CHECK_TIMEOUT_MS=3000
# Maximum time to drain HTTP requests and Nest lifecycle hooks before forcing
# process exit. Read by src/main.ts via common/graceful-shutdown.ts; invalid or
# non-positive values use the 30000ms default.
GRACEFUL_SHUTDOWN_TIMEOUT_MS=30000

# OpenTelemetry distributed tracing
# Tracing is OFF by default. Manual payment spans are safe no-ops while
Expand All @@ -10,12 +33,21 @@ OTEL_SERVICE_NAME=managehub-backend
# Optional — defaults to http://localhost:4318/v1/traces.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces

# Database
# Read by the TypeORM application configuration in src/app.module.ts and by
# src/database/data-source.ts for migrations and audit tooling.
DATABASE_NAME=your_database_name
DATABASE_PASSWORD=your_database_password
DATABASE_USERNAME=your_database_username
DATABASE_PORT=5432
DATABASE_HOST=your_database_host

# Auth / JWT
# JWT_SECRET is read by src/auth/auth.module.ts.
JWT_SECRET=your_jwt_secret_here
# [RESERVED/UNUSED] The current backend issues and verifies one JWT and does
# not read these legacy expiration names; retained so older setup files remain
# recognizable.
JWT_ACCESS_EXPIRATION=86400000
JWT_REFRESH_EXPIRATION=604800000

Expand All @@ -24,32 +56,38 @@ JWT_REFRESH_EXPIRATION=604800000
# The guard + socket gateway verify these tokens with JWT_SECRET.

# Email Configuration
# Production SMTP Settings (Gmail, SendGrid, AWS SES, etc.)
# Production SMTP settings read by ReconciliationService when it sends a
# manual-review queue alert (Gmail, SendGrid, AWS SES, etc.).
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
SMTP_FROM_EMAIL=noreply@yourdomain.com

# Mailtrap (Development/Testing Only)
# [RESERVED/UNUSED] Mailtrap (Development/Testing Only). No backend source
# currently reads these names; outbound mail uses the SMTP_* variables above.
MAILTRAP_HOST=smtp.mailtrap.io
MAILTRAP_PORT=2525
MAILTRAP_USER=your_mailtrap_user
MAILTRAP_PASS=your_mailtrap_pass

# Company Information
# Company and frontend information
# COMPANY_NAME and SUPPORT_EMAIL are read by the reconciliation alert email;
# FRONTEND_URL is the separate Socket.IO gateway origin in payments.gateway.ts.
COMPANY_NAME=ManageHub
SUPPORT_EMAIL=support@yourdomain.com
FRONTEND_URL=http://localhost:3000

# Redis Configuration (for Bull Queue)
# Read by the Bull root configuration and AppService's dependency check.
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# Email Rate Limiting
# [RESERVED/UNUSED] No current backend source reads this legacy rate-limit
# setting; retained as a placeholder for a future mail policy.
EMAIL_RATE_LIMIT_PER_HOUR=100

# Email is sent over plain SMTP (nodemailer) via the vars above; no single
Expand All @@ -60,8 +98,10 @@ EMAIL_RATE_LIMIT_PER_HOUR=100
# On-chain escrow for long-term (MONTHLY/QUARTERLY/YEARLY) bookings.
# Leave SOROBAN_ENABLED=false (or unset) to skip escrow cleanly — no Stellar
# config is required and long-term bookings confirm normally.
# When SOROBAN_ENABLED=true, ALL of the variables below are required or the
# app will refuse to start with an error naming the missing variable(s).
# When SOROBAN_ENABLED=true, the required STELLAR_* identity keys are checked
# by payments/soroban/soroban-config.ts; the app refuses to start with an error
# naming any missing required key. Poll and RPC settings are read by the
# Soroban rail and submission processor.
SOROBAN_ENABLED=false
STELLAR_ESCROW_CONTRACT_ID=your_stellar_escrow_contract_id
STELLAR_SECRET_KEY=your_stellar_secret_key
Expand All @@ -79,7 +119,8 @@ STELLAR_RPC_URLS=https://soroban-testnet.stellar.org,https://rpc-futurenet.stell
SOROBAN_POLL_TIMEOUT_MS=15000
SOROBAN_POLL_INTERVAL_MS=2000
# Scheduled Jobs
# Minutes a PENDING booking may wait for payment before it is auto-cancelled
# [RESERVED/UNUSED] No current backend source reads this legacy booking TTL;
# payment expiry is configured by PAYMENT_INITIATED_TTL_MINUTES below.
BOOKING_PAYMENT_TTL_MINUTES=120

# Payments module (issue #1570)
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@nestjs/platform-socket.io": "^10.4.22",
"@nestjs/schedule": "^6.0.1",
"@nestjs/swagger": "^7.4.2",
"@nestjs/terminus": "^10.2.3",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^10.4.22",
Expand Down
7 changes: 6 additions & 1 deletion backend/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export class AppController {
private readonly metrics: MetricsService,
) {}

@Get('health')
/**
* Legacy combined dependency view retained for operators that used the old
* health route. Orchestrator probes use the dedicated liveness and readiness
* routes in HealthController, so this route must not shadow `/health`.
*/
@Get('health/dependencies')
async getHealth(@Res({ passthrough: true }) res: Response) {
const health = await this.appService.getHealth();
if (health.status !== 'ok') {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { WalletsModule } from './wallets/wallets.module';
import { CreditsModule } from './credits/credits.module';
import { RetentionModule } from './retention/retention.module';
import { AdminAuditModule } from './admin-audit/admin-audit.module';
import { HealthModule } from './health/health.module';
import { MetricsService } from './common/metrics.service';
import { MetricsController } from './common/metrics.controller';
import { RequestContextMiddleware } from './common/request-context.middleware';
Expand Down Expand Up @@ -77,6 +78,7 @@ import { TracingMiddleware } from './common/tracing.middleware';
// Structured audit trail for admin actions (issue #1612). Consumed by
// the payments and credits admin controllers; read via /admin/audit.
AdminAuditModule,
HealthModule,
],
controllers: [AppController, MetricsController],
providers: [
Expand Down
58 changes: 58 additions & 0 deletions backend/src/common/cors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
DEVELOPMENT_AND_TEST_CORS_ORIGINS,
isOriginAllowed,
parseAllowedOrigins,
resolveAllowedOrigins,
} from './cors';

describe('CORS origin helpers', () => {
it('parses comma-separated origins while trimming and dropping empty entries', () => {
expect(
parseAllowedOrigins(
' https://app.example.com, ,https://admin.example.com, ',
),
).toEqual([
'https://app.example.com',
'https://admin.example.com',
]);
});

it('uses the documented local origins for development and test defaults', () => {
expect(resolveAllowedOrigins({ NODE_ENV: 'development' })).toEqual([
...DEVELOPMENT_AND_TEST_CORS_ORIGINS,
]);
expect(resolveAllowedOrigins({ NODE_ENV: 'test' })).toEqual([
...DEVELOPMENT_AND_TEST_CORS_ORIGINS,
]);
});

it('resolves to an empty allowlist in production when configuration is empty', () => {
expect(resolveAllowedOrigins({ NODE_ENV: 'production' })).toEqual([]);
expect(
resolveAllowedOrigins({
NODE_ENV: 'production',
CORS_ORIGINS: ' , ',
}),
).toEqual([]);
});

it('prefers configured origins over environment defaults', () => {
expect(
resolveAllowedOrigins({
NODE_ENV: 'development',
CORS_ORIGINS: 'https://app.example.com',
}),
).toEqual(['https://app.example.com']);
});

it('allows originless requests but only exact allowlisted origins', () => {
const allowed = ['https://app.example.com'];

expect(isOriginAllowed(undefined, allowed)).toBe(true);
expect(isOriginAllowed('https://app.example.com', allowed)).toBe(true);
expect(isOriginAllowed('https://app.example.com.evil.test', allowed)).toBe(
false,
);
expect(isOriginAllowed('https://other.example.com', allowed)).toBe(false);
});
});
60 changes: 60 additions & 0 deletions backend/src/common/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Origins used by the local frontend during development and automated tests.
*
* Keep the development defaults together so a change to the documented local
* frontend ports cannot accidentally make one environment more permissive
* than the other.
*/
export const DEVELOPMENT_AND_TEST_CORS_ORIGINS = [
'http://localhost:3000',
'http://localhost:3001',
] as const;

/**
* Parse a comma-separated CORS_ORIGINS value into exact origins.
*
* Whitespace around entries is insignificant, and empty entries are ignored
* so a trailing comma cannot accidentally broaden or confuse the allowlist.
*/
export function parseAllowedOrigins(value: string | undefined): string[] {
return (value ?? '')
.split(',')
.map((origin) => origin.trim())
.filter((origin) => origin.length > 0);
}

/**
* Resolve the CORS allowlist using the explicit configuration first and the
* environment-specific safe default second.
*
* Development and test may use the two local frontend origins when no value
* is configured. Every other environment, including production, resolves to
* an empty allowlist when CORS_ORIGINS is missing or empty so a deployment
* fails closed rather than inheriting a wildcard policy.
*/
export function resolveAllowedOrigins(env: NodeJS.ProcessEnv): string[] {
const configuredOrigins = parseAllowedOrigins(env.CORS_ORIGINS);
if (configuredOrigins.length > 0) {
return configuredOrigins;
}

if (env.NODE_ENV === 'development' || env.NODE_ENV === 'test') {
return [...DEVELOPMENT_AND_TEST_CORS_ORIGINS];
}

return [];
}

/**
* Decide whether a request origin may receive credentialed CORS headers.
*
* Requests without an Origin header are deliberately allowed: they are
* same-origin/non-browser clients, curl, or health checkers rather than a
* browser cross-origin request. Present origins must match one exact entry.
*/
export function isOriginAllowed(
origin: string | undefined,
allowedOrigins: readonly string[],
): boolean {
return !origin || allowedOrigins.includes(origin);
}
Loading
Loading