feat(dns): add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-08 - 13.9.0 - feat(dns)
|
||||||
|
add built-in dcrouter DNS provider support and rename manual domains to dcrouter-hosted/local
|
||||||
|
|
||||||
|
- Expose a synthetic built-in "DcRouter" provider in provider listings and block create, edit, delete, test, and external domain listing operations for it
|
||||||
|
- Rename domain and record source semantics from "manual" to "dcrouter" and "local" across backend, interfaces, and UI
|
||||||
|
- Add database migrations to convert existing DomainDoc.source and DnsRecordDoc.source values to the new naming
|
||||||
|
- Update domain creation flows and provider UI labels to reflect dcrouter-hosted authoritative domains
|
||||||
|
|
||||||
## 2026-04-08 - 13.8.0 - feat(acme)
|
## 2026-04-08 - 13.8.0 - feat(acme)
|
||||||
add DB-backed ACME configuration management and OpsServer certificate settings UI
|
add DB-backed ACME configuration management and OpsServer certificate settings UI
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"@api.global/typedserver": "^8.4.6",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.69.1",
|
"@design.estate/dees-catalog": "^3.70.0",
|
||||||
"@design.estate/dees-element": "^2.2.4",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@push.rocks/lik": "^6.4.0",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/projectinfo": "^5.1.0",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.2.2",
|
"@push.rocks/smartlog": "^3.2.2",
|
||||||
"@push.rocks/smartmetrics": "^3.0.3",
|
"@push.rocks/smartmetrics": "^3.0.3",
|
||||||
"@push.rocks/smartmigration": "1.1.1",
|
"@push.rocks/smartmigration": "1.2.0",
|
||||||
"@push.rocks/smartmta": "^5.3.1",
|
"@push.rocks/smartmta": "^5.3.1",
|
||||||
"@push.rocks/smartnetwork": "^4.5.2",
|
"@push.rocks/smartnetwork": "^4.5.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
|||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.69.1
|
specifier: ^3.70.0
|
||||||
version: 3.69.1(@tiptap/pm@2.27.2)
|
version: 3.70.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.2.4
|
version: 2.2.4
|
||||||
@@ -66,8 +66,8 @@ importers:
|
|||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
'@push.rocks/smartmigration':
|
'@push.rocks/smartmigration':
|
||||||
specifier: 1.1.1
|
specifier: 1.2.0
|
||||||
version: 1.1.1(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))
|
version: 1.2.0(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))
|
||||||
'@push.rocks/smartmta':
|
'@push.rocks/smartmta':
|
||||||
specifier: ^5.3.1
|
specifier: ^5.3.1
|
||||||
version: 5.3.1
|
version: 5.3.1
|
||||||
@@ -353,8 +353,8 @@ packages:
|
|||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.69.1':
|
'@design.estate/dees-catalog@3.70.0':
|
||||||
resolution: {integrity: sha512-OSpHB/hfOrL2mkAfF50TqTKJ2hvPd7Cj1WklAmFckyjloE4fd7DRDeXdI/Bziq9152gExipX5VoofTAOr4rF5w==}
|
resolution: {integrity: sha512-bNqOxxl83FNCCV+7QoUj6oeRC0VTExWOClrLrHNMoLIU0TCtzhcmQqiuJhdWrcCwZ5RBhXHGMSFsR62d2RcWpw==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -1231,8 +1231,8 @@ packages:
|
|||||||
'@push.rocks/smartmetrics@3.0.3':
|
'@push.rocks/smartmetrics@3.0.3':
|
||||||
resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==}
|
resolution: {integrity: sha512-RYY4NOla3kraZYVF9TBHgIz4/hSkqVDVNP7tLwhLK5mGBPBy8I/9NWXX6txZKQw6QihP85YD8mWUuUu2xS4D6Q==}
|
||||||
|
|
||||||
'@push.rocks/smartmigration@1.1.1':
|
'@push.rocks/smartmigration@1.2.0':
|
||||||
resolution: {integrity: sha512-K/eLN9cNy+CLOT73rhI93vOy/vGwpV46iJpjRUyPwHsMcQcV6po2idk5+XZQzeuq2x7KpKuUPtZ6gXMtf5Y/ig==}
|
resolution: {integrity: sha512-H2diE1UbZm4cXjxgxkt2YQW3aUQ3QVVU/e8Ws30hzIep0xIqL1BH6//WawA5ZBQhnAOBssZpVOuWOd3GIeBq+Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@push.rocks/smartbucket': ^4.6.0
|
'@push.rocks/smartbucket': ^4.6.0
|
||||||
'@push.rocks/smartdata': ^7.1.7
|
'@push.rocks/smartdata': ^7.1.7
|
||||||
@@ -4315,7 +4315,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
||||||
'@cloudflare/workers-types': 4.20260405.1
|
'@cloudflare/workers-types': 4.20260405.1
|
||||||
'@design.estate/dees-catalog': 3.69.1(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.70.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.4.0
|
'@push.rocks/lik': 6.4.0
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4844,7 +4844,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.69.1(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.70.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
@@ -6354,7 +6354,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
|
||||||
'@push.rocks/smartmigration@1.1.1(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))':
|
'@push.rocks/smartmigration@1.2.0(@push.rocks/smartbucket@4.6.0)(@push.rocks/smartdata@7.1.7(socks@2.8.7))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
'@push.rocks/smartversion': 3.1.0
|
'@push.rocks/smartversion': 3.1.0
|
||||||
@@ -6900,7 +6900,7 @@ snapshots:
|
|||||||
|
|
||||||
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
|
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-catalog': 3.69.1(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.70.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
'@design.estate/dees-wcctools': 3.8.0
|
'@design.estate/dees-wcctools': 3.8.0
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.8.0',
|
version: '13.9.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1792,7 +1792,8 @@ export class DcRouter {
|
|||||||
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
|
logger.log('info', `Registered ${allRecords.length} DNS records (${authoritativeRecords.length} authoritative, ${emailDnsRecords.length} email, ${dkimRecords.length} DKIM, ${this.options.dnsRecords?.length || 0} user-defined)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hand the DnsServer to DnsManager so DB-backed manual records get registered too.
|
// Hand the DnsServer to DnsManager so DB-backed local records on
|
||||||
|
// dcrouter-hosted domains get registered too.
|
||||||
if (this.dnsManager && this.dnsServer) {
|
if (this.dnsManager && this.dnsServer) {
|
||||||
await this.dnsManager.attachDnsServer(this.dnsServer);
|
await this.dnsManager.attachDnsServer(this.dnsServer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import type {
|
|||||||
* Responsibilities:
|
* Responsibilities:
|
||||||
* - Load Domain/DnsRecord docs from the DB on start
|
* - Load Domain/DnsRecord docs from the DB on start
|
||||||
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
|
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
|
||||||
* - Register manual-domain records with smartdns.DnsServer at startup
|
* - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
|
||||||
* - Provide CRUD methods used by OpsServer handlers (manual domains hit smartdns,
|
* - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
|
||||||
* provider domains hit the provider API)
|
* smartdns, provider domains hit the provider API)
|
||||||
* - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
|
* - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy()
|
||||||
*
|
*
|
||||||
* Provider-managed domains are NEVER served from the embedded DnsServer — the
|
* Provider-managed domains are NEVER served from the embedded DnsServer — the
|
||||||
@@ -69,12 +69,12 @@ export class DnsManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire the embedded DnsServer instance after it has been created by
|
* Wire the embedded DnsServer instance after it has been created by
|
||||||
* DcRouter.setupDnsWithSocketHandler(). After this, manual records loaded
|
* DcRouter.setupDnsWithSocketHandler(). After this, local records on
|
||||||
* from the DB are registered with the server.
|
* dcrouter-hosted domains loaded from the DB are registered with the server.
|
||||||
*/
|
*/
|
||||||
public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
|
public async attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise<void> {
|
||||||
this.dnsServer = dnsServer;
|
this.dnsServer = dnsServer;
|
||||||
await this.applyManualDomainsToDnsServer();
|
await this.applyDcrouterDomainsToDnsServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -83,7 +83,8 @@ export class DnsManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
|
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
|
||||||
* seed them as `source: 'manual'` records. On subsequent boots (DB has
|
* seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
|
||||||
|
* local (`record.source: 'local'`) records. On subsequent boots (DB has
|
||||||
* entries), constructor config is ignored with a warning.
|
* entries), constructor config is ignored with a warning.
|
||||||
*/
|
*/
|
||||||
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
|
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
|
||||||
@@ -117,7 +118,7 @@ export class DnsManager {
|
|||||||
const domain = new DomainDoc();
|
const domain = new DomainDoc();
|
||||||
domain.id = plugins.uuid.v4();
|
domain.id = plugins.uuid.v4();
|
||||||
domain.name = scope.toLowerCase();
|
domain.name = scope.toLowerCase();
|
||||||
domain.source = 'manual';
|
domain.source = 'dcrouter';
|
||||||
domain.authoritative = true;
|
domain.authoritative = true;
|
||||||
domain.createdAt = now;
|
domain.createdAt = now;
|
||||||
domain.updatedAt = now;
|
domain.updatedAt = now;
|
||||||
@@ -144,7 +145,7 @@ export class DnsManager {
|
|||||||
record.type = rec.type as TDnsRecordType;
|
record.type = rec.type as TDnsRecordType;
|
||||||
record.value = rec.value;
|
record.value = rec.value;
|
||||||
record.ttl = rec.ttl ?? 300;
|
record.ttl = rec.ttl ?? 300;
|
||||||
record.source = 'manual';
|
record.source = 'local';
|
||||||
record.createdAt = now;
|
record.createdAt = now;
|
||||||
record.updatedAt = now;
|
record.updatedAt = now;
|
||||||
record.createdBy = 'seed';
|
record.createdBy = 'seed';
|
||||||
@@ -174,28 +175,31 @@ export class DnsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// Manual-domain DnsServer wiring
|
// DcRouter-hosted domain DnsServer wiring
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all manual-domain records from the DB with the embedded DnsServer.
|
* Register all records from dcrouter-hosted domains in the DB with the
|
||||||
* Called once after attachDnsServer().
|
* embedded DnsServer. Called once after attachDnsServer().
|
||||||
*/
|
*/
|
||||||
private async applyManualDomainsToDnsServer(): Promise<void> {
|
private async applyDcrouterDomainsToDnsServer(): Promise<void> {
|
||||||
if (!this.dnsServer) {
|
if (!this.dnsServer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const allDomains = await DomainDoc.findAll();
|
const allDomains = await DomainDoc.findAll();
|
||||||
const manualDomains = allDomains.filter((d) => d.source === 'manual');
|
const dcrouterDomains = allDomains.filter((d) => d.source === 'dcrouter');
|
||||||
let registered = 0;
|
let registered = 0;
|
||||||
for (const domain of manualDomains) {
|
for (const domain of dcrouterDomains) {
|
||||||
const records = await DnsRecordDoc.findByDomainId(domain.id);
|
const records = await DnsRecordDoc.findByDomainId(domain.id);
|
||||||
for (const rec of records) {
|
for (const rec of records) {
|
||||||
this.registerRecordWithDnsServer(rec);
|
this.registerRecordWithDnsServer(rec);
|
||||||
registered++;
|
registered++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.log('info', `DnsManager: registered ${registered} manual DNS record(s) from DB`);
|
logger.log(
|
||||||
|
'info',
|
||||||
|
`DnsManager: registered ${registered} dcrouter-hosted DNS record(s) from DB`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -381,6 +385,12 @@ export class DnsManager {
|
|||||||
credentials: TDnsProviderCredentials;
|
credentials: TDnsProviderCredentials;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
|
if (args.type === 'dcrouter') {
|
||||||
|
throw new Error(
|
||||||
|
'createProvider: cannot create a DnsProviderDoc with type "dcrouter" — ' +
|
||||||
|
'that type is reserved for the built-in pseudo-provider surfaced at read time.',
|
||||||
|
);
|
||||||
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const doc = new DnsProviderDoc();
|
const doc = new DnsProviderDoc();
|
||||||
doc.id = plugins.uuid.v4();
|
doc.id = plugins.uuid.v4();
|
||||||
@@ -473,10 +483,10 @@ export class DnsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a manual (authoritative) domain. dcrouter will serve DNS records
|
* Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
|
||||||
* for this domain via the embedded smartdns.DnsServer.
|
* DNS records for this domain via the embedded smartdns.DnsServer.
|
||||||
*/
|
*/
|
||||||
public async createManualDomain(args: {
|
public async createDcrouterDomain(args: {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
@@ -485,7 +495,7 @@ export class DnsManager {
|
|||||||
const doc = new DomainDoc();
|
const doc = new DomainDoc();
|
||||||
doc.id = plugins.uuid.v4();
|
doc.id = plugins.uuid.v4();
|
||||||
doc.name = args.name.toLowerCase();
|
doc.name = args.name.toLowerCase();
|
||||||
doc.source = 'manual';
|
doc.source = 'dcrouter';
|
||||||
doc.authoritative = true;
|
doc.authoritative = true;
|
||||||
doc.description = args.description;
|
doc.description = args.description;
|
||||||
doc.createdAt = now;
|
doc.createdAt = now;
|
||||||
@@ -571,10 +581,11 @@ export class DnsManager {
|
|||||||
/**
|
/**
|
||||||
* Delete a domain and all of its DNS records. For provider domains, only
|
* Delete a domain and all of its DNS records. For provider domains, only
|
||||||
* removes the local mirror — does NOT touch the provider.
|
* removes the local mirror — does NOT touch the provider.
|
||||||
* For manual domains, also unregisters records from the embedded DnsServer.
|
* For dcrouter-hosted domains, also unregisters records from the embedded
|
||||||
|
* DnsServer.
|
||||||
*
|
*
|
||||||
* Note: smartdns has no public unregister-by-name API in the version pinned
|
* Note: smartdns has no public unregister-by-name API in the version pinned
|
||||||
* here, so manual record deletes only take effect after a restart. The DB
|
* here, so local record deletes only take effect after a restart. The DB
|
||||||
* is the source of truth and the next start will not register the deleted
|
* is the source of truth and the next start will not register the deleted
|
||||||
* record.
|
* record.
|
||||||
*/
|
*/
|
||||||
@@ -652,7 +663,7 @@ export class DnsManager {
|
|||||||
doc.value = args.value;
|
doc.value = args.value;
|
||||||
doc.ttl = args.ttl ?? 300;
|
doc.ttl = args.ttl ?? 300;
|
||||||
if (args.proxied !== undefined) doc.proxied = args.proxied;
|
if (args.proxied !== undefined) doc.proxied = args.proxied;
|
||||||
doc.source = 'manual';
|
doc.source = 'local';
|
||||||
doc.createdAt = now;
|
doc.createdAt = now;
|
||||||
doc.updatedAt = now;
|
doc.updatedAt = now;
|
||||||
doc.createdBy = args.createdBy;
|
doc.createdBy = args.createdBy;
|
||||||
@@ -678,7 +689,7 @@ export class DnsManager {
|
|||||||
return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
|
return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Manual / authoritative — register with embedded DnsServer immediately
|
// dcrouter-hosted / authoritative — register with embedded DnsServer immediately
|
||||||
this.registerRecordWithDnsServer(doc);
|
this.registerRecordWithDnsServer(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,7 +733,7 @@ export class DnsManager {
|
|||||||
return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
|
return { success: false, message: `Provider rejected update: ${(err as Error).message}` };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Re-register the manual record so the new closure picks up the updated fields
|
// Re-register the local record so the new closure picks up the updated fields
|
||||||
this.registerRecordWithDnsServer(doc);
|
this.registerRecordWithDnsServer(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,7 +759,7 @@ export class DnsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// For manual records: smartdns has no unregister API in the pinned version,
|
// For local records: smartdns has no unregister API in the pinned version,
|
||||||
// so the record stays served until the next restart. The DB delete still
|
// so the record stays served until the next restart. The DB delete still
|
||||||
// takes effect — on restart, the record will not be re-registered.
|
// takes effect — on restart, the record will not be re-registered.
|
||||||
|
|
||||||
@@ -807,7 +818,7 @@ export class DnsManager {
|
|||||||
public toPublicDomain(doc: DomainDoc): {
|
public toPublicDomain(doc: DomainDoc): {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
source: 'manual' | 'provider';
|
source: 'dcrouter' | 'provider';
|
||||||
providerId?: string;
|
providerId?: string;
|
||||||
authoritative: boolean;
|
authoritative: boolean;
|
||||||
nameservers?: string[];
|
nameservers?: string[];
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ export function createDnsProvider(
|
|||||||
}
|
}
|
||||||
return new CloudflareDnsProvider(credentials.apiToken);
|
return new CloudflareDnsProvider(credentials.apiToken);
|
||||||
}
|
}
|
||||||
|
case 'dcrouter': {
|
||||||
|
// The built-in DcRouter pseudo-provider has no runtime client — dcrouter
|
||||||
|
// itself serves the records via the embedded smartdns.DnsServer. This
|
||||||
|
// case exists only to satisfy the exhaustive switch; it should never
|
||||||
|
// actually run because the handler layer rejects any CRUD that would
|
||||||
|
// result in a DnsProviderDoc with type: 'dcrouter'.
|
||||||
|
throw new Error(
|
||||||
|
`createDnsProvider: 'dcrouter' is a built-in pseudo-provider — no runtime client exists. ` +
|
||||||
|
`This call indicates a DnsProviderDoc with type: 'dcrouter' was persisted, which should never happen.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// If you see a TypeScript error here after extending TDnsProviderType,
|
// If you see a TypeScript error here after extending TDnsProviderType,
|
||||||
// add a `case` for the new type above. The `never` enforces exhaustiveness.
|
// add a `case` for the new type above. The `never` enforces exhaustiveness.
|
||||||
|
|||||||
@@ -46,15 +46,28 @@ export class DnsProviderHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
// Get all providers
|
// Get all providers — prepends the built-in DcRouter pseudo-provider
|
||||||
|
// so operators see a uniform "who serves this?" list that includes the
|
||||||
|
// authoritative dcrouter alongside external accounts.
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsProviders>(
|
||||||
'getDnsProviders',
|
'getDnsProviders',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) return { providers: [] };
|
const synthetic: interfaces.data.IDnsProviderPublic = {
|
||||||
return { providers: await dnsManager.listProviders() };
|
id: interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID,
|
||||||
|
name: 'DcRouter',
|
||||||
|
type: 'dcrouter',
|
||||||
|
status: 'ok',
|
||||||
|
createdAt: 0,
|
||||||
|
updatedAt: 0,
|
||||||
|
createdBy: 'system',
|
||||||
|
hasCredentials: false,
|
||||||
|
builtIn: true,
|
||||||
|
};
|
||||||
|
const real = dnsManager ? await dnsManager.listProviders() : [];
|
||||||
|
return { providers: [synthetic, ...real] };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -78,6 +91,12 @@ export class DnsProviderHandler {
|
|||||||
'createDnsProvider',
|
'createDnsProvider',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const userId = await this.requireAuth(dataArg, 'dns-providers:write');
|
const userId = await this.requireAuth(dataArg, 'dns-providers:write');
|
||||||
|
if (dataArg.type === 'dcrouter') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'cannot create built-in provider',
|
||||||
|
};
|
||||||
|
}
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) {
|
if (!dnsManager) {
|
||||||
return { success: false, message: 'DnsManager not initialized (DB disabled?)' };
|
return { success: false, message: 'DnsManager not initialized (DB disabled?)' };
|
||||||
@@ -99,6 +118,9 @@ export class DnsProviderHandler {
|
|||||||
'updateDnsProvider',
|
'updateDnsProvider',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAuth(dataArg, 'dns-providers:write');
|
await this.requireAuth(dataArg, 'dns-providers:write');
|
||||||
|
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||||
|
return { success: false, message: 'cannot edit built-in provider' };
|
||||||
|
}
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||||
const ok = await dnsManager.updateProvider(dataArg.id, {
|
const ok = await dnsManager.updateProvider(dataArg.id, {
|
||||||
@@ -116,6 +138,9 @@ export class DnsProviderHandler {
|
|||||||
'deleteDnsProvider',
|
'deleteDnsProvider',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAuth(dataArg, 'dns-providers:write');
|
await this.requireAuth(dataArg, 'dns-providers:write');
|
||||||
|
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||||
|
return { success: false, message: 'cannot delete built-in provider' };
|
||||||
|
}
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||||
return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
|
return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
|
||||||
@@ -129,6 +154,13 @@ export class DnsProviderHandler {
|
|||||||
'testDnsProvider',
|
'testDnsProvider',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||||
|
if (dataArg.id === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'built-in provider has no external connection to test',
|
||||||
|
testedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) {
|
if (!dnsManager) {
|
||||||
return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() };
|
return { ok: false, error: 'DnsManager not initialized', testedAt: Date.now() };
|
||||||
@@ -144,6 +176,12 @@ export class DnsProviderHandler {
|
|||||||
'listProviderDomains',
|
'listProviderDomains',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAuth(dataArg, 'dns-providers:read');
|
await this.requireAuth(dataArg, 'dns-providers:read');
|
||||||
|
if (dataArg.providerId === interfaces.data.DCROUTER_BUILTIN_PROVIDER_ID) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'built-in provider has no external domain listing — use "Add DcRouter Domain" instead',
|
||||||
|
};
|
||||||
|
}
|
||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class DomainHandler {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create manual domain
|
// Create dcrouter-hosted domain
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateDomain>(
|
||||||
'createDomain',
|
'createDomain',
|
||||||
@@ -80,7 +80,7 @@ export class DomainHandler {
|
|||||||
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
||||||
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
||||||
try {
|
try {
|
||||||
const id = await dnsManager.createManualDomain({
|
const id = await dnsManager.createDcrouterDomain({
|
||||||
name: dataArg.name,
|
name: dataArg.name,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* Supported DNS provider types. Initially Cloudflare; the abstraction is
|
* Stable ID for the built-in DcRouter pseudo-provider. The Providers list
|
||||||
* designed so additional providers (Route53, Gandi, DigitalOcean…) can be
|
* surfaces this as the first, non-deletable row so operators see a uniform
|
||||||
* added by implementing the IDnsProvider class interface in ts/dns/providers/.
|
* "who serves this?" answer for every domain. The ID is magic — it never
|
||||||
|
* exists in the DnsProviderDoc collection; handlers inject it at read time
|
||||||
|
* and reject any mutation that targets it.
|
||||||
*/
|
*/
|
||||||
export type TDnsProviderType = 'cloudflare';
|
export const DCROUTER_BUILTIN_PROVIDER_ID = '__dcrouter__';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported DNS provider types.
|
||||||
|
*
|
||||||
|
* - 'cloudflare' → Cloudflare account (API token-based). Provider stays
|
||||||
|
* authoritative; dcrouter pushes record changes via API.
|
||||||
|
* - 'dcrouter' → Built-in pseudo-provider for dcrouter-hosted zones.
|
||||||
|
* dcrouter itself is the authoritative DNS server. No
|
||||||
|
* credentials, cannot be created/edited/deleted through
|
||||||
|
* the provider CRUD — the Providers view renders it from
|
||||||
|
* a handler-level synthetic row.
|
||||||
|
*
|
||||||
|
* The abstraction is designed so additional providers (Route53, Gandi,
|
||||||
|
* DigitalOcean, foreign dcrouters…) can be added by implementing the
|
||||||
|
* IDnsProvider class interface in ts/dns/providers/.
|
||||||
|
*/
|
||||||
|
export type TDnsProviderType = 'cloudflare' | 'dcrouter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of the last connection test against a provider.
|
* Status of the last connection test against a provider.
|
||||||
@@ -58,6 +77,12 @@ export interface IDnsProviderPublic {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
/** Whether credentials are configured (true after creation). Never the secret itself. */
|
/** Whether credentials are configured (true after creation). Never the secret itself. */
|
||||||
hasCredentials: boolean;
|
hasCredentials: boolean;
|
||||||
|
/**
|
||||||
|
* True for the built-in DcRouter pseudo-provider — read-only, cannot be
|
||||||
|
* created / edited / deleted. Injected by the handler layer, never
|
||||||
|
* persisted in the DnsProviderDoc collection.
|
||||||
|
*/
|
||||||
|
builtIn?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,6 +139,13 @@ export interface IDnsProviderTypeDescriptor {
|
|||||||
* credentials each one needs. Used by both backend and frontend.
|
* credentials each one needs. Used by both backend and frontend.
|
||||||
*/
|
*/
|
||||||
export const dnsProviderTypeDescriptors: ReadonlyArray<IDnsProviderTypeDescriptor> = [
|
export const dnsProviderTypeDescriptors: ReadonlyArray<IDnsProviderTypeDescriptor> = [
|
||||||
|
{
|
||||||
|
type: 'dcrouter',
|
||||||
|
displayName: 'DcRouter (built-in)',
|
||||||
|
description:
|
||||||
|
'Built-in authoritative DNS. Records are served directly by dcrouter — delegate the domain\'s NS records to make this effective.',
|
||||||
|
credentialFields: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'cloudflare',
|
type: 'cloudflare',
|
||||||
displayName: 'Cloudflare',
|
displayName: 'Cloudflare',
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ export type TDnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA
|
|||||||
/**
|
/**
|
||||||
* Where a DNS record came from.
|
* Where a DNS record came from.
|
||||||
*
|
*
|
||||||
* - 'manual' → created in the dcrouter UI / API
|
* - 'local' → originated in this dcrouter (created via UI / API)
|
||||||
* - 'synced' → pulled from a provider during a sync operation
|
* - 'synced' → pulled from an upstream provider (Cloudflare, foreign
|
||||||
|
* dcrouter, …) during a sync operation
|
||||||
*/
|
*/
|
||||||
export type TDnsRecordSource = 'manual' | 'synced';
|
export type TDnsRecordSource = 'local' | 'synced';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A DNS record. For manual (authoritative) domains, the record is registered
|
* A DNS record. For dcrouter-hosted (authoritative) domains, the record is
|
||||||
* with the embedded smartdns.DnsServer. For provider-managed domains, the
|
* registered with the embedded smartdns.DnsServer. For provider-managed
|
||||||
* record is mirrored from / pushed to the provider API and `providerRecordId`
|
* domains, the record is mirrored from / pushed to the provider API and
|
||||||
* holds the provider's internal record id (for updates and deletes).
|
* `providerRecordId` holds the provider's internal record id (for updates
|
||||||
|
* and deletes).
|
||||||
*/
|
*/
|
||||||
export interface IDnsRecord {
|
export interface IDnsRecord {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Where a domain came from / how it is managed.
|
* Where a domain came from / how it is managed.
|
||||||
*
|
*
|
||||||
* - 'manual' → operator added the domain manually. dcrouter is the
|
* - 'dcrouter' → dcrouter is the authoritative DNS server for this domain;
|
||||||
* authoritative DNS server for it; records are served by
|
* records are served by the embedded smartdns.DnsServer.
|
||||||
* the embedded smartdns.DnsServer.
|
* Operators delegate the domain's NS records to make this
|
||||||
|
* effective.
|
||||||
* - 'provider' → domain was imported from an external DNS provider
|
* - 'provider' → domain was imported from an external DNS provider
|
||||||
* (e.g. Cloudflare). The provider stays authoritative;
|
* (e.g. Cloudflare). The provider stays authoritative;
|
||||||
* dcrouter only reads/writes records via the provider API.
|
* dcrouter only reads/writes records via the provider API.
|
||||||
*/
|
*/
|
||||||
export type TDomainSource = 'manual' | 'provider';
|
export type TDomainSource = 'dcrouter' | 'provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A domain under management by dcrouter.
|
* A domain under management by dcrouter.
|
||||||
@@ -20,7 +21,7 @@ export interface IDomain {
|
|||||||
source: TDomainSource;
|
source: TDomainSource;
|
||||||
/** ID of the DnsProvider that owns this domain — only set when source === 'provider'. */
|
/** ID of the DnsProvider that owns this domain — only set when source === 'provider'. */
|
||||||
providerId?: string;
|
providerId?: string;
|
||||||
/** True when dcrouter is the authoritative DNS server for this domain (source === 'manual'). */
|
/** True when dcrouter is the authoritative DNS server for this domain (source === 'dcrouter'). */
|
||||||
authoritative: boolean;
|
authoritative: boolean;
|
||||||
/** Authoritative nameservers (display only — populated from provider for imported domains). */
|
/** Authoritative nameservers (display only — populated from provider for imported domains). */
|
||||||
nameservers?: string[];
|
nameservers?: string[];
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface IReq_GetDnsRecord extends plugins.typedrequestInterfaces.implem
|
|||||||
/**
|
/**
|
||||||
* Create a new DNS record.
|
* Create a new DNS record.
|
||||||
*
|
*
|
||||||
* For manual domains: registers the record with the embedded DnsServer.
|
* For dcrouter-hosted domains: registers the record with the embedded DnsServer.
|
||||||
* For provider domains: pushes the record to the provider API.
|
* For provider domains: pushes the record to the provider API.
|
||||||
*/
|
*/
|
||||||
export interface IReq_CreateDnsRecord extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_CreateDnsRecord extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export interface IReq_GetDomain extends plugins.typedrequestInterfaces.implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a manual (authoritative) domain. dcrouter will serve DNS
|
* Create a dcrouter-hosted (authoritative) domain. dcrouter will serve
|
||||||
* records for this domain via the embedded smartdns.DnsServer.
|
* DNS records for this domain via the embedded smartdns.DnsServer.
|
||||||
*/
|
*/
|
||||||
export interface IReq_CreateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_CreateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
@@ -130,7 +130,7 @@ export interface IReq_DeleteDomain extends plugins.typedrequestInterfaces.implem
|
|||||||
/**
|
/**
|
||||||
* Force-resync a provider-managed domain: re-pulls all records from the
|
* Force-resync a provider-managed domain: re-pulls all records from the
|
||||||
* provider API, replacing the cached DnsRecordDocs.
|
* provider API, replacing the cached DnsRecordDocs.
|
||||||
* No-op for manual domains.
|
* No-op for dcrouter-hosted domains.
|
||||||
*/
|
*/
|
||||||
export interface IReq_SyncDomain extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_SyncDomain extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
|||||||
@@ -64,6 +64,34 @@ export async function createMigrationRunner(
|
|||||||
migrated++;
|
migrated++;
|
||||||
}
|
}
|
||||||
ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`);
|
ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`);
|
||||||
|
})
|
||||||
|
.step('rename-domain-source-manual-to-dcrouter')
|
||||||
|
.from('13.1.0').to('13.8.1')
|
||||||
|
.description('Rename DomainDoc.source value from "manual" to "dcrouter"')
|
||||||
|
.up(async (ctx) => {
|
||||||
|
const collection = ctx.mongo!.collection('domaindoc');
|
||||||
|
const result = await collection.updateMany(
|
||||||
|
{ source: 'manual' },
|
||||||
|
{ $set: { source: 'dcrouter' } },
|
||||||
|
);
|
||||||
|
ctx.log.log(
|
||||||
|
'info',
|
||||||
|
`rename-domain-source-manual-to-dcrouter: migrated ${result.modifiedCount} domain(s)`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.step('rename-record-source-manual-to-local')
|
||||||
|
.from('13.8.1').to('13.8.2')
|
||||||
|
.description('Rename DnsRecordDoc.source value from "manual" to "local"')
|
||||||
|
.up(async (ctx) => {
|
||||||
|
const collection = ctx.mongo!.collection('dnsrecorddoc');
|
||||||
|
const result = await collection.updateMany(
|
||||||
|
{ source: 'manual' },
|
||||||
|
{ $set: { source: 'local' } },
|
||||||
|
);
|
||||||
|
ctx.log.log(
|
||||||
|
'info',
|
||||||
|
`rename-record-source-manual-to-local: migrated ${result.modifiedCount} record(s)`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return migration;
|
return migration;
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.8.0',
|
version: '13.9.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1793,7 +1793,7 @@ export async function fetchProviderDomains(
|
|||||||
return await request.fire({ identity: context.identity, providerId });
|
return await request.fire({ identity: context.identity, providerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createManualDomainAction = domainsStatePart.createAction<{
|
export const createDcrouterDomainAction = domainsStatePart.createAction<{
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
|
}>(async (statePartArg, dataArg, actionContext): Promise<IDomainsState> => {
|
||||||
|
|||||||
@@ -44,12 +44,15 @@ export class DnsProviderForm extends DeesElement {
|
|||||||
accessor providerName: string = '';
|
accessor providerName: string = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently selected provider type. Initialized to the first descriptor;
|
* Currently selected provider type. Initialized to the first user-creatable
|
||||||
* caller can override before mounting (e.g. for edit dialogs).
|
* descriptor; caller can override before mounting (e.g. for edit dialogs).
|
||||||
|
* The built-in 'dcrouter' pseudo-provider is excluded from the picker —
|
||||||
|
* operators cannot create another one.
|
||||||
*/
|
*/
|
||||||
@state()
|
@state()
|
||||||
accessor selectedType: interfaces.data.TDnsProviderType =
|
accessor selectedType: interfaces.data.TDnsProviderType =
|
||||||
interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare';
|
interfaces.data.dnsProviderTypeDescriptors.find((d) => d.type !== 'dcrouter')?.type ??
|
||||||
|
'cloudflare';
|
||||||
|
|
||||||
/** When true, hide the type picker — used in edit dialogs. */
|
/** When true, hide the type picker — used in edit dialogs. */
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
@@ -102,7 +105,12 @@ export class DnsProviderForm extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const descriptors = interfaces.data.dnsProviderTypeDescriptors;
|
// Exclude the built-in 'dcrouter' pseudo-provider from the type picker —
|
||||||
|
// operators cannot create another one, it's surfaced at read time by the
|
||||||
|
// backend handler instead.
|
||||||
|
const descriptors = interfaces.data.dnsProviderTypeDescriptors.filter(
|
||||||
|
(d) => d.type !== 'dcrouter',
|
||||||
|
);
|
||||||
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
|
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class OpsViewDns extends DeesElement {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceBadge.manual {
|
.sourceBadge.local {
|
||||||
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
||||||
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export class OpsViewDns extends DeesElement {
|
|||||||
private domainHint(domainId: string): string {
|
private domainHint(domainId: string): string {
|
||||||
const domain = this.domainsState.domains.find((d) => d.id === domainId);
|
const domain = this.domainsState.domains.find((d) => d.id === domainId);
|
||||||
if (!domain) return '';
|
if (!domain) return '';
|
||||||
if (domain.source === 'manual') {
|
if (domain.source === 'dcrouter') {
|
||||||
return 'Records are served by dcrouter (authoritative).';
|
return 'Records are served by dcrouter (authoritative).';
|
||||||
}
|
}
|
||||||
return 'Records are stored at the provider — changes here are pushed via the provider API.';
|
return 'Records are stored at the provider — changes here are pushed via the provider API.';
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceBadge.manual {
|
.sourceBadge.dcrouter {
|
||||||
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
||||||
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
<div class="domainsContainer">
|
<div class="domainsContainer">
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Domains'}
|
.heading1=${'Domains'}
|
||||||
.heading2=${'Domains under management — manual (authoritative) or imported from a provider'}
|
.heading2=${'Domains under management — served by dcrouter (authoritative) or imported from a provider'}
|
||||||
.data=${domains}
|
.data=${domains}
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.displayFunction=${(d: interfaces.data.IDomain) => ({
|
.displayFunction=${(d: interfaces.data.IDomain) => ({
|
||||||
@@ -90,11 +90,11 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'Add Manual Domain',
|
name: 'Add DcRouter Domain',
|
||||||
iconName: 'lucide:plus',
|
iconName: 'lucide:plus',
|
||||||
type: ['header' as const],
|
type: ['header' as const],
|
||||||
actionFunc: async () => {
|
actionFunc: async () => {
|
||||||
await this.showCreateManualDialog();
|
await this.showCreateDcrouterDialog();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -168,17 +168,17 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
d: interfaces.data.IDomain,
|
d: interfaces.data.IDomain,
|
||||||
providersById: Map<string, interfaces.data.IDnsProviderPublic>,
|
providersById: Map<string, interfaces.data.IDnsProviderPublic>,
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
if (d.source === 'manual') {
|
if (d.source === 'dcrouter') {
|
||||||
return html`<span class="sourceBadge manual">Manual</span>`;
|
return html`<span class="sourceBadge dcrouter">DcRouter</span>`;
|
||||||
}
|
}
|
||||||
const provider = d.providerId ? providersById.get(d.providerId) : undefined;
|
const provider = d.providerId ? providersById.get(d.providerId) : undefined;
|
||||||
return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
|
return html`<span class="sourceBadge provider">${provider?.name || 'Provider'}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showCreateManualDialog() {
|
private async showCreateDcrouterDialog() {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
DeesModal.createAndShow({
|
DeesModal.createAndShow({
|
||||||
heading: 'Add Manual Domain',
|
heading: 'Add DcRouter Domain',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'FQDN (e.g. example.com)'} .required=${true}></dees-input-text>
|
||||||
@@ -199,7 +199,7 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
?.querySelector('dees-form');
|
?.querySelector('dees-form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
const data = await form.collectFormData();
|
const data = await form.collectFormData();
|
||||||
await appstate.domainsStatePart.dispatchAction(appstate.createManualDomainAction, {
|
await appstate.domainsStatePart.dispatchAction(appstate.createDcrouterDomainAction, {
|
||||||
name: String(data.name),
|
name: String(data.name),
|
||||||
description: data.description ? String(data.description) : undefined,
|
description: data.description ? String(data.description) : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
|
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
|
||||||
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
|
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusBadge.builtin {
|
||||||
|
background: ${cssManager.bdTheme('#e0e7ff', '#1e1b4b')};
|
||||||
|
color: ${cssManager.bdTheme('#3730a3', '#a5b4fc')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -82,15 +87,21 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
<div class="providersContainer">
|
<div class="providersContainer">
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Providers'}
|
.heading1=${'Providers'}
|
||||||
.heading2=${'External DNS provider accounts'}
|
.heading2=${'Built-in dcrouter + external DNS provider accounts'}
|
||||||
.data=${providers}
|
.data=${providers}
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
|
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
|
||||||
Name: p.name,
|
Name: p.name,
|
||||||
Type: this.providerTypeLabel(p.type),
|
Type: this.providerTypeLabel(p.type),
|
||||||
Status: this.renderStatusBadge(p.status),
|
Status: p.builtIn
|
||||||
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
|
? html`<span class="statusBadge builtin">built-in</span>`
|
||||||
Error: p.lastError || '-',
|
: this.renderStatusBadge(p.status),
|
||||||
|
'Last Tested': p.builtIn
|
||||||
|
? '—'
|
||||||
|
: p.lastTestedAt
|
||||||
|
? new Date(p.lastTestedAt).toLocaleString()
|
||||||
|
: 'never',
|
||||||
|
Error: p.builtIn ? '—' : p.lastError || '-',
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
@@ -116,6 +127,7 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
name: 'Test Connection',
|
name: 'Test Connection',
|
||||||
iconName: 'lucide:plug',
|
iconName: 'lucide:plug',
|
||||||
type: ['inRow', 'contextmenu'] as any,
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
|
||||||
actionFunc: async (actionData: any) => {
|
actionFunc: async (actionData: any) => {
|
||||||
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
||||||
await this.testProvider(provider);
|
await this.testProvider(provider);
|
||||||
@@ -125,6 +137,7 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
name: 'Edit',
|
name: 'Edit',
|
||||||
iconName: 'lucide:pencil',
|
iconName: 'lucide:pencil',
|
||||||
type: ['inRow', 'contextmenu'] as any,
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
|
||||||
actionFunc: async (actionData: any) => {
|
actionFunc: async (actionData: any) => {
|
||||||
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
||||||
await this.showEditDialog(provider);
|
await this.showEditDialog(provider);
|
||||||
@@ -134,6 +147,7 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
name: 'Delete',
|
name: 'Delete',
|
||||||
iconName: 'lucide:trash2',
|
iconName: 'lucide:trash2',
|
||||||
type: ['inRow', 'contextmenu'] as any,
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (p: interfaces.data.IDnsProviderPublic) => !p.builtIn,
|
||||||
actionFunc: async (actionData: any) => {
|
actionFunc: async (actionData: any) => {
|
||||||
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
const provider = actionData.item as interfaces.data.IDnsProviderPublic;
|
||||||
await this.deleteProvider(provider);
|
await this.deleteProvider(provider);
|
||||||
|
|||||||
Reference in New Issue
Block a user