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:
2025-09-09 15:08:28 +00:00
parent 38e8b4086d
commit 766191899c
19 changed files with 2174 additions and 99 deletions

View File

@@ -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
View File

@@ -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: {}

View File

@@ -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);

View 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;
}
}

View 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');
}
}

View 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;
}
}

View 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
View 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;
};
}

View 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;
};
}

View File

@@ -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';

View 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[];
};
}

View 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;
};
};
}

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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',

View File

@@ -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
<dees-input-text .key=${'type'}
.key=${'data.secretGroupIds'} .label=${'Record Type'}
.label=${'secretGroupIds'} .options=${[
.value=${''} {key: 'A', option: 'A - IPv4 Address'},
></dees-input-text> {key: 'AAAA', option: 'AAAA - IPv6 Address'},
<dees-input-text {key: 'CNAME', option: 'CNAME - Canonical Name'},
.key=${'data.includedTags'} {key: 'MX', option: 'MX - Mail Exchange'},
.label=${'includedTags'} {key: 'TXT', option: 'TXT - Text Record'},
.value=${''} {key: 'NS', option: 'NS - Name Server'},
></dees-input-text> {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
.key=${'zone'}
.label=${'Zone (Domain)'}
.placeholder=${'example.com'}
.required=${true}>
</dees-input-text>
<dees-input-text
.key=${'name'}
.label=${'Name'}
.placeholder=${'@ for root, www, mail, etc.'}
.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();
}, },
@@ -126,4 +410,4 @@ export class CloudlyViewDns extends DeesElement {
></dees-table> ></dees-table>
`; `;
} }
} }

View 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>
`;
}
}

View File

@@ -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',

View File

@@ -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>