Checkpoint: Réception automatique e-mail sécurisée : le planificateur est créé uniquement après un test IMAP concluant, les erreurs d’authentification sont affichées explicitement et ne déclenchent plus de boucle. Les administrateurs peuvent sélectionner, démarrer, arrêter et suivre le compte source déjà configuré sans afficher ni recopier de secrets. Tests de démarrage réussi/refusé, arrêt et ciblage de compte ajoutés ; 59 tests, TypeScript et build validés.

This commit is contained in:
Manus
2026-09-24 09:34:22 +00:00
parent fcde121438
commit 5aa2b9e285
5 changed files with 307 additions and 45 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { trpc } from "@/lib/trpc";
import AzureAdExportSettingsCard from "@/components/AzureAdExportSettingsCard";
import { Button } from "@/components/ui/button";
@@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuth } from "@/_core/hooks/useAuth";
import { toast } from "sonner";
import {
Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download,
@@ -153,6 +154,8 @@ function BackupSection() {
export default function ImportSettings() {
const { user } = useAuth();
const isAdmin = user?.role === "admin";
// Chargement différé : on ne charge les statuts de services qu'après le montage
const { data: settings, isLoading } = trpc.importSettings.get.useQuery(undefined, {
staleTime: 30_000,
@@ -173,6 +176,18 @@ export default function ImportSettings() {
const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation();
const testSharePointUploadMutation = trpc.importSettings.testSharePointUpload.useMutation();
const testEmailConnectionMutation = trpc.importSettings.testEmailConnection.useMutation();
const { data: emailAccounts, isLoading: emailAccountsLoading } = trpc.emailImportService.getConfiguredAccounts.useQuery(undefined, {
enabled: isAdmin,
staleTime: 30_000,
});
const [selectedEmailAccountId, setSelectedEmailAccountId] = useState("");
const selectedEmailAccountUserId = Number(selectedEmailAccountId);
const { data: selectedEmailAccountStatus } = trpc.emailImportService.accountStatus.useQuery(
{ userId: selectedEmailAccountUserId },
{ enabled: isAdmin && Number.isInteger(selectedEmailAccountUserId) && selectedEmailAccountUserId > 0 },
);
const startEmailAccountMutation = trpc.emailImportService.startAccount.useMutation();
const stopEmailAccountMutation = trpc.emailImportService.stopAccount.useMutation();
// Manual import
const [manualImportEnabled, setManualImportEnabled] = useState(true);
@@ -257,6 +272,22 @@ export default function ImportSettings() {
}
}, [settings]);
const configuredEmailAccounts = useMemo(
() => (emailAccounts ?? []).filter((account) => account.isConfigured === 1),
[emailAccounts],
);
useEffect(() => {
if (!isAdmin || configuredEmailAccounts.length === 0) {
setSelectedEmailAccountId("");
return;
}
if (!configuredEmailAccounts.some((account) => String(account.userId) === selectedEmailAccountId)) {
setSelectedEmailAccountId(String(configuredEmailAccounts[0].userId));
}
}, [isAdmin, configuredEmailAccounts, selectedEmailAccountId]);
const computedExportMode = (): "browser" | "folder" | "both" => {
if (bapExportBrowser && bapExportFolder) return "both";
if (bapExportFolder) return "folder";
@@ -537,8 +568,9 @@ export default function ImportSettings() {
/>
</div>
{emailImportEnabled && (
<div className="space-y-4 p-4 bg-green-50/50 dark:bg-green-950/10 rounded-lg border-2 border-green-200 dark:border-green-800">
<div className="space-y-4 p-4 bg-green-50/50 dark:bg-green-950/10 rounded-lg border-2 border-green-200 dark:border-green-800">
{emailImportEnabled && (
<>
{/* Mode d'authentification */}
<div className="space-y-2">
<Label className="text-base font-medium">Mode d'authentification IMAP</Label>
@@ -568,7 +600,7 @@ export default function ImportSettings() {
</div>
{emailImportAuthMode === "oauth2" && (
<div className="p-3 bg-purple-50 dark:bg-purple-950/20 rounded-lg border border-purple-200 dark:border-purple-800 text-sm text-purple-700 dark:text-purple-300">
<strong>Office 365 :</strong> Microsoft a désactivé l'auth basique pour Exchange Online. Utilisez ce mode avec les credentials Azure AD déjà configurés dans l'onglet Export.
<strong>Office 365 :</strong> Microsoft a désactivé l'auth basique pour Exchange Online. Utilisez ce mode avec les paramètres Azure AD configurés dans la carte Microsoft Azure AD au début de cette page.
</div>
)}
</div>
@@ -600,7 +632,7 @@ export default function ImportSettings() {
{emailImportAuthMode === "oauth2" && (
<div className="p-3 bg-muted/30 rounded-lg border text-sm text-muted-foreground">
<Wifi className="inline h-4 w-4 mr-1" />
En mode OAuth2, les credentials Azure AD (Tenant ID, Client ID, Client Secret) configurés dans l'onglet <strong>Export &gt; SharePoint</strong> seront utilisés automatiquement.
En mode OAuth2, les paramètres Azure AD (Tenant ID, Client ID, Client Secret) configurés dans la <strong>carte Microsoft Azure AD</strong> seront utilisés automatiquement.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -741,12 +773,12 @@ export default function ImportSettings() {
const result = await startEmailServiceMutation.mutateAsync();
if (result.success) {
await utils.emailImportService.status.invalidate();
toast.success("Service démarré");
toast.success(result.message);
} else {
toast.error("Impossible de démarrer le service");
toast.error(result.message || "Impossible de démarrer le service", { duration: 8000 });
}
} catch {
toast.error("Impossible de démarrer le service");
} catch (error: any) {
toast.error(error?.message || "Impossible de démarrer le service", { duration: 8000 });
}
}}
disabled={!emailImportEnabled || startEmailServiceMutation.isPending}
@@ -757,8 +789,82 @@ export default function ImportSettings() {
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
Enregistrez les paramètres avant de démarrer. Le service ne démarre que si le test IMAP est concluant.
</p>
</>
)}
{isAdmin && (
<div className="space-y-3 border-t border-green-200 pt-4 dark:border-green-800">
<div>
<p className="text-sm font-medium">Compte source administré</p>
<p className="text-xs text-muted-foreground">
Démarrez la boîte déjà configurée, même si ses paramètres appartiennent à un autre compte applicatif. Aucun identifiant n’est affiché ni recopié.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<select
aria-label="Compte source de réception e-mail"
className="h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-3 text-sm"
value={selectedEmailAccountId}
onChange={(event) => setSelectedEmailAccountId(event.target.value)}
disabled={emailAccountsLoading || configuredEmailAccounts.length === 0}
>
{configuredEmailAccounts.length === 0 ? (
<option value="">Aucune boîte e-mail configurée</option>
) : (
configuredEmailAccounts.map((account) => (
<option key={account.userId} value={String(account.userId)}>
{account.userName || account.userEmail || `Compte #${account.userId}`}
{account.automaticEnabled === 1 ? " — réception activée" : " — réception désactivée"}
</option>
))
)}
</select>
{selectedEmailAccountStatus?.isRunning ? (
<Button
variant="destructive"
onClick={async () => {
try {
const result = await stopEmailAccountMutation.mutateAsync({ userId: selectedEmailAccountUserId });
await utils.emailImportService.accountStatus.invalidate();
toast.success(result.message);
} catch (error: any) {
toast.error(error?.message || "Impossible d’arrêter le service.");
}
}}
disabled={stopEmailAccountMutation.isPending}
>
{stopEmailAccountMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Square className="mr-2 h-4 w-4" />}
Arrêter
</Button>
) : (
<Button
onClick={async () => {
try {
const result = await startEmailAccountMutation.mutateAsync({ userId: selectedEmailAccountUserId });
if (result.success) {
await utils.emailImportService.accountStatus.invalidate();
toast.success(result.message);
} else {
toast.error(result.message, { duration: 8000 });
}
} catch (error: any) {
toast.error(error?.message || "Impossible de démarrer le service.", { duration: 8000 });
}
}}
disabled={!selectedEmailAccountUserId || startEmailAccountMutation.isPending}
className="bg-green-600 hover:bg-green-700"
>
{startEmailAccountMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Play className="mr-2 h-4 w-4" />}
Démarrer
</Button>
)}
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>