feat(dns): Implement DNS management functionality
- Added DnsManager and DnsEntry classes to handle DNS entries. - Introduced new interfaces for DNS entry requests and data structures. - Updated Cloudly class to include DnsManager instance. - Enhanced app state to manage DNS entries and actions for creating, updating, and deleting DNS records. - Created UI components for DNS management, including forms for adding and editing DNS entries. - Updated overview and services views to reflect DNS entries. - Added validation and formatting methods for DNS entries.
This commit is contained in:
@@ -39,7 +39,7 @@
|
|||||||
"@apiclient.xyz/docker": "^1.3.5",
|
"@apiclient.xyz/docker": "^1.3.5",
|
||||||
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
||||||
"@apiclient.xyz/slack": "^3.0.9",
|
"@apiclient.xyz/slack": "^3.0.9",
|
||||||
"@design.estate/dees-catalog": "^1.11.2",
|
"@design.estate/dees-catalog": "^1.11.3",
|
||||||
"@design.estate/dees-domtools": "^2.3.3",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.1.2",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
|
98
pnpm-lock.yaml
generated
98
pnpm-lock.yaml
generated
@@ -33,8 +33,8 @@ importers:
|
|||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^1.11.2
|
specifier: ^1.11.3
|
||||||
version: 1.11.2(@tiptap/pm@2.26.1)
|
version: 1.11.3(@tiptap/pm@2.26.1)
|
||||||
'@design.estate/dees-domtools':
|
'@design.estate/dees-domtools':
|
||||||
specifier: ^2.3.3
|
specifier: ^2.3.3
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
@@ -476,8 +476,8 @@ packages:
|
|||||||
'@dabh/diagnostics@2.0.3':
|
'@dabh/diagnostics@2.0.3':
|
||||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.11.2':
|
'@design.estate/dees-catalog@1.11.3':
|
||||||
resolution: {integrity: sha512-gMK+wDKXDBPzfWmaJySotjjp5A9rwk2PQANQF8V6Q52xUfKKUv7gHj4eju+pN6qkUA5OUzdCDplUeUCrA8i37w==}
|
resolution: {integrity: sha512-gXGi6PlaHY4+lXHo17p+R/L6/QaqtN/3JFzTUXPl4J0fVKqVrEp22+lf7uvgAhs4WpV1Vd/c9yoyQ6JmrNSj4g==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.27':
|
'@design.estate/dees-comms@1.0.27':
|
||||||
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
||||||
@@ -1200,68 +1200,68 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.3.0':
|
'@mongodb-js/saslprep@1.3.0':
|
||||||
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.78':
|
'@napi-rs/canvas-android-arm64@0.1.79':
|
||||||
resolution: {integrity: sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==}
|
resolution: {integrity: sha512-ih6ZIztNDEXl7axvC4swOwLFrM9lOyJa9VAMq7xIBtEZhR/8IVDa0ZTup2fZEiTCmnjmXolzv7uDviHkOTEMKQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.78':
|
'@napi-rs/canvas-darwin-arm64@0.1.79':
|
||||||
resolution: {integrity: sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==}
|
resolution: {integrity: sha512-REMz1Fac2VlOYJDg+JjmQWSJc459cCgVom6GvKwWkDqzSjvG9BSo72MDmQY3uhb7r49Xuz5gTFcLYTfNcm4MoA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.78':
|
'@napi-rs/canvas-darwin-x64@0.1.79':
|
||||||
resolution: {integrity: sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==}
|
resolution: {integrity: sha512-uQxLg6Bll7zv/ljp/YIeiUFWfV9C/ESv+2ioUh60hIAypuhtg6hhtWE/KnoW7G48wQls5VUStvEnJbnJ7bPKlA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
|
||||||
resolution: {integrity: sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==}
|
resolution: {integrity: sha512-X37B//TVIipL/3RyvyfNlbQK2uyIaK3PJ2bH7ZeU+jpkaYprBsV15GCN/LHTYAi6R0F/c53zK3aSFNKkGHM/Og==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==}
|
resolution: {integrity: sha512-+T1fuau1heabE6zGXiqZBGPH5fTIQF+xEu/u4fuugxEiChRYlhnPjkw26MBi8ePg/jmzxLfJEij6LMJQ4AQa2A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.78':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
|
||||||
resolution: {integrity: sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==}
|
resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==}
|
resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==}
|
resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.78':
|
'@napi-rs/canvas-linux-x64-musl@0.1.79':
|
||||||
resolution: {integrity: sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==}
|
resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.78':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
|
||||||
resolution: {integrity: sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==}
|
resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.78':
|
'@napi-rs/canvas@0.1.79':
|
||||||
resolution: {integrity: sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==}
|
resolution: {integrity: sha512-0SkvRRjyxY35eniEsQsjPYUMWunKlAWvionJOzJJADZF5ZDf/sL+ncJbMTV5LUiHg1iHOvVjWcuDOx/GNXr/lA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.3':
|
'@napi-rs/wasm-runtime@1.0.3':
|
||||||
@@ -6684,7 +6684,7 @@ snapshots:
|
|||||||
enabled: 2.0.0
|
enabled: 2.0.0
|
||||||
kuler: 2.0.0
|
kuler: 2.0.0
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.11.2(@tiptap/pm@2.26.1)':
|
'@design.estate/dees-catalog@1.11.3(@tiptap/pm@2.26.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.3
|
'@design.estate/dees-domtools': 2.3.3
|
||||||
'@design.estate/dees-element': 2.1.2
|
'@design.estate/dees-element': 2.1.2
|
||||||
@@ -7578,48 +7578,48 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.78':
|
'@napi-rs/canvas-android-arm64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.78':
|
'@napi-rs/canvas-darwin-arm64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.78':
|
'@napi-rs/canvas-darwin-x64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.78':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.78':
|
'@napi-rs/canvas-linux-x64-musl@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.78':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.78':
|
'@napi-rs/canvas@0.1.79':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.78
|
'@napi-rs/canvas-android-arm64': 0.1.79
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.78
|
'@napi-rs/canvas-darwin-arm64': 0.1.79
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.78
|
'@napi-rs/canvas-darwin-x64': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.78
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.78
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.79
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.78
|
'@napi-rs/canvas-linux-x64-musl': 0.1.79
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.78
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.79
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.3':
|
'@napi-rs/wasm-runtime@1.0.3':
|
||||||
@@ -12345,7 +12345,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.78
|
'@napi-rs/canvas': 0.1.79
|
||||||
|
|
||||||
peek-readable@4.1.0: {}
|
peek-readable@4.1.0: {}
|
||||||
|
|
||||||
|
@@ -25,6 +25,8 @@ import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
|||||||
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
||||||
import { ServiceManager } from './manager.service/classes.servicemanager.js';
|
import { ServiceManager } from './manager.service/classes.servicemanager.js';
|
||||||
import { DeploymentManager } from './manager.deployment/classes.deploymentmanager.js';
|
import { DeploymentManager } from './manager.deployment/classes.deploymentmanager.js';
|
||||||
|
import { DnsManager } from './manager.dns/classes.dnsmanager.js';
|
||||||
|
import { DomainManager } from './manager.domain/classes.domainmanager.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
||||||
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
|
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
|
||||||
@@ -64,6 +66,8 @@ export class Cloudly {
|
|||||||
public imageManager: ImageManager;
|
public imageManager: ImageManager;
|
||||||
public serviceManager: ServiceManager;
|
public serviceManager: ServiceManager;
|
||||||
public deploymentManager: DeploymentManager;
|
public deploymentManager: DeploymentManager;
|
||||||
|
public dnsManager: DnsManager;
|
||||||
|
public domainManager: DomainManager;
|
||||||
public taskManager: CloudlyTaskmanager;
|
public taskManager: CloudlyTaskmanager;
|
||||||
public nodeManager: CloudlyNodeManager;
|
public nodeManager: CloudlyNodeManager;
|
||||||
public baremetalManager: CloudlyBaremetalManager;
|
public baremetalManager: CloudlyBaremetalManager;
|
||||||
@@ -95,6 +99,8 @@ export class Cloudly {
|
|||||||
this.imageManager = new ImageManager(this);
|
this.imageManager = new ImageManager(this);
|
||||||
this.serviceManager = new ServiceManager(this);
|
this.serviceManager = new ServiceManager(this);
|
||||||
this.deploymentManager = new DeploymentManager(this);
|
this.deploymentManager = new DeploymentManager(this);
|
||||||
|
this.dnsManager = new DnsManager(this);
|
||||||
|
this.domainManager = new DomainManager(this);
|
||||||
this.taskManager = new CloudlyTaskmanager(this);
|
this.taskManager = new CloudlyTaskmanager(this);
|
||||||
this.secretManager = new CloudlySecretManager(this);
|
this.secretManager = new CloudlySecretManager(this);
|
||||||
this.nodeManager = new CloudlyNodeManager(this);
|
this.nodeManager = new CloudlyNodeManager(this);
|
||||||
|
148
ts/manager.dns/classes.dnsentry.ts
Normal file
148
ts/manager.dns/classes.dnsentry.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DnsManager } from './classes.dnsmanager.js';
|
||||||
|
|
||||||
|
export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
DnsEntry,
|
||||||
|
plugins.servezoneInterfaces.data.IDnsEntry,
|
||||||
|
DnsManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getDnsEntryById(dnsEntryIdArg: string) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDnsEntries(filterArg?: { zone?: string }) {
|
||||||
|
const filter: any = {};
|
||||||
|
if (filterArg?.zone) {
|
||||||
|
filter['data.zone'] = filterArg.zone;
|
||||||
|
}
|
||||||
|
const dnsEntries = await this.getInstances(filter);
|
||||||
|
return dnsEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDnsZones() {
|
||||||
|
const dnsEntries = await this.getInstances({});
|
||||||
|
const zones = new Set<string>();
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
if (entry.data.zone) {
|
||||||
|
zones.add(entry.data.zone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(zones).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createDnsEntry(dnsEntryDataArg: plugins.servezoneInterfaces.data.IDnsEntry['data']) {
|
||||||
|
const dnsEntry = new DnsEntry();
|
||||||
|
dnsEntry.id = await DnsEntry.getNewId();
|
||||||
|
dnsEntry.data = {
|
||||||
|
...dnsEntryDataArg,
|
||||||
|
ttl: dnsEntryDataArg.ttl || 3600, // Default TTL: 1 hour
|
||||||
|
active: dnsEntryDataArg.active !== false, // Default to active
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
await dnsEntry.save();
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateDnsEntry(
|
||||||
|
dnsEntryIdArg: string,
|
||||||
|
dnsEntryDataArg: Partial<plugins.servezoneInterfaces.data.IDnsEntry['data']>
|
||||||
|
) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
|
||||||
|
}
|
||||||
|
Object.assign(dnsEntry.data, dnsEntryDataArg, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
await dnsEntry.save();
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteDnsEntry(dnsEntryIdArg: string) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
|
||||||
|
}
|
||||||
|
await dnsEntry.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the DNS entry data
|
||||||
|
*/
|
||||||
|
public validateData(): boolean {
|
||||||
|
const { type, name, value, zone } = this.data;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!type || !name || !value || !zone) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-specific validation
|
||||||
|
switch (type) {
|
||||||
|
case 'A':
|
||||||
|
// Validate IPv4 address
|
||||||
|
return /^(\d{1,3}\.){3}\d{1,3}$/.test(value);
|
||||||
|
case 'AAAA':
|
||||||
|
// Validate IPv6 address (simplified)
|
||||||
|
return /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/.test(value);
|
||||||
|
case 'MX':
|
||||||
|
// MX records must have priority
|
||||||
|
return this.data.priority !== undefined && this.data.priority >= 0;
|
||||||
|
case 'SRV':
|
||||||
|
// SRV records must have priority, weight, and port
|
||||||
|
return (
|
||||||
|
this.data.priority !== undefined &&
|
||||||
|
this.data.weight !== undefined &&
|
||||||
|
this.data.port !== undefined
|
||||||
|
);
|
||||||
|
case 'CNAME':
|
||||||
|
case 'NS':
|
||||||
|
case 'PTR':
|
||||||
|
// These should point to valid domain names
|
||||||
|
return /^[a-zA-Z0-9.-]+$/.test(value);
|
||||||
|
case 'TXT':
|
||||||
|
case 'CAA':
|
||||||
|
case 'SOA':
|
||||||
|
// These can contain any text
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a formatted string representation of the DNS entry
|
||||||
|
*/
|
||||||
|
public toFormattedString(): string {
|
||||||
|
const { type, name, value, ttl, priority } = this.data;
|
||||||
|
let result = `${name} ${ttl} IN ${type}`;
|
||||||
|
|
||||||
|
if (priority !== undefined) {
|
||||||
|
result += ` ${priority}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'SRV' && this.data.weight !== undefined && this.data.port !== undefined) {
|
||||||
|
result += ` ${this.data.weight} ${this.data.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += ` ${value}`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
152
ts/manager.dns/classes.dnsmanager.ts
Normal file
152
ts/manager.dns/classes.dnsmanager.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DnsEntry } from './classes.dnsentry.js';
|
||||||
|
|
||||||
|
export class DnsManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CDnsEntry = plugins.smartdata.setDefaultManagerForDoc(this, DnsEntry);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Get all DNS entries
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(
|
||||||
|
'getDnsEntries',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntries = await this.CDnsEntry.getDnsEntries(
|
||||||
|
reqArg.zone ? { zone: reqArg.zone } : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntries: await Promise.all(
|
||||||
|
dnsEntries.map((entry) => entry.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get DNS entry by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(
|
||||||
|
'getDnsEntryById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.getDnsEntryById(reqArg.dnsEntryId);
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${reqArg.dnsEntryId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(
|
||||||
|
'createDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.createDnsEntry(reqArg.dnsEntryData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(
|
||||||
|
'updateDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.updateDnsEntry(
|
||||||
|
reqArg.dnsEntryId,
|
||||||
|
reqArg.dnsEntryData
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(
|
||||||
|
'deleteDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.CDnsEntry.deleteDnsEntry(reqArg.dnsEntryId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get DNS zones
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(
|
||||||
|
'getDnsZones',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const zones = await this.CDnsEntry.getDnsZones();
|
||||||
|
|
||||||
|
return {
|
||||||
|
zones,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the DNS manager
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
console.log('DNS Manager initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the DNS manager
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
console.log('DNS Manager stopped');
|
||||||
|
}
|
||||||
|
}
|
203
ts/manager.domain/classes.domain.ts
Normal file
203
ts/manager.domain/classes.domain.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DomainManager } from './classes.domainmanager.js';
|
||||||
|
|
||||||
|
export class Domain extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Domain,
|
||||||
|
plugins.servezoneInterfaces.data.IDomain,
|
||||||
|
DomainManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getDomainById(domainIdArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDomainByName(domainNameArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
'data.name': domainNameArg,
|
||||||
|
});
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDomains() {
|
||||||
|
const domains = await this.getInstances({});
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createDomain(domainDataArg: plugins.servezoneInterfaces.data.IDomain['data']) {
|
||||||
|
const domain = new Domain();
|
||||||
|
domain.id = await Domain.getNewId();
|
||||||
|
domain.data = {
|
||||||
|
...domainDataArg,
|
||||||
|
status: domainDataArg.status || 'pending',
|
||||||
|
verificationStatus: domainDataArg.verificationStatus || 'pending',
|
||||||
|
nameservers: domainDataArg.nameservers || [],
|
||||||
|
autoRenew: domainDataArg.autoRenew !== false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
await domain.save();
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateDomain(
|
||||||
|
domainIdArg: string,
|
||||||
|
domainDataArg: Partial<plugins.servezoneInterfaces.data.IDomain['data']>
|
||||||
|
) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${domainIdArg} not found`);
|
||||||
|
}
|
||||||
|
Object.assign(domain.data, domainDataArg, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
await domain.save();
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteDomain(domainIdArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${domainIdArg} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are DNS entries for this domain
|
||||||
|
const dnsManager = domain.manager.cloudlyRef.dnsManager;
|
||||||
|
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
|
||||||
|
'data.zone': domain.data.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dnsEntries.length > 0) {
|
||||||
|
console.log(`Warning: Deleting domain ${domain.data.name} with ${dnsEntries.length} DNS entries`);
|
||||||
|
// Optionally delete associated DNS entries
|
||||||
|
for (const dnsEntry of dnsEntries) {
|
||||||
|
await dnsEntry.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await domain.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IDomain['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify domain ownership
|
||||||
|
*/
|
||||||
|
public async verifyDomain(methodArg?: 'dns' | 'http' | 'email' | 'manual') {
|
||||||
|
const method = methodArg || this.data.verificationMethod || 'dns';
|
||||||
|
|
||||||
|
// Generate verification token if not exists
|
||||||
|
if (!this.data.verificationToken) {
|
||||||
|
this.data.verificationToken = plugins.smartunique.shortId();
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
let verificationResult = {
|
||||||
|
success: false,
|
||||||
|
message: '',
|
||||||
|
details: {} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'dns':
|
||||||
|
// Check for TXT record with verification token
|
||||||
|
verificationResult = await this.verifyViaDns();
|
||||||
|
break;
|
||||||
|
case 'http':
|
||||||
|
// Check for file at well-known URL
|
||||||
|
verificationResult = await this.verifyViaHttp();
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
// Send verification email
|
||||||
|
verificationResult = await this.verifyViaEmail();
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
// Manual verification
|
||||||
|
verificationResult.success = true;
|
||||||
|
verificationResult.message = 'Manually verified';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update verification status
|
||||||
|
if (verificationResult.success) {
|
||||||
|
this.data.verificationStatus = 'verified';
|
||||||
|
this.data.lastVerificationAt = Date.now();
|
||||||
|
this.data.verificationMethod = method;
|
||||||
|
} else {
|
||||||
|
this.data.verificationStatus = 'failed';
|
||||||
|
this.data.lastVerificationAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
return verificationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaDns(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement DNS verification
|
||||||
|
// Look for TXT record _cloudly-verify.{domain} with value {verificationToken}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'DNS verification not yet implemented',
|
||||||
|
details: {
|
||||||
|
expectedRecord: `_cloudly-verify.${this.data.name}`,
|
||||||
|
expectedValue: this.data.verificationToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaHttp(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement HTTP verification
|
||||||
|
// Check for file at http://{domain}/.well-known/cloudly-verify.txt
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'HTTP verification not yet implemented',
|
||||||
|
details: {
|
||||||
|
expectedUrl: `http://${this.data.name}/.well-known/cloudly-verify.txt`,
|
||||||
|
expectedContent: this.data.verificationToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaEmail(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement email verification
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Email verification not yet implemented',
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if domain is expiring soon
|
||||||
|
*/
|
||||||
|
public isExpiringSoon(daysThreshold: number = 30): boolean {
|
||||||
|
if (!this.data.expiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const daysUntilExpiry = (this.data.expiresAt - Date.now()) / (1000 * 60 * 60 * 24);
|
||||||
|
return daysUntilExpiry <= daysThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all DNS entries for this domain
|
||||||
|
*/
|
||||||
|
public async getDnsEntries() {
|
||||||
|
const dnsManager = this.manager.cloudlyRef.dnsManager;
|
||||||
|
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
|
||||||
|
'data.zone': this.data.name,
|
||||||
|
});
|
||||||
|
return dnsEntries;
|
||||||
|
}
|
||||||
|
}
|
188
ts/manager.domain/classes.domainmanager.ts
Normal file
188
ts/manager.domain/classes.domainmanager.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Domain } from './classes.domain.js';
|
||||||
|
|
||||||
|
export class DomainManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CDomain = plugins.smartdata.setDefaultManagerForDoc(this, Domain);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Get all domains
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(
|
||||||
|
'getDomains',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domains = await this.CDomain.getDomains();
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains: await Promise.all(
|
||||||
|
domains.map((domain) => domain.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get domain by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(
|
||||||
|
'getDomainById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.getDomainById(reqArg.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.domainId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(
|
||||||
|
'createDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if domain already exists
|
||||||
|
const existingDomain = await this.CDomain.getDomainByName(reqArg.domainData.name);
|
||||||
|
if (existingDomain) {
|
||||||
|
throw new Error(`Domain ${reqArg.domainData.name} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = await this.CDomain.createDomain(reqArg.domainData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(
|
||||||
|
'updateDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.updateDomain(
|
||||||
|
reqArg.domainId,
|
||||||
|
reqArg.domainData
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(
|
||||||
|
'deleteDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.CDomain.deleteDomain(reqArg.domainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(
|
||||||
|
'verifyDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.getDomainById(reqArg.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.domainId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationResult = await domain.verifyDomain(reqArg.verificationMethod);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
verificationResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the domain manager
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
console.log('Domain Manager initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the domain manager
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
console.log('Domain Manager stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active domains
|
||||||
|
*/
|
||||||
|
public async getActiveDomains() {
|
||||||
|
const domains = await this.CDomain.getInstances({
|
||||||
|
'data.status': 'active',
|
||||||
|
});
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get domains that are expiring soon
|
||||||
|
*/
|
||||||
|
public async getExpiringDomains(daysThreshold: number = 30) {
|
||||||
|
const domains = await this.CDomain.getDomains();
|
||||||
|
return domains.filter(domain => domain.isExpiringSoon(daysThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a domain name is available (not in our system)
|
||||||
|
*/
|
||||||
|
public async isDomainAvailable(domainName: string): Promise<boolean> {
|
||||||
|
const existingDomain = await this.CDomain.getDomainByName(domainName);
|
||||||
|
return !existingDomain;
|
||||||
|
}
|
||||||
|
}
|
81
ts_interfaces/data/dns.ts
Normal file
81
ts_interfaces/data/dns.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
export type TDnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA' | 'SRV' | 'CAA' | 'PTR';
|
||||||
|
|
||||||
|
export interface IDnsEntry {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* The DNS record type
|
||||||
|
*/
|
||||||
|
type: TDnsRecordType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DNS record name (e.g., www, @, mail)
|
||||||
|
* @ represents the root domain
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the DNS record
|
||||||
|
* - For A/AAAA: IP address
|
||||||
|
* - For CNAME: Target domain
|
||||||
|
* - For MX: Mail server hostname
|
||||||
|
* - For TXT: Text value
|
||||||
|
* - For NS: Nameserver hostname
|
||||||
|
* - For SRV: Target hostname
|
||||||
|
* - For CAA: CAA record value
|
||||||
|
* - For PTR: Domain name
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to live in seconds
|
||||||
|
* Default: 3600 (1 hour)
|
||||||
|
*/
|
||||||
|
ttl: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority (used for MX and SRV records)
|
||||||
|
* Lower values have higher priority
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DNS zone this entry belongs to
|
||||||
|
* e.g., example.com
|
||||||
|
* @deprecated Use domainId instead
|
||||||
|
*/
|
||||||
|
zone: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The domain ID this DNS entry belongs to
|
||||||
|
* Links to the Domain entity
|
||||||
|
*/
|
||||||
|
domainId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional fields for SRV records
|
||||||
|
*/
|
||||||
|
weight?: number;
|
||||||
|
port?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this DNS entry is active
|
||||||
|
*/
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional description for documentation
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the entry was created
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the entry was last updated
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
}
|
110
ts_interfaces/data/domain.ts
Normal file
110
ts_interfaces/data/domain.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
export type TDomainStatus = 'active' | 'pending' | 'expired' | 'suspended' | 'transferred';
|
||||||
|
export type TDomainVerificationStatus = 'verified' | 'pending' | 'failed' | 'not_required';
|
||||||
|
|
||||||
|
export interface IDomain {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* The domain name (e.g., example.com)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description or notes about the domain
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the domain
|
||||||
|
*/
|
||||||
|
status: TDomainStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain verification status
|
||||||
|
*/
|
||||||
|
verificationStatus: TDomainVerificationStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nameservers for the domain
|
||||||
|
*/
|
||||||
|
nameservers: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain registrar information
|
||||||
|
*/
|
||||||
|
registrar?: {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain registration date (timestamp)
|
||||||
|
*/
|
||||||
|
registeredAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain expiration date (timestamp)
|
||||||
|
*/
|
||||||
|
expiresAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether auto-renewal is enabled
|
||||||
|
*/
|
||||||
|
autoRenew: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DNSSEC enabled
|
||||||
|
*/
|
||||||
|
dnssecEnabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags for categorization
|
||||||
|
*/
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this domain is primary for the organization
|
||||||
|
*/
|
||||||
|
isPrimary?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSL certificate status
|
||||||
|
*/
|
||||||
|
sslStatus?: 'active' | 'pending' | 'expired' | 'none';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last verification attempt timestamp
|
||||||
|
*/
|
||||||
|
lastVerificationAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verification method used
|
||||||
|
*/
|
||||||
|
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verification token (for DNS/HTTP verification)
|
||||||
|
*/
|
||||||
|
verificationToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloudflare zone ID if managed by Cloudflare
|
||||||
|
*/
|
||||||
|
cloudflareZoneId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether domain is managed externally
|
||||||
|
*/
|
||||||
|
isExternal?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creation timestamp
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
}
|
@@ -2,7 +2,9 @@ export * from './cloudlyconfig.js';
|
|||||||
export * from './cluster.js';
|
export * from './cluster.js';
|
||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
export * from './deployment.js';
|
export * from './deployment.js';
|
||||||
|
export * from './dns.js';
|
||||||
export * from './docker.js';
|
export * from './docker.js';
|
||||||
|
export * from './domain.js';
|
||||||
export * from './event.js';
|
export * from './event.js';
|
||||||
export * from './externalregistry.js';
|
export * from './externalregistry.js';
|
||||||
export * from './image.js';
|
export * from './image.js';
|
||||||
|
93
ts_interfaces/requests/dns.ts
Normal file
93
ts_interfaces/requests/dns.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDnsEntry } from '../data/dns.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsEntries
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsEntries
|
||||||
|
> {
|
||||||
|
method: 'getDnsEntries';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
zone?: string; // Optional filter by zone
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntries: IDnsEntry[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsEntryById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsEntryById
|
||||||
|
> {
|
||||||
|
method: 'getDnsEntryById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_CreateDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_CreateDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'createDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryData: IDnsEntry['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_UpdateDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_UpdateDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'updateDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
dnsEntryData: IDnsEntry['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_DeleteDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_DeleteDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'deleteDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsZones
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsZones
|
||||||
|
> {
|
||||||
|
method: 'getDnsZones';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
zones: string[];
|
||||||
|
};
|
||||||
|
}
|
99
ts_interfaces/requests/domain.ts
Normal file
99
ts_interfaces/requests/domain.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDomain } from '../data/domain.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDomains
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDomains
|
||||||
|
> {
|
||||||
|
method: 'getDomains';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domains: IDomain[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDomainById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDomainById
|
||||||
|
> {
|
||||||
|
method: 'getDomainById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_CreateDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_CreateDomain
|
||||||
|
> {
|
||||||
|
method: 'createDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainData: IDomain['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_UpdateDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_UpdateDomain
|
||||||
|
> {
|
||||||
|
method: 'updateDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
domainData: IDomain['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_DeleteDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_DeleteDomain
|
||||||
|
> {
|
||||||
|
method: 'deleteDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_VerifyDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_VerifyDomain
|
||||||
|
> {
|
||||||
|
method: 'verifyDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
verificationResult: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@@ -6,6 +6,8 @@ import * as certificateRequests from './certificate.js';
|
|||||||
import * as clusterRequests from './cluster.js';
|
import * as clusterRequests from './cluster.js';
|
||||||
import * as configRequests from './config.js';
|
import * as configRequests from './config.js';
|
||||||
import * as deploymentRequests from './deployment.js';
|
import * as deploymentRequests from './deployment.js';
|
||||||
|
import * as dnsRequests from './dns.js';
|
||||||
|
import * as domainRequests from './domain.js';
|
||||||
import * as externalRegistryRequests from './externalregistry.js';
|
import * as externalRegistryRequests from './externalregistry.js';
|
||||||
import * as identityRequests from './identity.js';
|
import * as identityRequests from './identity.js';
|
||||||
import * as imageRequests from './image.js';
|
import * as imageRequests from './image.js';
|
||||||
@@ -29,6 +31,8 @@ export {
|
|||||||
clusterRequests as cluster,
|
clusterRequests as cluster,
|
||||||
configRequests as config,
|
configRequests as config,
|
||||||
deploymentRequests as deployment,
|
deploymentRequests as deployment,
|
||||||
|
dnsRequests as dns,
|
||||||
|
domainRequests as domain,
|
||||||
externalRegistryRequests as externalRegistry,
|
externalRegistryRequests as externalRegistry,
|
||||||
identityRequests as identity,
|
identityRequests as identity,
|
||||||
imageRequests as image,
|
imageRequests as image,
|
||||||
|
@@ -50,7 +50,8 @@ export interface IDataState {
|
|||||||
images?: any[];
|
images?: any[];
|
||||||
services?: plugins.interfaces.data.IService[];
|
services?: plugins.interfaces.data.IService[];
|
||||||
deployments?: plugins.interfaces.data.IDeployment[];
|
deployments?: plugins.interfaces.data.IDeployment[];
|
||||||
dns?: any[];
|
domains?: plugins.interfaces.data.IDomain[];
|
||||||
|
dnsEntries?: plugins.interfaces.data.IDnsEntry[];
|
||||||
mails?: any[];
|
mails?: any[];
|
||||||
logs?: any[];
|
logs?: any[];
|
||||||
s3?: any[];
|
s3?: any[];
|
||||||
@@ -66,7 +67,8 @@ export const dataState = await appstate.getStatePart<IDataState>(
|
|||||||
images: [],
|
images: [],
|
||||||
services: [],
|
services: [],
|
||||||
deployments: [],
|
deployments: [],
|
||||||
dns: [],
|
domains: [],
|
||||||
|
dnsEntries: [],
|
||||||
mails: [],
|
mails: [],
|
||||||
logs: [],
|
logs: [],
|
||||||
s3: [],
|
s3: [],
|
||||||
@@ -180,6 +182,50 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Domains
|
||||||
|
const trGetDomains =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(
|
||||||
|
'/typedrequest',
|
||||||
|
'getDomains'
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const responseDomains = await trGetDomains.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
});
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
domains: responseDomains?.domains || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch domains:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
domains: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS Entries
|
||||||
|
const trGetDnsEntries =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(
|
||||||
|
'/typedrequest',
|
||||||
|
'getDnsEntries'
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const responseDnsEntries = await trGetDnsEntries.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
});
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
dnsEntries: responseDnsEntries?.dnsEntries || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch DNS entries:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
dnsEntries: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return currentState;
|
return currentState;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -389,6 +435,130 @@ export const deleteDeploymentAction = dataState.createAction(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// DNS Actions
|
||||||
|
export const createDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trCreateDnsEntry =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(
|
||||||
|
'/typedrequest',
|
||||||
|
'createDnsEntry'
|
||||||
|
);
|
||||||
|
const response = await trCreateDnsEntry.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
dnsEntryData: payloadArg.dnsEntryData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trUpdateDnsEntry =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(
|
||||||
|
'/typedrequest',
|
||||||
|
'updateDnsEntry'
|
||||||
|
);
|
||||||
|
const response = await trUpdateDnsEntry.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
dnsEntryId: payloadArg.dnsEntryId,
|
||||||
|
dnsEntryData: payloadArg.dnsEntryData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trDeleteDnsEntry =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(
|
||||||
|
'/typedrequest',
|
||||||
|
'deleteDnsEntry'
|
||||||
|
);
|
||||||
|
const response = await trDeleteDnsEntry.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
dnsEntryId: payloadArg.dnsEntryId,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Domain Actions
|
||||||
|
export const createDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trCreateDomain =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(
|
||||||
|
'/typedrequest',
|
||||||
|
'createDomain'
|
||||||
|
);
|
||||||
|
const response = await trCreateDomain.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
domainData: payloadArg.domainData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trUpdateDomain =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(
|
||||||
|
'/typedrequest',
|
||||||
|
'updateDomain'
|
||||||
|
);
|
||||||
|
const response = await trUpdateDomain.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
domainId: payloadArg.domainId,
|
||||||
|
domainData: payloadArg.domainData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trDeleteDomain =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(
|
||||||
|
'/typedrequest',
|
||||||
|
'deleteDomain'
|
||||||
|
);
|
||||||
|
const response = await trDeleteDomain.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
domainId: payloadArg.domainId,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const verifyDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trVerifyDomain =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(
|
||||||
|
'/typedrequest',
|
||||||
|
'verifyDomain'
|
||||||
|
);
|
||||||
|
const response = await trVerifyDomain.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
domainId: payloadArg.domainId,
|
||||||
|
verificationMethod: payloadArg.verificationMethod,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// cluster
|
// cluster
|
||||||
export const addClusterAction = dataState.createAction(
|
export const addClusterAction = dataState.createAction(
|
||||||
async (
|
async (
|
||||||
|
@@ -16,6 +16,7 @@ import { CloudlyViewClusters } from './cloudly-view-clusters.js';
|
|||||||
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
|
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
|
||||||
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
|
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
|
||||||
import { CloudlyViewDns } from './cloudly-view-dns.js';
|
import { CloudlyViewDns } from './cloudly-view-dns.js';
|
||||||
|
import { CloudlyViewDomains } from './cloudly-view-domains.js';
|
||||||
import { CloudlyViewImages } from './cloudly-view-images.js';
|
import { CloudlyViewImages } from './cloudly-view-images.js';
|
||||||
import { CloudlyViewLogs } from './cloudly-view-logs.js';
|
import { CloudlyViewLogs } from './cloudly-view-logs.js';
|
||||||
import { CloudlyViewMails } from './cloudly-view-mails.js';
|
import { CloudlyViewMails } from './cloudly-view-mails.js';
|
||||||
@@ -125,6 +126,11 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
iconName: 'lucide:Rocket',
|
iconName: 'lucide:Rocket',
|
||||||
element: CloudlyViewDeployments,
|
element: CloudlyViewDeployments,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Domains',
|
||||||
|
iconName: 'lucide:Globe2',
|
||||||
|
element: CloudlyViewDomains,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'DNS',
|
name: 'DNS',
|
||||||
iconName: 'lucide:Globe',
|
iconName: 'lucide:Globe',
|
||||||
|
@@ -18,65 +18,191 @@ export class CloudlyViewDns extends DeesElement {
|
|||||||
private data: appstate.IDataState = {
|
private data: appstate.IDataState = {
|
||||||
secretGroups: [],
|
secretGroups: [],
|
||||||
secretBundles: [],
|
secretBundles: [],
|
||||||
|
dnsEntries: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subecription = appstate.dataState
|
const subscription = appstate.dataState
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
.subscribe((dataArg) => {
|
.subscribe((dataArg) => {
|
||||||
this.data = dataArg;
|
this.data = dataArg;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(subecription);
|
this.rxSubscriptions.push(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
|
.dns-type-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.type-A { background: #4CAF50; }
|
||||||
|
.type-AAAA { background: #45a049; }
|
||||||
|
.type-CNAME { background: #2196F3; }
|
||||||
|
.type-MX { background: #FF9800; }
|
||||||
|
.type-TXT { background: #9C27B0; }
|
||||||
|
.type-NS { background: #795548; }
|
||||||
|
.type-SOA { background: #607D8B; }
|
||||||
|
.type-SRV { background: #E91E63; }
|
||||||
|
.type-CAA { background: #00BCD4; }
|
||||||
|
.type-PTR { background: #673AB7; }
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
.status-inactive {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private getRecordTypeBadge(type: string) {
|
||||||
|
return html`<span class="dns-type-badge type-${type}">${type}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusBadge(active: boolean) {
|
||||||
|
return html`<span class="${active ? 'status-active' : 'status-inactive'}">
|
||||||
|
${active ? '✓ Active' : '✗ Inactive'}
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<cloudly-sectionheading>DNS</cloudly-sectionheading>
|
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'DNS'}
|
.heading1=${'DNS Entries'}
|
||||||
.heading2=${'decoded in client'}
|
.heading2=${'Manage DNS records for your domains'}
|
||||||
.data=${this.data.deployments}
|
.data=${this.data.dnsEntries || []}
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
|
||||||
return {
|
return {
|
||||||
id: itemArg.id,
|
Type: this.getRecordTypeBadge(itemArg.data.type),
|
||||||
serverAmount: itemArg.data.servers.length,
|
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
|
||||||
|
Value: itemArg.data.value,
|
||||||
|
TTL: `${itemArg.data.ttl}s`,
|
||||||
|
Priority: itemArg.data.priority || '-',
|
||||||
|
Zone: itemArg.data.zone,
|
||||||
|
Status: this.getStatusBadge(itemArg.data.active),
|
||||||
|
Description: itemArg.data.description || '-',
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'add configBundle',
|
name: 'Add DNS Entry',
|
||||||
iconName: 'plus',
|
iconName: 'plus',
|
||||||
type: ['header', 'footer'],
|
type: ['header', 'footer'],
|
||||||
actionFunc: async (dataActionArg) => {
|
actionFunc: async (dataActionArg) => {
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: 'Add ConfigBundle',
|
heading: 'Add DNS Entry',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Record Type'}
|
||||||
|
.options=${[
|
||||||
|
{key: 'A', option: 'A - IPv4 Address'},
|
||||||
|
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||||
|
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||||
|
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||||
|
{key: 'TXT', option: 'TXT - Text Record'},
|
||||||
|
{key: 'NS', option: 'NS - Name Server'},
|
||||||
|
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||||
|
{key: 'SRV', option: 'SRV - Service'},
|
||||||
|
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||||
|
{key: 'PTR', option: 'PTR - Pointer'},
|
||||||
|
]}
|
||||||
|
.value=${'A'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.key=${'data.secretGroupIds'}
|
.key=${'zone'}
|
||||||
.label=${'secretGroupIds'}
|
.label=${'Zone (Domain)'}
|
||||||
.value=${''}
|
.placeholder=${'example.com'}
|
||||||
></dees-input-text>
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.key=${'data.includedTags'}
|
.key=${'name'}
|
||||||
.label=${'includedTags'}
|
.label=${'Name'}
|
||||||
.value=${''}
|
.placeholder=${'@ for root, www, mail, etc.'}
|
||||||
></dees-input-text>
|
.value=${'@'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'value'}
|
||||||
|
.label=${'Value'}
|
||||||
|
.placeholder=${'IP address, domain, or text value'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'ttl'}
|
||||||
|
.label=${'TTL (seconds)'}
|
||||||
|
.value=${'3600'}
|
||||||
|
.type=${'number'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'priority'}
|
||||||
|
.label=${'Priority (MX/SRV only)'}
|
||||||
|
.type=${'number'}
|
||||||
|
.placeholder=${'10'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'weight'}
|
||||||
|
.label=${'Weight (SRV only)'}
|
||||||
|
.type=${'number'}
|
||||||
|
.placeholder=${'0'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'port'}
|
||||||
|
.label=${'Port (SRV only)'}
|
||||||
|
.type=${'number'}
|
||||||
|
.placeholder=${'443'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'active'}
|
||||||
|
.label=${'Active'}
|
||||||
|
.value=${true}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'description'}
|
||||||
|
.label=${'Description (optional)'}
|
||||||
|
.placeholder=${'What is this record for?'}>
|
||||||
|
</dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Create DNS Entry',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||||
|
dnsEntryData: {
|
||||||
|
type: formData.type,
|
||||||
|
zone: formData.zone,
|
||||||
|
name: formData.name || '@',
|
||||||
|
value: formData.value,
|
||||||
|
ttl: parseInt(formData.ttl) || 3600,
|
||||||
|
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||||
|
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||||
|
port: formData.port ? parseInt(formData.port) : undefined,
|
||||||
|
active: formData.active,
|
||||||
|
description: formData.description || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
modalArg.destroy();
|
modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -86,34 +212,192 @@ export class CloudlyViewDns extends DeesElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Edit',
|
||||||
|
iconName: 'edit',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit DNS Entry`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Record Type'}
|
||||||
|
.options=${[
|
||||||
|
{key: 'A', option: 'A - IPv4 Address'},
|
||||||
|
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||||
|
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||||
|
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||||
|
{key: 'TXT', option: 'TXT - Text Record'},
|
||||||
|
{key: 'NS', option: 'NS - Name Server'},
|
||||||
|
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||||
|
{key: 'SRV', option: 'SRV - Service'},
|
||||||
|
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||||
|
{key: 'PTR', option: 'PTR - Pointer'},
|
||||||
|
]}
|
||||||
|
.value=${dnsEntry.data.type}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'zone'}
|
||||||
|
.label=${'Zone (Domain)'}
|
||||||
|
.value=${dnsEntry.data.zone}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'name'}
|
||||||
|
.label=${'Name'}
|
||||||
|
.value=${dnsEntry.data.name}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'value'}
|
||||||
|
.label=${'Value'}
|
||||||
|
.value=${dnsEntry.data.value}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'ttl'}
|
||||||
|
.label=${'TTL (seconds)'}
|
||||||
|
.value=${dnsEntry.data.ttl}
|
||||||
|
.type=${'number'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'priority'}
|
||||||
|
.label=${'Priority (MX/SRV only)'}
|
||||||
|
.value=${dnsEntry.data.priority || ''}
|
||||||
|
.type=${'number'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'weight'}
|
||||||
|
.label=${'Weight (SRV only)'}
|
||||||
|
.value=${dnsEntry.data.weight || ''}
|
||||||
|
.type=${'number'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'port'}
|
||||||
|
.label=${'Port (SRV only)'}
|
||||||
|
.value=${dnsEntry.data.port || ''}
|
||||||
|
.type=${'number'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'active'}
|
||||||
|
.label=${'Active'}
|
||||||
|
.value=${dnsEntry.data.active}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'description'}
|
||||||
|
.label=${'Description (optional)'}
|
||||||
|
.value=${dnsEntry.data.description || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Update DNS Entry',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||||
|
dnsEntryId: dnsEntry.id,
|
||||||
|
dnsEntryData: {
|
||||||
|
...dnsEntry.data,
|
||||||
|
type: formData.type,
|
||||||
|
zone: formData.zone,
|
||||||
|
name: formData.name || '@',
|
||||||
|
value: formData.value,
|
||||||
|
ttl: parseInt(formData.ttl) || 3600,
|
||||||
|
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||||
|
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||||
|
port: formData.port ? parseInt(formData.port) : undefined,
|
||||||
|
active: formData.active,
|
||||||
|
description: formData.description || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Duplicate',
|
||||||
|
iconName: 'copy',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||||
|
dnsEntryData: {
|
||||||
|
...dnsEntry.data,
|
||||||
|
description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Toggle Active',
|
||||||
|
iconName: 'power',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||||
|
dnsEntryId: dnsEntry.id,
|
||||||
|
dnsEntryData: {
|
||||||
|
...dnsEntry.data,
|
||||||
|
active: !dnsEntry.data.active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
iconName: 'trash',
|
iconName: 'trash',
|
||||||
type: ['contextmenu', 'inRow'],
|
type: ['contextmenu', 'inRow'],
|
||||||
actionFunc: async (actionDataArg) => {
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
heading: `Delete DNS Entry`,
|
||||||
content: html`
|
content: html`
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
Do you really want to delete the ConfigBundle?
|
Are you sure you want to delete this DNS entry?
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
<div style="color: #fff; font-weight: bold;">
|
||||||
>
|
${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}
|
||||||
${actionDataArg.item.id}
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
|
||||||
|
${dnsEntry.data.value}
|
||||||
|
</div>
|
||||||
|
${dnsEntry.data.description ? html`
|
||||||
|
<div style="color: #888; font-size: 0.85em; margin-top: 8px;">
|
||||||
|
${dnsEntry.data.description}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Delete',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, {
|
||||||
configBundleId: actionDataArg.item.id,
|
dnsEntryId: dnsEntry.id,
|
||||||
});
|
});
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
|
529
ts_web/elements/cloudly-view-domains.ts
Normal file
529
ts_web/elements/cloudly-view-domains.ts
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as shared from '../elements/shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-domains')
|
||||||
|
export class CloudlyViewDomains extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {
|
||||||
|
secretGroups: [],
|
||||||
|
secretBundles: [],
|
||||||
|
domains: [],
|
||||||
|
dnsEntries: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-active { background: #4CAF50; }
|
||||||
|
.status-pending { background: #FF9800; }
|
||||||
|
.status-expired { background: #f44336; }
|
||||||
|
.status-suspended { background: #9E9E9E; }
|
||||||
|
.status-transferred { background: #607D8B; }
|
||||||
|
|
||||||
|
.verification-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.verification-verified { background: #4CAF50; color: white; }
|
||||||
|
.verification-pending { background: #FF9800; color: white; }
|
||||||
|
.verification-failed { background: #f44336; color: white; }
|
||||||
|
.verification-not_required { background: #E0E0E0; color: #333; }
|
||||||
|
|
||||||
|
.ssl-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.ssl-active { color: #4CAF50; }
|
||||||
|
.ssl-pending { color: #FF9800; }
|
||||||
|
.ssl-expired { color: #f44336; }
|
||||||
|
.ssl-none { color: #9E9E9E; }
|
||||||
|
|
||||||
|
.nameserver-list {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiry-warning {
|
||||||
|
color: #FF9800;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expiry-critical {
|
||||||
|
color: #f44336;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getStatusBadge(status: string) {
|
||||||
|
return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVerificationBadge(status: string) {
|
||||||
|
const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase();
|
||||||
|
return html`<span class="verification-badge verification-${status}">${displayText}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSslBadge(sslStatus?: string) {
|
||||||
|
if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`;
|
||||||
|
const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓';
|
||||||
|
return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(timestamp?: number) {
|
||||||
|
if (!timestamp) return '—';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDaysUntilExpiry(expiresAt?: number) {
|
||||||
|
if (!expiresAt) return null;
|
||||||
|
const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExpiryDisplay(expiresAt?: number) {
|
||||||
|
const days = this.getDaysUntilExpiry(expiresAt);
|
||||||
|
if (days === null) return '—';
|
||||||
|
|
||||||
|
if (days < 0) {
|
||||||
|
return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`;
|
||||||
|
} else if (days <= 30) {
|
||||||
|
return html`<span class="expiry-warning">Expires in ${days} days</span>`;
|
||||||
|
} else {
|
||||||
|
return `${days} days`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Domains'}
|
||||||
|
.heading2=${'Manage your domains and DNS zones'}
|
||||||
|
.data=${this.data.domains || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
|
||||||
|
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
|
||||||
|
return {
|
||||||
|
Domain: html`
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 500;">${itemArg.data.name}</div>
|
||||||
|
${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
Status: this.getStatusBadge(itemArg.data.status),
|
||||||
|
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
|
||||||
|
SSL: this.getSslBadge(itemArg.data.sslStatus),
|
||||||
|
'DNS Records': dnsCount,
|
||||||
|
Registrar: itemArg.data.registrar?.name || '—',
|
||||||
|
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
|
||||||
|
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
|
||||||
|
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Add Domain',
|
||||||
|
iconName: 'plus',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
actionFunc: async (dataActionArg) => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add Domain',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'name'}
|
||||||
|
.label=${'Domain Name'}
|
||||||
|
.placeholder=${'example.com'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'description'}
|
||||||
|
.label=${'Description'}
|
||||||
|
.placeholder=${'Main company domain'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'status'}
|
||||||
|
.label=${'Status'}
|
||||||
|
.options=${[
|
||||||
|
{key: 'active', option: 'Active'},
|
||||||
|
{key: 'pending', option: 'Pending'},
|
||||||
|
{key: 'expired', option: 'Expired'},
|
||||||
|
{key: 'suspended', option: 'Suspended'},
|
||||||
|
]}
|
||||||
|
.value=${'pending'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'nameservers'}
|
||||||
|
.label=${'Nameservers (comma separated)'}
|
||||||
|
.placeholder=${'ns1.example.com, ns2.example.com'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'registrarName'}
|
||||||
|
.label=${'Registrar Name'}
|
||||||
|
.placeholder=${'GoDaddy, Namecheap, etc.'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'registrarUrl'}
|
||||||
|
.label=${'Registrar URL'}
|
||||||
|
.placeholder=${'https://registrar.com'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'expiresAt'}
|
||||||
|
.label=${'Expiration Date'}
|
||||||
|
.type=${'date'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'autoRenew'}
|
||||||
|
.label=${'Auto-Renew Enabled'}
|
||||||
|
.value=${true}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'dnssecEnabled'}
|
||||||
|
.label=${'DNSSEC Enabled'}
|
||||||
|
.value=${false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'isPrimary'}
|
||||||
|
.label=${'Primary Domain'}
|
||||||
|
.value=${false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'tags'}
|
||||||
|
.label=${'Tags (comma separated)'}
|
||||||
|
.placeholder=${'production, critical'}>
|
||||||
|
</dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Create Domain',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
const nameservers = formData.nameservers
|
||||||
|
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags = formData.tags
|
||||||
|
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDomainAction, {
|
||||||
|
domainData: {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description || undefined,
|
||||||
|
status: formData.status,
|
||||||
|
verificationStatus: 'pending',
|
||||||
|
nameservers,
|
||||||
|
registrar: formData.registrarName ? {
|
||||||
|
name: formData.registrarName,
|
||||||
|
url: formData.registrarUrl || undefined,
|
||||||
|
} : undefined,
|
||||||
|
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
|
||||||
|
autoRenew: formData.autoRenew,
|
||||||
|
dnssecEnabled: formData.dnssecEnabled,
|
||||||
|
isPrimary: formData.isPrimary,
|
||||||
|
tags: tags.length > 0 ? tags : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
iconName: 'edit',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit Domain: ${domain.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'name'}
|
||||||
|
.label=${'Domain Name'}
|
||||||
|
.value=${domain.data.name}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'description'}
|
||||||
|
.label=${'Description'}
|
||||||
|
.value=${domain.data.description || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'status'}
|
||||||
|
.label=${'Status'}
|
||||||
|
.options=${[
|
||||||
|
{key: 'active', option: 'Active'},
|
||||||
|
{key: 'pending', option: 'Pending'},
|
||||||
|
{key: 'expired', option: 'Expired'},
|
||||||
|
{key: 'suspended', option: 'Suspended'},
|
||||||
|
{key: 'transferred', option: 'Transferred'},
|
||||||
|
]}
|
||||||
|
.value=${domain.data.status}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'nameservers'}
|
||||||
|
.label=${'Nameservers (comma separated)'}
|
||||||
|
.value=${domain.data.nameservers?.join(', ') || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'registrarName'}
|
||||||
|
.label=${'Registrar Name'}
|
||||||
|
.value=${domain.data.registrar?.name || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'registrarUrl'}
|
||||||
|
.label=${'Registrar URL'}
|
||||||
|
.value=${domain.data.registrar?.url || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'expiresAt'}
|
||||||
|
.label=${'Expiration Date'}
|
||||||
|
.type=${'date'}
|
||||||
|
.value=${domain.data.expiresAt ? new Date(domain.data.expiresAt).toISOString().split('T')[0] : ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'autoRenew'}
|
||||||
|
.label=${'Auto-Renew Enabled'}
|
||||||
|
.value=${domain.data.autoRenew}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'dnssecEnabled'}
|
||||||
|
.label=${'DNSSEC Enabled'}
|
||||||
|
.value=${domain.data.dnssecEnabled || false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'isPrimary'}
|
||||||
|
.label=${'Primary Domain'}
|
||||||
|
.value=${domain.data.isPrimary || false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'tags'}
|
||||||
|
.label=${'Tags (comma separated)'}
|
||||||
|
.value=${domain.data.tags?.join(', ') || ''}>
|
||||||
|
</dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Update Domain',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
const nameservers = formData.nameservers
|
||||||
|
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags = formData.tags
|
||||||
|
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDomainAction, {
|
||||||
|
domainId: domain.id,
|
||||||
|
domainData: {
|
||||||
|
...domain.data,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description || undefined,
|
||||||
|
status: formData.status,
|
||||||
|
nameservers,
|
||||||
|
registrar: formData.registrarName ? {
|
||||||
|
name: formData.registrarName,
|
||||||
|
url: formData.registrarUrl || undefined,
|
||||||
|
} : undefined,
|
||||||
|
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
|
||||||
|
autoRenew: formData.autoRenew,
|
||||||
|
dnssecEnabled: formData.dnssecEnabled,
|
||||||
|
isPrimary: formData.isPrimary,
|
||||||
|
tags: tags.length > 0 ? tags : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Verify',
|
||||||
|
iconName: 'check-circle',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Verify Domain: ${domain.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center; padding: 20px;">
|
||||||
|
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'method'}
|
||||||
|
.label=${'Verification Method'}
|
||||||
|
.options=${[
|
||||||
|
{key: 'dns', option: 'DNS TXT Record'},
|
||||||
|
{key: 'http', option: 'HTTP File Upload'},
|
||||||
|
{key: 'email', option: 'Email Verification'},
|
||||||
|
{key: 'manual', option: 'Manual Verification'},
|
||||||
|
]}
|
||||||
|
.value=${'dns'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
${domain.data.verificationToken ? html`
|
||||||
|
<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #aaa; font-size: 0.9em;">Verification Token:</div>
|
||||||
|
<code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Start Verification',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.verifyDomainAction, {
|
||||||
|
domainId: domain.id,
|
||||||
|
verificationMethod: formData.method,
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View DNS Records',
|
||||||
|
iconName: 'list',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
// Navigate to DNS view with filter for this domain
|
||||||
|
// TODO: Implement navigation with filter
|
||||||
|
console.log('View DNS records for domain:', domain.data.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
|
||||||
|
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Domain`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">
|
||||||
|
Are you sure you want to delete this domain?
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">
|
||||||
|
${domain.data.name}
|
||||||
|
</div>
|
||||||
|
${domain.data.description ? html`
|
||||||
|
<div style="color: #aaa; margin-top: 4px;">
|
||||||
|
${domain.data.description}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${dnsCount > 0 ? html`
|
||||||
|
<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">
|
||||||
|
⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await appstate.dataState.dispatchAction(appstate.deleteDomainAction, {
|
||||||
|
domainId: domain.id,
|
||||||
|
});
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@@ -104,11 +104,11 @@ export class CloudlyViewOverview extends DeesElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dns',
|
id: 'dns',
|
||||||
title: 'DNS Zones',
|
title: 'DNS Entries',
|
||||||
value: this.data.dns?.length || 0,
|
value: this.data.dnsEntries?.length || 0,
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
iconName: 'lucide:Globe',
|
iconName: 'lucide:Globe',
|
||||||
description: 'Managed DNS zones'
|
description: 'Managed DNS records'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'databases',
|
id: 'databases',
|
||||||
|
@@ -120,21 +120,21 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'serviceCategory'}
|
.key=${'serviceCategory'}
|
||||||
.label=${'Service Category'}
|
.label=${'Service Category'}
|
||||||
.options=${['base', 'distributed', 'workload']}
|
.options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]}
|
||||||
.value=${'workload'}
|
.value=${'workload'}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'deploymentStrategy'}
|
.key=${'deploymentStrategy'}
|
||||||
.label=${'Deployment Strategy'}
|
.label=${'Deployment Strategy'}
|
||||||
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
.options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]}
|
||||||
.value=${'custom'}
|
.value=${'custom'}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.key=${'maxReplicas'}
|
.key=${'maxReplicas'}
|
||||||
.label=${'Max Replicas (for distributed services)'}
|
.label=${'Max Replicas (for distributed services)'}
|
||||||
.value=${'3'}
|
.value=${'1'}
|
||||||
.type=${'number'}>
|
.type=${'number'}>
|
||||||
</dees-input-text>
|
</dees-input-text>
|
||||||
<dees-input-checkbox
|
<dees-input-checkbox
|
||||||
@@ -154,7 +154,7 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'balancingStrategy'}
|
.key=${'balancingStrategy'}
|
||||||
.label=${'Balancing Strategy'}
|
.label=${'Balancing Strategy'}
|
||||||
.options=${['round-robin', 'least-connections']}
|
.options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]}
|
||||||
.value=${'round-robin'}
|
.value=${'round-robin'}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
@@ -223,14 +223,14 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'serviceCategory'}
|
.key=${'serviceCategory'}
|
||||||
.label=${'Service Category'}
|
.label=${'Service Category'}
|
||||||
.options=${['base', 'distributed', 'workload']}
|
.options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]}
|
||||||
.value=${service.data.serviceCategory || 'workload'}
|
.value=${service.data.serviceCategory || 'workload'}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'deploymentStrategy'}
|
.key=${'deploymentStrategy'}
|
||||||
.label=${'Deployment Strategy'}
|
.label=${'Deployment Strategy'}
|
||||||
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
.options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]}
|
||||||
.value=${service.data.deploymentStrategy || 'custom'}
|
.value=${service.data.deploymentStrategy || 'custom'}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
@@ -256,7 +256,7 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.key=${'balancingStrategy'}
|
.key=${'balancingStrategy'}
|
||||||
.label=${'Balancing Strategy'}
|
.label=${'Balancing Strategy'}
|
||||||
.options=${['round-robin', 'least-connections']}
|
.options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]}
|
||||||
.value=${service.data.balancingStrategy}
|
.value=${service.data.balancingStrategy}
|
||||||
.required=${true}>
|
.required=${true}>
|
||||||
</dees-input-dropdown>
|
</dees-input-dropdown>
|
||||||
|
Reference in New Issue
Block a user