Spaces:
Paused
Paused
| const fs = require("fs"); | |
| const path = require("path"); | |
| const dotenv = require("dotenv"); | |
| function loadEnvironment() { | |
| const backendEnvPath = path.resolve(__dirname, "../../.env"); | |
| const rootEnvPath = path.resolve(__dirname, "../../../.env"); | |
| if (fs.existsSync(backendEnvPath)) { | |
| dotenv.config({ path: backendEnvPath }); | |
| return; | |
| } | |
| if (fs.existsSync(rootEnvPath)) { | |
| dotenv.config({ path: rootEnvPath }); | |
| return; | |
| } | |
| dotenv.config(); | |
| } | |
| loadEnvironment(); | |
| function readBool(name, fallback) { | |
| const raw = process.env[name]; | |
| if (raw === undefined || raw === null || raw === "") { | |
| return fallback; | |
| } | |
| return ["1", "true", "yes", "on"].includes(String(raw).toLowerCase()); | |
| } | |
| function readNumber(name, fallback) { | |
| const raw = process.env[name]; | |
| if (raw === undefined || raw === null || raw === "") { | |
| return fallback; | |
| } | |
| const parsed = Number(raw); | |
| if (!Number.isFinite(parsed)) { | |
| throw new Error(`Environment variable ${name} must be a valid number.`); | |
| } | |
| return parsed; | |
| } | |
| function readString(name, fallback = "") { | |
| const raw = process.env[name]; | |
| if (raw === undefined || raw === null || raw === "") { | |
| return fallback; | |
| } | |
| return String(raw); | |
| } | |
| function assertJwtSecret(name, value) { | |
| if (!value || value.length < 32) { | |
| throw new Error( | |
| `Environment variable ${name} must be set and at least 32 characters long for JWT security.` | |
| ); | |
| } | |
| } | |
| function resolveFilePath(value) { | |
| if (!value) { | |
| return ""; | |
| } | |
| return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value); | |
| } | |
| function readOptionalFile(filePathValue, label) { | |
| if (!filePathValue) { | |
| return undefined; | |
| } | |
| const absolutePath = resolveFilePath(filePathValue); | |
| if (!fs.existsSync(absolutePath)) { | |
| throw new Error(`${label} file does not exist: ${absolutePath}`); | |
| } | |
| return fs.readFileSync(absolutePath); | |
| } | |
| function parseBrokerUrl() { | |
| const brokerUrl = readString("MQTT_BROKER_URL", ""); | |
| if (!brokerUrl) { | |
| return null; | |
| } | |
| const url = new URL(brokerUrl); | |
| return { | |
| host: url.hostname, | |
| port: Number(url.port || 8883), | |
| username: decodeURIComponent(url.username || ""), | |
| password: decodeURIComponent(url.password || ""), | |
| }; | |
| } | |
| function buildDatabaseConfig() { | |
| const connectionString = readString("DATABASE_URL", ""); | |
| const sslEnabled = readBool("DB_SSL", false) || readBool("PGSSL", false); | |
| const sslRejectUnauthorized = readBool("DB_SSL_REJECT_UNAUTHORIZED", true); | |
| const sslCaPath = readString("DB_SSL_CA_PATH", ""); | |
| const base = { | |
| max: readNumber("PGPOOL_MAX", 20), | |
| idleTimeoutMillis: readNumber("PG_IDLE_TIMEOUT_MS", 30000), | |
| connectionTimeoutMillis: readNumber("PG_CONNECT_TIMEOUT_MS", 15000), | |
| }; | |
| if (sslEnabled) { | |
| base.ssl = { | |
| rejectUnauthorized: sslRejectUnauthorized, | |
| ca: readOptionalFile(sslCaPath, "DB SSL CA") || undefined, | |
| }; | |
| } | |
| if (connectionString) { | |
| return { | |
| ...base, | |
| connectionString, | |
| }; | |
| } | |
| return { | |
| ...base, | |
| host: readString("PGHOST", "127.0.0.1"), | |
| port: readNumber("PGPORT", 5432), | |
| user: readString("PGUSER", "postgres"), | |
| password: readString("PGPASSWORD", "postgres"), | |
| database: readString("PGDATABASE", "smart_cargo"), | |
| }; | |
| } | |
| const brokerFromUrl = parseBrokerUrl(); | |
| const legacyBrokerHost = readString("MQTT_BROKER", ""); | |
| const brokerHost = | |
| readString("MQTT_BROKER_HOST", "") || | |
| legacyBrokerHost || | |
| (brokerFromUrl && brokerFromUrl.host) || | |
| ""; | |
| const brokerPort = readNumber( | |
| "MQTT_BROKER_PORT", | |
| readNumber("MQTT_PORT", (brokerFromUrl && brokerFromUrl.port) || 8883) | |
| ); | |
| const mqttCaPath = readString("MQTT_CA_PATH", ""); | |
| const jwtAccessSecret = readString("JWT_ACCESS_SECRET", ""); | |
| const jwtRefreshSecret = readString("JWT_REFRESH_SECRET", ""); | |
| assertJwtSecret("JWT_ACCESS_SECRET", jwtAccessSecret); | |
| assertJwtSecret("JWT_REFRESH_SECRET", jwtRefreshSecret); | |
| const config = { | |
| nodeEnv: readString("NODE_ENV", "development"), | |
| logLevel: readString("LOG_LEVEL", "info"), | |
| server: { | |
| port: readNumber("PORT", 5000), | |
| apiPrefix: readString("API_PREFIX", "/api"), | |
| }, | |
| cors: { | |
| origin: readString("CORS_ORIGIN", "*"), | |
| }, | |
| auth: { | |
| jwtAccessSecret, | |
| jwtRefreshSecret, | |
| jwtAccessExpiresIn: readString("JWT_ACCESS_EXPIRES_IN", "15m"), | |
| jwtRefreshExpiresIn: readString("JWT_REFRESH_EXPIRES_IN", "7d"), | |
| jwtIssuer: readString("JWT_ISSUER", "smart-cargo-backend"), | |
| jwtAudience: readString("JWT_AUDIENCE", "smart-cargo-api"), | |
| }, | |
| mqtt: { | |
| enabled: Boolean(brokerHost), | |
| host: brokerHost, | |
| port: brokerPort, | |
| username: | |
| readString("MQTT_USERNAME", "") || | |
| readString("MQTT_USER", "") || | |
| (brokerFromUrl && brokerFromUrl.username) || | |
| "", | |
| password: readString("MQTT_PASSWORD", "") || (brokerFromUrl && brokerFromUrl.password) || "", | |
| topicFilter: readString( | |
| "MQTT_TOPIC_FILTER", | |
| "tenant/+/truck/+/container/+/telemetry" | |
| ), | |
| clientIdPrefix: readString("MQTT_CLIENT_ID_PREFIX", "smart-cargo-backend"), | |
| connectTimeoutMs: readNumber("MQTT_CONNECT_TIMEOUT_MS", 30000), | |
| reconnectPeriodMs: readNumber("MQTT_RECONNECT_PERIOD_MS", 5000), | |
| rejectUnauthorized: readBool("MQTT_REJECT_UNAUTHORIZED", true), | |
| caPath: mqttCaPath, | |
| ca: readOptionalFile(mqttCaPath, "MQTT CA"), | |
| }, | |
| db: { | |
| runMigrationsOnBoot: readBool("RUN_MIGRATIONS_ON_BOOT", false), | |
| runSeedsOnBoot: readBool("RUN_SEEDS_ON_BOOT", false), | |
| pg: buildDatabaseConfig(), | |
| }, | |
| alerts: { | |
| highTemperatureThresholdC: readNumber("ALERT_HIGH_TEMPERATURE_THRESHOLD_C", 35), | |
| gasSpikeThresholdRaw: readNumber("ALERT_GAS_SPIKE_THRESHOLD_RAW", 1500), | |
| autoResolveShock: readBool("ALERT_AUTO_RESOLVE_SHOCK", false), | |
| offlineThresholdMs: readNumber("OFFLINE_THRESHOLD_MS", 30000), | |
| }, | |
| jobs: { | |
| offlineScanIntervalMs: readNumber("OFFLINE_SCAN_INTERVAL_MS", 5000), | |
| }, | |
| ingest: { | |
| queueConcurrency: readNumber("INGEST_QUEUE_CONCURRENCY", 6), | |
| queueMaxBacklog: readNumber("INGEST_QUEUE_MAX_BACKLOG", 5000), | |
| }, | |
| query: { | |
| historyDefaultLimit: readNumber("HISTORY_DEFAULT_LIMIT", 240), | |
| historyMaxLimit: readNumber("HISTORY_MAX_LIMIT", 2000), | |
| }, | |
| ai: { | |
| tripSummaryEnabled: readBool("AI_TRIP_SUMMARY_ENABLED", true), | |
| geminiModel: readString("GEMINI_MODEL", "gemini-flash-lite-latest"), | |
| geminiApiBaseUrl: readString( | |
| "GEMINI_API_BASE_URL", | |
| "https://generativelanguage.googleapis.com/v1beta" | |
| ), | |
| geminiApiKeyBase64: readString("GEMINI_API_KEY_BASE64", ""), | |
| tripSummaryTimeoutMs: readNumber("AI_TRIP_SUMMARY_TIMEOUT_MS", 15000), | |
| }, | |
| }; | |
| module.exports = { config }; | |