@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.
Files changed (43) hide show
  1. package/dist_serve/bundle.js +2801 -2735
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/classes.dcrouter.js +18 -10
  4. package/dist_ts/config/classes.route-config-manager.d.ts +6 -1
  5. package/dist_ts/config/classes.route-config-manager.js +4 -4
  6. package/dist_ts/config/classes.target-profile-manager.d.ts +20 -7
  7. package/dist_ts/config/classes.target-profile-manager.js +68 -29
  8. package/dist_ts/db/documents/classes.vpn-client.doc.d.ts +0 -1
  9. package/dist_ts/db/documents/classes.vpn-client.doc.js +2 -8
  10. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -2
  11. package/dist_ts/opsserver/handlers/certificate.handler.js +46 -16
  12. package/dist_ts/opsserver/handlers/vpn.handler.js +1 -5
  13. package/dist_ts/vpn/classes.vpn-manager.d.ts +2 -4
  14. package/dist_ts/vpn/classes.vpn-manager.js +9 -27
  15. package/dist_ts_interfaces/data/target-profile.d.ts +1 -1
  16. package/dist_ts_interfaces/data/vpn.d.ts +0 -1
  17. package/dist_ts_interfaces/requests/vpn.d.ts +0 -2
  18. package/dist_ts_migrations/index.d.ts +28 -0
  19. package/dist_ts_migrations/index.js +44 -0
  20. package/dist_ts_web/00_commitinfo_data.js +2 -2
  21. package/dist_ts_web/appstate.d.ts +2 -4
  22. package/dist_ts_web/appstate.js +1 -3
  23. package/dist_ts_web/elements/ops-view-security.d.ts +3 -0
  24. package/dist_ts_web/elements/ops-view-security.js +75 -170
  25. package/dist_ts_web/elements/ops-view-targetprofiles.js +8 -8
  26. package/dist_ts_web/elements/ops-view-vpn.js +8 -18
  27. package/package.json +9 -8
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/classes.dcrouter.ts +19 -9
  30. package/ts/config/classes.route-config-manager.ts +7 -4
  31. package/ts/config/classes.target-profile-manager.ts +80 -28
  32. package/ts/db/documents/classes.vpn-client.doc.ts +0 -3
  33. package/ts/opsserver/handlers/certificate.handler.ts +44 -15
  34. package/ts/opsserver/handlers/vpn.handler.ts +0 -4
  35. package/ts/tspublish.json +1 -1
  36. package/ts/vpn/classes.vpn-manager.ts +8 -26
  37. package/ts_apiclient/tspublish.json +1 -1
  38. package/ts_web/00_commitinfo_data.ts +1 -1
  39. package/ts_web/appstate.ts +6 -6
  40. package/ts_web/elements/ops-view-security.ts +78 -168
  41. package/ts_web/elements/ops-view-targetprofiles.ts +9 -9
  42. package/ts_web/elements/ops-view-vpn.ts +9 -16
  43. 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
- await smartProxy.provisionCertificate(routeName);
310
- // Clear event-based status for domains in this route
311
- for (const [domain, entry] of dcRouter.certificateStatusMap) {
312
- if (entry.routeNames.includes(routeName)) {
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, then triggers provision
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 — needed to provision through SmartProxy
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, invalidate SmartAcme's cache so the next provision gets a fresh cert
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
- // Cache invalidation failed proceed with provisioning anyway
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
- // Provision through SmartProxy this triggers the full pipeline:
357
- // certProvisionFunction bridge.loadCertificatecertificate-issued event status map updated
377
+ // Trigger the full route apply pipeline:
378
+ // applyRoutesupdateRoutesprovisionCertificatesViaCallback
379
+ // certProvisionFunction(domain) → smartAcme.getCertificateForDomain →
380
+ // bridge.loadCertificate → Rust hot-swaps `loaded_certs` →
381
+ // certificate-issued event → certificateStatusMap updated
358
382
  try {
359
- await smartProxy.provisionCertificate(routeNames[0]);
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "order": 2
2
+ "order": 3
3
3
  }
@@ -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
- * Maps dcrouter-level fields (forceDestinationSmartproxy, allow/block lists)
488
- * to smartvpn's IClientSecurity with a destinationPolicy.
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
- if (!forceSmartproxy) {
504
- // Client traffic goes directly — not forced to SmartProxy
505
- security.destinationPolicy = {
506
- default: 'allow' as const,
507
- blockList: client.destinationBlockList,
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
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "order": 4
2
+ "order": 5
3
3
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.0.11',
6
+ version: '13.1.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -1015,7 +1015,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
1015
1015
  clientId: string;
1016
1016
  targetProfileIds?: string[];
1017
1017
  description?: string;
1018
- forceDestinationSmartproxy?: boolean;
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
- forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
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
- forceDestinationSmartproxy?: boolean;
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
- forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
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<{ host: string; port: number }>;
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<{ host: string; port: number }>;
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
- .tabs {
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
- <div class="tabs">
198
- <button
199
- class="tab ${this.selectedTab === 'overview' ? 'active' : ''}"
200
- @click=${() => this.selectedTab = 'overview'}
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
- <div class="securityCard">
333
- <div class="cardHeader">
334
- <h3 class="cardTitle">Blocked IP Addresses</h3>
335
- <dees-button @click=${() => this.clearBlockedIPs()}>
336
- Clear All
337
- </dees-button>
338
- </div>
339
-
340
- <div class="blockedIpList">
341
- ${metrics.blockedIPs && metrics.blockedIPs.length > 0 ? metrics.blockedIPs.map((ipAddress, index) => html`
342
- <div class="blockedIpItem">
343
- <div>
344
- <div class="ipAddress">${ipAddress}</div>
345
- <div class="blockReason">Suspicious activity</div>
346
- <div class="blockTime">Blocked</div>
347
- </div>
348
- <dees-button @click=${() => this.unblockIP(ipAddress)}>
349
- Unblock
350
- </dees-button>
351
- </div>
352
- `) : html`
353
- <p>No blocked IPs</p>
354
- `}
355
- </div>
356
- </div>
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.host}:${t.port}</span>`)}`
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 (host:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
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
- host: s.substring(0, lastColon),
200
+ ip: s.substring(0, lastColon),
201
201
  port: parseInt(s.substring(lastColon + 1), 10),
202
202
  };
203
203
  })
204
- .filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port));
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.host}:${t.port}`) || [];
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 (host:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
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
- host: s.substring(0, lastColon),
258
+ ip: s.substring(0, lastColon),
259
259
  port: parseInt(s.substring(lastColon + 1), 10),
260
260
  };
261
261
  })
262
- .filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port));
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.host}:${t.port}</span>`)
330
+ ? profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)
331
331
  : '-'}
332
332
  </div>
333
333
  </div>