diff --git a/client/src/pages/WebImportSources.tsx b/client/src/pages/WebImportSources.tsx
index 9bf7bb4..6245ead 100644
--- a/client/src/pages/WebImportSources.tsx
+++ b/client/src/pages/WebImportSources.tsx
@@ -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://..."
/>
+
Le préfixe https:// est ajouté automatiquement si nécessaire.
diff --git a/server/routers.ts b/server/routers.ts
index 17485d6..042d8f5 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -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;
@@ -2577,7 +2578,7 @@ export const appRouter = router({
saveSettings: protectedProcedure
.input(
z.object({
- portalUrl: z.string().url().optional(),
+ portalUrl: webPortalUrlSchema.optional(),
loginEmail: z.string().email().optional().or(z.literal('')),
loginPassword: z.string().optional(), // vide = ne pas changer
frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).optional(),
@@ -2723,7 +2724,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'),
diff --git a/server/webPortalUrl.test.ts b/server/webPortalUrl.test.ts
new file mode 100644
index 0000000..e002211
--- /dev/null
+++ b/server/webPortalUrl.test.ts
@@ -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");
+ }
+ });
+});
diff --git a/server/webPortalUrl.ts b/server/webPortalUrl.ts
new file mode 100644
index 0000000..c5a48af
--- /dev/null
+++ b/server/webPortalUrl.ts
@@ -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.",
+ });
diff --git a/shared/webPortalUrl.ts b/shared/webPortalUrl.ts
new file mode 100644
index 0000000..fcf5a1a
--- /dev/null
+++ b/shared/webPortalUrl.ts
@@ -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;
+ }
+}