web: simplify service editors with single-source-of-truth state
Replace dueling parse/stringify $effects across service editors with
one-time top-level init plus a single write-back $effect. Remount
editors via {#key value} in ServiceEditor so children no longer need
inbound-sync logic.
This commit is contained in:
parent
93e148e8c0
commit
7251d93619
14 changed files with 120 additions and 192 deletions
|
|
@ -61,12 +61,14 @@
|
|||
</div>
|
||||
</div>
|
||||
{:then EditorComponent}
|
||||
<EditorComponent
|
||||
{dn}
|
||||
{origin}
|
||||
{type}
|
||||
bind:value={value}
|
||||
/>
|
||||
{#key value}
|
||||
<EditorComponent
|
||||
{dn}
|
||||
{origin}
|
||||
{type}
|
||||
bind:value={value}
|
||||
/>
|
||||
{/key}
|
||||
{:catch error}
|
||||
<div class="alert alert-warning">
|
||||
<p>Failed to load editor for type: {type}</p>
|
||||
|
|
|
|||
|
|
@ -56,16 +56,11 @@
|
|||
// GitHub verification code
|
||||
let verificationCode = $state(value["txt"]?.Txt || "");
|
||||
|
||||
// Sync data back to TXT record
|
||||
$effect(() => {
|
||||
if (value["txt"]) {
|
||||
// Construct subdomain from organization name
|
||||
if (organizationName) {
|
||||
value["txt"].Hdr.Name =
|
||||
`_github-challenge-${organizationName.replace(/^_github-challenge-(.+?)-org(\..*)?/, "$1")}-org`;
|
||||
}
|
||||
value["txt"].Txt = verificationCode;
|
||||
if (organizationName) {
|
||||
value["txt"]!.Hdr.Name = `_github-challenge-${organizationName}-org`;
|
||||
}
|
||||
value["txt"]!.Txt = verificationCode;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,9 +48,6 @@
|
|||
value.username = "";
|
||||
}
|
||||
|
||||
// Name hash state
|
||||
let nameHash = $state("");
|
||||
|
||||
// Compute SHA-224 hash from username
|
||||
async function computeHash(username: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
|
|
@ -64,7 +61,21 @@
|
|||
.join("");
|
||||
}
|
||||
|
||||
// When username changes, compute hash
|
||||
// Initial name hash: extract from existing Hdr.Name if present, else empty
|
||||
function initialNameHash(): string {
|
||||
const existing = value["openpgpkey"]?.Hdr?.Name;
|
||||
if (!value["username"] && existing) {
|
||||
const parts = existing.split("._openpgpkey");
|
||||
if (parts.length > 0 && parts[0]) {
|
||||
return parts[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
let nameHash = $state(initialNameHash());
|
||||
|
||||
// When username changes, recompute hash
|
||||
$effect(() => {
|
||||
if (value["username"]) {
|
||||
computeHash(value["username"]).then((hash) => {
|
||||
|
|
@ -73,16 +84,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Extract name hash from existing domain name on load
|
||||
$effect(() => {
|
||||
if (!value["username"] && value["openpgpkey"]?.Hdr?.Name && !nameHash) {
|
||||
const parts = value["openpgpkey"].Hdr.Name.split("._openpgpkey");
|
||||
if (parts.length > 0 && parts[0]) {
|
||||
nameHash = parts[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// When name hash changes, update the domain name
|
||||
$effect(() => {
|
||||
if (nameHash && value["openpgpkey"]?.Hdr) {
|
||||
|
|
|
|||
|
|
@ -42,9 +42,6 @@
|
|||
(value as any)["srv"] = [];
|
||||
}
|
||||
|
||||
// Type-safe accessor for srv as array
|
||||
const srvArray = $derived((value as any)["srv"] as Array<dnsTypeSRV>);
|
||||
|
||||
// Service type configurations with their prefixes
|
||||
const services = [
|
||||
{ key: "submission", prefix: "_submission._tcp", label: "Email Submission" },
|
||||
|
|
@ -59,40 +56,26 @@
|
|||
{ key: "pop3s", prefix: "_pop3s._tcp", label: "POP3 over TLS" },
|
||||
];
|
||||
|
||||
// Initialize service arrays from srv (one-time, breaks circular dependency)
|
||||
let initialized = $state(false);
|
||||
|
||||
// Initialize service arrays on mount (runs once regardless of whether srv is empty)
|
||||
$effect(() => {
|
||||
if (!initialized) {
|
||||
for (const service of services) {
|
||||
(value as any)[service.key] = (srvArray ?? []).filter(
|
||||
(srv: dnsTypeSRV) => srv?.Hdr?.Name?.startsWith(service.prefix) || false,
|
||||
);
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize empty arrays for services with no records
|
||||
for (const service of services) {
|
||||
if (!(value as any)[service.key]) {
|
||||
(value as any)[service.key] = [];
|
||||
// One-time init: split records from srv into per-service arrays
|
||||
{
|
||||
const srvArray = ((value as any)["srv"] ?? []) as Array<dnsTypeSRV>;
|
||||
for (const service of services) {
|
||||
(value as any)[service.key] = srvArray.filter(
|
||||
(srv: dnsTypeSRV) => srv?.Hdr?.Name?.startsWith(service.prefix) || false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// When records in service arrays change, sync back to main array (one-way sync)
|
||||
$effect(() => {
|
||||
if (initialized) {
|
||||
const allRecords: Array<dnsTypeSRV> = [];
|
||||
for (const service of services) {
|
||||
const serviceArray = (value as any)[service.key] as Array<dnsTypeSRV>;
|
||||
if (serviceArray) {
|
||||
allRecords.push(...serviceArray);
|
||||
}
|
||||
const allRecords: Array<dnsTypeSRV> = [];
|
||||
for (const service of services) {
|
||||
const serviceArray = (value as any)[service.key] as Array<dnsTypeSRV>;
|
||||
if (serviceArray) {
|
||||
allRecords.push(...serviceArray);
|
||||
}
|
||||
(value as any)["srv"] = allRecords;
|
||||
}
|
||||
(value as any)["srv"] = allRecords;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -48,8 +48,15 @@
|
|||
value.username = "";
|
||||
}
|
||||
|
||||
// Name hash state
|
||||
let nameHash = $state("");
|
||||
// One-time init: extract existing name hash from domain name if no username
|
||||
let initialNameHash = "";
|
||||
if (!value["username"] && value["smimea"]?.Hdr?.Name) {
|
||||
const parts = value["smimea"].Hdr.Name.split("._smimecert");
|
||||
if (parts.length > 0 && parts[0]) {
|
||||
initialNameHash = parts[0];
|
||||
}
|
||||
}
|
||||
let nameHash = $state(initialNameHash);
|
||||
|
||||
// Compute SHA-224 hash from username
|
||||
async function computeHash(username: string): Promise<string> {
|
||||
|
|
@ -73,16 +80,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Extract name hash from existing domain name on load
|
||||
$effect(() => {
|
||||
if (!value["username"] && value["smimea"]?.Hdr?.Name && !nameHash) {
|
||||
const parts = value["smimea"].Hdr.Name.split("._smimecert");
|
||||
if (parts.length > 0 && parts[0]) {
|
||||
nameHash = parts[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// When name hash changes, update the domain name
|
||||
$effect(() => {
|
||||
if (nameHash && value["smimea"]?.Hdr) {
|
||||
|
|
|
|||
|
|
@ -38,11 +38,9 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (!value["SSHFP"]) {
|
||||
value["SSHFP"] = [];
|
||||
}
|
||||
});
|
||||
if (!value["SSHFP"]) {
|
||||
value["SSHFP"] = [];
|
||||
}
|
||||
|
||||
const type = "abstract.Server";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -39,35 +39,18 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
// Initialize value["txt"] if it doesn't exist
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR("._domainkey", getRrtype("TXT")) as any;
|
||||
}
|
||||
|
||||
let val = $state(parseDKIM(value["txt"]?.Txt || ""));
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Txt !== undefined) {
|
||||
val = parseDKIM(value["txt"].Txt);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(selector + "._domainkey", getRrtype("TXT")) as any;
|
||||
}
|
||||
if (value["txt"]) {
|
||||
value["txt"].Txt = stringifyDKIM(val, value["txt"].Txt || "");
|
||||
}
|
||||
});
|
||||
let val = $state(parseDKIM(value["txt"]!.Txt || ""));
|
||||
let selector = $state(value["txt"]!.Hdr?.Name?.replace("._domainkey", "") || "");
|
||||
|
||||
let selector = $state(value["txt"]?.Hdr?.Name?.replace("._domainkey", "") || "");
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Hdr?.Name) {
|
||||
selector = value["txt"].Hdr.Name.replace("._domainkey", "");
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Hdr) {
|
||||
value["txt"].Hdr.Name = selector + "._domainkey";
|
||||
const txt = value["txt"]!;
|
||||
txt.Txt = stringifyDKIM(val, txt.Txt || "");
|
||||
if (txt.Hdr) {
|
||||
txt.Hdr.Name = selector + "._domainkey";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -39,20 +39,14 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as any;
|
||||
}
|
||||
|
||||
let val = $state(parseDMARC(value["txt"]?.Txt || ""));
|
||||
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Txt !== undefined) {
|
||||
val = parseDMARC(value["txt"].Txt);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as any;
|
||||
}
|
||||
if (value["txt"]) {
|
||||
value["txt"].Txt = stringifyDMARC(val, value["txt"]?.Txt || "");
|
||||
}
|
||||
value["txt"]!.Txt = stringifyDMARC(val, value["txt"]?.Txt || "");
|
||||
});
|
||||
|
||||
const type = "svcs.DMARC";
|
||||
|
|
|
|||
|
|
@ -36,20 +36,14 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as any;
|
||||
}
|
||||
|
||||
let val = $state(parseMTASTS(value["txt"]?.Txt || ""));
|
||||
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Txt !== undefined) {
|
||||
val = parseMTASTS(value["txt"].Txt);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as any;
|
||||
}
|
||||
if (value["txt"]) {
|
||||
value["txt"].Txt = stringifyMTASTS(val, value["txt"]?.Txt || "");
|
||||
}
|
||||
value["txt"]!.Txt = stringifyMTASTS(val, value["txt"]?.Txt || "");
|
||||
});
|
||||
|
||||
const type = "svcs.MTA_STS";
|
||||
|
|
|
|||
|
|
@ -37,12 +37,9 @@
|
|||
|
||||
const type = "svcs.MXs";
|
||||
|
||||
// Ensure mx is always an array at runtime
|
||||
$effect(() => {
|
||||
if (value["mx"] && !Array.isArray(value["mx"])) {
|
||||
value["mx"] = [value["mx"]];
|
||||
}
|
||||
});
|
||||
if (value["mx"] && !Array.isArray(value["mx"])) {
|
||||
value["mx"] = [value["mx"]];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -39,33 +39,34 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
function parseSPF(val: string) {
|
||||
const fields = val.split(" ");
|
||||
const DEFAULT_SPF = "v=spf1 -all";
|
||||
const ALL_RE = /^[-~?+]?all$/;
|
||||
|
||||
return {
|
||||
v: fields[0].replace(/^v=/, ""),
|
||||
f: fields.slice(1),
|
||||
};
|
||||
function parseSPF(val: string): { v: string; f: string[] } {
|
||||
const tokens = val.trim().split(/\s+/).filter(Boolean);
|
||||
if (tokens.length === 0) return { v: "spf1", f: [] };
|
||||
if (tokens[0].startsWith("v=")) {
|
||||
return { v: tokens[0].slice(2), f: tokens.slice(1) };
|
||||
}
|
||||
return { v: "spf1", f: tokens };
|
||||
}
|
||||
function stringifySPF(val: { v?: string; f: string[] }) {
|
||||
return "v=" + (val["v"] ? val["v"] : "spf1") + " " + val.f.join(" ");
|
||||
|
||||
function stringifySPF(v: string, f: string[]): string {
|
||||
return "v=" + (v || "spf1") + (f.length ? " " + f.join(" ") : "");
|
||||
}
|
||||
let val = $state(parseSPF(value["txt"]?.Txt || "v=spf1 -all"));
|
||||
|
||||
if (!value["txt"]) {
|
||||
const txtRecord = newRR(dn, 16) as dnsTypeTXT; // TXT record type is 16
|
||||
txtRecord.Txt = DEFAULT_SPF;
|
||||
value["txt"] = txtRecord;
|
||||
}
|
||||
|
||||
const initial = parseSPF(value["txt"].Txt || DEFAULT_SPF);
|
||||
let v = $state(initial.v);
|
||||
let f = $state(initial.f);
|
||||
|
||||
$effect(() => {
|
||||
if (value["txt"]?.Txt) {
|
||||
val = parseSPF(value["txt"].Txt);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if (!value["txt"]) {
|
||||
const txtRecord = newRR(dn, 16) as dnsTypeTXT; // TXT record type is 16
|
||||
txtRecord.Txt = "v=spf1 -all";
|
||||
value["txt"] = txtRecord;
|
||||
}
|
||||
if (value["txt"]) {
|
||||
value["txt"].Txt = stringifySPF(val);
|
||||
}
|
||||
value["txt"]!.Txt = stringifySPF(v, f);
|
||||
});
|
||||
|
||||
const type = "svcs.SPF";
|
||||
|
|
@ -74,19 +75,19 @@
|
|||
|
||||
async function addDirective() {
|
||||
let newIdx: number;
|
||||
if (val.f.length >= 1 && val.f[val.f.length - 1].indexOf("all") >= 0) {
|
||||
newIdx = val.f.length - 1;
|
||||
val.f.splice(val.f.length - 1, 0, "");
|
||||
if (f.length >= 1 && ALL_RE.test(f[f.length - 1])) {
|
||||
newIdx = f.length - 1;
|
||||
f.splice(newIdx, 0, "");
|
||||
} else {
|
||||
newIdx = val.f.length;
|
||||
val.f.push("");
|
||||
newIdx = f.length;
|
||||
f.push("");
|
||||
}
|
||||
await tick();
|
||||
inputRefs[newIdx]?.focus();
|
||||
}
|
||||
|
||||
function delDirective(idx: number) {
|
||||
val.f.splice(idx, 1);
|
||||
f.splice(idx, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -103,17 +104,17 @@
|
|||
type: "string",
|
||||
description: "Defines the version of SPF to use.",
|
||||
}}
|
||||
bind:value={val.v}
|
||||
bind:value={v}
|
||||
/>
|
||||
|
||||
<h5 class="text-primary pb-1 border-bottom border-1">Directives</h5>
|
||||
<ListGroup>
|
||||
{#each val.f as directive, i}
|
||||
{#each f as _, i (i)}
|
||||
<ListGroupItem class="p-0">
|
||||
<InputGroup>
|
||||
<input
|
||||
class="form-control border-0"
|
||||
bind:value={val.f[i]}
|
||||
bind:value={f[i]}
|
||||
bind:this={inputRefs[i]}
|
||||
/>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -39,22 +39,11 @@
|
|||
|
||||
let { dn, origin, value = $bindable({}) }: Props = $props();
|
||||
|
||||
// Initialize TXT record if it doesn't exist
|
||||
$effect(() => {
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as dnsTypeTXT;
|
||||
}
|
||||
});
|
||||
if (!value["txt"]) {
|
||||
value["txt"] = newRR(dn, getRrtype("TXT")) as dnsTypeTXT;
|
||||
}
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
let val = $derived(
|
||||
value["txt"]
|
||||
? new TLSRPTPolicy(value["txt"])
|
||||
: new TLSRPTPolicy({
|
||||
Hdr: { Name: dn, Rrtype: 16, Class: 1, Ttl: 3600, Rdlength: 0 },
|
||||
Txt: "",
|
||||
}),
|
||||
);
|
||||
let val = $derived(new TLSRPTPolicy(value["txt"] as dnsTypeTXT));
|
||||
|
||||
const type = "svcs.TLS_RPT";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -46,20 +46,12 @@
|
|||
// Type-safe accessor for srv as array
|
||||
const srvArray = $derived(value["srv"] as any as Array<dnsTypeSRV>);
|
||||
|
||||
// Extract service name and protocol from first record's domain name
|
||||
let serviceName = $state<string>("http");
|
||||
let protocol = $state<string>("tcp");
|
||||
|
||||
// Initialize serviceName and protocol from first SRV record
|
||||
$effect(() => {
|
||||
if (srvArray?.[0]?.Hdr?.Name) {
|
||||
const match = srvArray[0].Hdr.Name.match(/^_([^.]+)\._(\w+)/);
|
||||
if (match) {
|
||||
serviceName = match[1];
|
||||
protocol = match[2];
|
||||
}
|
||||
}
|
||||
});
|
||||
// One-time extract of service name and protocol from first SRV record's name
|
||||
const initialMatch = (value["srv"] as any as Array<dnsTypeSRV>)?.[0]?.Hdr?.Name?.match(
|
||||
/^_([^.]+)\._([^.]+)/,
|
||||
);
|
||||
let serviceName = $state<string>(initialMatch?.[1] ?? "http");
|
||||
let protocol = $state<string>(initialMatch?.[2] ?? "tcp");
|
||||
|
||||
// Construct the full DN with service and protocol prefix
|
||||
let fullDn = $derived(`_${serviceName}._${protocol}`);
|
||||
|
|
|
|||
|
|
@ -167,12 +167,14 @@
|
|||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<ServiceEditor
|
||||
dn={service._domain}
|
||||
origin={data.domain}
|
||||
type={service._svctype}
|
||||
bind:value={service.Service}
|
||||
/>
|
||||
{#key data.serviceid}
|
||||
<ServiceEditor
|
||||
dn={service._domain}
|
||||
origin={data.domain}
|
||||
type={service._svctype}
|
||||
bind:value={service.Service}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue