Checkpoint: Fix testConnection : le backend lit désormais le mot de passe directement en base (jamais exposé au frontend). Timeout porté à 25s. Détection correcte de invalid_credentials dans la réponse JSON FreePro.

This commit is contained in:
Manus
2026-06-08 09:58:58 -04:00
parent f676395973
commit f3e985af5f
4 changed files with 119 additions and 44 deletions

View File

@@ -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/ ──

View File

@@ -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 */