feat: add notification microservices and transactional outbox
هذا الالتزام موجود في:
95
libs/common/src/rabbitmq.ts
Normal file
95
libs/common/src/rabbitmq.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
export interface RabbitConfigSource {
|
||||
get(key: string): string | undefined;
|
||||
}
|
||||
|
||||
export interface ResolvedRabbitMqConfig {
|
||||
url: string;
|
||||
sanitizedEndpoint: string;
|
||||
socketOptions: {
|
||||
heartbeatIntervalInSeconds: number;
|
||||
reconnectTimeInSeconds: number;
|
||||
connectionOptions: Record<string, unknown>;
|
||||
};
|
||||
tlsEnabled: boolean;
|
||||
}
|
||||
|
||||
const bool = (value: string | undefined, fallback = false) => value === undefined ? fallback : value === 'true';
|
||||
|
||||
function material(source: RabbitConfigSource, base64Key: string, pathKey: string): Buffer | undefined {
|
||||
const encoded = source.get(base64Key);
|
||||
if (encoded) return Buffer.from(encoded, 'base64');
|
||||
const path = source.get(pathKey);
|
||||
return path ? readFileSync(path) : undefined;
|
||||
}
|
||||
|
||||
export function resolveRabbitMqConfig(
|
||||
source: RabbitConfigSource,
|
||||
serviceName: string,
|
||||
requireProductionSafety: boolean,
|
||||
): ResolvedRabbitMqConfig {
|
||||
const explicitUrl = source.get('RABBITMQ_URL');
|
||||
const tlsEnabled = bool(source.get('RABBITMQ_TLS_ENABLED'), explicitUrl?.startsWith('amqps://'));
|
||||
const protocol = tlsEnabled ? 'amqps' : 'amqp';
|
||||
const host = source.get('RABBITMQ_HOST') || '127.0.0.1';
|
||||
const port = source.get('RABBITMQ_PORT') || (tlsEnabled ? '5671' : '5672');
|
||||
const username = source.get('RABBITMQ_USERNAME') || 'guest';
|
||||
const password = source.get('RABBITMQ_PASSWORD') || 'guest';
|
||||
const vhost = source.get('RABBITMQ_VHOST') || '/';
|
||||
let url = explicitUrl
|
||||
? (tlsEnabled ? explicitUrl.replace(/^amqp:\/\//, 'amqps://') : explicitUrl)
|
||||
: `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}/${vhost === '/' ? '' : encodeURIComponent(vhost.replace(/^\//, ''))}`;
|
||||
let parsed: URL;
|
||||
try { parsed = new URL(url); }
|
||||
catch { throw new Error('RabbitMQ configuration contains an invalid URL'); }
|
||||
if (explicitUrl && !parsed.username && source.get('RABBITMQ_USERNAME')) {
|
||||
parsed.username = username;
|
||||
parsed.password = password;
|
||||
url = parsed.toString();
|
||||
}
|
||||
const configuredUser = decodeURIComponent(parsed.username || username);
|
||||
const configuredPassword = decodeURIComponent(parsed.password || password);
|
||||
const production = source.get('NODE_ENV') === 'production';
|
||||
const tlsRequired = bool(source.get('RABBITMQ_TLS_REQUIRED'), true);
|
||||
if (production && requireProductionSafety) {
|
||||
if (!configuredUser || !configuredPassword || configuredUser === 'guest' || configuredPassword === 'guest') {
|
||||
throw new Error('RabbitMQ production credentials must be non-guest and non-empty');
|
||||
}
|
||||
if (tlsRequired && parsed.protocol !== 'amqps:' && !tlsEnabled) {
|
||||
throw new Error('RabbitMQ TLS/amqps is required for production microservices');
|
||||
}
|
||||
}
|
||||
|
||||
const connectionOptions: Record<string, unknown> = {
|
||||
timeout: Number(source.get('RABBITMQ_CONNECTION_TIMEOUT_MS') || 10_000),
|
||||
clientProperties: { connection_name: `${serviceName}:${source.get('HOSTNAME') || process.pid}` },
|
||||
};
|
||||
if (tlsEnabled || parsed.protocol === 'amqps:') {
|
||||
connectionOptions.rejectUnauthorized = bool(source.get('RABBITMQ_TLS_REJECT_UNAUTHORIZED'), true);
|
||||
const servername = source.get('RABBITMQ_SERVERNAME');
|
||||
if (servername) connectionOptions.servername = servername;
|
||||
const ca = material(source, 'RABBITMQ_CA_CERT_BASE64', 'RABBITMQ_CA_CERT_PATH');
|
||||
const cert = material(source, 'RABBITMQ_CLIENT_CERT_BASE64', 'RABBITMQ_CLIENT_CERT_PATH');
|
||||
const key = material(source, 'RABBITMQ_CLIENT_KEY_BASE64', 'RABBITMQ_CLIENT_KEY_PATH');
|
||||
if (ca) connectionOptions.ca = [ca];
|
||||
if (cert) connectionOptions.cert = cert;
|
||||
if (key) connectionOptions.key = key;
|
||||
if ((cert && !key) || (!cert && key)) throw new Error('RabbitMQ client certificate and key must be configured together');
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
sanitizedEndpoint: `${parsed.protocol}//${parsed.hostname}:${parsed.port || (parsed.protocol === 'amqps:' ? '5671' : '5672')}${parsed.pathname}`,
|
||||
tlsEnabled: tlsEnabled || parsed.protocol === 'amqps:',
|
||||
socketOptions: {
|
||||
heartbeatIntervalInSeconds: Number(source.get('RABBITMQ_HEARTBEAT_SECONDS') || 10),
|
||||
reconnectTimeInSeconds: Number(source.get('RABBITMQ_RECONNECT_SECONDS') || 5),
|
||||
connectionOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function processEnvSource(): RabbitConfigSource {
|
||||
return { get: (key) => process.env[key] };
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم