@serve.zone/dcrouter 13.0.11 → 13.1.1
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 +2801 -2735
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.dcrouter.js +18 -10
- package/dist_ts/config/classes.route-config-manager.d.ts +6 -1
- package/dist_ts/config/classes.route-config-manager.js +4 -4
- package/dist_ts/config/classes.target-profile-manager.d.ts +20 -7
- package/dist_ts/config/classes.target-profile-manager.js +68 -29
- package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.vpn-client.doc.js +2 -8
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -2
- package/dist_ts/opsserver/handlers/certificate.handler.js +46 -16
- package/dist_ts/opsserver/handlers/vpn.handler.js +1 -5
- package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -4
- package/dist_ts/vpn/classes.vpn-manager.js +9 -27
- package/dist_ts_interfaces/data/target-profile.d.ts +1 -1
- package/dist_ts_interfaces/data/vpn.d.ts +0 -1
- package/dist_ts_interfaces/requests/vpn.d.ts +0 -2
- package/dist_ts_migrations/index.d.ts +28 -0
- package/dist_ts_migrations/index.js +44 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/appstate.d.ts +2 -4
- package/dist_ts_web/appstate.js +1 -3
- package/dist_ts_web/elements/ops-view-security.d.ts +3 -0
- package/dist_ts_web/elements/ops-view-security.js +75 -170
- package/dist_ts_web/elements/ops-view-targetprofiles.js +8 -8
- package/dist_ts_web/elements/ops-view-vpn.js +8 -18
- package/package.json +9 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -9
- package/ts/config/classes.route-config-manager.ts +7 -4
- package/ts/config/classes.target-profile-manager.ts +80 -28
- package/ts/db/documents/classes.vpn-client.doc.ts +0 -3
- package/ts/opsserver/handlers/certificate.handler.ts +44 -15
- package/ts/opsserver/handlers/vpn.handler.ts +0 -4
- package/ts/tspublish.json +1 -1
- package/ts/vpn/classes.vpn-manager.ts +8 -26
- package/ts_apiclient/tspublish.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +6 -6
- package/ts_web/elements/ops-view-security.ts +78 -168
- package/ts_web/elements/ops-view-targetprofiles.ts +9 -9
- package/ts_web/elements/ops-view-vpn.ts +9 -16
- package/ts_web/tspublish.json +1 -1
|
@@ -295,7 +295,12 @@ export class CertificateHandler {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
/**
|
|
298
|
-
* Legacy route-based reprovisioning
|
|
298
|
+
* Legacy route-based reprovisioning. Kept for backward compatibility with
|
|
299
|
+
* older clients that send `reprovisionCertificate` typed-requests.
|
|
300
|
+
*
|
|
301
|
+
* Like reprovisionCertificateDomain, this triggers the full route apply
|
|
302
|
+
* pipeline rather than smartProxy.provisionCertificate(routeName) — which
|
|
303
|
+
* is a no-op when certProvisionFunction is set (Rust ACME disabled).
|
|
299
304
|
*/
|
|
300
305
|
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
|
301
306
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
@@ -305,13 +310,19 @@ export class CertificateHandler {
|
|
|
305
310
|
return { success: false, message: 'SmartProxy is not running' };
|
|
306
311
|
}
|
|
307
312
|
|
|
313
|
+
// Clear event-based status for domains in this route so the
|
|
314
|
+
// certificate-issued event can refresh them
|
|
315
|
+
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
|
316
|
+
if (entry.routeNames.includes(routeName)) {
|
|
317
|
+
dcRouter.certificateStatusMap.delete(domain);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
308
321
|
try {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
dcRouter.certificateStatusMap.delete(domain);
|
|
314
|
-
}
|
|
322
|
+
if (dcRouter.routeConfigManager) {
|
|
323
|
+
await dcRouter.routeConfigManager.applyRoutes();
|
|
324
|
+
} else {
|
|
325
|
+
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
|
315
326
|
}
|
|
316
327
|
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
|
317
328
|
} catch (err: unknown) {
|
|
@@ -320,7 +331,16 @@ export class CertificateHandler {
|
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
/**
|
|
323
|
-
* Domain-based reprovisioning — clears backoff first,
|
|
334
|
+
* Domain-based reprovisioning — clears backoff first, refreshes the smartacme
|
|
335
|
+
* cert (when forceRenew is set), then re-applies routes so the running Rust
|
|
336
|
+
* proxy actually picks up the new cert.
|
|
337
|
+
*
|
|
338
|
+
* Why applyRoutes (not smartProxy.provisionCertificate)?
|
|
339
|
+
* smartProxy.provisionCertificate(routeName) routes through the Rust ACME
|
|
340
|
+
* path, which is forcibly disabled whenever certProvisionFunction is set
|
|
341
|
+
* (smart-proxy.ts:168-171). The only path that re-invokes
|
|
342
|
+
* certProvisionFunction → bridge.loadCertificate is updateRoutes(), which
|
|
343
|
+
* we trigger via routeConfigManager.applyRoutes().
|
|
324
344
|
*/
|
|
325
345
|
private async reprovisionCertificateDomain(domain: string, forceRenew?: boolean): Promise<{ success: boolean; message?: string }> {
|
|
326
346
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
@@ -335,28 +355,37 @@ export class CertificateHandler {
|
|
|
335
355
|
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
|
336
356
|
}
|
|
337
357
|
|
|
338
|
-
// Find routes matching this domain —
|
|
358
|
+
// Find routes matching this domain — fail early if none exist
|
|
339
359
|
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
|
340
360
|
if (routeNames.length === 0) {
|
|
341
361
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
|
342
362
|
}
|
|
343
363
|
|
|
344
|
-
// If forceRenew,
|
|
364
|
+
// If forceRenew, order a fresh cert from ACME now so it's already in
|
|
365
|
+
// AcmeCertDoc by the time certProvisionFunction is invoked below.
|
|
345
366
|
if (forceRenew && dcRouter.smartAcme) {
|
|
346
367
|
try {
|
|
347
368
|
await dcRouter.smartAcme.getCertificateForDomain(domain, { forceRenew: true });
|
|
348
|
-
} catch {
|
|
349
|
-
|
|
369
|
+
} catch (err: unknown) {
|
|
370
|
+
return { success: false, message: `Failed to renew certificate for ${domain}: ${(err as Error).message}` };
|
|
350
371
|
}
|
|
351
372
|
}
|
|
352
373
|
|
|
353
374
|
// Clear status map entry so it gets refreshed by the certificate-issued event
|
|
354
375
|
dcRouter.certificateStatusMap.delete(domain);
|
|
355
376
|
|
|
356
|
-
//
|
|
357
|
-
//
|
|
377
|
+
// Trigger the full route apply pipeline:
|
|
378
|
+
// applyRoutes → updateRoutes → provisionCertificatesViaCallback →
|
|
379
|
+
// certProvisionFunction(domain) → smartAcme.getCertificateForDomain →
|
|
380
|
+
// bridge.loadCertificate → Rust hot-swaps `loaded_certs` →
|
|
381
|
+
// certificate-issued event → certificateStatusMap updated
|
|
358
382
|
try {
|
|
359
|
-
|
|
383
|
+
if (dcRouter.routeConfigManager) {
|
|
384
|
+
await dcRouter.routeConfigManager.applyRoutes();
|
|
385
|
+
} else {
|
|
386
|
+
// Fallback when DB is disabled and there is no RouteConfigManager
|
|
387
|
+
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
|
388
|
+
}
|
|
360
389
|
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
|
|
361
390
|
} catch (err: unknown) {
|
|
362
391
|
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
|
@@ -31,7 +31,6 @@ export class VpnHandler {
|
|
|
31
31
|
createdAt: c.createdAt,
|
|
32
32
|
updatedAt: c.updatedAt,
|
|
33
33
|
expiresAt: c.expiresAt,
|
|
34
|
-
forceDestinationSmartproxy: c.forceDestinationSmartproxy ?? true,
|
|
35
34
|
destinationAllowList: c.destinationAllowList,
|
|
36
35
|
destinationBlockList: c.destinationBlockList,
|
|
37
36
|
useHostIp: c.useHostIp,
|
|
@@ -122,7 +121,6 @@ export class VpnHandler {
|
|
|
122
121
|
clientId: dataArg.clientId,
|
|
123
122
|
targetProfileIds: dataArg.targetProfileIds,
|
|
124
123
|
description: dataArg.description,
|
|
125
|
-
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
126
124
|
destinationAllowList: dataArg.destinationAllowList,
|
|
127
125
|
destinationBlockList: dataArg.destinationBlockList,
|
|
128
126
|
useHostIp: dataArg.useHostIp,
|
|
@@ -148,7 +146,6 @@ export class VpnHandler {
|
|
|
148
146
|
createdAt: Date.now(),
|
|
149
147
|
updatedAt: Date.now(),
|
|
150
148
|
expiresAt: bundle.entry.expiresAt,
|
|
151
|
-
forceDestinationSmartproxy: persistedClient?.forceDestinationSmartproxy ?? true,
|
|
152
149
|
destinationAllowList: persistedClient?.destinationAllowList,
|
|
153
150
|
destinationBlockList: persistedClient?.destinationBlockList,
|
|
154
151
|
useHostIp: persistedClient?.useHostIp,
|
|
@@ -180,7 +177,6 @@ export class VpnHandler {
|
|
|
180
177
|
await manager.updateClient(dataArg.clientId, {
|
|
181
178
|
description: dataArg.description,
|
|
182
179
|
targetProfileIds: dataArg.targetProfileIds,
|
|
183
|
-
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
184
180
|
destinationAllowList: dataArg.destinationAllowList,
|
|
185
181
|
destinationBlockList: dataArg.destinationBlockList,
|
|
186
182
|
useHostIp: dataArg.useHostIp,
|
package/ts/tspublish.json
CHANGED
|
@@ -201,7 +201,6 @@ export class VpnManager {
|
|
|
201
201
|
clientId: string;
|
|
202
202
|
targetProfileIds?: string[];
|
|
203
203
|
description?: string;
|
|
204
|
-
forceDestinationSmartproxy?: boolean;
|
|
205
204
|
destinationAllowList?: string[];
|
|
206
205
|
destinationBlockList?: string[];
|
|
207
206
|
useHostIp?: boolean;
|
|
@@ -242,9 +241,6 @@ export class VpnManager {
|
|
|
242
241
|
doc.createdAt = Date.now();
|
|
243
242
|
doc.updatedAt = Date.now();
|
|
244
243
|
doc.expiresAt = bundle.entry.expiresAt;
|
|
245
|
-
if (opts.forceDestinationSmartproxy !== undefined) {
|
|
246
|
-
doc.forceDestinationSmartproxy = opts.forceDestinationSmartproxy;
|
|
247
|
-
}
|
|
248
244
|
if (opts.destinationAllowList !== undefined) {
|
|
249
245
|
doc.destinationAllowList = opts.destinationAllowList;
|
|
250
246
|
}
|
|
@@ -349,7 +345,6 @@ export class VpnManager {
|
|
|
349
345
|
public async updateClient(clientId: string, update: {
|
|
350
346
|
description?: string;
|
|
351
347
|
targetProfileIds?: string[];
|
|
352
|
-
forceDestinationSmartproxy?: boolean;
|
|
353
348
|
destinationAllowList?: string[];
|
|
354
349
|
destinationBlockList?: string[];
|
|
355
350
|
useHostIp?: boolean;
|
|
@@ -362,7 +357,6 @@ export class VpnManager {
|
|
|
362
357
|
if (!client) throw new Error(`Client not found: ${clientId}`);
|
|
363
358
|
if (update.description !== undefined) client.description = update.description;
|
|
364
359
|
if (update.targetProfileIds !== undefined) client.targetProfileIds = update.targetProfileIds;
|
|
365
|
-
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
|
|
366
360
|
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
|
367
361
|
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
|
368
362
|
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
|
@@ -484,12 +478,11 @@ export class VpnManager {
|
|
|
484
478
|
|
|
485
479
|
/**
|
|
486
480
|
* Build per-client security settings for the smartvpn daemon.
|
|
487
|
-
*
|
|
488
|
-
*
|
|
481
|
+
* All VPN traffic is forced through SmartProxy (forceTarget to 127.0.0.1).
|
|
482
|
+
* TargetProfile direct IP:port targets bypass SmartProxy via allowList.
|
|
489
483
|
*/
|
|
490
484
|
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
|
491
485
|
const security: plugins.smartvpn.IClientSecurity = {};
|
|
492
|
-
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
493
486
|
|
|
494
487
|
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
|
|
495
488
|
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
|
|
@@ -500,23 +493,12 @@ export class VpnManager {
|
|
|
500
493
|
...profileDirectTargets,
|
|
501
494
|
];
|
|
502
495
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
} else if (mergedAllowList.length || client.destinationBlockList?.length) {
|
|
510
|
-
// Client is forced to SmartProxy, but with allow/block overrides
|
|
511
|
-
// (includes TargetProfile direct targets that bypass SmartProxy)
|
|
512
|
-
security.destinationPolicy = {
|
|
513
|
-
default: 'forceTarget' as const,
|
|
514
|
-
target: '127.0.0.1',
|
|
515
|
-
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
|
516
|
-
blockList: client.destinationBlockList,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
// else: no per-client policy, server-wide applies
|
|
496
|
+
security.destinationPolicy = {
|
|
497
|
+
default: 'forceTarget' as const,
|
|
498
|
+
target: '127.0.0.1',
|
|
499
|
+
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
|
500
|
+
blockList: client.destinationBlockList,
|
|
501
|
+
};
|
|
520
502
|
|
|
521
503
|
return security;
|
|
522
504
|
}
|
package/ts_web/appstate.ts
CHANGED
|
@@ -1015,7 +1015,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|
|
1015
1015
|
clientId: string;
|
|
1016
1016
|
targetProfileIds?: string[];
|
|
1017
1017
|
description?: string;
|
|
1018
|
-
|
|
1018
|
+
|
|
1019
1019
|
destinationAllowList?: string[];
|
|
1020
1020
|
destinationBlockList?: string[];
|
|
1021
1021
|
useHostIp?: boolean;
|
|
@@ -1037,7 +1037,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|
|
1037
1037
|
clientId: dataArg.clientId,
|
|
1038
1038
|
targetProfileIds: dataArg.targetProfileIds,
|
|
1039
1039
|
description: dataArg.description,
|
|
1040
|
-
|
|
1040
|
+
|
|
1041
1041
|
destinationAllowList: dataArg.destinationAllowList,
|
|
1042
1042
|
destinationBlockList: dataArg.destinationBlockList,
|
|
1043
1043
|
useHostIp: dataArg.useHostIp,
|
|
@@ -1113,7 +1113,7 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|
|
1113
1113
|
clientId: string;
|
|
1114
1114
|
description?: string;
|
|
1115
1115
|
targetProfileIds?: string[];
|
|
1116
|
-
|
|
1116
|
+
|
|
1117
1117
|
destinationAllowList?: string[];
|
|
1118
1118
|
destinationBlockList?: string[];
|
|
1119
1119
|
useHostIp?: boolean;
|
|
@@ -1135,7 +1135,7 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|
|
1135
1135
|
clientId: dataArg.clientId,
|
|
1136
1136
|
description: dataArg.description,
|
|
1137
1137
|
targetProfileIds: dataArg.targetProfileIds,
|
|
1138
|
-
|
|
1138
|
+
|
|
1139
1139
|
destinationAllowList: dataArg.destinationAllowList,
|
|
1140
1140
|
destinationBlockList: dataArg.destinationBlockList,
|
|
1141
1141
|
useHostIp: dataArg.useHostIp,
|
|
@@ -1223,7 +1223,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1223
1223
|
name: string;
|
|
1224
1224
|
description?: string;
|
|
1225
1225
|
domains?: string[];
|
|
1226
|
-
targets?: Array<{
|
|
1226
|
+
targets?: Array<{ ip: string; port: number }>;
|
|
1227
1227
|
routeRefs?: string[];
|
|
1228
1228
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
|
1229
1229
|
const context = getActionContext();
|
|
@@ -1259,7 +1259,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
|
|
|
1259
1259
|
name?: string;
|
|
1260
1260
|
description?: string;
|
|
1261
1261
|
domains?: string[];
|
|
1262
|
-
targets?: Array<{
|
|
1262
|
+
targets?: Array<{ ip: string; port: number }>;
|
|
1263
1263
|
routeRefs?: string[];
|
|
1264
1264
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
|
1265
1265
|
const context = getActionContext();
|
|
@@ -30,6 +30,20 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
30
30
|
@state()
|
|
31
31
|
accessor selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
|
32
32
|
|
|
33
|
+
private tabLabelMap: Record<string, string> = {
|
|
34
|
+
'overview': 'Overview',
|
|
35
|
+
'blocked': 'Blocked IPs',
|
|
36
|
+
'authentication': 'Authentication',
|
|
37
|
+
'email-security': 'Email Security',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
private labelToTab: Record<string, 'overview' | 'blocked' | 'authentication' | 'email-security'> = {
|
|
41
|
+
'Overview': 'overview',
|
|
42
|
+
'Blocked IPs': 'blocked',
|
|
43
|
+
'Authentication': 'authentication',
|
|
44
|
+
'Email Security': 'email-security',
|
|
45
|
+
};
|
|
46
|
+
|
|
33
47
|
constructor() {
|
|
34
48
|
super();
|
|
35
49
|
const subscription = appstate.statsStatePart
|
|
@@ -40,35 +54,23 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
40
54
|
this.rxSubscriptions.push(subscription);
|
|
41
55
|
}
|
|
42
56
|
|
|
57
|
+
async firstUpdated() {
|
|
58
|
+
const toggle = this.shadowRoot!.querySelector('dees-input-multitoggle') as any;
|
|
59
|
+
if (toggle) {
|
|
60
|
+
const sub = toggle.changeSubject.subscribe(() => {
|
|
61
|
+
const tab = this.labelToTab[toggle.selectedOption];
|
|
62
|
+
if (tab) this.selectedTab = tab;
|
|
63
|
+
});
|
|
64
|
+
this.rxSubscriptions.push(sub);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
public static styles = [
|
|
44
69
|
cssManager.defaultStyles,
|
|
45
70
|
shared.viewHostCss,
|
|
46
71
|
css`
|
|
47
|
-
|
|
48
|
-
display: flex;
|
|
49
|
-
gap: 8px;
|
|
72
|
+
dees-input-multitoggle {
|
|
50
73
|
margin-bottom: 24px;
|
|
51
|
-
border-bottom: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.tab {
|
|
55
|
-
padding: 12px 24px;
|
|
56
|
-
background: none;
|
|
57
|
-
border: none;
|
|
58
|
-
border-bottom: 2px solid transparent;
|
|
59
|
-
cursor: pointer;
|
|
60
|
-
font-size: 16px;
|
|
61
|
-
color: ${cssManager.bdTheme('#666', '#999')};
|
|
62
|
-
transition: all 0.2s ease;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.tab:hover {
|
|
66
|
-
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.tab.active {
|
|
70
|
-
color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
|
71
|
-
border-bottom-color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
h2 {
|
|
@@ -91,135 +93,22 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
91
93
|
overflow: hidden;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
.securityCard.alert {
|
|
95
|
-
border-color: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
|
96
|
-
background: ${cssManager.bdTheme('#ffebee', '#4a1f1f')};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.securityCard.warning {
|
|
100
|
-
border-color: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
|
101
|
-
background: ${cssManager.bdTheme('#fff3e0', '#4a3a1f')};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.securityCard.success {
|
|
105
|
-
border-color: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
|
106
|
-
background: ${cssManager.bdTheme('#e8f5e9', '#1f3f1f')};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.cardHeader {
|
|
110
|
-
display: flex;
|
|
111
|
-
justify-content: space-between;
|
|
112
|
-
align-items: center;
|
|
113
|
-
margin-bottom: 16px;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
.cardTitle {
|
|
117
|
-
font-size: 18px;
|
|
118
|
-
font-weight: 600;
|
|
119
|
-
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.cardStatus {
|
|
123
|
-
font-size: 14px;
|
|
124
|
-
padding: 4px 12px;
|
|
125
|
-
border-radius: 16px;
|
|
126
|
-
font-weight: 500;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.status-critical {
|
|
130
|
-
background: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
|
131
|
-
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.status-warning {
|
|
135
|
-
background: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
|
136
|
-
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.status-good {
|
|
140
|
-
background: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
|
141
|
-
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.metricValue {
|
|
145
|
-
font-size: 32px;
|
|
146
|
-
font-weight: 700;
|
|
147
|
-
margin-bottom: 8px;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.metricLabel {
|
|
151
|
-
font-size: 14px;
|
|
152
|
-
color: ${cssManager.bdTheme('#666', '#999')};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
96
|
.actionButton {
|
|
156
97
|
margin-top: 16px;
|
|
157
98
|
}
|
|
158
99
|
|
|
159
|
-
.blockedIpList {
|
|
160
|
-
max-height: 400px;
|
|
161
|
-
overflow-y: auto;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.blockedIpItem {
|
|
165
|
-
display: flex;
|
|
166
|
-
justify-content: space-between;
|
|
167
|
-
align-items: center;
|
|
168
|
-
padding: 12px;
|
|
169
|
-
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.blockedIpItem:last-child {
|
|
173
|
-
border-bottom: none;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.ipAddress {
|
|
177
|
-
font-family: 'Consolas', 'Monaco', monospace;
|
|
178
|
-
font-weight: 600;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.blockReason {
|
|
182
|
-
font-size: 14px;
|
|
183
|
-
color: ${cssManager.bdTheme('#666', '#999')};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.blockTime {
|
|
187
|
-
font-size: 12px;
|
|
188
|
-
color: ${cssManager.bdTheme('#999', '#666')};
|
|
189
|
-
}
|
|
190
100
|
`,
|
|
191
101
|
];
|
|
192
102
|
|
|
193
103
|
public render() {
|
|
194
104
|
return html`
|
|
195
105
|
<dees-heading level="2">Security</dees-heading>
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Overview
|
|
203
|
-
</button>
|
|
204
|
-
<button
|
|
205
|
-
class="tab ${this.selectedTab === 'blocked' ? 'active' : ''}"
|
|
206
|
-
@click=${() => this.selectedTab = 'blocked'}
|
|
207
|
-
>
|
|
208
|
-
Blocked IPs
|
|
209
|
-
</button>
|
|
210
|
-
<button
|
|
211
|
-
class="tab ${this.selectedTab === 'authentication' ? 'active' : ''}"
|
|
212
|
-
@click=${() => this.selectedTab = 'authentication'}
|
|
213
|
-
>
|
|
214
|
-
Authentication
|
|
215
|
-
</button>
|
|
216
|
-
<button
|
|
217
|
-
class="tab ${this.selectedTab === 'email-security' ? 'active' : ''}"
|
|
218
|
-
@click=${() => this.selectedTab = 'email-security'}
|
|
219
|
-
>
|
|
220
|
-
Email Security
|
|
221
|
-
</button>
|
|
222
|
-
</div>
|
|
106
|
+
|
|
107
|
+
<dees-input-multitoggle
|
|
108
|
+
.type=${'single'}
|
|
109
|
+
.options=${['Overview', 'Blocked IPs', 'Authentication', 'Email Security']}
|
|
110
|
+
.selectedOption=${this.tabLabelMap[this.selectedTab]}
|
|
111
|
+
></dees-input-multitoggle>
|
|
223
112
|
|
|
224
113
|
${this.renderTabContent()}
|
|
225
114
|
`;
|
|
@@ -328,32 +217,53 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
328
217
|
}
|
|
329
218
|
|
|
330
219
|
private renderBlockedIPs(metrics: any) {
|
|
220
|
+
const blockedIPs: string[] = metrics.blockedIPs || [];
|
|
221
|
+
|
|
222
|
+
const tiles: IStatsTile[] = [
|
|
223
|
+
{
|
|
224
|
+
id: 'totalBlocked',
|
|
225
|
+
title: 'Blocked IPs',
|
|
226
|
+
value: blockedIPs.length,
|
|
227
|
+
type: 'number',
|
|
228
|
+
icon: 'lucide:ShieldBan',
|
|
229
|
+
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
|
|
230
|
+
description: 'Currently blocked addresses',
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
331
234
|
return html`
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
235
|
+
<dees-statsgrid
|
|
236
|
+
.tiles=${tiles}
|
|
237
|
+
.minTileWidth=${200}
|
|
238
|
+
></dees-statsgrid>
|
|
239
|
+
|
|
240
|
+
<dees-table
|
|
241
|
+
.heading1=${'Blocked IP Addresses'}
|
|
242
|
+
.heading2=${'IPs blocked due to suspicious activity'}
|
|
243
|
+
.data=${blockedIPs.map((ip) => ({ ip }))}
|
|
244
|
+
.displayFunction=${(item) => ({
|
|
245
|
+
'IP Address': item.ip,
|
|
246
|
+
'Reason': 'Suspicious activity',
|
|
247
|
+
})}
|
|
248
|
+
.dataActions=${[
|
|
249
|
+
{
|
|
250
|
+
name: 'Unblock',
|
|
251
|
+
iconName: 'lucide:shield-off',
|
|
252
|
+
type: ['contextmenu' as const],
|
|
253
|
+
actionFunc: async (item) => {
|
|
254
|
+
await this.unblockIP(item.ip);
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'Clear All',
|
|
259
|
+
iconName: 'lucide:trash-2',
|
|
260
|
+
type: ['header' as const],
|
|
261
|
+
actionFunc: async () => {
|
|
262
|
+
await this.clearBlockedIPs();
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
]}
|
|
266
|
+
></dees-table>
|
|
357
267
|
`;
|
|
358
268
|
}
|
|
359
269
|
|
|
@@ -91,7 +91,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
91
91
|
? html`${profile.domains.map(d => html`<span class="tagBadge">${d}</span>`)}`
|
|
92
92
|
: '-',
|
|
93
93
|
Targets: profile.targets?.length
|
|
94
|
-
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.
|
|
94
|
+
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)}`
|
|
95
95
|
: '-',
|
|
96
96
|
'Route Refs': profile.routeRefs?.length
|
|
97
97
|
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)}`
|
|
@@ -175,7 +175,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
175
175
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
|
176
176
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
|
177
177
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
|
|
178
|
-
<dees-input-list .key=${'targets'} .label=${'Targets (
|
|
178
|
+
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
|
|
179
179
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list>
|
|
180
180
|
</dees-form>
|
|
181
181
|
`,
|
|
@@ -197,11 +197,11 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
197
197
|
const lastColon = s.lastIndexOf(':');
|
|
198
198
|
if (lastColon === -1) return null;
|
|
199
199
|
return {
|
|
200
|
-
|
|
200
|
+
ip: s.substring(0, lastColon),
|
|
201
201
|
port: parseInt(s.substring(lastColon + 1), 10),
|
|
202
202
|
};
|
|
203
203
|
})
|
|
204
|
-
.filter((t): t is {
|
|
204
|
+
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
|
205
205
|
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
|
206
206
|
|
|
207
207
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.createTargetProfileAction, {
|
|
@@ -220,7 +220,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
220
220
|
|
|
221
221
|
private async showEditProfileDialog(profile: interfaces.data.ITargetProfile) {
|
|
222
222
|
const currentDomains = profile.domains || [];
|
|
223
|
-
const currentTargets = profile.targets?.map(t => `${t.
|
|
223
|
+
const currentTargets = profile.targets?.map(t => `${t.ip}:${t.port}`) || [];
|
|
224
224
|
const currentRouteRefs = profile.routeRefs || [];
|
|
225
225
|
|
|
226
226
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
@@ -234,7 +234,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
234
234
|
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
|
|
235
235
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
|
|
236
236
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
|
|
237
|
-
<dees-input-list .key=${'targets'} .label=${'Targets (
|
|
237
|
+
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
|
|
238
238
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list>
|
|
239
239
|
</dees-form>
|
|
240
240
|
`,
|
|
@@ -255,11 +255,11 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
255
255
|
const lastColon = s.lastIndexOf(':');
|
|
256
256
|
if (lastColon === -1) return null;
|
|
257
257
|
return {
|
|
258
|
-
|
|
258
|
+
ip: s.substring(0, lastColon),
|
|
259
259
|
port: parseInt(s.substring(lastColon + 1), 10),
|
|
260
260
|
};
|
|
261
261
|
})
|
|
262
|
-
.filter((t): t is {
|
|
262
|
+
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
|
263
263
|
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
|
264
264
|
|
|
265
265
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.updateTargetProfileAction, {
|
|
@@ -327,7 +327,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|
|
327
327
|
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Targets</div>
|
|
328
328
|
<div style="font-size: 14px; margin-top: 4px;">
|
|
329
329
|
${profile.targets?.length
|
|
330
|
-
? profile.targets.map(t => html`<span class="tagBadge">${t.
|
|
330
|
+
? profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)
|
|
331
331
|
: '-'}
|
|
332
332
|
</div>
|
|
333
333
|
</div>
|