diff --git a/client/src/components/AzureAdExportSettingsCard.tsx b/client/src/components/AzureAdExportSettingsCard.tsx new file mode 100644 index 0000000..fa0b7a6 --- /dev/null +++ b/client/src/components/AzureAdExportSettingsCard.tsx @@ -0,0 +1,101 @@ +import { useEffect, useMemo, useState } from "react"; +import { Calendar, CheckCircle2, Cloud, Loader2, Save, Upload, Wifi } from "lucide-react"; +import { trpc } from "@/lib/trpc"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; + +/** Les identifiants Azure restent centralisés dans Paramètres, hors des règles métier. */ +export default function AzureAdExportSettingsCard() { + const utils = trpc.useUtils(); + const { data: settings } = trpc.importSettings.get.useQuery(); + const [sharepointUrl, setSharepointUrl] = useState(""); + const [tenantId, setTenantId] = useState(""); + const [clientId, setClientId] = useState(""); + const [clientSecret, setClientSecret] = useState(""); + const [secretExpiresAt, setSecretExpiresAt] = useState(""); + + useEffect(() => { + if (!settings) return; + setSharepointUrl(settings.exportFolder || ""); + setTenantId(settings.azureTenantId || ""); + setClientId(settings.azureClientId || ""); + setSecretExpiresAt(settings.azureSecretExpiresAt ? new Date(settings.azureSecretExpiresAt).toISOString().slice(0, 10) : ""); + }, [settings]); + + const updateMutation = trpc.importSettings.update.useMutation({ + onSuccess: () => { + toast.success("Configuration Microsoft Azure AD enregistrée"); + setClientSecret(""); + utils.importSettings.get.invalidate(); + }, + onError: (error) => toast.error(error.message || "Impossible d’enregistrer la configuration Azure AD"), + }); + const testConnectionMutation = trpc.importSettings.testAzureConnection.useMutation(); + const testUploadMutation = trpc.importSettings.testSharePointUpload.useMutation(); + + const expiration = useMemo(() => { + if (!secretExpiresAt) return null; + const date = new Date(`${secretExpiresAt}T12:00:00Z`); + if (Number.isNaN(date.getTime())) return null; + return Math.ceil((date.getTime() - Date.now()) / 86_400_000); + }, [secretExpiresAt]); + + const save = () => { + if (!settings) return; + if (!sharepointUrl.trim() || !tenantId.trim() || !clientId.trim()) { + toast.error("URL SharePoint, Tenant ID et Client ID sont obligatoires"); + return; + } + updateMutation.mutate({ + manualImportEnabled: settings.manualImportEnabled, + autoImportEnabled: settings.autoImportEnabled, + emailImportEnabled: settings.emailImportEnabled, + exportFolder: sharepointUrl.trim(), + exportFolderType: "sharepoint", + bapExportMode: settings.bapExportMode, + azureTenantId: tenantId.trim(), + azureClientId: clientId.trim(), + azureSecretExpiresAt: secretExpiresAt || null, + ...(clientSecret ? { azureClientSecret: clientSecret } : {}), + }); + }; + + const testConnection = async () => { + try { + const result = await testConnectionMutation.mutateAsync(); + result.success ? toast.success(result.message || "Connexion Azure AD réussie") : toast.error(result.error || "Connexion Azure AD impossible"); + } catch (error: any) { + toast.error(error.message || "Connexion Azure AD impossible"); + } + }; + + const testUpload = async () => { + try { + const result = await testUploadMutation.mutateAsync(); + if (!result.success) { + toast.error(result.error || "Upload SharePoint impossible"); + return; + } + toast.success("Fichier test déposé dans SharePoint"); + if (result.webUrl) window.open(result.webUrl, "_blank", "noopener,noreferrer"); + } catch (error: any) { + toast.error(error.message || "Upload SharePoint impossible"); + } + }; + + const canTest = Boolean(sharepointUrl && tenantId && clientId); + + return +
Configuration Microsoft Azure ADAccès partagé utilisé par les destinations SharePoint et Teams des automatismes d’export.
+ +
setSharepointUrl(event.target.value)} placeholder="https://…sharepoint.com/:f:/r/sites/…/Documents/Factures" />

Pour un canal Teams, utilisez l’URL SharePoint du dossier Fichiers concerné.

+
setTenantId(event.target.value)} />
setClientId(event.target.value)} />
+
setClientSecret(event.target.value)} placeholder="Conserver le secret configuré" />

Laissez vide pour conserver le secret enregistré. Permissions requises : Files.ReadWrite.All et Sites.ReadWrite.All.

setSecretExpiresAt(event.target.value)} />{expiration !== null &&

{expiration < 0 ? "Secret expiré" : `Secret valide encore ${expiration} jours`}

}
+
+ {settings?.azureClientSecret &&

Un secret client est déjà enregistré de manière masquée.

} +
+
; +} diff --git a/client/src/components/ExportAutomationRulesPanel.tsx b/client/src/components/ExportAutomationRulesPanel.tsx index 5fc7a24..b972f8e 100644 --- a/client/src/components/ExportAutomationRulesPanel.tsx +++ b/client/src/components/ExportAutomationRulesPanel.tsx @@ -40,17 +40,12 @@ export default function ExportAutomationRulesPanel() { const [defaultExportMode, setDefaultExportMode] = useState<"browser" | "folder" | "both">("browser"); const [defaultDestinationType, setDefaultDestinationType] = useState("local"); const [defaultDestinationPath, setDefaultDestinationPath] = useState(""); - const [azureTenantId, setAzureTenantId] = useState(""); - const [azureClientId, setAzureClientId] = useState(""); - const [azureClientSecret, setAzureClientSecret] = useState(""); useEffect(() => { if (!importSettings) return; setDefaultExportMode(importSettings.bapExportMode || "browser"); setDefaultDestinationType((importSettings.exportFolderType || "local") as DestinationType); setDefaultDestinationPath(importSettings.exportFolder || ""); - setAzureTenantId(importSettings.azureTenantId || ""); - setAzureClientId(importSettings.azureClientId || ""); }, [importSettings]); const conditionValues = useMemo(() => { @@ -94,7 +89,6 @@ export default function ExportAutomationRulesPanel() { const updateDefaultExportMutation = trpc.importSettings.update.useMutation({ onSuccess: () => { toast.success("Paramètres d’export enregistrés"); - setAzureClientSecret(""); utils.importSettings.get.invalidate(); }, onError: (error) => toast.error(error.message || "Impossible d’enregistrer les paramètres d’export"), @@ -143,9 +137,6 @@ export default function ExportAutomationRulesPanel() { exportFolder: defaultDestinationPath.trim() || null, exportFolderType: defaultDestinationType, bapExportMode: defaultExportMode, - azureTenantId: azureTenantId.trim() || null, - azureClientId: azureClientId.trim() || null, - ...(azureClientSecret ? { azureClientSecret } : {}), }); }; @@ -156,7 +147,7 @@ export default function ExportAutomationRulesPanel() {
setDefaultDestinationPath(event.target.value)} placeholder={defaultDestinationType === "local" ? "/chemin/serveur/factures-bap" : "https://…sharepoint.com/sites/…/Documents/Factures"} />
- {(defaultDestinationType === "teams" || defaultDestinationType === "sharepoint") &&
setAzureTenantId(event.target.value)} />
setAzureClientId(event.target.value)} />
setAzureClientSecret(event.target.value)} placeholder="Conserver le secret actuel" />

Teams utilise l’URL SharePoint du dossier Fichiers du canal. Laissez le secret vide pour conserver celui déjà enregistré.

} + {(defaultDestinationType === "teams" || defaultDestinationType === "sharepoint") &&

La configuration Microsoft Azure AD est centralisée dans Paramètres. Teams utilise l’URL SharePoint du dossier Fichiers du canal.

}
diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 51782b5..ae94326 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { trpc } from "@/lib/trpc"; +import AzureAdExportSettingsCard from "@/components/AzureAdExportSettingsCard"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -357,6 +358,8 @@ export default function ImportSettings() { {/* ===== ONGLET IMPORT ===== */} + + {/* Import manuel */} diff --git a/server/routers.ts b/server/routers.ts index fba4422..0d802fd 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1766,6 +1766,7 @@ export const appRouter = router({ azureTenantId: null, azureClientId: null, azureClientSecret: null, + azureSecretExpiresAt: null, }; } diff --git a/todo.md b/todo.md index d069a22..dad6210 100644 --- a/todo.md +++ b/todo.md @@ -866,3 +866,9 @@ - [x] Appliquer les règles de destination lors des exports de factures BAP - [x] Déplacer les paramètres d’export dans Automatismes et renommer la page en Paramètres - [x] Ajouter les tests et valider les nouveaux parcours en sandbox + +## Répartition des paramètres Azure AD +- [x] Rétablir dans Paramètres l’URL SharePoint, Tenant ID, Client ID, secret et date d’expiration +- [x] Rétablir les tests Azure AD et upload SharePoint depuis Paramètres +- [x] Retirer les accès Microsoft Azure AD des automatismes d’export +- [x] Valider les deux emplacements en sandbox