Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new components for managing admin settings, including AdminListControls, AdminSettingsCard, and TechBrandSelectLabel. - Implemented dynamic loading for admin settings sections to optimize performance. - Enhanced the layout of various admin settings sections for better user experience. - Updated the AiAssistantSection to include LLM provider management and improved model selection. - Refactored authentication settings to streamline configuration and improve accessibility.
284 lines
9.5 KiB
TypeScript
284 lines
9.5 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useState } from "react"
|
|
import { Check, Copy } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Switch } from "@/components/ui/switch"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { AutomationTabMasonry } from "@/components/gmail/settings/automation/automation-tab-masonry"
|
|
import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form"
|
|
import { MigrationProjectsPanel } from "@/components/admin/settings/sections/migration-projects-panel"
|
|
import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store"
|
|
import {
|
|
useCreateMailDomain,
|
|
useMailDomains,
|
|
useVerifyMailDomainMX,
|
|
useVerifyMailDomainTXT,
|
|
} from "@/lib/api/hooks/use-hosted-mail"
|
|
|
|
export function MailDomainsSection() {
|
|
const domainsQuery = useMailDomains()
|
|
const createDomain = useCreateMailDomain()
|
|
const [domainName, setDomainName] = useState("")
|
|
const mailing = useOrgSettingsStore((s) => s.mailing)
|
|
const setMailing = useOrgSettingsStore((s) => s.setMailing)
|
|
|
|
const domains = domainsQuery.data?.domains ?? []
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<OrgSettingsSection
|
|
title="Mail"
|
|
description="Domaines hébergés Stalwart, relais SMTP des notifications suite et migration."
|
|
policySection="mailing"
|
|
>
|
|
<AutomationTabMasonry columns={2}>
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium">Domaines hébergés</CardTitle>
|
|
<CardDescription>
|
|
Vérification DNS, DKIM et provisioning des boîtes @domaine.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-4 md:grid-cols-[1fr_auto]">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="new-domain">Nouveau domaine</Label>
|
|
<Input
|
|
id="new-domain"
|
|
value={domainName}
|
|
onChange={(e) => setDomainName(e.target.value)}
|
|
placeholder="entreprise.com"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<Button
|
|
disabled={!domainName || createDomain.isPending}
|
|
onClick={() => {
|
|
void createDomain.mutateAsync({ name: domainName }).then(() => setDomainName(""))
|
|
}}
|
|
>
|
|
Ajouter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="mt-6 space-y-3">
|
|
{domains.map((domain) => (
|
|
<DomainRow key={domain.id} domain={domain} />
|
|
))}
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<CardTitle className="text-sm font-medium">Notifications suite (SMTP)</CardTitle>
|
|
<CardDescription>
|
|
Partages de fichiers, mentions, invitations — distinct des comptes mail utilisateur.
|
|
</CardDescription>
|
|
</div>
|
|
<Switch
|
|
checked={mailing.enabled}
|
|
onCheckedChange={(enabled) => setMailing({ enabled })}
|
|
/>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Label>Hôte SMTP</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={mailing.smtp_host}
|
|
onChange={(e) => setMailing({ smtp_host: e.target.value })}
|
|
placeholder="smtp.example.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Port</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="number"
|
|
value={mailing.smtp_port}
|
|
onChange={(e) => setMailing({ smtp_port: Number(e.target.value) || 587 })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Utilisateur</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={mailing.smtp_user}
|
|
onChange={(e) => setMailing({ smtp_user: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Mot de passe</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="password"
|
|
value={mailing.smtp_password}
|
|
onChange={(e) => setMailing({ smtp_password: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Chiffrement</Label>
|
|
<Select
|
|
value={mailing.tls_mode}
|
|
onValueChange={(tls_mode) =>
|
|
setMailing({ tls_mode: tls_mode as typeof mailing.tls_mode })
|
|
}
|
|
>
|
|
<SelectTrigger className="mt-1 h-9">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="starttls">STARTTLS</SelectItem>
|
|
<SelectItem value="ssl">SSL/TLS</SelectItem>
|
|
<SelectItem value="none">Aucun</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label>Adresse d'expédition</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="email"
|
|
value={mailing.from_email}
|
|
onChange={(e) => setMailing({ from_email: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label>Nom affiché</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
value={mailing.from_name}
|
|
onChange={(e) => setMailing({ from_name: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="sm:col-span-2">
|
|
<Label>Reply-To (optionnel)</Label>
|
|
<Input
|
|
className="mt-1 h-9"
|
|
type="email"
|
|
value={mailing.reply_to ?? ""}
|
|
onChange={(e) => setMailing({ reply_to: e.target.value })}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</AutomationTabMasonry>
|
|
</OrgSettingsSection>
|
|
|
|
<MigrationProjectsPanel domains={domains} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CopyTxtButton({
|
|
label,
|
|
copied,
|
|
onCopy,
|
|
}: {
|
|
label: string
|
|
copied: boolean
|
|
onCopy: () => void
|
|
}) {
|
|
return (
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-6 w-6 shrink-0"
|
|
aria-label={label}
|
|
title={label}
|
|
onClick={onCopy}
|
|
>
|
|
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5 opacity-60" />}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function DomainRow({
|
|
domain,
|
|
}: {
|
|
domain: {
|
|
id: string
|
|
name: string
|
|
status: string
|
|
verification_token?: string
|
|
is_platform_domain: boolean
|
|
}
|
|
}) {
|
|
const verifyTxt = useVerifyMailDomainTXT(domain.id)
|
|
const verifyMx = useVerifyMailDomainMX(domain.id)
|
|
const [copiedField, setCopiedField] = useState<"name" | "value" | null>(null)
|
|
const txtName = `_ultisuite-verify.${domain.name}`
|
|
|
|
const copyTxtField = useCallback(async (text: string, field: "name" | "value", successLabel: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
setCopiedField(field)
|
|
toast.success(successLabel)
|
|
window.setTimeout(() => setCopiedField(null), 2000)
|
|
} catch {
|
|
toast.error("Impossible de copier")
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<li className="rounded-lg border p-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div>
|
|
<p className="font-medium">
|
|
{domain.name}
|
|
{domain.is_platform_domain ? " (plateforme)" : ""}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">Statut : {domain.status}</p>
|
|
{domain.verification_token && (
|
|
<div className="mt-2 space-y-1 text-xs text-muted-foreground">
|
|
<p>Enregistrement TXT :</p>
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">{txtName}</code>
|
|
<CopyTxtButton
|
|
label="Copier le nom TXT"
|
|
copied={copiedField === "name"}
|
|
onCopy={() => void copyTxtField(txtName, "name", "Nom TXT copié")}
|
|
/>
|
|
<span className="px-0.5">=</span>
|
|
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">{domain.verification_token}</code>
|
|
<CopyTxtButton
|
|
label="Copier la valeur TXT"
|
|
copied={copiedField === "value"}
|
|
onCopy={() =>
|
|
void copyTxtField(domain.verification_token!, "value", "Valeur TXT copiée")
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button size="sm" variant="outline" onClick={() => void verifyTxt.mutateAsync()}>
|
|
Vérifier TXT
|
|
</Button>
|
|
<Button size="sm" variant="secondary" onClick={() => void verifyMx.mutateAsync()}>
|
|
Vérifier MX
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
)
|
|
}
|