feat(admin): add VirusTotal scan settings and mail UI
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
File policies API key field, conditional scan badge, safe policy merge.
This commit is contained in:
parent
8b9717861c
commit
f44dadc453
@ -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 (
|
||||
<OrgSettingsSection
|
||||
@ -105,12 +112,36 @@ export function FilePoliciesSection() {
|
||||
<label className="flex items-center justify-between gap-4 rounded-lg border p-3 sm:col-span-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Analyse antivirus à l'upload</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
VirusTotal — scan synchrone à l'upload Drive et pièces jointes mail
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={filePolicies.virus_scan_enabled}
|
||||
onCheckedChange={(virus_scan_enabled) => setFilePolicies({ virus_scan_enabled })}
|
||||
/>
|
||||
</label>
|
||||
{filePolicies.virus_scan_enabled ? (
|
||||
<div className="sm:col-span-2">
|
||||
<Label>Clé API VirusTotal</Label>
|
||||
<Input
|
||||
className="mt-1 h-9"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
value={filePolicies.virustotal_api_key ?? ""}
|
||||
onChange={(e) => setFilePolicies({ virustotal_api_key: e.target.value })}
|
||||
placeholder={vtKeyConfigured ? "•••••••• (laisser vide pour conserver)" : "Coller la clé API"}
|
||||
/>
|
||||
{vtKeyConfigured && !(filePolicies.virustotal_api_key ?? "").trim() ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Clé configurée</p>
|
||||
) : null}
|
||||
{vtKeyMissing ? (
|
||||
<p className="mt-1 text-xs text-amber-600 dark:text-amber-500">
|
||||
Analyse activée sans clé API — les uploads ne seront pas scannés.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</OrgSettingsSection>
|
||||
)
|
||||
|
||||
@ -85,6 +85,34 @@ function DriveLocationBadge({ folderPath }: { folderPath: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function attachmentsVirusTotalScanned(attachments: EmailAttachment[]): boolean {
|
||||
return attachments.some((a) => a.virusScanStatus === "clean")
|
||||
}
|
||||
|
||||
function VirusTotalScanBadge() {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden> · </span>
|
||||
<span>Analysé par VirusTotal</span>
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent"
|
||||
aria-label="Informations sur l'analyse VirusTotal des pièces jointes"
|
||||
>
|
||||
<Info className="size-4" strokeWidth={1.75} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={cn(MAIL_TOOLTIP_CONTENT_CLASS, "max-w-xs text-xs")}>
|
||||
VirusTotal analyse les pièces jointes et les compare à une base de signatures pour
|
||||
repérer les virus et logiciels malveillants.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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({
|
||||
<div className="flex min-w-0 max-w-[min(100%,28rem)] items-center gap-1 text-sm text-muted-foreground">
|
||||
<span className="min-w-0 truncate">
|
||||
{summary}
|
||||
<span aria-hidden> · </span>
|
||||
<span>Analysé par VirusTotal</span>
|
||||
{showVirusTotal ? <VirusTotalScanBadge /> : null}
|
||||
</span>
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent"
|
||||
aria-label="Informations sur l'analyse VirusTotal des pièces jointes"
|
||||
>
|
||||
<Info className="size-4" strokeWidth={1.75} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={cn(MAIL_TOOLTIP_CONTENT_CLASS, "max-w-xs text-xs")}>
|
||||
VirusTotal analyse les pièces jointes et les compare à une base de signatures pour
|
||||
repérer les virus et logiciels malveillants.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{allSaved && uniqueSaveFolders.length === 1 ? (
|
||||
<DriveLocationBadge folderPath={uniqueSaveFolders[0]!} />
|
||||
@ -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({
|
||||
<div className="flex min-w-0 max-w-[min(100%,28rem)] items-center gap-1 text-sm text-muted-foreground">
|
||||
<span className="min-w-0 truncate">
|
||||
{summary}
|
||||
<span aria-hidden> · </span>
|
||||
<span>Analysé par VirusTotal</span>
|
||||
{showVirusTotal ? <VirusTotalScanBadge /> : null}
|
||||
</span>
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent"
|
||||
aria-label="Informations sur l'analyse VirusTotal des pièces jointes"
|
||||
>
|
||||
<Info className="size-4" strokeWidth={1.75} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className={cn(MAIL_TOOLTIP_CONTENT_CLASS, "max-w-xs text-xs")}>
|
||||
VirusTotal analyse les pièces jointes et les compare à une base de signatures pour
|
||||
repérer les virus et logiciels malveillants.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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<string, string> = {
|
||||
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<FilePolicySettings> | undefined): FilePolicySettings {
|
||||
return {
|
||||
...DEFAULT_FILE_POLICIES,
|
||||
...fromApi,
|
||||
virustotal_api_key: fromApi?.virustotal_api_key ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial<OrgSettingsState> {
|
||||
return {
|
||||
authentik: {
|
||||
@ -39,7 +58,7 @@ export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial<OrgSettingsSt
|
||||
},
|
||||
storageQuotas: { ...policy.storage_quotas },
|
||||
usageQuotas: { ...policy.usage_quotas },
|
||||
filePolicies: { ...policy.file_policies },
|
||||
filePolicies: mergeFilePolicies(policy.file_policies),
|
||||
llm: {
|
||||
...policy.llm,
|
||||
providers: policy.llm.providers ?? [],
|
||||
|
||||
@ -58,6 +58,7 @@ const DEFAULT_FILE_POLICIES: FilePolicySettings = {
|
||||
external_sharing: "authenticated",
|
||||
default_link_expiry_days: 30,
|
||||
virus_scan_enabled: false,
|
||||
virustotal_api_key: "",
|
||||
retention_trash_days: 30,
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ export type FilePolicySettings = {
|
||||
external_sharing: "disabled" | "authenticated" | "public_link"
|
||||
default_link_expiry_days: number
|
||||
virus_scan_enabled: boolean
|
||||
virustotal_api_key: string
|
||||
retention_trash_days: number
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ export type ApiOrgFilePolicies = {
|
||||
external_sharing: "disabled" | "authenticated" | "public_link"
|
||||
default_link_expiry_days: number
|
||||
virus_scan_enabled: boolean
|
||||
virustotal_api_key: string
|
||||
retention_trash_days: number
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ export interface ApiMessageAttachment {
|
||||
is_inline?: boolean
|
||||
content_id?: string
|
||||
drive_path?: string
|
||||
virus_scan_status?: "clean" | "skipped" | "malicious"
|
||||
}
|
||||
|
||||
export function mapApiAttachmentsToEmail(
|
||||
@ -24,6 +25,7 @@ export function mapApiAttachmentsToEmail(
|
||||
contentType: a.content_type || undefined,
|
||||
drivePath: a.drive_path || undefined,
|
||||
sizeBytes: a.size > 0 ? a.size : undefined,
|
||||
virusScanStatus: a.virus_scan_status,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user