@serve.zone/dcrouter 13.10.0 → 13.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +1075 -967
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -0
- package/dist_ts/classes.dcrouter.js +15 -1
- package/dist_ts/db/documents/classes.email-domain.doc.d.ts +17 -0
- package/dist_ts/db/documents/classes.email-domain.doc.js +124 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +3 -1
- package/dist_ts/email/classes.email-domain.manager.d.ts +46 -0
- package/dist_ts/email/classes.email-domain.manager.js +276 -0
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/email-domain.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/email-domain.handler.js +150 -0
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts_interfaces/data/email-domain.d.ts +70 -0
- package/dist_ts_interfaces/data/email-domain.js +2 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/requests/email-domains.d.ts +142 -0
- package/dist_ts_interfaces/requests/email-domains.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +21 -0
- package/dist_ts_web/appstate.js +81 -1
- package/dist_ts_web/elements/email/index.d.ts +1 -0
- package/dist_ts_web/elements/email/index.js +2 -1
- package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +19 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +410 -0
- package/dist_ts_web/elements/ops-dashboard.js +3 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +17 -0
- package/ts/db/documents/classes.email-domain.doc.ts +56 -0
- package/ts/db/documents/index.ts +3 -0
- package/ts/email/classes.email-domain.manager.ts +321 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/email-domain.handler.ts +195 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +124 -0
- package/ts_web/elements/email/index.ts +1 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +396 -0
- package/ts_web/elements/ops-dashboard.ts +2 -0
- package/ts_web/router.ts +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
3
|
+
import type {
|
|
4
|
+
IEmailDomainDkim,
|
|
5
|
+
IEmailDomainRateLimits,
|
|
6
|
+
IEmailDomainDnsStatus,
|
|
7
|
+
} from '../../../ts_interfaces/data/email-domain.js';
|
|
8
|
+
|
|
9
|
+
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
10
|
+
|
|
11
|
+
@plugins.smartdata.Collection(() => getDb())
|
|
12
|
+
export class EmailDomainDoc extends plugins.smartdata.SmartDataDbDoc<EmailDomainDoc, EmailDomainDoc> {
|
|
13
|
+
@plugins.smartdata.unI()
|
|
14
|
+
@plugins.smartdata.svDb()
|
|
15
|
+
public id!: string;
|
|
16
|
+
|
|
17
|
+
@plugins.smartdata.svDb()
|
|
18
|
+
public domain: string = '';
|
|
19
|
+
|
|
20
|
+
@plugins.smartdata.svDb()
|
|
21
|
+
public linkedDomainId: string = '';
|
|
22
|
+
|
|
23
|
+
@plugins.smartdata.svDb()
|
|
24
|
+
public subdomain?: string;
|
|
25
|
+
|
|
26
|
+
@plugins.smartdata.svDb()
|
|
27
|
+
public dkim!: IEmailDomainDkim;
|
|
28
|
+
|
|
29
|
+
@plugins.smartdata.svDb()
|
|
30
|
+
public rateLimits?: IEmailDomainRateLimits;
|
|
31
|
+
|
|
32
|
+
@plugins.smartdata.svDb()
|
|
33
|
+
public dnsStatus!: IEmailDomainDnsStatus;
|
|
34
|
+
|
|
35
|
+
@plugins.smartdata.svDb()
|
|
36
|
+
public createdAt!: string;
|
|
37
|
+
|
|
38
|
+
@plugins.smartdata.svDb()
|
|
39
|
+
public updatedAt!: string;
|
|
40
|
+
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static async findById(id: string): Promise<EmailDomainDoc | null> {
|
|
46
|
+
return await EmailDomainDoc.getInstance({ id });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static async findByDomain(domain: string): Promise<EmailDomainDoc | null> {
|
|
50
|
+
return await EmailDomainDoc.getInstance({ domain: domain.toLowerCase() });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static async findAll(): Promise<EmailDomainDoc[]> {
|
|
54
|
+
return await EmailDomainDoc.getInstances({});
|
|
55
|
+
}
|
|
56
|
+
}
|
package/ts/db/documents/index.ts
CHANGED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js';
|
|
4
|
+
import { DomainDoc } from '../db/documents/classes.domain.doc.js';
|
|
5
|
+
import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
|
|
6
|
+
import type { DnsManager } from '../dns/manager.dns.js';
|
|
7
|
+
import type { IEmailDomain, IEmailDnsRecord, TDnsRecordStatus } from '../../ts_interfaces/data/email-domain.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* EmailDomainManager — orchestrates email domain setup.
|
|
11
|
+
*
|
|
12
|
+
* Wires smartmta's DKIMCreator (key generation) with dcrouter's DnsManager
|
|
13
|
+
* (record creation for dcrouter-hosted and provider-managed zones) to provide
|
|
14
|
+
* a single entry point for setting up an email domain from A to Z.
|
|
15
|
+
*/
|
|
16
|
+
export class EmailDomainManager {
|
|
17
|
+
private dcRouter: any; // DcRouter — avoids circular import
|
|
18
|
+
|
|
19
|
+
constructor(dcRouterRef: any) {
|
|
20
|
+
this.dcRouter = dcRouterRef;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private get dnsManager(): DnsManager | undefined {
|
|
24
|
+
return this.dcRouter.dnsManager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private get dkimCreator(): any | undefined {
|
|
28
|
+
return this.dcRouter.emailServer?.dkimCreator;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private get emailHostname(): string {
|
|
32
|
+
return this.dcRouter.options?.emailConfig?.hostname || this.dcRouter.options?.tls?.domain || 'localhost';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// CRUD
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
public async getAll(): Promise<IEmailDomain[]> {
|
|
40
|
+
const docs = await EmailDomainDoc.findAll();
|
|
41
|
+
return docs.map((d) => this.docToInterface(d));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async getById(id: string): Promise<IEmailDomain | null> {
|
|
45
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
46
|
+
return doc ? this.docToInterface(doc) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async createEmailDomain(opts: {
|
|
50
|
+
linkedDomainId: string;
|
|
51
|
+
subdomain?: string;
|
|
52
|
+
dkimSelector?: string;
|
|
53
|
+
dkimKeySize?: number;
|
|
54
|
+
rotateKeys?: boolean;
|
|
55
|
+
rotationIntervalDays?: number;
|
|
56
|
+
}): Promise<IEmailDomain> {
|
|
57
|
+
// Resolve the linked DNS domain
|
|
58
|
+
const domainDoc = await DomainDoc.findById(opts.linkedDomainId);
|
|
59
|
+
if (!domainDoc) {
|
|
60
|
+
throw new Error(`DNS domain not found: ${opts.linkedDomainId}`);
|
|
61
|
+
}
|
|
62
|
+
const baseDomain = domainDoc.name;
|
|
63
|
+
const subdomain = opts.subdomain?.trim() || undefined;
|
|
64
|
+
const domainName = subdomain ? `${subdomain}.${baseDomain}` : baseDomain;
|
|
65
|
+
|
|
66
|
+
// Check for duplicates
|
|
67
|
+
const existing = await EmailDomainDoc.findByDomain(domainName);
|
|
68
|
+
if (existing) {
|
|
69
|
+
throw new Error(`Email domain already exists for ${domainName}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const selector = opts.dkimSelector || 'default';
|
|
73
|
+
const keySize = opts.dkimKeySize || 2048;
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
// Generate DKIM keys
|
|
77
|
+
let publicKey: string | undefined;
|
|
78
|
+
if (this.dkimCreator) {
|
|
79
|
+
try {
|
|
80
|
+
await this.dkimCreator.handleDKIMKeysForDomain(domainName);
|
|
81
|
+
const dnsRecord = await this.dkimCreator.getDNSRecordForSelector(domainName, selector);
|
|
82
|
+
// Extract public key from the DNS record value
|
|
83
|
+
const match = dnsRecord?.value?.match(/p=([A-Za-z0-9+/=]+)/);
|
|
84
|
+
publicKey = match ? match[1] : undefined;
|
|
85
|
+
logger.log('info', `DKIM keys generated for ${domainName} (selector: ${selector})`);
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
logger.log('warn', `DKIM key generation failed for ${domainName}: ${(err as Error).message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create the document
|
|
92
|
+
const doc = new EmailDomainDoc();
|
|
93
|
+
doc.id = plugins.smartunique.shortId();
|
|
94
|
+
doc.domain = domainName.toLowerCase();
|
|
95
|
+
doc.linkedDomainId = opts.linkedDomainId;
|
|
96
|
+
doc.subdomain = subdomain;
|
|
97
|
+
doc.dkim = {
|
|
98
|
+
selector,
|
|
99
|
+
keySize,
|
|
100
|
+
publicKey,
|
|
101
|
+
rotateKeys: opts.rotateKeys ?? false,
|
|
102
|
+
rotationIntervalDays: opts.rotationIntervalDays ?? 90,
|
|
103
|
+
};
|
|
104
|
+
doc.dnsStatus = {
|
|
105
|
+
mx: 'unchecked',
|
|
106
|
+
spf: 'unchecked',
|
|
107
|
+
dkim: 'unchecked',
|
|
108
|
+
dmarc: 'unchecked',
|
|
109
|
+
};
|
|
110
|
+
doc.createdAt = now;
|
|
111
|
+
doc.updatedAt = now;
|
|
112
|
+
await doc.save();
|
|
113
|
+
|
|
114
|
+
logger.log('info', `Email domain created: ${domainName}`);
|
|
115
|
+
return this.docToInterface(doc);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public async updateEmailDomain(
|
|
119
|
+
id: string,
|
|
120
|
+
changes: {
|
|
121
|
+
rotateKeys?: boolean;
|
|
122
|
+
rotationIntervalDays?: number;
|
|
123
|
+
rateLimits?: IEmailDomain['rateLimits'];
|
|
124
|
+
},
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
127
|
+
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
128
|
+
|
|
129
|
+
if (changes.rotateKeys !== undefined) doc.dkim.rotateKeys = changes.rotateKeys;
|
|
130
|
+
if (changes.rotationIntervalDays !== undefined) doc.dkim.rotationIntervalDays = changes.rotationIntervalDays;
|
|
131
|
+
if (changes.rateLimits !== undefined) doc.rateLimits = changes.rateLimits;
|
|
132
|
+
doc.updatedAt = new Date().toISOString();
|
|
133
|
+
await doc.save();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async deleteEmailDomain(id: string): Promise<void> {
|
|
137
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
138
|
+
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
139
|
+
await doc.delete();
|
|
140
|
+
logger.log('info', `Email domain deleted: ${doc.domain}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// DNS record computation
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Compute the 4 required DNS records for an email domain.
|
|
149
|
+
*/
|
|
150
|
+
public async getRequiredDnsRecords(id: string): Promise<IEmailDnsRecord[]> {
|
|
151
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
152
|
+
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
153
|
+
|
|
154
|
+
const domain = doc.domain;
|
|
155
|
+
const selector = doc.dkim.selector;
|
|
156
|
+
const publicKey = doc.dkim.publicKey || '';
|
|
157
|
+
const hostname = this.emailHostname;
|
|
158
|
+
|
|
159
|
+
const records: IEmailDnsRecord[] = [
|
|
160
|
+
{
|
|
161
|
+
type: 'MX',
|
|
162
|
+
name: domain,
|
|
163
|
+
value: `10 ${hostname}`,
|
|
164
|
+
status: doc.dnsStatus.mx,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'TXT',
|
|
168
|
+
name: domain,
|
|
169
|
+
value: 'v=spf1 a mx ~all',
|
|
170
|
+
status: doc.dnsStatus.spf,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'TXT',
|
|
174
|
+
name: `${selector}._domainkey.${domain}`,
|
|
175
|
+
value: `v=DKIM1; h=sha256; k=rsa; p=${publicKey}`,
|
|
176
|
+
status: doc.dnsStatus.dkim,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'TXT',
|
|
180
|
+
name: `_dmarc.${domain}`,
|
|
181
|
+
value: `v=DMARC1; p=none; rua=mailto:dmarc@${domain}`,
|
|
182
|
+
status: doc.dnsStatus.dmarc,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
return records;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// DNS provisioning
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Auto-create missing DNS records via the linked domain's DNS path.
|
|
195
|
+
*/
|
|
196
|
+
public async provisionDnsRecords(id: string): Promise<number> {
|
|
197
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
198
|
+
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
199
|
+
if (!this.dnsManager) throw new Error('DnsManager not available');
|
|
200
|
+
|
|
201
|
+
const requiredRecords = await this.getRequiredDnsRecords(id);
|
|
202
|
+
const domainId = doc.linkedDomainId;
|
|
203
|
+
|
|
204
|
+
// Get existing DNS records for the linked domain
|
|
205
|
+
const existingRecords = await DnsRecordDoc.findByDomainId(domainId);
|
|
206
|
+
let provisioned = 0;
|
|
207
|
+
|
|
208
|
+
for (const required of requiredRecords) {
|
|
209
|
+
// Check if a matching record already exists
|
|
210
|
+
const exists = existingRecords.some((r) => {
|
|
211
|
+
if (required.type === 'MX') {
|
|
212
|
+
return r.type === 'MX' && r.name.toLowerCase() === required.name.toLowerCase();
|
|
213
|
+
}
|
|
214
|
+
// For TXT records, match by name AND check value prefix (v=spf1, v=DKIM1, v=DMARC1)
|
|
215
|
+
if (r.type !== 'TXT' || r.name.toLowerCase() !== required.name.toLowerCase()) return false;
|
|
216
|
+
if (required.value.startsWith('v=spf1')) return r.value.includes('v=spf1');
|
|
217
|
+
if (required.value.startsWith('v=DKIM1')) return r.value.includes('v=DKIM1');
|
|
218
|
+
if (required.value.startsWith('v=DMARC1')) return r.value.includes('v=DMARC1');
|
|
219
|
+
return false;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!exists) {
|
|
223
|
+
try {
|
|
224
|
+
await this.dnsManager.createRecord({
|
|
225
|
+
domainId,
|
|
226
|
+
name: required.name,
|
|
227
|
+
type: required.type as any,
|
|
228
|
+
value: required.value,
|
|
229
|
+
ttl: 3600,
|
|
230
|
+
createdBy: 'email-domain-manager',
|
|
231
|
+
});
|
|
232
|
+
provisioned++;
|
|
233
|
+
logger.log('info', `Provisioned ${required.type} record for ${required.name}`);
|
|
234
|
+
} catch (err: unknown) {
|
|
235
|
+
logger.log('warn', `Failed to provision ${required.type} for ${required.name}: ${(err as Error).message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Re-validate after provisioning
|
|
241
|
+
await this.validateDns(id);
|
|
242
|
+
|
|
243
|
+
return provisioned;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// DNS validation
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validate DNS records via live lookups.
|
|
252
|
+
*/
|
|
253
|
+
public async validateDns(id: string): Promise<IEmailDnsRecord[]> {
|
|
254
|
+
const doc = await EmailDomainDoc.findById(id);
|
|
255
|
+
if (!doc) throw new Error(`Email domain not found: ${id}`);
|
|
256
|
+
|
|
257
|
+
const domain = doc.domain;
|
|
258
|
+
const selector = doc.dkim.selector;
|
|
259
|
+
const resolver = new plugins.dns.promises.Resolver();
|
|
260
|
+
|
|
261
|
+
// MX check
|
|
262
|
+
doc.dnsStatus.mx = await this.checkMx(resolver, domain);
|
|
263
|
+
|
|
264
|
+
// SPF check
|
|
265
|
+
doc.dnsStatus.spf = await this.checkTxtRecord(resolver, domain, 'v=spf1');
|
|
266
|
+
|
|
267
|
+
// DKIM check
|
|
268
|
+
doc.dnsStatus.dkim = await this.checkTxtRecord(resolver, `${selector}._domainkey.${domain}`, 'v=DKIM1');
|
|
269
|
+
|
|
270
|
+
// DMARC check
|
|
271
|
+
doc.dnsStatus.dmarc = await this.checkTxtRecord(resolver, `_dmarc.${domain}`, 'v=DMARC1');
|
|
272
|
+
|
|
273
|
+
doc.dnsStatus.lastCheckedAt = new Date().toISOString();
|
|
274
|
+
doc.updatedAt = new Date().toISOString();
|
|
275
|
+
await doc.save();
|
|
276
|
+
|
|
277
|
+
return this.getRequiredDnsRecords(id);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async checkMx(resolver: plugins.dns.promises.Resolver, domain: string): Promise<TDnsRecordStatus> {
|
|
281
|
+
try {
|
|
282
|
+
const records = await resolver.resolveMx(domain);
|
|
283
|
+
return records && records.length > 0 ? 'valid' : 'missing';
|
|
284
|
+
} catch {
|
|
285
|
+
return 'missing';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async checkTxtRecord(
|
|
290
|
+
resolver: plugins.dns.promises.Resolver,
|
|
291
|
+
name: string,
|
|
292
|
+
prefix: string,
|
|
293
|
+
): Promise<TDnsRecordStatus> {
|
|
294
|
+
try {
|
|
295
|
+
const records = await resolver.resolveTxt(name);
|
|
296
|
+
const flat = records.map((r) => r.join(''));
|
|
297
|
+
const found = flat.some((r) => r.startsWith(prefix));
|
|
298
|
+
return found ? 'valid' : 'missing';
|
|
299
|
+
} catch {
|
|
300
|
+
return 'missing';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Helpers
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
private docToInterface(doc: EmailDomainDoc): IEmailDomain {
|
|
309
|
+
return {
|
|
310
|
+
id: doc.id,
|
|
311
|
+
domain: doc.domain,
|
|
312
|
+
linkedDomainId: doc.linkedDomainId,
|
|
313
|
+
subdomain: doc.subdomain,
|
|
314
|
+
dkim: doc.dkim,
|
|
315
|
+
rateLimits: doc.rateLimits,
|
|
316
|
+
dnsStatus: doc.dnsStatus,
|
|
317
|
+
createdAt: doc.createdAt,
|
|
318
|
+
updatedAt: doc.updatedAt,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './classes.email-domain.manager.js';
|
|
@@ -37,6 +37,7 @@ export class OpsServer {
|
|
|
37
37
|
private domainHandler!: handlers.DomainHandler;
|
|
38
38
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
|
39
39
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
40
|
+
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
40
41
|
|
|
41
42
|
constructor(dcRouterRefArg: DcRouter) {
|
|
42
43
|
this.dcRouterRef = dcRouterRefArg;
|
|
@@ -104,6 +105,7 @@ export class OpsServer {
|
|
|
104
105
|
this.domainHandler = new handlers.DomainHandler(this);
|
|
105
106
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
|
106
107
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
108
|
+
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
107
109
|
|
|
108
110
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
|
109
111
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CRUD + DNS provisioning handler for email domains.
|
|
7
|
+
*
|
|
8
|
+
* Auth: admin JWT or API token with `email-domains:read` / `email-domains:write` scope.
|
|
9
|
+
*/
|
|
10
|
+
export class EmailDomainHandler {
|
|
11
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
12
|
+
|
|
13
|
+
constructor(private opsServerRef: OpsServer) {
|
|
14
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
15
|
+
this.registerHandlers();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private async requireAuth(
|
|
19
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
20
|
+
requiredScope?: interfaces.data.TApiTokenScope,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
if (request.identity?.jwt) {
|
|
23
|
+
try {
|
|
24
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
|
25
|
+
identity: request.identity,
|
|
26
|
+
});
|
|
27
|
+
if (isAdmin) return request.identity.userId;
|
|
28
|
+
} catch { /* fall through */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (request.apiToken) {
|
|
32
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
33
|
+
if (tokenManager) {
|
|
34
|
+
const token = await tokenManager.validateToken(request.apiToken);
|
|
35
|
+
if (token) {
|
|
36
|
+
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
37
|
+
return token.createdBy;
|
|
38
|
+
}
|
|
39
|
+
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private get manager() {
|
|
48
|
+
return this.opsServerRef.dcRouterRef.emailDomainManager;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private registerHandlers(): void {
|
|
52
|
+
// List all email domains
|
|
53
|
+
this.typedrouter.addTypedHandler(
|
|
54
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomains>(
|
|
55
|
+
'getEmailDomains',
|
|
56
|
+
async (dataArg) => {
|
|
57
|
+
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
58
|
+
if (!this.manager) return { domains: [] };
|
|
59
|
+
return { domains: await this.manager.getAll() };
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Get single email domain
|
|
65
|
+
this.typedrouter.addTypedHandler(
|
|
66
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomain>(
|
|
67
|
+
'getEmailDomain',
|
|
68
|
+
async (dataArg) => {
|
|
69
|
+
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
70
|
+
if (!this.manager) return { domain: null };
|
|
71
|
+
return { domain: await this.manager.getById(dataArg.id) };
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Create email domain
|
|
77
|
+
this.typedrouter.addTypedHandler(
|
|
78
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateEmailDomain>(
|
|
79
|
+
'createEmailDomain',
|
|
80
|
+
async (dataArg) => {
|
|
81
|
+
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
82
|
+
if (!this.manager) {
|
|
83
|
+
return { success: false, message: 'EmailDomainManager not initialized' };
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const domain = await this.manager.createEmailDomain({
|
|
87
|
+
linkedDomainId: dataArg.linkedDomainId,
|
|
88
|
+
subdomain: dataArg.subdomain,
|
|
89
|
+
dkimSelector: dataArg.dkimSelector,
|
|
90
|
+
dkimKeySize: dataArg.dkimKeySize,
|
|
91
|
+
rotateKeys: dataArg.rotateKeys,
|
|
92
|
+
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
93
|
+
});
|
|
94
|
+
return { success: true, domain };
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
return { success: false, message: (err as Error).message };
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Update email domain
|
|
103
|
+
this.typedrouter.addTypedHandler(
|
|
104
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateEmailDomain>(
|
|
105
|
+
'updateEmailDomain',
|
|
106
|
+
async (dataArg) => {
|
|
107
|
+
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
108
|
+
if (!this.manager) {
|
|
109
|
+
return { success: false, message: 'EmailDomainManager not initialized' };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await this.manager.updateEmailDomain(dataArg.id, {
|
|
113
|
+
rotateKeys: dataArg.rotateKeys,
|
|
114
|
+
rotationIntervalDays: dataArg.rotationIntervalDays,
|
|
115
|
+
rateLimits: dataArg.rateLimits,
|
|
116
|
+
});
|
|
117
|
+
return { success: true };
|
|
118
|
+
} catch (err: unknown) {
|
|
119
|
+
return { success: false, message: (err as Error).message };
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Delete email domain
|
|
126
|
+
this.typedrouter.addTypedHandler(
|
|
127
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteEmailDomain>(
|
|
128
|
+
'deleteEmailDomain',
|
|
129
|
+
async (dataArg) => {
|
|
130
|
+
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
131
|
+
if (!this.manager) {
|
|
132
|
+
return { success: false, message: 'EmailDomainManager not initialized' };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await this.manager.deleteEmailDomain(dataArg.id);
|
|
136
|
+
return { success: true };
|
|
137
|
+
} catch (err: unknown) {
|
|
138
|
+
return { success: false, message: (err as Error).message };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Validate DNS records
|
|
145
|
+
this.typedrouter.addTypedHandler(
|
|
146
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ValidateEmailDomain>(
|
|
147
|
+
'validateEmailDomain',
|
|
148
|
+
async (dataArg) => {
|
|
149
|
+
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
150
|
+
if (!this.manager) {
|
|
151
|
+
return { success: false, message: 'EmailDomainManager not initialized' };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const records = await this.manager.validateDns(dataArg.id);
|
|
155
|
+
const domain = await this.manager.getById(dataArg.id);
|
|
156
|
+
return { success: true, domain: domain ?? undefined, records };
|
|
157
|
+
} catch (err: unknown) {
|
|
158
|
+
return { success: false, message: (err as Error).message };
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Get required DNS records
|
|
165
|
+
this.typedrouter.addTypedHandler(
|
|
166
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDomainDnsRecords>(
|
|
167
|
+
'getEmailDomainDnsRecords',
|
|
168
|
+
async (dataArg) => {
|
|
169
|
+
await this.requireAuth(dataArg, 'email-domains:read' as any);
|
|
170
|
+
if (!this.manager) return { records: [] };
|
|
171
|
+
return { records: await this.manager.getRequiredDnsRecords(dataArg.id) };
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Auto-provision DNS records
|
|
177
|
+
this.typedrouter.addTypedHandler(
|
|
178
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ProvisionEmailDomainDns>(
|
|
179
|
+
'provisionEmailDomainDns',
|
|
180
|
+
async (dataArg) => {
|
|
181
|
+
await this.requireAuth(dataArg, 'email-domains:write' as any);
|
|
182
|
+
if (!this.manager) {
|
|
183
|
+
return { success: false, message: 'EmailDomainManager not initialized' };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const provisioned = await this.manager.provisionDnsRecords(dataArg.id);
|
|
187
|
+
return { success: true, provisioned };
|
|
188
|
+
} catch (err: unknown) {
|
|
189
|
+
return { success: false, message: (err as Error).message };
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -17,4 +17,5 @@ export * from './users.handler.js';
|
|
|
17
17
|
export * from './dns-provider.handler.js';
|
|
18
18
|
export * from './domain.handler.js';
|
|
19
19
|
export * from './dns-record.handler.js';
|
|
20
|
-
export * from './acme-config.handler.js';
|
|
20
|
+
export * from './acme-config.handler.js';
|
|
21
|
+
export * from './email-domain.handler.js';
|