From f44dadc45343571ecfbc1b9ab061b44733fef369 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Sun, 7 Jun 2026 22:05:28 +0200 Subject: [PATCH] feat(admin): add VirusTotal scan settings and mail UI File policies API key field, conditional scan badge, safe policy merge. --- .../sections/file-policies-section.tsx | 31 +++++++++ .../gmail/email-view/message-attachments.tsx | 66 +++++++++---------- lib/admin-settings/map-api-org-settings.ts | 23 ++++++- lib/admin-settings/org-settings-store.ts | 1 + lib/admin-settings/org-settings-types.ts | 1 + lib/api/admin-org-types.ts | 1 + lib/api/map-message-attachments.ts | 2 + lib/email-data.ts | 2 + 8 files changed, 91 insertions(+), 36 deletions(-) diff --git a/components/admin/settings/sections/file-policies-section.tsx b/components/admin/settings/sections/file-policies-section.tsx index 2507949..3660159 100644 --- a/components/admin/settings/sections/file-policies-section.tsx +++ b/components/admin/settings/sections/file-policies-section.tsx @@ -17,6 +17,13 @@ import { export function FilePoliciesSection() { const filePolicies = useOrgSettingsStore((s) => s.filePolicies) const setFilePolicies = useOrgSettingsStore((s) => s.setFilePolicies) + const vtKeyConfigured = useOrgSettingsStore( + (s) => s.meta?.secrets?.virustotal_api_key?.configured ?? false + ) + const vtKeyMissing = + filePolicies.virus_scan_enabled && + !vtKeyConfigured && + !(filePolicies.virustotal_api_key ?? "").trim() return (

Analyse antivirus à l'upload

+

+ VirusTotal — scan synchrone à l'upload Drive et pièces jointes mail +

setFilePolicies({ virus_scan_enabled })} /> + {filePolicies.virus_scan_enabled ? ( +
+ + setFilePolicies({ virustotal_api_key: e.target.value })} + placeholder={vtKeyConfigured ? "•••••••• (laisser vide pour conserver)" : "Coller la clé API"} + /> + {vtKeyConfigured && !(filePolicies.virustotal_api_key ?? "").trim() ? ( +

Clé configurée

+ ) : null} + {vtKeyMissing ? ( +

+ Analyse activée sans clé API — les uploads ne seront pas scannés. +

+ ) : null} +
+ ) : null}
) diff --git a/components/gmail/email-view/message-attachments.tsx b/components/gmail/email-view/message-attachments.tsx index 06fc497..4e41419 100644 --- a/components/gmail/email-view/message-attachments.tsx +++ b/components/gmail/email-view/message-attachments.tsx @@ -85,6 +85,34 @@ function DriveLocationBadge({ folderPath }: { folderPath: string }) { ) } +function attachmentsVirusTotalScanned(attachments: EmailAttachment[]): boolean { + return attachments.some((a) => a.virusScanStatus === "clean") +} + +function VirusTotalScanBadge() { + return ( + <> + · + Analysé par VirusTotal + + + + + + VirusTotal analyse les pièces jointes et les compare à une base de signatures pour + repérer les virus et logiciels malveillants. + + + + ) +} + export function MessageAttachmentsSection({ messageId, attachments, @@ -117,6 +145,7 @@ export function MessageAttachmentsSection({ const summary = n === 1 ? "Une pièce jointe" : `${n} pièces jointes` const asPills = shouldUseAttachmentPillsInPreview(attachments) + const showVirusTotal = attachmentsVirusTotalScanned(attachments) const openPreview = (index: number) => { if (!attachments.some((a) => a.id)) { @@ -167,24 +196,8 @@ export function MessageAttachmentsSection({
{summary} - · - Analysé par VirusTotal + {showVirusTotal ? : null} - - - - - - VirusTotal analyse les pièces jointes et les compare à une base de signatures pour - repérer les virus et logiciels malveillants. - -
{allSaved && uniqueSaveFolders.length === 1 ? ( @@ -316,6 +329,7 @@ export function ConversationAttachmentsSection({ ? "Une pièce jointe dans cette conversation" : `${n} pièces jointes dans cette conversation` const asPills = shouldUseAttachmentPillsInPreview(flat.map((item) => item.attachment)) + const showVirusTotal = attachmentsVirusTotalScanned(flat.map((item) => item.attachment)) const openPreview = (messageId: string, attachments: EmailAttachment[], index: number) => { if (!attachments.some((a) => a.id)) { @@ -336,24 +350,8 @@ export function ConversationAttachmentsSection({
{summary} - · - Analysé par VirusTotal + {showVirusTotal ? : null} - - - - - - VirusTotal analyse les pièces jointes et les compare à une base de signatures pour - repérer les virus et logiciels malveillants. - -
diff --git a/lib/admin-settings/map-api-org-settings.ts b/lib/admin-settings/map-api-org-settings.ts index 3216ab5..c16b05b 100644 --- a/lib/admin-settings/map-api-org-settings.ts +++ b/lib/admin-settings/map-api-org-settings.ts @@ -1,6 +1,6 @@ import type { ApiOrgPolicy, ApiOrgSettingsResponse } from "@/lib/api/admin-org-types" import type { OrgPolicySectionKey } from "@/lib/api/admin-org-types" -import type { IntegrationEntry, OrgSettingsState } from "@/lib/admin-settings/org-settings-types" +import type { IntegrationEntry, OrgSettingsState, FilePolicySettings } from "@/lib/admin-settings/org-settings-types" const INTEGRATION_HREFS: Record = { authentik: "/admin/settings/authentication", @@ -19,6 +19,25 @@ function mergeIntegrations( })) } +const DEFAULT_FILE_POLICIES: FilePolicySettings = { + max_upload_mib: 512, + allowed_extensions: "", + block_executable: true, + external_sharing: "authenticated", + default_link_expiry_days: 30, + virus_scan_enabled: false, + virustotal_api_key: "", + retention_trash_days: 30, +} + +function mergeFilePolicies(fromApi: Partial | undefined): FilePolicySettings { + return { + ...DEFAULT_FILE_POLICIES, + ...fromApi, + virustotal_api_key: fromApi?.virustotal_api_key ?? "", + } +} + export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial { return { authentik: { @@ -39,7 +58,7 @@ export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial 0 ? a.size : undefined, + virusScanStatus: a.virus_scan_status, })) } diff --git a/lib/email-data.ts b/lib/email-data.ts index 298f6c0..0ab607a 100644 --- a/lib/email-data.ts +++ b/lib/email-data.ts @@ -11,6 +11,8 @@ export interface EmailAttachment { drivePath?: string /** Taille en octets (optionnelle) — tooltips / pills vs cartes dans l’aperçu */ sizeBytes?: number + /** Statut scan VirusTotal côté serveur */ + virusScanStatus?: "clean" | "skipped" | "malicious" /** Contenu texte inline (ex. ICS) — fixtures / import */ inlineText?: string }