diff --git a/messages/en-US.json b/messages/en-US.json index a6aa0060f..6cb69d057 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3956,6 +3956,7 @@ "mtlsCertificateErrorRemoveDescription": "The certificate could not be removed.", "mtlsLastCertificateRemoved": "mTLS was turned off because the last CA certificate was removed.", "mtlsNoCertificates": "No CA certificates added yet", + "mtlsSummary": "{count} trusted CA certificates", "mtlsRemoveCertificate": "Remove CA certificate", "mtlsExpired": "Expired", "mtlsExpiresSoon": "Expires soon", diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx index 29c2f4825..ed04e2e1d 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx @@ -1,7 +1,14 @@ "use client"; +import { ResourceMtlsSection } from "@app/components/ResourceMtlsSection"; import { ResourcePolicyEditForm } from "@app/components/resource-policy/ResourcePolicyEditForm"; +import { SettingsContainer } from "@app/components/Settings"; export default function ResourceAuthenticationPage() { - return ; + return ( + + + + + ); } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx index 511fa6ac4..564abad66 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx @@ -114,13 +114,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { href: `/{orgId}/settings/resources/public/{niceId}/maintenance` }); } - - if (!env.flags.disableEnterpriseFeatures && resource.mode === "http") { - navItems.push({ - title: t("mtls"), - href: `/{orgId}/settings/resources/public/{niceId}/mtls` - }); - } } if (resource.mode === "inference") { diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx deleted file mode 100644 index 955eb041c..000000000 --- a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx +++ /dev/null @@ -1,485 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { Badge } from "@app/components/ui/badge"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { resourceQueries } from "@app/lib/queries"; -import type { - CreateResourceMtlsCertificateResponse, - DeleteResourceMtlsCertificateResponse, - UpdateResourceResponse -} from "@server/routers/resource"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { AxiosResponse } from "axios"; -import { AlertCircle, FileKey2, Info, ShieldCheck, Trash2 } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; - -const DAY_MS = 24 * 60 * 60 * 1000; -const EXPIRES_SOON_MS = 30 * DAY_MS; - -export default function ResourceMtlsPage() { - const params = useParams(); - const router = useRouter(); - const { env } = useEnvContext(); - const { resource, updateResource } = useResourceContext(); - const t = useTranslations(); - const api = createApiClient({ env }); - const queryClient = useQueryClient(); - - const supportsMtls = resource.mode === "http"; - const isMtlsDisabled = false; - - useEffect(() => { - if (env.flags.disableEnterpriseFeatures || !supportsMtls) { - router.replace( - `/${params.orgId}/settings/resources/public/${resource.niceId}/general` - ); - } - }, [ - env.flags.disableEnterpriseFeatures, - params.orgId, - resource.niceId, - router, - supportsMtls - ]); - - const { data: certificates = [], isLoading: isLoadingCertificates } = - useQuery( - resourceQueries.resourceMtlsCertificates({ - resourceId: resource.resourceId - }) - ); - - const [name, setName] = useState(""); - const [pem, setPem] = useState(""); - const [adding, setAdding] = useState(false); - const [removingId, setRemovingId] = useState(null); - const fileInputRef = useRef(null); - - const [mtlsEnabled, setMtlsEnabled] = useState(resource.mtlsEnabled); - const [saving, setSaving] = useState(false); - - // The server switches mTLS off when the last CA is removed. - useEffect(() => { - setMtlsEnabled(resource.mtlsEnabled); - }, [resource.mtlsEnabled]); - - if (env.flags.disableEnterpriseFeatures || !supportsMtls) { - return null; - } - - const hasCertificates = certificates.length > 0; - const canEnable = hasCertificates && resource.ssl; - - async function invalidateCertificates() { - await queryClient.invalidateQueries( - resourceQueries.resourceMtlsCertificates({ - resourceId: resource.resourceId - }) - ); - } - - async function onFileSelected(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - e.target.value = ""; - if (!file) return; - const text = await file.text(); - setPem((prev) => (prev.trim() ? `${prev.trim()}\n${text}` : text)); - if (!name && certificates.length === 0) { - setName(file.name.replace(/\.(pem|crt|cer|ca-bundle)$/i, "")); - } - } - - async function onAddCertificate() { - if (!pem.trim()) return; - setAdding(true); - try { - await api.put>( - `/resource/${resource.resourceId}/mtls/certificate`, - { - certificate: pem, - ...(name.trim() ? { name: name.trim() } : {}) - } - ); - setPem(""); - setName(""); - await invalidateCertificates(); - toast({ - title: t("mtlsCertificateAdded"), - description: t("mtlsCertificateAddedDescription") - }); - } catch (e) { - toast({ - variant: "destructive", - title: t("mtlsCertificateErrorAdd"), - description: formatAxiosError( - e, - t("mtlsCertificateErrorAddDescription") - ) - }); - } finally { - setAdding(false); - } - } - - async function onRemoveCertificate(mtlsCertificateId: number) { - setRemovingId(mtlsCertificateId); - try { - const res = await api.delete< - AxiosResponse - >( - `/resource/${resource.resourceId}/mtls/certificate/${mtlsCertificateId}` - ); - await invalidateCertificates(); - - if (res.data.data.mtlsDisabled) { - updateResource({ mtlsEnabled: false }); - toast({ - title: t("mtlsCertificateRemoved"), - description: t("mtlsLastCertificateRemoved") - }); - } else { - toast({ title: t("mtlsCertificateRemoved") }); - } - } catch (e) { - toast({ - variant: "destructive", - title: t("mtlsCertificateErrorRemove"), - description: formatAxiosError( - e, - t("mtlsCertificateErrorRemoveDescription") - ) - }); - } finally { - setRemovingId(null); - } - } - - async function onSaveEnforcement() { - setSaving(true); - try { - await api.post>( - `/resource/${resource.resourceId}`, - { mtlsEnabled } - ); - updateResource({ mtlsEnabled }); - toast({ - title: t("resourceUpdated"), - description: t("resourceUpdatedDescription") - }); - } catch (e) { - toast({ - variant: "destructive", - title: t("resourceErrorUpdate"), - description: formatAxiosError( - e, - t("resourceErrorUpdateDescription") - ) - }); - } finally { - setSaving(false); - } - } - - return ( - <> -
- - - - - {t("mtlsCaCertificates")} - - - {t("mtlsCaCertificatesDescription")} - - - - -
- {isLoadingCertificates ? null : hasCertificates ? ( -
    - {certificates.map((cert) => { - const now = Date.now(); - const expired = - cert.notAfter != null && - cert.notAfter < now; - const expiresSoon = - !expired && - cert.notAfter != null && - cert.notAfter - now < - EXPIRES_SOON_MS; - return ( -
  • -
    - -
    -
    - - {cert.name || - cert.subject || - t( - "mtlsCaCertificates" - )} - - {expired && ( - - {t( - "mtlsExpired" - )} - - )} - {expiresSoon && ( - - {t( - "mtlsExpiresSoon" - )} - - )} -
    - {cert.name && - cert.subject && ( -

    - { - cert.subject - } -

    - )} - {cert.issuer && ( -

    - {t( - "mtlsIssuedBy", - { - issuer: cert.issuer - } - )} -

    - )} - {cert.notAfter != - null && ( -

    - {t( - "mtlsExpiresOn", - { - date: new Date( - cert.notAfter - ).toLocaleDateString() - } - )} -

    - )} - {cert.fingerprint && ( -

    - { - cert.fingerprint - } -

    - )} -
    -
    - -
  • - ); - })} -
- ) : ( -

- {t("mtlsNoCertificates")} -

- )} - -
-
- - - setName(e.target.value) - } - /> -
-
- -