Checkpoint: Les connecteurs web acceptent désormais les noms de domaine saisis sans protocole : HTTPS est ajouté automatiquement dans l’interface et à l’API. Les URL HTTP(S) valides restent acceptées et les adresses incorrectes affichent un message métier clair. Sept tests dédiés, la suite complète (71 tests), TypeScript et build ont été validés.
This commit is contained in:
@@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { isValidWebPortalUrl, normalizeWebPortalUrl } from "@shared/webPortalUrl";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -158,12 +159,20 @@ export default function WebImportSources() {
|
||||
toast.error("Champs requis", { description: "Nom, identifiant et mot de passe sont obligatoires." });
|
||||
return;
|
||||
}
|
||||
const portalUrl = normalizeWebPortalUrl(form.portalUrl);
|
||||
if (!isValidWebPortalUrl(portalUrl)) {
|
||||
toast.error("URL de portail invalide", {
|
||||
description: "Saisissez une adresse HTTP(S), par exemple https://espaceclient.exemple.fr.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (editSource) {
|
||||
const updateData: any = { id: editSource.id, ...form };
|
||||
const updateData: any = { id: editSource.id, ...form, portalUrl };
|
||||
if (!form.loginPassword) delete updateData.loginPassword; // Ne pas écraser si vide
|
||||
updateMutation.mutate(updateData);
|
||||
} else {
|
||||
createMutation.mutate(form);
|
||||
createMutation.mutate({ ...form, portalUrl });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +325,7 @@ export default function WebImportSources() {
|
||||
onChange={e => setForm(f => ({ ...f, portalUrl: e.target.value }))}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Le préfixe https:// est ajouté automatiquement si nécessaire.</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Identifiant (email ou login)</Label>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { COOKIE_NAME } from "@shared/const";
|
||||
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||
import { calculateInvoiceQualityScore } from "@shared/invoiceQuality";
|
||||
import { webPortalUrlSchema } from "./webPortalUrl";
|
||||
|
||||
interface Condition {
|
||||
field: string;
|
||||
@@ -2743,7 +2744,7 @@ export const appRouter = router({
|
||||
.input(z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
connectorType: z.string().min(1).max(50),
|
||||
portalUrl: z.string().url(),
|
||||
portalUrl: webPortalUrlSchema,
|
||||
loginEmail: z.string().min(1),
|
||||
loginPassword: z.string().min(1),
|
||||
frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).default('monthly'),
|
||||
@@ -2758,7 +2759,7 @@ export const appRouter = router({
|
||||
id: z.number(),
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
connectorType: z.string().min(1).max(50).optional(),
|
||||
portalUrl: z.string().url().optional(),
|
||||
portalUrl: webPortalUrlSchema.optional(),
|
||||
loginEmail: z.string().min(1).optional(),
|
||||
loginPassword: z.string().optional(),
|
||||
frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).optional(),
|
||||
|
||||
49
server/webPortalUrl.test.ts
Normal file
49
server/webPortalUrl.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isValidWebPortalUrl, normalizeWebPortalUrl } from "@shared/webPortalUrl";
|
||||
import { webPortalUrlSchema } from "./webPortalUrl";
|
||||
|
||||
describe("normalizeWebPortalUrl", () => {
|
||||
it("ajoute HTTPS aux adresses de portail saisies sans protocole", () => {
|
||||
expect(normalizeWebPortalUrl("www.sfr.fr/mon-espace-client")).toBe(
|
||||
"https://www.sfr.fr/mon-espace-client",
|
||||
);
|
||||
});
|
||||
|
||||
it("retire les espaces sans modifier une URL HTTPS déjà valide", () => {
|
||||
expect(normalizeWebPortalUrl(" https://pro.free.fr/ ")).toBe("https://pro.free.fr/");
|
||||
});
|
||||
|
||||
it("ne transforme pas un protocole non web en URL HTTPS", () => {
|
||||
expect(normalizeWebPortalUrl("ftp://example.org/factures")).toBe("ftp://example.org/factures");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidWebPortalUrl", () => {
|
||||
it("accepte exclusivement les URLs HTTP ou HTTPS avec un hôte", () => {
|
||||
expect(isValidWebPortalUrl("https://espaceclient.orange.fr/")).toBe(true);
|
||||
expect(isValidWebPortalUrl("http://intranet.local/factures")).toBe(true);
|
||||
});
|
||||
|
||||
it("refuse les valeurs vides, incomplètes ou avec un protocole non web", () => {
|
||||
expect(isValidWebPortalUrl("")).toBe(false);
|
||||
expect(isValidWebPortalUrl("https://")).toBe(false);
|
||||
expect(isValidWebPortalUrl("ftp://example.org/factures")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("webPortalUrlSchema", () => {
|
||||
it("normalise l’URL reçue par l’API avant de la persister", () => {
|
||||
expect(webPortalUrlSchema.parse("espaceclient.orange.fr")).toBe(
|
||||
"https://espaceclient.orange.fr",
|
||||
);
|
||||
});
|
||||
|
||||
it("retourne un message métier pour une URL de portail non HTTP(S)", () => {
|
||||
const result = webPortalUrlSchema.safeParse("ftp://example.org/factures");
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0]?.message).toContain("URL HTTP(S) valide");
|
||||
}
|
||||
});
|
||||
});
|
||||
13
server/webPortalUrl.ts
Normal file
13
server/webPortalUrl.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { isValidWebPortalUrl, normalizeWebPortalUrl } from "@shared/webPortalUrl";
|
||||
|
||||
/**
|
||||
* Normalise les domaines saisis sans protocole puis impose une URL HTTP(S).
|
||||
* Le même schéma est utilisé à la création et à la mise à jour d’un connecteur.
|
||||
*/
|
||||
export const webPortalUrlSchema = z
|
||||
.string()
|
||||
.transform(normalizeWebPortalUrl)
|
||||
.refine(isValidWebPortalUrl, {
|
||||
message: "Saisissez une URL HTTP(S) valide, par exemple https://espaceclient.exemple.fr.",
|
||||
});
|
||||
24
shared/webPortalUrl.ts
Normal file
24
shared/webPortalUrl.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Ajoute HTTPS aux adresses de portail saisies sous forme de domaine simple.
|
||||
* Les protocoles explicites sont conservés afin que la validation puisse refuser
|
||||
* proprement ceux qui ne sont pas HTTP(S).
|
||||
*/
|
||||
export function normalizeWebPortalUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.startsWith("//")) return `https:${trimmed}`;
|
||||
if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed) || /^(?:mailto|tel|data):/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
}
|
||||
|
||||
/** Vérifie qu’une URL de portail est une adresse HTTP(S) avec un hôte. */
|
||||
export function isValidWebPortalUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return (url.protocol === "https:" || url.protocol === "http:") && Boolean(url.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
5
todo.md
5
todo.md
@@ -1,5 +1,10 @@
|
||||
# Invoice Analyzer - TODO
|
||||
|
||||
## Correction validation connecteur web — 30/09/2026
|
||||
- [x] Reproduire l’erreur `portalUrl` invalide et identifier l’écart formulaire/API
|
||||
- [x] Corriger la normalisation et la validation de l’URL du portail
|
||||
- [ ] Valider les tests, puis déployer en recette et production sans toucher à MySQL ni aux volumes
|
||||
|
||||
## Diagnostic réception e-mail automatique — 24/09/2026
|
||||
- [x] Reproduire le démarrage de la réception automatique et identifier le blocage interface → API → planificateur
|
||||
- [x] Corriger la cause sans lancer d’import ni créer de planification non sollicitée, avec sélection explicite du compte source pour les administrateurs
|
||||
|
||||
Reference in New Issue
Block a user