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:
20
.manus/db/db-query-1780926876439.json
Normal file
20
.manus/db/db-query-1780926876439.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -140,9 +140,11 @@ function ParametrageTab() {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setIsTesting(false);
|
setIsTesting(false);
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
toast.success(data.message);
|
const msg = (data as any).message ?? "Connexion réussie !";
|
||||||
|
toast.success(msg);
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.message);
|
const msg = (data as any).error ?? (data as any).message ?? "Identifiants incorrects.";
|
||||||
|
toast.error(msg);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
|
|||||||
@@ -40,11 +40,15 @@ interface AutoImportResult {
|
|||||||
const FREEPRO_BASE_URL = "https://pro.free.fr";
|
const FREEPRO_BASE_URL = "https://pro.free.fr";
|
||||||
const LOGIN_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/#/`;
|
const LOGIN_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/#/`;
|
||||||
const LOGIN_FORM_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 ACCOUNT_URL = `${FREEPRO_BASE_URL}/account/`;
|
||||||
const BILLING_URL = `${FREEPRO_BASE_URL}/account/billing`;
|
const BILLING_URL = `${FREEPRO_BASE_URL}/account/billing`;
|
||||||
|
|
||||||
/** Timeout en ms pour les requêtes HTTP vers FreePro */
|
/** Timeout en ms pour les requêtes HTTP vers FreePro.
|
||||||
const HTTP_TIMEOUT_MS = 15_000;
|
* L'endpoint do_login peut mettre jusqu'à ~16-17s à répondre.
|
||||||
|
*/
|
||||||
|
const HTTP_TIMEOUT_MS = 25_000;
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -118,22 +122,14 @@ async function loginToFreePro(
|
|||||||
// Ignorer les erreurs de la page de login — on continue sans cookies initiaux
|
// Ignorer les erreurs de la page de login — on continue sans cookies initiaux
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Étape 2 : soumettre les credentials ──
|
// ── Étape 2 : soumettre les credentials sur le vrai endpoint FreePro ──
|
||||||
// FreePro utilise plusieurs endpoints selon la version du portail.
|
// Endpoint capturé par analyse réseau : POST /account/security/do_login
|
||||||
// On essaie dans l'ordre les endpoints connus.
|
// Payload : { login: string, password: string } (NB: "login" et non "email")
|
||||||
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`,
|
|
||||||
];
|
|
||||||
|
|
||||||
let sessionCookies = "";
|
let sessionCookies = "";
|
||||||
let loginSuccess = false;
|
let loginSuccess = false;
|
||||||
|
|
||||||
for (const endpoint of loginEndpoints) {
|
|
||||||
try {
|
try {
|
||||||
const loginResp = await fetchWithTimeout(endpoint, {
|
const loginResp = await fetchWithTimeout(DO_LOGIN_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -141,24 +137,62 @@ async function loginToFreePro(
|
|||||||
"Accept": "application/json, text/plain, */*",
|
"Accept": "application/json, text/plain, */*",
|
||||||
"Accept-Language": "fr-FR,fr;q=0.9",
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
||||||
"Cookie": initialCookies,
|
"Cookie": initialCookies,
|
||||||
"Referer": LOGIN_URL,
|
"Referer": LOGIN_FORM_URL,
|
||||||
"Origin": FREEPRO_BASE_URL,
|
"Origin": FREEPRO_BASE_URL,
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, password }),
|
// Le portail utilise "login" (pas "email") comme clé d'identifiant
|
||||||
|
body: JSON.stringify({ login: email, password }),
|
||||||
redirect: "follow",
|
redirect: "follow",
|
||||||
}, 10_000);
|
}, 25_000);
|
||||||
|
|
||||||
|
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).",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (loginResp.ok || loginResp.status === 302) {
|
|
||||||
const newCookies = extractCookies(loginResp.headers.get("set-cookie"));
|
const newCookies = extractCookies(loginResp.headers.get("set-cookie"));
|
||||||
sessionCookies = [initialCookies, newCookies].filter(Boolean).join("; ");
|
sessionCookies = [initialCookies, newCookies].filter(Boolean).join("; ");
|
||||||
loginSuccess = true;
|
loginSuccess = true;
|
||||||
break;
|
} 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 {
|
} catch { /* ignorer */ }
|
||||||
// Essayer le prochain endpoint
|
return { success: false, cookies: "", error: errorMsg };
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
} 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/ ──
|
// ── Étape 3 : vérifier l'accès à /account/ ──
|
||||||
|
|||||||
@@ -2350,16 +2350,35 @@ export const appRouter = router({
|
|||||||
return { success: true };
|
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
|
testConnection: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().optional(),
|
||||||
password: z.string().min(1),
|
password: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
return testFreeproConnection(input.email, input.password);
|
// 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 */
|
/** Force la récupération immédiate des factures FreePro */
|
||||||
|
|||||||
Reference in New Issue
Block a user