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:
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user