]> git.digitality.be Git - pdw25-26/commitdiff
feat(microservices): ajout gateway + dépendances mises à jour
authorSteph Ponzo <ponzo.stephane2@gmail.com>
Fri, 27 Feb 2026 10:08:16 +0000 (11:08 +0100)
committerSteph Ponzo <ponzo.stephane2@gmail.com>
Fri, 27 Feb 2026 10:08:16 +0000 (11:08 +0100)
Wallette/gateway/gateway.js [new file with mode: 0644]
Wallette/gateway/package-lock.json [new file with mode: 0644]
Wallette/gateway/package.json [new file with mode: 0644]
Wallette/server/package-lock.json
Wallette/server/package.json

diff --git a/Wallette/gateway/gateway.js b/Wallette/gateway/gateway.js
new file mode 100644 (file)
index 0000000..239035d
--- /dev/null
@@ -0,0 +1,239 @@
+// =========================================================
+// API GATEWAY - Wall-e-tte
+// =========================================================
+// Point d'entrée unique pour le frontend.
+//
+// USAGE : node gateway.js   (depuis Wallette/gateway/)
+// PORT  : process.env.GATEWAY_PORT || process.env.PORT || 3000
+//
+// ROUTING TABLE :
+//   /api/price/*    → PRICE_BASE_URL    (défaut: http://127.0.0.1:3001)
+//   /api/alerts/*   → ALERTS_BASE_URL   (défaut: http://127.0.0.1:3003)
+//   /api/strategy/* → STRATEGY_BASE_URL (défaut: http://127.0.0.1:3002)
+//
+// PATH REWRITE :
+//   Les services exposent leurs routes avec le préfixe complet /api/*
+//   Le gateway transmet donc le path COMPLET tel quel.
+//   Ex: GET /api/price/current → http://127.0.0.1:3001/api/price/current
+// =========================================================
+
+import dotenv from 'dotenv';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import http from 'http';
+import https from 'https';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+dotenv.config({ path: path.resolve(__dirname, '../server/.env') });
+
+// =========================================================
+// CONFIGURATION
+// =========================================================
+
+const GATEWAY_PORT = process.env.GATEWAY_PORT || process.env.PORT || 3000;
+
+const UPSTREAMS = {
+  '/api/price':    process.env.PRICE_BASE_URL    || 'http://127.0.0.1:3001',
+  '/api/alerts':   process.env.ALERTS_BASE_URL   || 'http://127.0.0.1:3003',
+  '/api/strategy': process.env.STRATEGY_BASE_URL || 'http://127.0.0.1:3002',
+};
+
+const PROXY_TIMEOUT_MS = 5000;
+
+// =========================================================
+// SERVEUR HTTP NATIF (Node >= 18, pas de dépendances lourdes)
+// =========================================================
+
+const server = http.createServer(async (req, res) => {
+  const url = req.url || '/';
+
+  // -------------------------------------------------------
+  // ROUTES INTERNES DU GATEWAY
+  // -------------------------------------------------------
+
+  // GET /api/gateway/routes → table de routing (dev)
+  if (req.method === 'GET' && url === '/api/gateway/routes') {
+    return sendJSON(res, 200, {
+      ok: true,
+      data: {
+        gateway_port: GATEWAY_PORT,
+        path_rewrite: 'FULL PATH (path transmis tel quel aux services)',
+        routes: Object.entries(UPSTREAMS).map(([prefix, upstream]) => ({
+          prefix,
+          upstream,
+          example: `${prefix}/... → ${upstream}${prefix}/...`
+        }))
+      }
+    });
+  }
+
+  // GET /api/gateway/health → ping des services upstream
+  if (req.method === 'GET' && url === '/api/gateway/health') {
+    const checks = await Promise.all(
+      Object.entries(UPSTREAMS).map(async ([prefix, upstream]) => {
+        const healthUrl = `${upstream}/health`;
+        try {
+          const result = await proxyFetch('GET', healthUrl, null, 2000);
+          return { prefix, upstream, status: result.status, ok: result.status < 400 };
+        } catch (e) {
+          return { prefix, upstream, status: 'DOWN', ok: false, error: e.message };
+        }
+      })
+    );
+    const allOk = checks.every(c => c.ok);
+    return sendJSON(res, allOk ? 200 : 207, { ok: allOk, data: { services: checks } });
+  }
+
+  // -------------------------------------------------------
+  // PROXY VERS LES SERVICES
+  // -------------------------------------------------------
+
+  // Trouver quel upstream correspond au path
+  const matchedPrefix = Object.keys(UPSTREAMS).find(prefix => url.startsWith(prefix));
+
+  if (!matchedPrefix) {
+    return sendJSON(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: `No route for ${url}` } });
+  }
+
+  const upstreamBase = UPSTREAMS[matchedPrefix];
+
+  // PATH REWRITE : on garde le path complet (les services exposent /api/...)
+  const targetUrl = upstreamBase + url;
+
+  // Lire le body pour les méthodes non-GET/HEAD
+  let body = null;
+  if (!['GET', 'HEAD'].includes(req.method)) {
+    body = await readBody(req);
+  }
+
+  console.log(`[gateway] ${req.method} ${url} → ${targetUrl}`);
+
+  try {
+    const result = await proxyFetch(req.method, targetUrl, body, PROXY_TIMEOUT_MS, req.headers);
+
+    // Retransmettre le statut et le body au client
+    res.writeHead(result.status, { 'Content-Type': 'application/json' });
+    res.end(result.body);
+
+    console.log(`[gateway] ${req.method} ${url} → ${targetUrl} (${result.status})`);
+  } catch (err) {
+    // Service upstream injoignable → 502
+    const serviceName = matchedPrefix.replace('/api/', '') + '-service';
+    console.error(`[gateway] UPSTREAM_DOWN: ${targetUrl} — ${err.message}`);
+    return sendJSON(res, 502, {
+      ok: false,
+      error: {
+        code: 'UPSTREAM_DOWN',
+        message: `${serviceName} unavailable`
+      }
+    });
+  }
+});
+
+// =========================================================
+// HELPERS
+// =========================================================
+
+/**
+ * Lit le body d'une requête entrante et retourne un Buffer
+ */
+function readBody(req) {
+  return new Promise((resolve, reject) => {
+    const chunks = [];
+    req.on('data', chunk => chunks.push(chunk));
+    req.on('end', () => resolve(Buffer.concat(chunks)));
+    req.on('error', reject);
+  });
+}
+
+/**
+ * Envoie une requête HTTP vers un upstream
+ * @param {string} method
+ * @param {string} url
+ * @param {Buffer|null} body
+ * @param {number} timeoutMs
+ * @param {object} [incomingHeaders]
+ * @returns {Promise<{status: number, body: string}>}
+ */
+function proxyFetch(method, url, body, timeoutMs = PROXY_TIMEOUT_MS, incomingHeaders = {}) {
+  return new Promise((resolve, reject) => {
+    const parsed = new URL(url);
+    const isHttps = parsed.protocol === 'https:';
+    const lib = isHttps ? https : http;
+
+    // Headers à transmettre (on filtre les headers problématiques)
+    const headers = {
+      'Content-Type': 'application/json',
+    };
+    if (incomingHeaders['authorization']) headers['Authorization'] = incomingHeaders['authorization'];
+    if (body && body.length > 0) headers['Content-Length'] = body.length;
+
+    const options = {
+      hostname: parsed.hostname,
+      port: parsed.port || (isHttps ? 443 : 80),
+      path: parsed.pathname + parsed.search,
+      method,
+      headers,
+      timeout: timeoutMs,
+    };
+
+    const proxyReq = lib.request(options, (proxyRes) => {
+      const chunks = [];
+      proxyRes.on('data', chunk => chunks.push(chunk));
+      proxyRes.on('end', () => {
+        resolve({
+          status: proxyRes.statusCode,
+          body: Buffer.concat(chunks).toString('utf8')
+        });
+      });
+    });
+
+    proxyReq.on('timeout', () => {
+      proxyReq.destroy();
+      reject(new Error(`Timeout after ${timeoutMs}ms`));
+    });
+
+    proxyReq.on('error', reject);
+
+    if (body && body.length > 0) {
+      proxyReq.write(body);
+    }
+
+    proxyReq.end();
+  });
+}
+
+/**
+ * Envoie une réponse JSON
+ */
+function sendJSON(res, status, data) {
+  const body = JSON.stringify(data);
+  res.writeHead(status, {
+    'Content-Type': 'application/json',
+    'Content-Length': Buffer.byteLength(body)
+  });
+  res.end(body);
+}
+
+// =========================================================
+// DÉMARRAGE
+// =========================================================
+
+server.listen(GATEWAY_PORT, () => {
+  console.log(`
+╔══════════════════════════════════════════════════════════╗
+║                  API GATEWAY - Wall-e-tte                ║
+╠══════════════════════════════════════════════════════════╣
+║  Écoute sur  : http://localhost:${GATEWAY_PORT}                  ║
+║                                                          ║
+║  ROUTING :                                               ║
+║   /api/price/*    → ${UPSTREAMS['/api/price']}        ║
+║   /api/alerts/*   → ${UPSTREAMS['/api/alerts']}       ║
+║   /api/strategy/* → ${UPSTREAMS['/api/strategy']}     ║
+║                                                          ║
+║  ENDPOINTS GATEWAY :                                     ║
+║   GET /api/gateway/routes  → table de routing            ║
+║   GET /api/gateway/health  → état des services           ║
+╚══════════════════════════════════════════════════════════╝`);
+});
diff --git a/Wallette/gateway/package-lock.json b/Wallette/gateway/package-lock.json
new file mode 100644 (file)
index 0000000..3537176
--- /dev/null
@@ -0,0 +1,27 @@
+{
+  "name": "wallette-gateway",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "wallette-gateway",
+      "version": "1.0.0",
+      "dependencies": {
+        "dotenv": "^16.4.5"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    }
+  }
+}
diff --git a/Wallette/gateway/package.json b/Wallette/gateway/package.json
new file mode 100644 (file)
index 0000000..389391a
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "name": "wallette-gateway",
+  "version": "1.0.0",
+  "description": "API Gateway Wallette - Reverse proxy vers les micro-services",
+  "type": "module",
+  "main": "gateway.js",
+  "scripts": {
+    "start": "node gateway.js",
+    "dev": "node --watch gateway.js"
+  },
+  "dependencies": {
+    "dotenv": "^16.4.5"
+  }
+}
index 4ca2e4c5d4e0475042f07c37cf905217e0cecd89..b61ad79037a1c9dc7947203c0939c0a957dbcec1 100644 (file)
@@ -9,6 +9,7 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "axios": "^1.13.5",
         "cors": "^2.8.5",
         "dotenv": "^16.4.5",
         "express": "^4.18.2",
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
       "license": "MIT"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
     "node_modules/aws-ssl-profiles": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
         "node": ">= 6.0.0"
       }
     },
+    "node_modules/axios": {
+      "version": "1.13.5",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+      "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/base64id": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/content-disposition": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
         "ms": "2.0.0"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/denque": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
         "node": ">= 0.4"
       }
     },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/escape-html": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
         "node": ">= 0.8"
       }
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/forwarded": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/hasown": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
         "node": ">= 0.10"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/qs": {
       "version": "6.14.2",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
index 53e6e860f5fd7cf7fdde8223e88b5ea9783e7115..a342bc8f637b795e64019584946666fba689a718 100644 (file)
@@ -21,6 +21,7 @@
   "author": "Équipe Wall-e-tte",
   "license": "ISC",
   "dependencies": {
+    "axios": "^1.13.5",
     "cors": "^2.8.5",
     "dotenv": "^16.4.5",
     "express": "^4.18.2",