From f3e985af5f6e9db9c086928a507c4b07e304c8b9 Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 8 Jun 2026 09:58:58 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Fix=20testConnection=20:=20le=20b?= =?UTF-8?q?ackend=20lit=20d=C3=A9sormais=20le=20mot=20de=20passe=20directe?= =?UTF-8?q?ment=20en=20base=20(jamais=20expos=C3=A9=20au=20frontend).=20Ti?= =?UTF-8?q?meout=20port=C3=A9=20=C3=A0=2025s.=20D=C3=A9tection=20correcte?= =?UTF-8?q?=20de=20invalid=5Fcredentials=20dans=20la=20r=C3=A9ponse=20JSON?= =?UTF-8?q?=20FreePro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .manus/db/db-query-1780926876439.json | 20 +++++ client/src/pages/VentilationFreePro.tsx | 6 +- server/freeproAutoImport.ts | 108 ++++++++++++++++-------- server/routers.ts | 29 +++++-- 4 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 .manus/db/db-query-1780926876439.json diff --git a/.manus/db/db-query-1780926876439.json b/.manus/db/db-query-1780926876439.json new file mode 100644 index 0000000..5b4f381 --- /dev/null +++ b/.manus/db/db-query-1780926876439.json @@ -0,0 +1,20 @@ +{ + "query": "SELECT id, userId, portalUrl, loginEmail, loginPassword, autoEnabled, frequency, maxAnteriority FROM freeproSettings LIMIT 5;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.bd4328423008 --database fo4DRyBgjsuiigFAgNLuWm --execute SELECT id, userId, portalUrl, loginEmail, loginPassword, autoEnabled, frequency, maxAnteriority FROM freeproSettings LIMIT 5;", + "rows": [ + { + "id": "1", + "userId": "1", + "portalUrl": "https://pro.free.fr", + "loginEmail": "maintenance_it@itinova.org", + "loginPassword": "4y59yRjYC6r5wL!l45u", + "autoEnabled": "0", + "frequency": "daily", + "maxAnteriority": "1767225600" + } + ], + "messages": [], + "stdout": "id\tuserId\tportalUrl\tloginEmail\tloginPassword\tautoEnabled\tfrequency\tmaxAnteriority\n1\t1\thttps://pro.free.fr\tmaintenance_it@itinova.org\t4y59yRjYC6r5wL!l45u\t0\tdaily\t1767225600\n", + "stderr": "", + "execution_time_ms": 333 +} \ No newline at end of file diff --git a/client/src/pages/VentilationFreePro.tsx b/client/src/pages/VentilationFreePro.tsx index 8d64a86..3bfe505 100644 --- a/client/src/pages/VentilationFreePro.tsx +++ b/client/src/pages/VentilationFreePro.tsx @@ -140,9 +140,11 @@ function ParametrageTab() { onSuccess: (data) => { setIsTesting(false); if (data.success) { - toast.success(data.message); + const msg = (data as any).message ?? "Connexion réussie !"; + toast.success(msg); } else { - toast.error(data.message); + const msg = (data as any).error ?? (data as any).message ?? "Identifiants incorrects."; + toast.error(msg); } }, onError: (err) => { diff --git a/server/freeproAutoImport.ts b/server/freeproAutoImport.ts index d827161..5e4eb1d 100644 --- a/server/freeproAutoImport.ts +++ b/server/freeproAutoImport.ts @@ -40,11 +40,15 @@ interface AutoImportResult { const FREEPRO_BASE_URL = "https://pro.free.fr"; const LOGIN_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/#/`; const LOGIN_FORM_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/`; +// Endpoint réel capturé via analyse réseau du portail FreePro (XHR POST) +const DO_LOGIN_URL = `${FREEPRO_BASE_URL}/account/security/do_login`; const ACCOUNT_URL = `${FREEPRO_BASE_URL}/account/`; const BILLING_URL = `${FREEPRO_BASE_URL}/account/billing`; -/** Timeout en ms pour les requêtes HTTP vers FreePro */ -const HTTP_TIMEOUT_MS = 15_000; +/** Timeout en ms pour les requêtes HTTP vers FreePro. + * L'endpoint do_login peut mettre jusqu'à ~16-17s à répondre. + */ +const HTTP_TIMEOUT_MS = 25_000; // ── Helpers ──────────────────────────────────────────────────────────────── @@ -118,47 +122,77 @@ async function loginToFreePro( // Ignorer les erreurs de la page de login — on continue sans cookies initiaux } - // ── Étape 2 : soumettre les credentials ── - // FreePro utilise plusieurs endpoints selon la version du portail. - // On essaie dans l'ordre les endpoints connus. - const loginEndpoints = [ - `${FREEPRO_BASE_URL}/api/v1/auth/login`, - `${FREEPRO_BASE_URL}/api/auth/login`, - `${FREEPRO_BASE_URL}/account/api/auth/login`, - `${FREEPRO_BASE_URL}/espace-client/api/auth/login`, - ]; - + // ── Étape 2 : soumettre les credentials sur le vrai endpoint FreePro ── + // Endpoint capturé par analyse réseau : POST /account/security/do_login + // Payload : { login: string, password: string } (NB: "login" et non "email") let sessionCookies = ""; let loginSuccess = false; - for (const endpoint of loginEndpoints) { - try { - const loginResp = await fetchWithTimeout(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", - "Accept": "application/json, text/plain, */*", - "Accept-Language": "fr-FR,fr;q=0.9", - "Cookie": initialCookies, - "Referer": LOGIN_URL, - "Origin": FREEPRO_BASE_URL, - "X-Requested-With": "XMLHttpRequest", - }, - body: JSON.stringify({ email, password }), - redirect: "follow", - }, 10_000); + try { + const loginResp = await fetchWithTimeout(DO_LOGIN_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "fr-FR,fr;q=0.9", + "Cookie": initialCookies, + "Referer": LOGIN_FORM_URL, + "Origin": FREEPRO_BASE_URL, + "X-Requested-With": "XMLHttpRequest", + }, + // Le portail utilise "login" (pas "email") comme clé d'identifiant + body: JSON.stringify({ login: email, password }), + redirect: "follow", + }, 25_000); - if (loginResp.ok || loginResp.status === 302) { - const newCookies = extractCookies(loginResp.headers.get("set-cookie")); - sessionCookies = [initialCookies, newCookies].filter(Boolean).join("; "); - loginSuccess = true; - break; + if (loginResp.ok || loginResp.status === 302 || loginResp.status === 204) { + // Lire le corps pour vérifier l'absence d'erreur + // L'API FreePro retourne HTTP 200 même en cas d'échec, avec { error: "invalid_credentials" } + let bodyText = ""; + try { bodyText = await loginResp.text(); } catch { /* ignorer */ } + + if (bodyText.includes("invalid_credentials") || bodyText.includes("invalide")) { + return { + success: false, + cookies: "", + error: "Identifiants FreePro incorrects (email ou mot de passe invalide).", + }; } - } catch { - // Essayer le prochain endpoint - continue; + + const newCookies = extractCookies(loginResp.headers.get("set-cookie")); + sessionCookies = [initialCookies, newCookies].filter(Boolean).join("; "); + loginSuccess = true; + } else if (loginResp.status === 401 || loginResp.status === 403) { + return { + success: false, + cookies: "", + error: "Identifiants FreePro incorrects (email ou mot de passe invalide).", + }; + } else { + // Lire le corps pour avoir plus d'info + let errorMsg = `Échec de connexion (HTTP ${loginResp.status})`; + try { + const body = await loginResp.text(); + if (body.includes("invalid_credentials") || body.includes("invalide") || body.includes("invalid") || body.includes("incorrect")) { + errorMsg = "Identifiants FreePro incorrects (email ou mot de passe invalide)."; + } + } catch { /* ignorer */ } + return { success: false, cookies: "", error: errorMsg }; } + } catch (err: any) { + if (err.name === "AbortError") { + return { + success: false, + cookies: "", + error: "Délai d'attente dépassé lors de la connexion au portail FreePro (12s).", + }; + } + return { + success: false, + cookies: "", + error: `Erreur réseau lors de la connexion : ${err.message}`, + }; } // ── Étape 3 : vérifier l'accès à /account/ ── diff --git a/server/routers.ts b/server/routers.ts index 9db3958..38b2e13 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -2350,16 +2350,35 @@ export const appRouter = router({ return { success: true }; }), - /** Teste la connexion au portail FreePro */ + /** Teste la connexion au portail FreePro. + * Le mot de passe est récupéré depuis la base de données (le frontend ne le reçoit jamais en clair). + * L'email peut être passé en paramètre pour tester avant sauvegarde, ou laisser vide pour utiliser celui en base. + */ testConnection: protectedProcedure .input( z.object({ - email: z.string().email(), - password: z.string().min(1), + email: z.string().optional(), + password: z.string().optional(), }) ) - .mutation(async ({ input }) => { - return testFreeproConnection(input.email, input.password); + .mutation(async ({ input, ctx }) => { + // Récupérer les vrais credentials depuis la base de données + const settings = await getFreeproSettings(ctx.user.id); + if (!settings?.loginPassword) { + return { + success: false, + error: "Aucun mot de passe configuré. Veuillez sauvegarder les paramètres d'abord.", + }; + } + const email = input?.email && !input.email.includes("•") ? input.email : settings.loginEmail ?? ""; + const password = input?.password && !input.password.includes("•") ? input.password : settings.loginPassword; + if (!email || !email.includes("@")) { + return { + success: false, + error: "Aucun email configuré. Veuillez sauvegarder les paramètres d'abord.", + }; + } + return testFreeproConnection(email, password); }), /** Force la récupération immédiate des factures FreePro */