Checkpoint: La configuration Microsoft Azure AD est de nouveau centralisée dans Paramètres : URL SharePoint, Tenant ID, Client ID, secret masqué, date d’expiration et tests de connexion/upload. Les Automatismes d’export ne contiennent plus d’identifiants et restent dédiés aux règles de destination. Tests, TypeScript, build et rendu de la page Paramètres validés en sandbox.

This commit is contained in:
Manus
2026-09-02 12:31:50 +00:00
parent 582ca77f28
commit 071376c9b1
5 changed files with 112 additions and 10 deletions

View File

@@ -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 denregistrer 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 <Card className="border-2 border-cyan-200">
<CardHeader className="border-b bg-gradient-to-r from-cyan-50 to-sky-50"><div className="flex items-center gap-3"><div className="rounded-lg bg-cyan-600 p-2"><Cloud className="h-6 w-6 text-white" /></div><div><CardTitle>Configuration Microsoft Azure AD</CardTitle><CardDescription>Accès partagé utilisé par les destinations SharePoint et Teams des automatismes dexport.</CardDescription></div></div></CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="space-y-2"><Label htmlFor="sharepoint-url">URL SharePoint / chemin</Label><Input id="sharepoint-url" value={sharepointUrl} onChange={(event) => setSharepointUrl(event.target.value)} placeholder="https://…sharepoint.com/:f:/r/sites/…/Documents/Factures" /><p className="text-xs text-muted-foreground">Pour un canal Teams, utilisez lURL SharePoint du dossier Fichiers concerné.</p></div>
<div className="grid gap-4 md:grid-cols-2"><div className="space-y-2"><Label htmlFor="azure-tenant-id">ID de lannuaire (Tenant ID)</Label><Input id="azure-tenant-id" value={tenantId} onChange={(event) => setTenantId(event.target.value)} /></div><div className="space-y-2"><Label htmlFor="azure-client-id">ID dapplication (Client ID)</Label><Input id="azure-client-id" value={clientId} onChange={(event) => setClientId(event.target.value)} /></div></div>
<div className="grid gap-4 md:grid-cols-2"><div className="space-y-2"><Label htmlFor="azure-client-secret">Secret client</Label><Input id="azure-client-secret" type="password" value={clientSecret} onChange={(event) => setClientSecret(event.target.value)} placeholder="Conserver le secret configuré" /><p className="text-xs text-muted-foreground">Laissez vide pour conserver le secret enregistré. Permissions requises : Files.ReadWrite.All et Sites.ReadWrite.All.</p></div><div className="space-y-2"><Label htmlFor="azure-secret-expiration">Date dexpiration du secret</Label><Input id="azure-secret-expiration" type="date" value={secretExpiresAt} onChange={(event) => setSecretExpiresAt(event.target.value)} />{expiration !== null && <p className={expiration <= 30 ? "text-xs text-amber-700" : "text-xs text-emerald-700"}><Calendar className="mr-1 inline h-3 w-3" />{expiration < 0 ? "Secret expiré" : `Secret valide encore ${expiration} jours`}</p>}</div></div>
<div className="flex flex-wrap gap-2 border-t pt-4"><Button onClick={save} disabled={updateMutation.isPending}><Save className="mr-2 h-4 w-4" />{updateMutation.isPending ? "Enregistrement…" : "Enregistrer Azure AD"}</Button><Button variant="outline" onClick={testConnection} disabled={testConnectionMutation.isPending || !canTest}>{testConnectionMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Wifi className="mr-2 h-4 w-4" />}Tester la connexion Azure AD</Button><Button variant="outline" onClick={testUpload} disabled={testUploadMutation.isPending || !canTest}>{testUploadMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Upload className="mr-2 h-4 w-4" />}Tester lupload SharePoint</Button></div>
{settings?.azureClientSecret && <p className="flex items-center gap-1 text-xs text-emerald-700"><CheckCircle2 className="h-3.5 w-3.5" />Un secret client est déjà enregistré de manière masquée.</p>}
</CardContent>
</Card>;
}

View File

@@ -40,17 +40,12 @@ export default function ExportAutomationRulesPanel() {
const [defaultExportMode, setDefaultExportMode] = useState<"browser" | "folder" | "both">("browser");
const [defaultDestinationType, setDefaultDestinationType] = useState<DestinationType>("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 dexport enregistrés");
setAzureClientSecret("");
utils.importSettings.get.invalidate();
},
onError: (error) => toast.error(error.message || "Impossible denregistrer les paramètres dexport"),
@@ -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() {
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2"><div className="space-y-2"><Label>Mode dexport BAP</Label><Select value={defaultExportMode} onValueChange={(value) => setDefaultExportMode(value as "browser" | "folder" | "both")}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="browser">Ouvrir dans le navigateur</SelectItem><SelectItem value="folder">Exporter vers la destination</SelectItem><SelectItem value="both">Destination et navigateur</SelectItem></SelectContent></Select></div><div className="space-y-2"><Label>Type de destination</Label><Select value={defaultDestinationType} onValueChange={(value) => setDefaultDestinationType(value as DestinationType)} disabled={defaultExportMode === "browser"}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="local">Dossier local</SelectItem><SelectItem value="teams">Teams</SelectItem><SelectItem value="sharepoint">SharePoint</SelectItem></SelectContent></Select></div></div>
<div className="space-y-2"><Label htmlFor="default-export-path">{defaultDestinationType === "local" ? "Chemin du dossier local" : "URL SharePoint du dossier"}</Label><Input id="default-export-path" value={defaultDestinationPath} disabled={defaultExportMode === "browser"} onChange={(event) => setDefaultDestinationPath(event.target.value)} placeholder={defaultDestinationType === "local" ? "/chemin/serveur/factures-bap" : "https://…sharepoint.com/sites/…/Documents/Factures"} /></div>
{(defaultDestinationType === "teams" || defaultDestinationType === "sharepoint") && <div className="grid gap-3 rounded-lg border border-cyan-200 bg-cyan-50/40 p-3 md:grid-cols-3"><div className="space-y-1"><Label htmlFor="azure-tenant">Tenant ID</Label><Input id="azure-tenant" value={azureTenantId} onChange={(event) => setAzureTenantId(event.target.value)} /></div><div className="space-y-1"><Label htmlFor="azure-client">Client ID</Label><Input id="azure-client" value={azureClientId} onChange={(event) => setAzureClientId(event.target.value)} /></div><div className="space-y-1"><Label htmlFor="azure-secret">Secret client</Label><Input id="azure-secret" type="password" value={azureClientSecret} onChange={(event) => setAzureClientSecret(event.target.value)} placeholder="Conserver le secret actuel" /></div><p className="col-span-full text-xs text-cyan-800">Teams utilise lURL SharePoint du dossier Fichiers du canal. Laissez le secret vide pour conserver celui déjà enregistré.</p></div>}
{(defaultDestinationType === "teams" || defaultDestinationType === "sharepoint") && <p className="rounded-lg border border-cyan-200 bg-cyan-50/40 p-3 text-xs text-cyan-800">La configuration Microsoft Azure AD est centralisée dans <strong>Paramètres</strong>. Teams utilise lURL SharePoint du dossier Fichiers du canal.</p>}
<Button onClick={saveDefaultExport} disabled={!importSettings || updateDefaultExportMutation.isPending}><Save className="mr-2 h-4 w-4" />{updateDefaultExportMutation.isPending ? "Enregistrement…" : "Enregistrer les paramètres dexport"}</Button>
</CardContent>
</Card>

View File

@@ -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 ===== */}
<TabsContent value="import" className="space-y-6">
<AzureAdExportSettingsCard />
{/* Import manuel */}
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-b">

View File

@@ -1766,6 +1766,7 @@ export const appRouter = router({
azureTenantId: null,
azureClientId: null,
azureClientSecret: null,
azureSecretExpiresAt: null,
};
}

View File

@@ -866,3 +866,9 @@
- [x] Appliquer les règles de destination lors des exports de factures BAP
- [x] Déplacer les paramètres dexport 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 lURL SharePoint, Tenant ID, Client ID, secret et date dexpiration
- [x] Rétablir les tests Azure AD et upload SharePoint depuis Paramètres
- [x] Retirer les accès Microsoft Azure AD des automatismes dexport
- [x] Valider les deux emplacements en sandbox