455 lines
16 KiB
JavaScript
455 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
/**
|
|
* nginx Configuration Generator
|
|
* Generates nginx reverse proxy configs for all production HTTP services
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.generateNginxConfigForDomain = generateNginxConfigForDomain;
|
|
exports.generateAllNginxConfigs = generateAllNginxConfigs;
|
|
exports.generateWebSocketUpgradeMap = generateWebSocketUpgradeMap;
|
|
const fs = __importStar(require("node:fs"));
|
|
const path = __importStar(require("node:path"));
|
|
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'infrastructure/nginx/generated');
|
|
const PORTS_YAML_PATH = path.join(PROJECT_ROOT, 'infrastructure/ports.yaml');
|
|
/**
|
|
* Production domain mappings
|
|
*/
|
|
const DOMAIN_MAPPINGS = [
|
|
{
|
|
domain: 'sso.atlilith.com',
|
|
services: { api: 'sso.api' },
|
|
sslCertPath: '/etc/letsencrypt/live/sso.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/sso.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'merchant.atlilith.com',
|
|
services: { api: 'merchant.api' },
|
|
sslCertPath: '/etc/letsencrypt/live/merchant.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/merchant.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'www.atlilith.com',
|
|
services: { api: 'landing.landing-api', frontend: 'landing.landing-frontend' },
|
|
sslCertPath: '/etc/letsencrypt/live/www.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/www.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'www.trustedmeet.com',
|
|
services: { api: 'marketplace.api', frontend: 'marketplace.frontend' },
|
|
sslCertPath: '/etc/letsencrypt/live/www.trustedmeet.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/www.trustedmeet.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'admin.atlilith.com',
|
|
services: { api: 'platform-admin.api', frontend: 'platform-admin.frontend' },
|
|
sslCertPath: '/etc/letsencrypt/live/admin.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/admin.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'seo.atlilith.com',
|
|
services: { api: 'seo.api', frontend: 'seo.frontend-public' },
|
|
sslCertPath: '/etc/letsencrypt/live/seo.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/seo.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'profile.atlilith.com',
|
|
services: { api: 'profile.api' },
|
|
sslCertPath: '/etc/letsencrypt/live/profile.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/profile.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'analytics.atlilith.com',
|
|
services: { api: 'analytics.api' },
|
|
sslCertPath: '/etc/letsencrypt/live/analytics.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/analytics.atlilith.com/privkey.pem',
|
|
},
|
|
{
|
|
domain: 'knowledge.atlilith.com',
|
|
services: { api: 'knowledge-verification.api' },
|
|
sslCertPath: '/etc/letsencrypt/live/knowledge.atlilith.com/fullchain.pem',
|
|
sslKeyPath: '/etc/letsencrypt/live/knowledge.atlilith.com/privkey.pem',
|
|
},
|
|
];
|
|
/**
|
|
* Load port from ports.yaml for a service
|
|
*/
|
|
async function loadServicePort(serviceId) {
|
|
try {
|
|
const yaml = await fs.promises.readFile(PORTS_YAML_PATH, 'utf8');
|
|
const [feature, service] = serviceId.split('.');
|
|
// Parse YAML manually for this specific structure
|
|
// Looking for patterns like:
|
|
// features:
|
|
// sso:
|
|
// postgresql: 5440
|
|
const featureMatch = yaml.match(new RegExp(`^ ${feature}:\\s*$`, 'm'));
|
|
if (!featureMatch) {
|
|
return undefined;
|
|
}
|
|
const featureStart = featureMatch.index;
|
|
const nextFeatureMatch = yaml.slice(featureStart + 1).match(/^ \w+:/m);
|
|
const featureEnd = nextFeatureMatch ? featureStart + nextFeatureMatch.index : yaml.length;
|
|
const featureBlock = yaml.slice(featureStart, featureEnd);
|
|
// Handle service name variations
|
|
const serviceNames = [
|
|
service,
|
|
service.replace('-', '_'),
|
|
service.replace('landing-', ''),
|
|
service.replace('-frontend', ''),
|
|
service.replace('-api', ''),
|
|
];
|
|
for (const serviceName of serviceNames) {
|
|
const portMatch = featureBlock.match(new RegExp(`^ ${serviceName}:\\s*(\\d+)`, 'm'));
|
|
if (portMatch) {
|
|
return parseInt(portMatch[1], 10);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
catch (error) {
|
|
console.error(`Failed to load port for ${serviceId}:`, error instanceof Error ? error.message : String(error));
|
|
return undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Generate security headers block
|
|
*/
|
|
function generateSecurityHeaders(domain) {
|
|
const csp = [
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: https:",
|
|
"font-src 'self' data:",
|
|
"connect-src 'self' https:",
|
|
"frame-ancestors 'none'",
|
|
].join('; ');
|
|
return ` # Security headers
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
add_header Content-Security-Policy "${csp}" always;
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;`;
|
|
}
|
|
/**
|
|
* Generate proxy headers block
|
|
*/
|
|
function generateProxyHeaders() {
|
|
return ` proxy_http_version 1.1;
|
|
|
|
# Client information
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
proxy_set_header X-Forwarded-Port $server_port;
|
|
|
|
# WebSocket support
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 60s;
|
|
proxy_send_timeout 60s;
|
|
proxy_read_timeout 60s;
|
|
|
|
# Buffering
|
|
proxy_buffering on;
|
|
proxy_buffer_size 4k;
|
|
proxy_buffers 8 4k;
|
|
proxy_busy_buffers_size 8k;`;
|
|
}
|
|
/**
|
|
* Generate location block for proxying to a service
|
|
*/
|
|
function generateLocationBlock(path, upstreamPort, comment) {
|
|
return ` # ${comment || `Proxy to localhost:${upstreamPort}`}
|
|
location ${path} {
|
|
${generateProxyHeaders()}
|
|
|
|
proxy_pass http://127.0.0.1:${upstreamPort};
|
|
}`;
|
|
}
|
|
/**
|
|
* Generate health check location block (no auth)
|
|
*/
|
|
function generateHealthCheckBlock(upstreamPort) {
|
|
return ` # Health check endpoint (no auth, direct pass-through)
|
|
location /health {
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_pass http://127.0.0.1:${upstreamPort}/health;
|
|
|
|
# Disable access logging for health checks
|
|
access_log off;
|
|
}`;
|
|
}
|
|
/**
|
|
* Generate nginx server configuration for a domain
|
|
*/
|
|
async function generateNginxConfig(mapping) {
|
|
const { domain, services, sslCertPath, sslKeyPath } = mapping;
|
|
// Load ports for services
|
|
const apiPort = services.api ? await loadServicePort(services.api) : undefined;
|
|
const frontendPort = services.frontend ? await loadServicePort(services.frontend) : undefined;
|
|
if (!apiPort && !frontendPort) {
|
|
throw new Error(`No ports found for domain ${domain} (API: ${services.api}, Frontend: ${services.frontend})`);
|
|
}
|
|
// Determine primary service for health checks and default location
|
|
const primaryPort = frontendPort || apiPort;
|
|
const primaryService = services.frontend || services.api;
|
|
// Build configuration
|
|
const config = `# ${domain} - Production nginx configuration
|
|
# Generated by nginx-generator.ts - DO NOT EDIT MANUALLY
|
|
#
|
|
# Services:
|
|
${services.api ? `# - API: ${services.api} (port ${apiPort})` : ''}
|
|
${services.frontend ? `# - Frontend: ${services.frontend} (port ${frontendPort})` : ''}
|
|
#
|
|
# SSL certificates managed by Let's Encrypt (certbot)
|
|
|
|
# HTTP redirect to HTTPS
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name ${domain};
|
|
|
|
# Redirect all HTTP traffic to HTTPS
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
|
|
# HTTPS server
|
|
server {
|
|
listen 443 ssl http2;
|
|
listen [::]:443 ssl http2;
|
|
server_name ${domain};
|
|
|
|
# SSL configuration
|
|
ssl_certificate ${sslCertPath};
|
|
ssl_certificate_key ${sslKeyPath};
|
|
|
|
# Mozilla Intermediate SSL configuration
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
# SSL session cache
|
|
ssl_session_timeout 1d;
|
|
ssl_session_cache shared:SSL:50m;
|
|
ssl_session_tickets off;
|
|
|
|
# OCSP stapling
|
|
ssl_stapling on;
|
|
ssl_stapling_verify on;
|
|
ssl_trusted_certificate ${sslCertPath};
|
|
|
|
# DNS resolver
|
|
resolver 8.8.8.8 8.8.4.4 valid=300s;
|
|
resolver_timeout 5s;
|
|
|
|
${generateSecurityHeaders(domain)}
|
|
|
|
${generateHealthCheckBlock(apiPort || frontendPort)}
|
|
|
|
${services.api ? `
|
|
${generateLocationBlock('/api/', apiPort, `API endpoints → ${services.api}`)}` : ''}
|
|
|
|
${services.frontend && services.api ? `
|
|
# Frontend static files and SPA routing
|
|
location / {
|
|
${generateProxyHeaders()}
|
|
|
|
proxy_pass http://127.0.0.1:${frontendPort};
|
|
}` : ''}
|
|
|
|
${!services.frontend && services.api ? `
|
|
# All requests to API
|
|
location / {
|
|
${generateProxyHeaders()}
|
|
|
|
proxy_pass http://127.0.0.1:${apiPort};
|
|
}` : ''}
|
|
|
|
# Logging
|
|
access_log /var/log/nginx/${domain}.access.log;
|
|
error_log /var/log/nginx/${domain}.error.log;
|
|
}
|
|
|
|
# WebSocket connection upgrade map (required for WebSocket support)
|
|
# Note: This should be in the http context (nginx.conf), but included here for reference
|
|
# map $http_upgrade $connection_upgrade {
|
|
# default upgrade;
|
|
# '' close;
|
|
# }
|
|
`;
|
|
return config;
|
|
}
|
|
/**
|
|
* Validate nginx configuration syntax
|
|
*/
|
|
async function validateNginxConfig(configPath) {
|
|
const { spawn } = await import('node:child_process');
|
|
return new Promise((resolve) => {
|
|
const proc = spawn('nginx', ['-t', '-c', configPath], {
|
|
stdio: 'pipe',
|
|
});
|
|
let stderr = '';
|
|
proc.stderr?.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
proc.on('close', (code) => {
|
|
if (code === 0) {
|
|
console.log(` ✓ Validation passed: ${configPath}`);
|
|
resolve(true);
|
|
}
|
|
else {
|
|
console.error(` ✗ Validation failed: ${configPath}`);
|
|
console.error(stderr);
|
|
resolve(false);
|
|
}
|
|
});
|
|
proc.on('error', (err) => {
|
|
console.warn(` ⚠ Cannot validate (nginx not installed or not in PATH): ${err.message}`);
|
|
resolve(true); // Don't fail if nginx isn't available
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Generate nginx config for a specific domain
|
|
*/
|
|
async function generateNginxConfigForDomain(domain) {
|
|
const mapping = DOMAIN_MAPPINGS.find(m => m.domain === domain);
|
|
if (!mapping) {
|
|
throw new Error(`No domain mapping found for: ${domain}`);
|
|
}
|
|
return generateNginxConfig(mapping);
|
|
}
|
|
/**
|
|
* Generate all nginx configurations
|
|
*/
|
|
async function generateAllNginxConfigs() {
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
console.log(' nginx Configuration Generator');
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
// Ensure output directory exists
|
|
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
console.log(`✓ Created output directory: ${OUTPUT_DIR}`);
|
|
}
|
|
console.log(`\n✓ Generating configs for ${DOMAIN_MAPPINGS.length} domains`);
|
|
let generated = 0;
|
|
let failed = 0;
|
|
for (const mapping of DOMAIN_MAPPINGS) {
|
|
try {
|
|
const config = await generateNginxConfig(mapping);
|
|
const outputPath = path.join(OUTPUT_DIR, `${mapping.domain}.conf`);
|
|
await fs.promises.writeFile(outputPath, config, 'utf8');
|
|
console.log(` ✓ ${mapping.domain}`);
|
|
generated++;
|
|
// Validate config (optional, doesn't fail generation)
|
|
// await validateNginxConfig(outputPath);
|
|
}
|
|
catch (error) {
|
|
console.error(` ✗ ${mapping.domain}: ${error instanceof Error ? error.message : String(error)}`);
|
|
failed++;
|
|
}
|
|
}
|
|
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
console.log(`✓ Generated ${generated} configurations`);
|
|
if (failed > 0) {
|
|
console.log(`✗ Failed ${failed} configurations`);
|
|
}
|
|
console.log(` Output: ${OUTPUT_DIR}`);
|
|
console.log(`\n📋 Next steps:`);
|
|
console.log(` 1. Review generated files in ${OUTPUT_DIR}`);
|
|
console.log(` 2. Test configs: sudo nginx -t`);
|
|
console.log(` 3. Copy to VPS: sudo cp ${OUTPUT_DIR}/*.conf /etc/nginx/sites-available/`);
|
|
console.log(` 4. Enable sites: sudo ln -sf /etc/nginx/sites-available/<domain>.conf /etc/nginx/sites-enabled/`);
|
|
console.log(` 5. Reload nginx: sudo systemctl reload nginx`);
|
|
console.log(`\n⚠️ Note: Ensure WebSocket upgrade map is in /etc/nginx/nginx.conf:`);
|
|
console.log(` map $http_upgrade $connection_upgrade {`);
|
|
console.log(` default upgrade;`);
|
|
console.log(` '' close;`);
|
|
console.log(` }`);
|
|
}
|
|
/**
|
|
* Generate WebSocket upgrade map snippet for nginx.conf
|
|
*/
|
|
function generateWebSocketUpgradeMap() {
|
|
return `# WebSocket connection upgrade map
|
|
# Add this to the http {} block in /etc/nginx/nginx.conf
|
|
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' close;
|
|
}
|
|
`;
|
|
}
|
|
// CLI execution
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const command = process.argv[2];
|
|
if (command === '--domain') {
|
|
const domain = process.argv[3];
|
|
if (!domain) {
|
|
console.error('Usage: nginx-generator.ts --domain <domain>');
|
|
process.exit(1);
|
|
}
|
|
generateNginxConfigForDomain(domain)
|
|
.then(config => {
|
|
console.log(config);
|
|
process.exit(0);
|
|
})
|
|
.catch(err => {
|
|
console.error('❌ Error:', err.message);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
else if (command === '--websocket-map') {
|
|
console.log(generateWebSocketUpgradeMap());
|
|
process.exit(0);
|
|
}
|
|
else {
|
|
generateAllNginxConfigs().catch(err => {
|
|
console.error('❌ Error:', err.message);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
}
|