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() {
|
export function FilePoliciesSection() {
|
||||||
const filePolicies = useOrgSettingsStore((s) => s.filePolicies)
|
const filePolicies = useOrgSettingsStore((s) => s.filePolicies)
|
||||||
const setFilePolicies = useOrgSettingsStore((s) => s.setFilePolicies)
|
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 (
|
return (
|
||||||
<OrgSettingsSection
|
<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">
|
<label className="flex items-center justify-between gap-4 rounded-lg border p-3 sm:col-span-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Analyse antivirus à l'upload</p>
|
<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>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={filePolicies.virus_scan_enabled}
|
checked={filePolicies.virus_scan_enabled}
|
||||||
onCheckedChange={(virus_scan_enabled) => setFilePolicies({ virus_scan_enabled })}
|
onCheckedChange={(virus_scan_enabled) => setFilePolicies({ virus_scan_enabled })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</OrgSettingsSection>
|
</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({
|
export function MessageAttachmentsSection({
|
||||||
messageId,
|
messageId,
|
||||||
attachments,
|
attachments,
|
||||||
@ -117,6 +145,7 @@ export function MessageAttachmentsSection({
|
|||||||
|
|
||||||
const summary = n === 1 ? "Une pièce jointe" : `${n} pièces jointes`
|
const summary = n === 1 ? "Une pièce jointe" : `${n} pièces jointes`
|
||||||
const asPills = shouldUseAttachmentPillsInPreview(attachments)
|
const asPills = shouldUseAttachmentPillsInPreview(attachments)
|
||||||
|
const showVirusTotal = attachmentsVirusTotalScanned(attachments)
|
||||||
|
|
||||||
const openPreview = (index: number) => {
|
const openPreview = (index: number) => {
|
||||||
if (!attachments.some((a) => a.id)) {
|
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">
|
<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">
|
<span className="min-w-0 truncate">
|
||||||
{summary}
|
{summary}
|
||||||
<span aria-hidden> · </span>
|
{showVirusTotal ? <VirusTotalScanBadge /> : null}
|
||||||
<span>Analysé par VirusTotal</span>
|
|
||||||
</span>
|
</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>
|
||||||
{allSaved && uniqueSaveFolders.length === 1 ? (
|
{allSaved && uniqueSaveFolders.length === 1 ? (
|
||||||
<DriveLocationBadge folderPath={uniqueSaveFolders[0]!} />
|
<DriveLocationBadge folderPath={uniqueSaveFolders[0]!} />
|
||||||
@ -316,6 +329,7 @@ export function ConversationAttachmentsSection({
|
|||||||
? "Une pièce jointe dans cette conversation"
|
? "Une pièce jointe dans cette conversation"
|
||||||
: `${n} pièces jointes dans cette conversation`
|
: `${n} pièces jointes dans cette conversation`
|
||||||
const asPills = shouldUseAttachmentPillsInPreview(flat.map((item) => item.attachment))
|
const asPills = shouldUseAttachmentPillsInPreview(flat.map((item) => item.attachment))
|
||||||
|
const showVirusTotal = attachmentsVirusTotalScanned(flat.map((item) => item.attachment))
|
||||||
|
|
||||||
const openPreview = (messageId: string, attachments: EmailAttachment[], index: number) => {
|
const openPreview = (messageId: string, attachments: EmailAttachment[], index: number) => {
|
||||||
if (!attachments.some((a) => a.id)) {
|
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">
|
<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">
|
<span className="min-w-0 truncate">
|
||||||
{summary}
|
{summary}
|
||||||
<span aria-hidden> · </span>
|
{showVirusTotal ? <VirusTotalScanBadge /> : null}
|
||||||
<span>Analysé par VirusTotal</span>
|
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ApiOrgPolicy, ApiOrgSettingsResponse } from "@/lib/api/admin-org-types"
|
import type { ApiOrgPolicy, ApiOrgSettingsResponse } from "@/lib/api/admin-org-types"
|
||||||
import type { OrgPolicySectionKey } 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> = {
|
const INTEGRATION_HREFS: Record<string, string> = {
|
||||||
authentik: "/admin/settings/authentication",
|
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> {
|
export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial<OrgSettingsState> {
|
||||||
return {
|
return {
|
||||||
authentik: {
|
authentik: {
|
||||||
@ -39,7 +58,7 @@ export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial<OrgSettingsSt
|
|||||||
},
|
},
|
||||||
storageQuotas: { ...policy.storage_quotas },
|
storageQuotas: { ...policy.storage_quotas },
|
||||||
usageQuotas: { ...policy.usage_quotas },
|
usageQuotas: { ...policy.usage_quotas },
|
||||||
filePolicies: { ...policy.file_policies },
|
filePolicies: mergeFilePolicies(policy.file_policies),
|
||||||
llm: {
|
llm: {
|
||||||
...policy.llm,
|
...policy.llm,
|
||||||
providers: policy.llm.providers ?? [],
|
providers: policy.llm.providers ?? [],
|
||||||
|
|||||||
@ -58,6 +58,7 @@ const DEFAULT_FILE_POLICIES: FilePolicySettings = {
|
|||||||
external_sharing: "authenticated",
|
external_sharing: "authenticated",
|
||||||
default_link_expiry_days: 30,
|
default_link_expiry_days: 30,
|
||||||
virus_scan_enabled: false,
|
virus_scan_enabled: false,
|
||||||
|
virustotal_api_key: "",
|
||||||
retention_trash_days: 30,
|
retention_trash_days: 30,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export type FilePolicySettings = {
|
|||||||
external_sharing: "disabled" | "authenticated" | "public_link"
|
external_sharing: "disabled" | "authenticated" | "public_link"
|
||||||
default_link_expiry_days: number
|
default_link_expiry_days: number
|
||||||
virus_scan_enabled: boolean
|
virus_scan_enabled: boolean
|
||||||
|
virustotal_api_key: string
|
||||||
retention_trash_days: number
|
retention_trash_days: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export type ApiOrgFilePolicies = {
|
|||||||
external_sharing: "disabled" | "authenticated" | "public_link"
|
external_sharing: "disabled" | "authenticated" | "public_link"
|
||||||
default_link_expiry_days: number
|
default_link_expiry_days: number
|
||||||
virus_scan_enabled: boolean
|
virus_scan_enabled: boolean
|
||||||
|
virustotal_api_key: string
|
||||||
retention_trash_days: number
|
retention_trash_days: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface ApiMessageAttachment {
|
|||||||
is_inline?: boolean
|
is_inline?: boolean
|
||||||
content_id?: string
|
content_id?: string
|
||||||
drive_path?: string
|
drive_path?: string
|
||||||
|
virus_scan_status?: "clean" | "skipped" | "malicious"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapApiAttachmentsToEmail(
|
export function mapApiAttachmentsToEmail(
|
||||||
@ -24,6 +25,7 @@ export function mapApiAttachmentsToEmail(
|
|||||||
contentType: a.content_type || undefined,
|
contentType: a.content_type || undefined,
|
||||||
drivePath: a.drive_path || undefined,
|
drivePath: a.drive_path || undefined,
|
||||||
sizeBytes: a.size > 0 ? a.size : undefined,
|
sizeBytes: a.size > 0 ? a.size : undefined,
|
||||||
|
virusScanStatus: a.virus_scan_status,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ export interface EmailAttachment {
|
|||||||
drivePath?: string
|
drivePath?: string
|
||||||
/** Taille en octets (optionnelle) — tooltips / pills vs cartes dans l’aperçu */
|
/** Taille en octets (optionnelle) — tooltips / pills vs cartes dans l’aperçu */
|
||||||
sizeBytes?: number
|
sizeBytes?: number
|
||||||
|
/** Statut scan VirusTotal côté serveur */
|
||||||
|
virusScanStatus?: "clean" | "skipped" | "malicious"
|
||||||
/** Contenu texte inline (ex. ICS) — fixtures / import */
|
/** Contenu texte inline (ex. ICS) — fixtures / import */
|
||||||
inlineText?: string
|
inlineText?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user