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:
101
client/src/components/AzureAdExportSettingsCard.tsx
Normal file
101
client/src/components/AzureAdExportSettingsCard.tsx
Normal 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 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 <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 d’export.</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 l’URL 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 l’annuaire (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 d’application (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 d’expiration 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 l’upload 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>;
|
||||
}
|
||||
@@ -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 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() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2"><div className="space-y-2"><Label>Mode d’export 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 l’URL 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 l’URL 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 d’export"}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user