@rapidmx/web-client 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/admin/configuration-encryption/index.tsx +17 -0
- package/apps/admin/domains/[uid].tsx +17 -4
- package/apps/admin/integrations/dns-providers/[uid].tsx +205 -0
- package/apps/admin/integrations/dns-providers/index.tsx +164 -0
- package/apps/admin/integrations/dns-providers/new/index.tsx +55 -0
- package/apps/admin/integrations/dns-resolver/index.tsx +16 -0
- package/apps/admin/integrations/giphy/index.tsx +16 -0
- package/apps/admin/integrations/index.tsx +31 -0
- package/apps/admin/integrations/plugin-registry/index.tsx +16 -0
- package/apps/admin/integrations/signing-ca/index.tsx +16 -0
- package/apps/admin/integrations/stun/index.tsx +16 -0
- package/apps/shared/components/admin/configEncryption/ConfigKeysManager.tsx +529 -0
- package/apps/shared/components/admin/configEncryption/keyringParts.ts +37 -0
- package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +20 -4
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +23 -6
- package/apps/shared/components/admin/diagnostics/KeyringCard.tsx +144 -0
- package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +21 -6
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +15 -3
- package/apps/shared/components/admin/integrations/DnsProviderForm.tsx +354 -0
- package/apps/shared/components/admin/integrations/DnsProviderHosts.tsx +197 -0
- package/apps/shared/components/admin/integrations/DnsProviderOperations.tsx +115 -0
- package/apps/shared/components/admin/integrations/DnsResolverForm.tsx +61 -0
- package/apps/shared/components/admin/integrations/GiphyForm.tsx +59 -0
- package/apps/shared/components/admin/integrations/IntegrationSectionForm.tsx +234 -0
- package/apps/shared/components/admin/integrations/IntegrationSectionPage.tsx +40 -0
- package/apps/shared/components/admin/integrations/IntegrationsLayout.tsx +72 -0
- package/apps/shared/components/admin/integrations/PluginRegistryForm.tsx +230 -0
- package/apps/shared/components/admin/integrations/SecretField.tsx +91 -0
- package/apps/shared/components/admin/integrations/SigningCaForm.tsx +95 -0
- package/apps/shared/components/admin/integrations/StunForm.tsx +63 -0
- package/apps/shared/components/admin/integrations/dnsProviderParts.tsx +72 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +12 -1
- package/apps/shared/components/admin/settings/CopyableValue.tsx +16 -0
- package/apps/shared/components/admin/settings/DomainApexTxt.tsx +319 -0
- package/apps/shared/components/admin/settings/DomainDkimRotation.tsx +408 -0
- package/apps/shared/components/admin/settings/DomainDnsProvider.tsx +147 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +174 -17
- package/apps/shared/components/admin/setup/SetupWizard.tsx +39 -6
- package/dist/apps/admin/configuration-encryption/index.d.ts +4 -0
- package/dist/apps/admin/configuration-encryption/index.js +7 -0
- package/dist/apps/admin/domains/[uid].js +11 -4
- package/dist/apps/admin/integrations/dns-providers/[uid].d.ts +7 -0
- package/dist/apps/admin/integrations/dns-providers/[uid].js +117 -0
- package/dist/apps/admin/integrations/dns-providers/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-providers/index.js +68 -0
- package/dist/apps/admin/integrations/dns-providers/new/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-providers/new/index.js +31 -0
- package/dist/apps/admin/integrations/dns-resolver/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-resolver/index.js +6 -0
- package/dist/apps/admin/integrations/giphy/index.d.ts +3 -0
- package/dist/apps/admin/integrations/giphy/index.js +6 -0
- package/dist/apps/admin/integrations/index.d.ts +4 -0
- package/dist/apps/admin/integrations/index.js +7 -0
- package/dist/apps/admin/integrations/plugin-registry/index.d.ts +3 -0
- package/dist/apps/admin/integrations/plugin-registry/index.js +6 -0
- package/dist/apps/admin/integrations/signing-ca/index.d.ts +3 -0
- package/dist/apps/admin/integrations/signing-ca/index.js +6 -0
- package/dist/apps/admin/integrations/stun/index.d.ts +3 -0
- package/dist/apps/admin/integrations/stun/index.js +6 -0
- package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.d.ts +9 -0
- package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.js +187 -0
- package/dist/apps/shared/components/admin/configEncryption/keyringParts.d.ts +10 -0
- package/dist/apps/shared/components/admin/configEncryption/keyringParts.js +26 -0
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +20 -4
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +8 -4
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +7 -5
- package/dist/apps/shared/components/admin/diagnostics/KeyringCard.d.ts +18 -0
- package/dist/apps/shared/components/admin/diagnostics/KeyringCard.js +51 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +9 -1
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +10 -3
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +13 -3
- package/dist/apps/shared/components/admin/integrations/DnsProviderForm.d.ts +17 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderForm.js +147 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.d.ts +10 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.js +101 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.d.ts +8 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.js +60 -0
- package/dist/apps/shared/components/admin/integrations/DnsResolverForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/DnsResolverForm.js +15 -0
- package/dist/apps/shared/components/admin/integrations/GiphyForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/GiphyForm.js +11 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.d.ts +54 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.js +131 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.d.ts +12 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.js +21 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.d.ts +14 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.js +46 -0
- package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.js +90 -0
- package/dist/apps/shared/components/admin/integrations/SecretField.d.ts +25 -0
- package/dist/apps/shared/components/admin/integrations/SecretField.js +25 -0
- package/dist/apps/shared/components/admin/integrations/SigningCaForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/SigningCaForm.js +25 -0
- package/dist/apps/shared/components/admin/integrations/StunForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/integrations/StunForm.js +26 -0
- package/dist/apps/shared/components/admin/integrations/dnsProviderParts.d.ts +28 -0
- package/dist/apps/shared/components/admin/integrations/dnsProviderParts.js +43 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +8 -1
- package/dist/apps/shared/components/admin/settings/CopyableValue.d.ts +6 -0
- package/dist/apps/shared/components/admin/settings/CopyableValue.js +6 -0
- package/dist/apps/shared/components/admin/settings/DomainApexTxt.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/DomainApexTxt.js +148 -0
- package/dist/apps/shared/components/admin/settings/DomainDkimRotation.d.ts +15 -0
- package/dist/apps/shared/components/admin/settings/DomainDkimRotation.js +200 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsProvider.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsProvider.js +70 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +3 -3
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +61 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +9 -1
- package/dist/lib/admin/configKeysApi.d.ts +96 -0
- package/dist/lib/admin/configKeysApi.js +34 -0
- package/dist/lib/admin/dnsProvidersApi.d.ts +172 -0
- package/dist/lib/admin/dnsProvidersApi.js +70 -0
- package/dist/lib/admin/domainsApi.d.ts +116 -1
- package/dist/lib/admin/domainsApi.js +33 -0
- package/dist/lib/admin/integrationsApi.d.ts +75 -0
- package/dist/lib/admin/integrationsApi.js +31 -0
- package/lib/admin/configKeysApi.ts +117 -0
- package/lib/admin/dnsProvidersApi.ts +241 -0
- package/lib/admin/domainsApi.ts +145 -1
- package/lib/admin/integrationsApi.ts +107 -0
- package/package.json +1 -1
|
@@ -3,17 +3,22 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useEffect, useState } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import { takeoverDnsRecords } from "../../../../../lib/admin/dnsProvidersApi.js";
|
|
7
|
+
import { DnsManualReason, DnsRecordCheck, Domain, getDnsSetup, getDomain, verifyDomain } from "../../../../../lib/admin/domainsApi.js";
|
|
7
8
|
import Alert from "../ActionAlert.js";
|
|
8
9
|
import Button from "../../../../../lib/components/buttons/Button.js";
|
|
9
|
-
import
|
|
10
|
+
import CopyableValue from "./CopyableValue.js";
|
|
11
|
+
import Modal from "../../../../../lib/components/overlays/Modal.js";
|
|
10
12
|
import { actionErrorMessage } from "../elevation.js";
|
|
13
|
+
import { TypeMismatchNotice } from "../integrations/dnsProviderParts.js";
|
|
11
14
|
|
|
12
15
|
const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
|
|
13
16
|
ownership: "Ownership (TXT)",
|
|
14
17
|
mx: "MX",
|
|
15
18
|
spf: "SPF",
|
|
16
19
|
dkim: "DKIM",
|
|
20
|
+
dkim_next: "DKIM (new key)",
|
|
21
|
+
dkim_previous: "DKIM (previous key)",
|
|
17
22
|
dmarc: "DMARC",
|
|
18
23
|
openpgpkey_cname: "OpenPGP keys (CNAME)",
|
|
19
24
|
autodiscover_cname: "Autodiscover (CNAME)",
|
|
@@ -26,6 +31,8 @@ const RECORD_TYPE_NAMES: Record<DnsRecordCheck["type"], string> = {
|
|
|
26
31
|
mx: "MX",
|
|
27
32
|
spf: "SPF",
|
|
28
33
|
dkim: "DKIM",
|
|
34
|
+
dkim_next: "new DKIM key",
|
|
35
|
+
dkim_previous: "previous DKIM key",
|
|
29
36
|
dmarc: "DMARC",
|
|
30
37
|
openpgpkey_cname: "OpenPGP keys CNAME",
|
|
31
38
|
autodiscover_cname: "Autodiscover CNAME",
|
|
@@ -43,6 +50,14 @@ const RECORD_TYPE_HELP: Partial<Record<DnsRecordCheck["type"], string>> = {
|
|
|
43
50
|
"Same purpose as the CNAME above, for clients that check DNS SRV instead - and, unlike the CNAME, needs no extra TLS certificate, since it points at a hostname already covered. Prefer this one when a second certificate is hard to obtain.",
|
|
44
51
|
};
|
|
45
52
|
|
|
53
|
+
/** Why a record is added by hand, shown under "Manual" in the Published column. */
|
|
54
|
+
const MANUAL_REASONS: Record<DnsManualReason, string> = {
|
|
55
|
+
no_provider: "No DNS provider is assigned to this domain.",
|
|
56
|
+
kind_unsupported: "The DNS provider can't hold this kind of record.",
|
|
57
|
+
outside_zones: "This name is outside the zones of the DNS provider.",
|
|
58
|
+
apex_txt_unmanaged: "Shares the domain's root TXT records, which you opt in to manage below.",
|
|
59
|
+
};
|
|
60
|
+
|
|
46
61
|
/** An MX record's recommended value is `"<priority> <mail server>"` - two separate fields in a DNS provider's form. */
|
|
47
62
|
function splitMxValue(value: string): { priority: string; server: string } | null {
|
|
48
63
|
const match = /^(\d+)\s+(\S+)$/.exec(value.trim());
|
|
@@ -55,20 +70,10 @@ function splitSrvValue(value: string): { priority: string; weight: string; port:
|
|
|
55
70
|
return match ? { priority: match[1], weight: match[2], port: match[3], target: match[4] } : null;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
/** A value to type into a DNS provider's form, shown in full (long DKIM keys wrap) with a copy icon button beside it. */
|
|
59
|
-
function CopyableValue({ value, copyLabel }: { value: string; copyLabel: string }) {
|
|
60
|
-
return (
|
|
61
|
-
<div className="flex items-start gap-2">
|
|
62
|
-
<code className="flex-1 min-w-0 text-xs break-all">{value}</code>
|
|
63
|
-
<CopyIconButton value={value} label={copyLabel} className="shrink-0" />
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
73
|
export interface DomainDnsSetupProps {
|
|
69
74
|
uid: string;
|
|
70
|
-
/** Called with the domain every time
|
|
71
|
-
onLoaded?: (domain: Domain) => void;
|
|
75
|
+
/** Called with the domain and its checklist every time they are (re)loaded - e.g. after "Verify now". */
|
|
76
|
+
onLoaded?: (domain: Domain, checks: DnsRecordCheck[]) => void;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/**
|
|
@@ -82,6 +87,11 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
82
87
|
const [error, setError] = useState<string | null>(null);
|
|
83
88
|
const [verifying, setVerifying] = useState(false);
|
|
84
89
|
const [verifyError, setVerifyError] = useState<string | null>(null);
|
|
90
|
+
// The conflicting record the administrator is being asked to take over, the request in flight, why it failed, and the records whose takeover was queued.
|
|
91
|
+
const [takeOver, setTakeOver] = useState<DnsRecordCheck | null>(null);
|
|
92
|
+
const [takingOver, setTakingOver] = useState(false);
|
|
93
|
+
const [takeOverError, setTakeOverError] = useState<string | null>(null);
|
|
94
|
+
const [takeOverQueued, setTakeOverQueued] = useState<DnsRecordCheck["type"][]>([]);
|
|
85
95
|
|
|
86
96
|
function reload(id: string) {
|
|
87
97
|
setLoading(true);
|
|
@@ -90,7 +100,7 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
90
100
|
.then(([d, checks]) => {
|
|
91
101
|
setDomain(d);
|
|
92
102
|
setDnsSetup(checks);
|
|
93
|
-
onLoaded?.(d);
|
|
103
|
+
onLoaded?.(d, checks);
|
|
94
104
|
})
|
|
95
105
|
.catch((err) => setError(actionErrorMessage(err, "Could not load this domain.")))
|
|
96
106
|
.finally(() => setLoading(false));
|
|
@@ -111,7 +121,7 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
111
121
|
const [d, checks] = await Promise.all([getDomain(domain!.uid), getDnsSetup(domain!.uid)]);
|
|
112
122
|
setDomain(d);
|
|
113
123
|
setDnsSetup(checks);
|
|
114
|
-
onLoaded?.(d);
|
|
124
|
+
onLoaded?.(d, checks);
|
|
115
125
|
} catch (err) {
|
|
116
126
|
setVerifyError(actionErrorMessage(err, "Could not verify this domain."));
|
|
117
127
|
} finally {
|
|
@@ -119,6 +129,22 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
// The record is taken over through the provider the domain is assigned to (a conflict only exists with one). Only the DNS controller applies it, so
|
|
133
|
+
// the list shows the result some time later: "Check again" rather than a refresh that would still show the conflict.
|
|
134
|
+
async function handleTakeOver() {
|
|
135
|
+
setTakingOver(true);
|
|
136
|
+
setTakeOverError(null);
|
|
137
|
+
try {
|
|
138
|
+
await takeoverDnsRecords(domain!.dnsProviderUid!, domain!.uid, [takeOver!.type]);
|
|
139
|
+
setTakeOverQueued((queued) => [...queued, takeOver!.type]);
|
|
140
|
+
setTakeOver(null);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
setTakeOverError(actionErrorMessage(err, "Could not queue the takeover."));
|
|
143
|
+
} finally {
|
|
144
|
+
setTakingOver(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
if (loading) {
|
|
123
149
|
return <p className="text-sm text-text-muted">Loading…</p>;
|
|
124
150
|
}
|
|
@@ -129,6 +155,9 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
129
155
|
const ownership = dnsSetup.find((c) => c.type === "ownership");
|
|
130
156
|
const ownershipValue = ownership?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
|
|
131
157
|
const ownershipName = ownership?.recordName || domain.name;
|
|
158
|
+
// A server that doesn't publish says nothing about how a record gets published, so the column stays out.
|
|
159
|
+
const showPublished = dnsSetup.some((c) => c.publication);
|
|
160
|
+
const openpgpkey = dnsSetup.find((c) => c.type === "openpgpkey_cname");
|
|
132
161
|
|
|
133
162
|
return (
|
|
134
163
|
<div className="flex flex-col gap-5">
|
|
@@ -192,11 +221,38 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
192
221
|
{dnsSetup.length > 0 && (
|
|
193
222
|
<div className="bg-surface border border-border rounded-md p-6">
|
|
194
223
|
<h2 className="text-sm font-bold uppercase tracking-wide mb-3">DNS setup checklist</h2>
|
|
224
|
+
{openpgpkey?.configured && !openpgpkey.matches && (
|
|
225
|
+
<div role="note" aria-label="OpenPGP key host warning" className="text-sm rounded-sm py-3 px-3.5 mb-3 bg-warning text-warning-contrast">
|
|
226
|
+
{openpgpkey.recordName} doesn't resolve to this deployment yet. Until it does, other people's OpenPGP apps can't find your
|
|
227
|
+
users' keys (Web Key Directory), and this server can't get a certificate for it.
|
|
228
|
+
{openpgpkey.publication === "pending" && " It is being published automatically."}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
{takeOverQueued.length > 0 && (
|
|
232
|
+
<p className="text-sm mb-3">
|
|
233
|
+
Takeover queued for the {takeOverQueued.map((type) => RECORD_TYPE_NAMES[type]).join(", ")} record. The DNS controller overwrites it
|
|
234
|
+
shortly, and it shows as published here once it is live. If it stays in conflict, the result of the takeover is under Operations on{" "}
|
|
235
|
+
<a href={`/admin/integrations/dns-providers/${encodeURIComponent(domain.dnsProviderUid!)}`} className="text-primary-dark hover:underline">
|
|
236
|
+
the DNS provider's page
|
|
237
|
+
</a>
|
|
238
|
+
.{" "}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
className="font-semibold underline"
|
|
242
|
+
onClick={() => {
|
|
243
|
+
setTakeOverQueued([]);
|
|
244
|
+
void reload(uid);
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
Check again
|
|
248
|
+
</button>
|
|
249
|
+
</p>
|
|
250
|
+
)}
|
|
195
251
|
<div className="overflow-x-auto">
|
|
196
252
|
<table className="w-full text-sm border-collapse">
|
|
197
253
|
<thead>
|
|
198
254
|
<tr>
|
|
199
|
-
{["Record", "Status", "Name / host", "Recommended value"].map((h) => (
|
|
255
|
+
{["Record", "Status", ...(showPublished ? ["Published"] : []), "Name / host", "Recommended value"].map((h) => (
|
|
200
256
|
<th
|
|
201
257
|
key={h}
|
|
202
258
|
className="text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border"
|
|
@@ -215,6 +271,12 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
215
271
|
{RECORD_TYPE_HELP[check.type] && (
|
|
216
272
|
<div className="text-xs text-text-muted mt-1 max-w-xs">{RECORD_TYPE_HELP[check.type]}</div>
|
|
217
273
|
)}
|
|
274
|
+
{check.legacyApex && (
|
|
275
|
+
<div className="text-xs text-text-muted mt-1 max-w-xs">
|
|
276
|
+
Found at the domain's root (the old location). Publish it at <strong>{check.recordName}</strong>; the root
|
|
277
|
+
value can then be removed.
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
218
280
|
</td>
|
|
219
281
|
<td className="py-2.5 px-2.5 border-b border-border">
|
|
220
282
|
{!check.configured ? (
|
|
@@ -229,6 +291,18 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
229
291
|
</span>
|
|
230
292
|
)}
|
|
231
293
|
</td>
|
|
294
|
+
{showPublished && (
|
|
295
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
296
|
+
<PublishedCell
|
|
297
|
+
check={check}
|
|
298
|
+
queued={takeOverQueued.includes(check.type)}
|
|
299
|
+
onTakeOver={() => {
|
|
300
|
+
setTakeOverError(null);
|
|
301
|
+
setTakeOver(check);
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</td>
|
|
305
|
+
)}
|
|
232
306
|
{/* A floor on the name column: beside a long DKIM key (which breaks anywhere) the table gave it a few characters, and a name wrapped one letter to a line. */}
|
|
233
307
|
<td className="py-2.5 px-2.5 border-b border-border text-text-muted min-w-[10rem]">
|
|
234
308
|
{check.recordName ? (
|
|
@@ -258,10 +332,93 @@ export default function DomainDnsSetup({ uid, onLoaded }: DomainDnsSetupProps) {
|
|
|
258
332
|
</p>
|
|
259
333
|
</div>
|
|
260
334
|
)}
|
|
335
|
+
|
|
336
|
+
<Modal open={takeOver !== null} onClose={() => setTakeOver(null)} title="Take over this record">
|
|
337
|
+
{takeOver && (
|
|
338
|
+
<>
|
|
339
|
+
<p className="text-sm mb-3">
|
|
340
|
+
Taking over overwrites the record at <code className="text-xs break-all">{takeOver.recordName}</code> with the value below. From then
|
|
341
|
+
on RapidMX manages it, and whatever relied on the old value stops getting it.
|
|
342
|
+
</p>
|
|
343
|
+
<dl className="text-sm flex flex-col gap-1 mb-3">
|
|
344
|
+
<dt className="text-text-muted">Existing value</dt>
|
|
345
|
+
<dd>{takeOver.actualValue ? <code className="text-xs break-all">{takeOver.actualValue}</code> : "(none found)"}</dd>
|
|
346
|
+
<dt className="text-text-muted mt-2">New value</dt>
|
|
347
|
+
<dd>
|
|
348
|
+
<code className="text-xs break-all">{takeOver.recommendedValue}</code>
|
|
349
|
+
</dd>
|
|
350
|
+
</dl>
|
|
351
|
+
{takeOverError && <Alert>{takeOverError}</Alert>}
|
|
352
|
+
<div className="flex gap-3 justify-end mt-5">
|
|
353
|
+
<Button type="button" variant="secondary" className="!w-auto" disabled={takingOver} onClick={() => setTakeOver(null)}>
|
|
354
|
+
Cancel
|
|
355
|
+
</Button>
|
|
356
|
+
<Button type="button" className="!w-auto" loading={takingOver} disabled={takingOver} onClick={handleTakeOver}>
|
|
357
|
+
Take over
|
|
358
|
+
</Button>
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
</Modal>
|
|
261
363
|
</div>
|
|
262
364
|
);
|
|
263
365
|
}
|
|
264
366
|
|
|
367
|
+
/** How a record is published, for the Published column: automatically (with its state; for a conflict also the value in the way and the takeover), or
|
|
368
|
+
* by hand, with the reason. */
|
|
369
|
+
function PublishedCell({ check, queued, onTakeOver }: { check: DnsRecordCheck; queued: boolean; onTakeOver: () => void }) {
|
|
370
|
+
if (!check.publication) {
|
|
371
|
+
return <span className="text-xs text-text-muted">—</span>;
|
|
372
|
+
}
|
|
373
|
+
if (check.publication === "manual") {
|
|
374
|
+
return (
|
|
375
|
+
<>
|
|
376
|
+
<span className="text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted">Manual</span>
|
|
377
|
+
{check.manualReason && <div className="text-xs text-text-muted mt-1 max-w-xs">{MANUAL_REASONS[check.manualReason]}</div>}
|
|
378
|
+
</>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const state = check.publication;
|
|
382
|
+
return (
|
|
383
|
+
<>
|
|
384
|
+
<span
|
|
385
|
+
className={`text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${
|
|
386
|
+
state === "published" ? "bg-success text-white" : state === "conflict" ? "bg-danger-bg text-danger" : "bg-surface-alt text-text-muted"
|
|
387
|
+
}`}
|
|
388
|
+
>
|
|
389
|
+
Automatic: {state}
|
|
390
|
+
</span>
|
|
391
|
+
{state === "conflict" && check.conflictReason === "type_mismatch" && <TypeMismatchNotice existing={check.existing} />}
|
|
392
|
+
{state === "conflict" && check.conflictReason !== "type_mismatch" && (
|
|
393
|
+
<div className="text-xs mt-1 max-w-xs">
|
|
394
|
+
{check.actualValue ? (
|
|
395
|
+
<>
|
|
396
|
+
Not published by RapidMX, and in the way: <code className="break-all">{check.actualValue}</code>
|
|
397
|
+
</>
|
|
398
|
+
) : (
|
|
399
|
+
"A record is there that this deployment doesn't own."
|
|
400
|
+
)}
|
|
401
|
+
{/* The root TXT set is taken over only through its own opt-in. */}
|
|
402
|
+
{check.type !== "spf" && (
|
|
403
|
+
<div className="mt-1.5">
|
|
404
|
+
<Button
|
|
405
|
+
type="button"
|
|
406
|
+
variant="secondary"
|
|
407
|
+
className="!w-auto !py-1"
|
|
408
|
+
aria-label={`Take over ${RECORD_TYPE_NAMES[check.type]}`}
|
|
409
|
+
disabled={queued}
|
|
410
|
+
onClick={onTakeOver}
|
|
411
|
+
>
|
|
412
|
+
{queued ? "Queued" : "Take over"}
|
|
413
|
+
</Button>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
</>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
265
422
|
/** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), a SRV value as
|
|
266
423
|
* its four form fields (priority, weight, port and target), anything else whole. */
|
|
267
424
|
function DnsRecordValue({ check, value }: { check: DnsRecordCheck; value: string }) {
|
|
@@ -343,6 +343,9 @@ function DomainStep({ domains, onCreated }: { domains: Domain[]; onCreated: (dom
|
|
|
343
343
|
const [name, setName] = useState("");
|
|
344
344
|
const [error, setError] = useState<string | null>(null);
|
|
345
345
|
const [saving, setSaving] = useState(false);
|
|
346
|
+
// The installation adds the domains it was installed with before anyone signs in: they're shown as already added.
|
|
347
|
+
const seeded = domains.filter((domain) => domain.origin === "seed");
|
|
348
|
+
const added = domains.filter((domain) => domain.origin !== "seed");
|
|
346
349
|
|
|
347
350
|
async function handleSubmit(e: FormEvent) {
|
|
348
351
|
e.preventDefault();
|
|
@@ -364,12 +367,31 @@ function DomainStep({ domains, onCreated }: { domains: Domain[]; onCreated: (dom
|
|
|
364
367
|
|
|
365
368
|
return (
|
|
366
369
|
<div className="flex flex-col gap-5 max-w-3xl">
|
|
367
|
-
{
|
|
368
|
-
<
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
{seeded.length > 0 && (
|
|
371
|
+
<section aria-labelledby="setupSeededDomains" className="flex flex-col gap-5">
|
|
372
|
+
<div className="flex flex-col gap-1">
|
|
373
|
+
<h3 id="setupSeededDomains" className="text-sm font-bold uppercase tracking-wide">
|
|
374
|
+
Added from your installation
|
|
375
|
+
</h3>
|
|
376
|
+
<p className="text-sm text-text-muted">Your installation already added these domains. Publish the DNS records for each one.</p>
|
|
377
|
+
</div>
|
|
378
|
+
{seeded.map((domain) => (
|
|
379
|
+
<DomainPanel key={domain.uid} domain={domain} grouped />
|
|
380
|
+
))}
|
|
381
|
+
</section>
|
|
382
|
+
)}
|
|
383
|
+
{seeded.length > 0 && added.length > 0 ? (
|
|
384
|
+
<section aria-labelledby="setupAddedDomains" className="flex flex-col gap-5">
|
|
385
|
+
<h3 id="setupAddedDomains" className="text-sm font-bold uppercase tracking-wide">
|
|
386
|
+
Domains you added
|
|
387
|
+
</h3>
|
|
388
|
+
{added.map((domain) => (
|
|
389
|
+
<DomainPanel key={domain.uid} domain={domain} grouped />
|
|
390
|
+
))}
|
|
391
|
+
</section>
|
|
392
|
+
) : (
|
|
393
|
+
added.map((domain) => <DomainPanel key={domain.uid} domain={domain} />)
|
|
394
|
+
)}
|
|
373
395
|
{error && <Alert>{error}</Alert>}
|
|
374
396
|
<form onSubmit={handleSubmit} className={CARD_CLASS}>
|
|
375
397
|
<FormField label={domains.length === 0 ? "Domain name" : "Add another domain"} htmlFor="setupDomainName">
|
|
@@ -390,6 +412,17 @@ function DomainStep({ domains, onCreated }: { domains: Domain[]; onCreated: (dom
|
|
|
390
412
|
);
|
|
391
413
|
}
|
|
392
414
|
|
|
415
|
+
// `grouped`: the panel sits under a region's own heading, so its domain heading is one level lower.
|
|
416
|
+
function DomainPanel({ domain, grouped = false }: { domain: Domain; grouped?: boolean }) {
|
|
417
|
+
const Heading = grouped ? "h4" : "h3";
|
|
418
|
+
return (
|
|
419
|
+
<div className="flex flex-col gap-3">
|
|
420
|
+
<Heading className="text-base font-bold">{domain.name}</Heading>
|
|
421
|
+
<DomainDnsSetup uid={domain.uid} />
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
393
426
|
function MailboxesStep({ userUid, domain }: { userUid: string; domain?: string }) {
|
|
394
427
|
const client = useApiClient();
|
|
395
428
|
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
3
|
+
/** The configuration keyring: the keys that encrypt the credentials saved in the console, and their rotation. */
|
|
4
|
+
export default function ConfigurationEncryptionPage(props: Omit<AdminShellProps, "active">): React.JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
3
|
+
import ConfigKeysManager from "../../shared/components/admin/configEncryption/ConfigKeysManager.js";
|
|
4
|
+
/** The configuration keyring: the keys that encrypt the credentials saved in the console, and their rotation. */
|
|
5
|
+
export default function ConfigurationEncryptionPage(props) {
|
|
6
|
+
return (_jsxs(AdminShell, { ...props, active: "configurationEncryption", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-3", children: "Configuration encryption" }), _jsx(ConfigKeysManager, {})] }));
|
|
7
|
+
}
|
|
@@ -7,6 +7,9 @@ import { useState } from "react";
|
|
|
7
7
|
import { useRouter } from "@rapidrest/react/client";
|
|
8
8
|
import { deleteDomain, updateDomain } from "../../../lib/admin/domainsApi.js";
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
|
+
import DomainApexTxt from "../../shared/components/admin/settings/DomainApexTxt.js";
|
|
11
|
+
import DomainDkimRotation from "../../shared/components/admin/settings/DomainDkimRotation.js";
|
|
12
|
+
import DomainDnsProvider from "../../shared/components/admin/settings/DomainDnsProvider.js";
|
|
10
13
|
import DomainDnsSetup from "../../shared/components/admin/settings/DomainDnsSetup.js";
|
|
11
14
|
import Alert from "../../shared/components/admin/ActionAlert.js";
|
|
12
15
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
@@ -20,6 +23,8 @@ export default function DomainDetailPage(props) {
|
|
|
20
23
|
function DomainDetailContent({ uid }) {
|
|
21
24
|
const { navigate } = useRouter();
|
|
22
25
|
const [domain, setDomain] = useState(null);
|
|
26
|
+
// The checklist the DNS setup panel loaded, which the DKIM rotation panel reads the rotation's records from.
|
|
27
|
+
const [checks, setChecks] = useState([]);
|
|
23
28
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
24
29
|
const [deleting, setDeleting] = useState(false);
|
|
25
30
|
const [deleteError, setDeleteError] = useState(null);
|
|
@@ -29,9 +34,10 @@ function DomainDetailContent({ uid }) {
|
|
|
29
34
|
const [savingAlias, setSavingAlias] = useState(false);
|
|
30
35
|
const [aliasError, setAliasError] = useState(null);
|
|
31
36
|
const [aliasSaved, setAliasSaved] = useState(false);
|
|
32
|
-
// Bumped after a successful
|
|
37
|
+
// Bumped after a successful save (the alias, the DNS provider, the root TXT records) to force `DomainDnsSetup` (which owns its own `domain` state,
|
|
33
38
|
// fetched only on mount/uid change) to reload and reflect the new value in its own status panel.
|
|
34
39
|
const [refreshCount, setRefreshCount] = useState(0);
|
|
40
|
+
const refresh = () => setRefreshCount((c) => c + 1);
|
|
35
41
|
function closeDeleteModal() {
|
|
36
42
|
setConfirmingDelete(false);
|
|
37
43
|
}
|
|
@@ -48,7 +54,8 @@ function DomainDetailContent({ uid }) {
|
|
|
48
54
|
setDeleting(false);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
function handleLoaded(d) {
|
|
57
|
+
function handleLoaded(d, loadedChecks) {
|
|
58
|
+
setChecks(loadedChecks);
|
|
52
59
|
setDomain(d);
|
|
53
60
|
setAliasInput((current) => (current === null ? d?.aliasOf ?? "" : current));
|
|
54
61
|
}
|
|
@@ -62,7 +69,7 @@ function DomainDetailContent({ uid }) {
|
|
|
62
69
|
setDomain(updated);
|
|
63
70
|
setAliasInput(updated.aliasOf ?? "");
|
|
64
71
|
setAliasSaved(true);
|
|
65
|
-
|
|
72
|
+
refresh();
|
|
66
73
|
}
|
|
67
74
|
catch (err) {
|
|
68
75
|
setAliasError(actionErrorMessage(err, "Could not update this domain."));
|
|
@@ -71,7 +78,7 @@ function DomainDetailContent({ uid }) {
|
|
|
71
78
|
setSavingAlias(false);
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
|
-
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [domain && (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] })), _jsx(DomainDnsSetup, { uid: uid, onLoaded: handleLoaded }, refreshCount), domain && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "Alias" }), _jsx("p", { className: "text-sm text-text-muted mb-3", children: "Make this domain a pure alias of another one - it will still receive (and may send as) mail for the same addresses, but have no mailboxes of its own. Leave blank for a regular domain." }), aliasError && _jsx(Alert, { children: aliasError }), _jsx(FormField, { label: "Alias of", htmlFor: "aliasOf", children: _jsx("input", { id: "aliasOf", type: "text", className: INPUT_CLASS,
|
|
81
|
+
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [domain && (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] })), _jsx(DomainDnsSetup, { uid: uid, onLoaded: handleLoaded }, refreshCount), domain && _jsx(DomainDnsProvider, { domain: domain, onChanged: refresh }), domain && _jsx(DomainDkimRotation, { domain: domain, checks: checks, onChanged: refresh }), domain?.dnsProviderUid && _jsx(DomainApexTxt, { domain: domain, onChanged: refresh }), domain && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "Alias" }), _jsx("p", { className: "text-sm text-text-muted mb-3", children: "Make this domain a pure alias of another one - it will still receive (and may send as) mail for the same addresses, but have no mailboxes of its own. Leave blank for a regular domain." }), aliasError && _jsx(Alert, { children: aliasError }), _jsx(FormField, { label: "Alias of", htmlFor: "aliasOf", children: _jsx("input", { id: "aliasOf", type: "text", className: INPUT_CLASS,
|
|
75
82
|
// Set in the same update as `domain` (see `handleLoaded()`), which is what this section waits for.
|
|
76
83
|
value: aliasInput, onChange: (e) => {
|
|
77
84
|
setAliasInput(e.target.value);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
8
|
+
import { deleteDnsProvider, getDnsProvider, listDnsProviderTypes, updateDnsProvider, } from "../../../../lib/admin/dnsProvidersApi.js";
|
|
9
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
10
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
11
|
+
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
12
|
+
import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
|
|
13
|
+
import DnsProviderForm from "../../../shared/components/admin/integrations/DnsProviderForm.js";
|
|
14
|
+
import DnsProviderHosts from "../../../shared/components/admin/integrations/DnsProviderHosts.js";
|
|
15
|
+
import DnsProviderOperations from "../../../shared/components/admin/integrations/DnsProviderOperations.js";
|
|
16
|
+
import IntegrationsLayout from "../../../shared/components/admin/integrations/IntegrationsLayout.js";
|
|
17
|
+
import { Badge, dnsProviderStatus, domainsText, formatTime } from "../../../shared/components/admin/integrations/dnsProviderParts.js";
|
|
18
|
+
import Alert from "../../../shared/components/admin/ActionAlert.js";
|
|
19
|
+
import { actionErrorMessage } from "../../../shared/components/admin/elevation.js";
|
|
20
|
+
const LIST_HREF = "/admin/integrations/dns-providers";
|
|
21
|
+
/**
|
|
22
|
+
* What of a config an administrator can change, as the server shows it: the controller's status reports move the row's version without touching any of it.
|
|
23
|
+
* A credential's `unreadable` flag is the server's own finding (a keyring that changed), not something an administrator set, so it is left out.
|
|
24
|
+
*/
|
|
25
|
+
function configOf(provider) {
|
|
26
|
+
const { name, zones, options, credentials, enabled, allowTakeover } = provider;
|
|
27
|
+
const shown = {};
|
|
28
|
+
for (const [key, mask] of Object.entries(credentials)) {
|
|
29
|
+
shown[key] = { ...mask, unreadable: undefined };
|
|
30
|
+
}
|
|
31
|
+
return JSON.stringify({ name, zones, options, credentials: shown, enabled, allowTakeover });
|
|
32
|
+
}
|
|
33
|
+
export default function DnsProviderPage(props) {
|
|
34
|
+
return (_jsx(AdminShell, { ...props, active: "integrations", children: _jsx(IntegrationsLayout, { active: "dnsProviders", children: _jsx(DnsProviderDetail, { uid: props.params.uid }) }) }));
|
|
35
|
+
}
|
|
36
|
+
function DnsProviderDetail({ uid }) {
|
|
37
|
+
const { navigate } = useRouter();
|
|
38
|
+
const [provider, setProvider] = useState(null);
|
|
39
|
+
// The version a save is made against: the one shown, or a newer one found after a refusal (the form isn't reset to show it).
|
|
40
|
+
const version = useRef(0);
|
|
41
|
+
// The config an administrator last saw, which a refusal compares the latest one against: the one loaded or saved, then the one a refusal reported.
|
|
42
|
+
const seen = useRef("");
|
|
43
|
+
const [types, setTypes] = useState([]);
|
|
44
|
+
const [loadError, setLoadError] = useState(null);
|
|
45
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
46
|
+
const [deleting, setDeleting] = useState(false);
|
|
47
|
+
const [deleteError, setDeleteError] = useState(null);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
Promise.all([getDnsProvider(uid), listDnsProviderTypes()])
|
|
50
|
+
.then(([loaded, loadedTypes]) => {
|
|
51
|
+
setProvider(loaded);
|
|
52
|
+
version.current = loaded.version;
|
|
53
|
+
seen.current = configOf(loaded);
|
|
54
|
+
setTypes(loadedTypes);
|
|
55
|
+
})
|
|
56
|
+
.catch((err) => setLoadError(actionErrorMessage(err, "Could not load the DNS provider.")));
|
|
57
|
+
}, [uid]);
|
|
58
|
+
if (loadError) {
|
|
59
|
+
return _jsx(Alert, { children: loadError });
|
|
60
|
+
}
|
|
61
|
+
if (!provider) {
|
|
62
|
+
return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
|
|
63
|
+
}
|
|
64
|
+
const spec = types.find((candidate) => candidate.type === provider.type);
|
|
65
|
+
const status = dnsProviderStatus(provider);
|
|
66
|
+
function accept(saved) {
|
|
67
|
+
version.current = saved.version;
|
|
68
|
+
seen.current = configOf(saved);
|
|
69
|
+
setProvider(saved);
|
|
70
|
+
}
|
|
71
|
+
// Made against the version shown, so a change someone else saved meanwhile is refused (409) rather than overwritten. The DNS controller moves the
|
|
72
|
+
// version too (every status report that changes, and at least every few minutes), so a refusal reads the config again: when nothing an administrator
|
|
73
|
+
// sets differs, only the status moved and the save is made again against the new version; otherwise the entries stay in the form, the new version is
|
|
74
|
+
// kept, and saving once more replaces the other change (a refusal after that compares against what the first one reported, so the change is told once).
|
|
75
|
+
async function handleSave(input) {
|
|
76
|
+
const sent = version.current;
|
|
77
|
+
try {
|
|
78
|
+
accept(await updateDnsProvider(uid, { ...input, version: sent }));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (!(err instanceof ApiRequestError) || err.status !== 409) {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
let latest;
|
|
85
|
+
try {
|
|
86
|
+
latest = await getDnsProvider(uid);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
if (latest.version === sent) {
|
|
92
|
+
// Refused for another reason than a newer version.
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
version.current = latest.version;
|
|
96
|
+
const reported = configOf(latest);
|
|
97
|
+
if (reported !== seen.current) {
|
|
98
|
+
seen.current = reported;
|
|
99
|
+
throw new ApiRequestError("This DNS provider was changed by someone else. Your entries are kept; save again to replace their change, or reload the page to see it.", 409);
|
|
100
|
+
}
|
|
101
|
+
accept(await updateDnsProvider(uid, { ...input, version: latest.version }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function handleDelete() {
|
|
105
|
+
setDeleting(true);
|
|
106
|
+
setDeleteError(null);
|
|
107
|
+
try {
|
|
108
|
+
await deleteDnsProvider(provider.uid);
|
|
109
|
+
void navigate(LIST_HREF);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
setDeleteError(actionErrorMessage(err, "Could not delete the DNS provider."));
|
|
113
|
+
setDeleting(false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return (_jsxs(_Fragment, { children: [_jsx("a", { href: LIST_HREF, className: "text-sm text-primary-dark hover:underline", children: "\u2190 DNS providers" }), _jsxs("h2", { className: "text-lg font-bold uppercase tracking-wide mt-2 mb-1", children: [provider.name, provider.origin === "installer" && _jsx(Badge, { children: "Added by the installer" })] }), _jsxs("div", { className: "flex items-center text-sm text-text-muted mb-4", children: [_jsxs("p", { children: ["Type: ", spec?.label ?? provider.type] }), spec && !spec.tested && _jsx(Badge, { children: "Not yet live-tested" })] }), _jsxs("div", { className: "border border-border rounded-sm bg-surface p-4 mb-6 max-w-xl text-sm flex flex-col gap-1", children: [_jsxs("p", { className: "font-semibold", children: ["Status: ", status.label] }), status.error && _jsx("p", { className: "text-danger", children: status.error }), _jsx("p", { className: "text-text-muted", children: provider.domainCount === 0 ? "No domains are published through it yet." : `Publishing ${domainsText(provider.domainCount)}.` }), provider.status?.lastSyncAt && _jsxs("p", { className: "text-text-muted", children: ["Last synchronized ", formatTime(provider.status.lastSyncAt), "."] })] }), spec ? (_jsx(DnsProviderForm, { types: types, provider: provider, submitLabel: "Save", onSave: handleSave })) : (_jsx("p", { className: "text-sm text-text-muted max-w-xl", children: "This console doesn\u2019t know this type, so its settings can\u2019t be edited here. A newer admin console can edit it." })), _jsx(DnsProviderHosts, { uid: provider.uid }), _jsx(DnsProviderOperations, { uid: provider.uid }), _jsx("div", { className: "mt-8", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDelete(true), children: "Delete DNS provider" }) }), _jsx(Modal, { open: confirmingDelete, onClose: () => setConfirmingDelete(false), title: "Delete DNS provider?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [deleteError && _jsx(Alert, { children: deleteError }), provider.domainCount > 0 && (_jsxs("p", { children: ["The server refuses while domains are assigned to it (", domainsText(provider.domainCount), "): assign them to another provider, or to manual publishing, first."] })), _jsx("p", { children: "The records it publishes for this server\u2019s own host names are removed from your DNS host, then its external-dns instance and its credentials are deleted." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] }) })] }));
|
|
117
|
+
}
|