Skip to content

Commit 8b01677

Browse files
authored
Merge pull request #375 from AbayomiCoded/feature/cors-production-allowlist-payment-method-enum
feat: cors production allowlist payment method enum
2 parents 698e255 + 5ef0b77 commit 8b01677

14 files changed

Lines changed: 1564 additions & 126 deletions

nftopia-backend/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ GRAPHQL_INTROSPECTION_ENABLED=true
88
# CORS Configuration
99
CORS_ORIGIN=http://localhost:3001
1010

11+
# CORS Configuration
12+
# Development: comma-separated list of additional origins to allow
13+
# Production: comma-separated list of allowed origins (REQUIRED)
14+
CORS_ALLOWED_ORIGINS=https://nftopia.com,https://app.nftopia.com
15+
CORS_ORIGIN_DEV=http://localhost:3001
16+
1117
# Redis Configuration
1218
REDIS_HOST=localhost
1319
REDIS_PORT=6379

nftopia-backend/src/app.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { TransactionModule } from './modules/transaction/transaction.module';
3434
import { AuditModule } from './common/audit/audit.module';
3535
import { MetricsModule } from './common/metrics/metrics.module';
3636
import { SocialModule } from './modules/social/social.module';
37+
import { PaymentModule } from './modules/payment/payment.module';
38+
// import { CorsConfig } from './config/cors.config';
3739

3840
@Module({
3941
imports: [
@@ -143,6 +145,7 @@ import { SocialModule } from './modules/social/social.module';
143145
AuditModule,
144146
MetricsModule,
145147
SocialModule,
148+
PaymentModule,
146149
],
147150
controllers: [AppController],
148151
providers: [
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { BadRequestException, Logger } from '@nestjs/common';
2+
3+
export interface CorsConfig {
4+
origins: string[];
5+
credentials: boolean;
6+
methods: string[];
7+
allowedHeaders: string[];
8+
exposedHeaders: string[];
9+
maxAge: number;
10+
}
11+
12+
export interface CorsEnvironment {
13+
nodeEnv: string;
14+
corsAllowedOrigins?: string;
15+
corsOriginDev?: string;
16+
}
17+
18+
/**
19+
* Get the list of allowed origins based on environment
20+
*/
21+
export function getAllowedOrigins(env: CorsEnvironment): string[] {
22+
const { nodeEnv, corsAllowedOrigins, corsOriginDev } = env;
23+
24+
// Production environment - strict allowlist required
25+
if (nodeEnv === 'production') {
26+
if (!corsAllowedOrigins || corsAllowedOrigins.trim() === '') {
27+
throw new BadRequestException(
28+
'CORS_ALLOWED_ORIGINS must be defined and non-empty in production',
29+
);
30+
}
31+
32+
// Parse comma-separated list and trim whitespace
33+
const origins = corsAllowedOrigins
34+
.split(',')
35+
.map((origin) => origin.trim())
36+
.filter((origin) => origin.length > 0);
37+
38+
if (origins.length === 0) {
39+
throw new BadRequestException(
40+
'CORS_ALLOWED_ORIGINS must contain at least one valid domain in production',
41+
);
42+
}
43+
44+
// Validate each origin is a valid URL
45+
for (const origin of origins) {
46+
try {
47+
const url = new URL(origin);
48+
// Ensure it's https in production (except localhost for testing)
49+
if (url.protocol !== 'https:' && !url.hostname.includes('localhost')) {
50+
throw new BadRequestException(
51+
`CORS origin ${origin} must use HTTPS in production`,
52+
);
53+
}
54+
} catch {
55+
throw new BadRequestException(
56+
`Invalid CORS origin: ${origin}. Must be a valid URL`,
57+
);
58+
}
59+
}
60+
61+
return origins;
62+
}
63+
64+
// Development environment - permissive
65+
const devOrigins = [
66+
'http://localhost:3000',
67+
'http://localhost:3001',
68+
'http://localhost:5000',
69+
];
70+
71+
if (corsOriginDev && corsOriginDev.trim() !== '') {
72+
try {
73+
const customOrigins = corsOriginDev
74+
.split(',')
75+
.map((o) => o.trim())
76+
.filter((o) => o.length > 0);
77+
devOrigins.push(...customOrigins);
78+
} catch {
79+
// Ignore invalid custom origins in dev
80+
}
81+
}
82+
83+
return devOrigins;
84+
}
85+
86+
/**
87+
* Create CORS configuration for the application
88+
*/
89+
export function createCorsConfig(env: CorsEnvironment): CorsConfig {
90+
const origins = getAllowedOrigins(env);
91+
92+
const logger = new Logger('CorsConfig');
93+
logger.log(`CORS allowed origins: ${origins.join(', ')}`);
94+
95+
return {
96+
origins,
97+
credentials: true,
98+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'],
99+
allowedHeaders: [
100+
'Content-Type',
101+
'Authorization',
102+
'Accept',
103+
'Origin',
104+
'X-Requested-With',
105+
'X-API-Key',
106+
'X-CSRF-Token',
107+
'Cache-Control',
108+
'Pragma',
109+
],
110+
exposedHeaders: [
111+
'Content-Length',
112+
'X-Content-Type-Options',
113+
'X-Frame-Options',
114+
'X-XSS-Protection',
115+
],
116+
maxAge: 86400, // 24 hours
117+
};
118+
}
119+
120+
/**
121+
* Log rejected origins for security auditing
122+
*/
123+
export function logRejectedOrigin(
124+
origin: string | undefined,
125+
path: string,
126+
): void {
127+
const logger = new Logger('CorsSecurity');
128+
if (origin) {
129+
logger.warn(
130+
`Rejected CORS request from origin: ${origin}, path: ${path}, timestamp: ${new Date().toISOString()}`,
131+
);
132+
} else {
133+
logger.warn(
134+
`Rejected CORS request with no origin header, path: ${path}, timestamp: ${new Date().toISOString()}`,
135+
);
136+
}
137+
}

0 commit comments

Comments
 (0)