@serve.zone/dcrouter 11.12.3 → 11.13.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.
Files changed (50) hide show
  1. package/dist_serve/bundle.js +705 -548
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +30 -0
  4. package/dist_ts/classes.dcrouter.js +104 -5
  5. package/dist_ts/config/classes.route-config-manager.d.ts +2 -1
  6. package/dist_ts/config/classes.route-config-manager.js +21 -5
  7. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  8. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  9. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  10. package/dist_ts/opsserver/handlers/index.js +2 -1
  11. package/dist_ts/opsserver/handlers/vpn.handler.d.ts +6 -0
  12. package/dist_ts/opsserver/handlers/vpn.handler.js +199 -0
  13. package/dist_ts/plugins.d.ts +2 -1
  14. package/dist_ts/plugins.js +3 -2
  15. package/dist_ts/vpn/classes.vpn-manager.d.ts +113 -0
  16. package/dist_ts/vpn/classes.vpn-manager.js +297 -0
  17. package/dist_ts/vpn/index.d.ts +1 -0
  18. package/dist_ts/vpn/index.js +2 -0
  19. package/dist_ts_interfaces/data/index.d.ts +1 -0
  20. package/dist_ts_interfaces/data/index.js +2 -1
  21. package/dist_ts_interfaces/data/remoteingress.d.ts +10 -1
  22. package/dist_ts_interfaces/data/vpn.d.ts +43 -0
  23. package/dist_ts_interfaces/data/vpn.js +2 -0
  24. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  25. package/dist_ts_interfaces/requests/index.js +2 -1
  26. package/dist_ts_interfaces/requests/vpn.d.ts +135 -0
  27. package/dist_ts_interfaces/requests/vpn.js +3 -0
  28. package/dist_ts_web/00_commitinfo_data.js +1 -1
  29. package/dist_ts_web/appstate.d.ts +22 -0
  30. package/dist_ts_web/appstate.js +111 -1
  31. package/dist_ts_web/elements/index.d.ts +1 -0
  32. package/dist_ts_web/elements/index.js +2 -1
  33. package/dist_ts_web/elements/ops-dashboard.js +7 -1
  34. package/dist_ts_web/elements/ops-view-vpn.d.ts +14 -0
  35. package/dist_ts_web/elements/ops-view-vpn.js +369 -0
  36. package/package.json +4 -3
  37. package/ts/00_commitinfo_data.ts +1 -1
  38. package/ts/classes.dcrouter.ts +137 -3
  39. package/ts/config/classes.route-config-manager.ts +20 -3
  40. package/ts/opsserver/classes.opsserver.ts +2 -0
  41. package/ts/opsserver/handlers/index.ts +2 -1
  42. package/ts/opsserver/handlers/vpn.handler.ts +257 -0
  43. package/ts/plugins.ts +2 -1
  44. package/ts/vpn/classes.vpn-manager.ts +378 -0
  45. package/ts/vpn/index.ts +1 -0
  46. package/ts_web/00_commitinfo_data.ts +1 -1
  47. package/ts_web/appstate.ts +164 -0
  48. package/ts_web/elements/index.ts +1 -0
  49. package/ts_web/elements/ops-dashboard.ts +6 -0
  50. package/ts_web/elements/ops-view-vpn.ts +330 -0
@@ -0,0 +1,330 @@
1
+ import {
2
+ DeesElement,
3
+ html,
4
+ customElement,
5
+ type TemplateResult,
6
+ css,
7
+ state,
8
+ cssManager,
9
+ } from '@design.estate/dees-element';
10
+ import * as appstate from '../appstate.js';
11
+ import * as interfaces from '../../dist_ts_interfaces/index.js';
12
+ import { viewHostCss } from './shared/css.js';
13
+ import { type IStatsTile } from '@design.estate/dees-catalog';
14
+
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ 'ops-view-vpn': OpsViewVpn;
18
+ }
19
+ }
20
+
21
+ @customElement('ops-view-vpn')
22
+ export class OpsViewVpn extends DeesElement {
23
+ @state()
24
+ accessor vpnState: appstate.IVpnState = appstate.vpnStatePart.getState()!;
25
+
26
+ constructor() {
27
+ super();
28
+ const sub = appstate.vpnStatePart.select().subscribe((newState) => {
29
+ this.vpnState = newState;
30
+ });
31
+ this.rxSubscriptions.push(sub);
32
+ }
33
+
34
+ async connectedCallback() {
35
+ await super.connectedCallback();
36
+ await appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null);
37
+ }
38
+
39
+ public static styles = [
40
+ cssManager.defaultStyles,
41
+ viewHostCss,
42
+ css`
43
+ .vpnContainer {
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 24px;
47
+ }
48
+
49
+ .statusBadge {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ padding: 3px 10px;
53
+ border-radius: 12px;
54
+ font-size: 12px;
55
+ font-weight: 600;
56
+ letter-spacing: 0.02em;
57
+ text-transform: uppercase;
58
+ }
59
+
60
+ .statusBadge.enabled {
61
+ background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
62
+ color: ${cssManager.bdTheme('#166534', '#4ade80')};
63
+ }
64
+
65
+ .statusBadge.disabled {
66
+ background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
67
+ color: ${cssManager.bdTheme('#991b1b', '#f87171')};
68
+ }
69
+
70
+ .configDialog {
71
+ padding: 16px;
72
+ background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
73
+ border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')};
74
+ border-radius: 8px;
75
+ margin-bottom: 16px;
76
+ }
77
+
78
+ .configDialog pre {
79
+ display: block;
80
+ padding: 12px;
81
+ background: ${cssManager.bdTheme('#1f2937', '#111827')};
82
+ color: #10b981;
83
+ border-radius: 4px;
84
+ font-family: monospace;
85
+ font-size: 12px;
86
+ white-space: pre-wrap;
87
+ word-break: break-all;
88
+ margin: 8px 0;
89
+ user-select: all;
90
+ max-height: 300px;
91
+ overflow-y: auto;
92
+ }
93
+
94
+ .configDialog .warning {
95
+ font-size: 12px;
96
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
97
+ margin-top: 8px;
98
+ }
99
+
100
+ .tagBadge {
101
+ display: inline-flex;
102
+ padding: 2px 8px;
103
+ border-radius: 4px;
104
+ font-size: 12px;
105
+ font-weight: 500;
106
+ background: ${cssManager.bdTheme('#eff6ff', '#172554')};
107
+ color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
108
+ margin-right: 4px;
109
+ }
110
+
111
+ .serverInfo {
112
+ display: grid;
113
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
114
+ gap: 12px;
115
+ padding: 16px;
116
+ background: ${cssManager.bdTheme('#f9fafb', '#111827')};
117
+ border-radius: 8px;
118
+ border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#1f2937')};
119
+ }
120
+
121
+ .serverInfo .infoItem {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: 4px;
125
+ }
126
+
127
+ .serverInfo .infoLabel {
128
+ font-size: 11px;
129
+ font-weight: 600;
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.05em;
132
+ color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
133
+ }
134
+
135
+ .serverInfo .infoValue {
136
+ font-size: 14px;
137
+ font-family: monospace;
138
+ color: ${cssManager.bdTheme('#111827', '#f9fafb')};
139
+ }
140
+ `,
141
+ ];
142
+
143
+ render(): TemplateResult {
144
+ const status = this.vpnState.status;
145
+ const clients = this.vpnState.clients;
146
+ const connectedCount = status?.connectedClients ?? 0;
147
+ const totalClients = clients.length;
148
+ const enabledClients = clients.filter(c => c.enabled).length;
149
+
150
+ const statsTiles: IStatsTile[] = [
151
+ {
152
+ id: 'totalClients',
153
+ title: 'Total Clients',
154
+ type: 'number',
155
+ value: totalClients,
156
+ icon: 'lucide:users',
157
+ description: 'Registered VPN clients',
158
+ color: '#3b82f6',
159
+ },
160
+ {
161
+ id: 'connectedClients',
162
+ title: 'Connected',
163
+ type: 'number',
164
+ value: connectedCount,
165
+ icon: 'lucide:link',
166
+ description: 'Currently connected',
167
+ color: '#10b981',
168
+ },
169
+ {
170
+ id: 'enabledClients',
171
+ title: 'Enabled',
172
+ type: 'number',
173
+ value: enabledClients,
174
+ icon: 'lucide:shieldCheck',
175
+ description: 'Active client registrations',
176
+ color: '#8b5cf6',
177
+ },
178
+ {
179
+ id: 'serverStatus',
180
+ title: 'Server',
181
+ type: 'text',
182
+ value: status?.running ? 'Running' : 'Stopped',
183
+ icon: 'lucide:server',
184
+ description: status?.running ? `${status.forwardingMode} mode` : 'VPN server not running',
185
+ color: status?.running ? '#10b981' : '#ef4444',
186
+ },
187
+ ];
188
+
189
+ return html`
190
+ <ops-sectionheading>VPN</ops-sectionheading>
191
+
192
+ ${this.vpnState.newClientConfig ? html`
193
+ <div class="configDialog">
194
+ <strong>Client created successfully!</strong>
195
+ <div class="warning">Copy the WireGuard config now. It contains private keys that won't be shown again.</div>
196
+ <pre>${this.vpnState.newClientConfig}</pre>
197
+ <dees-button
198
+ @click=${async () => {
199
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
200
+ await navigator.clipboard.writeText(this.vpnState.newClientConfig!);
201
+ }
202
+ const { DeesToast } = await import('@design.estate/dees-catalog');
203
+ DeesToast.createAndShow({ message: 'Config copied to clipboard', type: 'success', duration: 3000 });
204
+ }}
205
+ >Copy to Clipboard</dees-button>
206
+ <dees-button
207
+ @click=${() => {
208
+ const blob = new Blob([this.vpnState.newClientConfig!], { type: 'text/plain' });
209
+ const url = URL.createObjectURL(blob);
210
+ const a = document.createElement('a');
211
+ a.href = url;
212
+ a.download = 'wireguard.conf';
213
+ a.click();
214
+ URL.revokeObjectURL(url);
215
+ }}
216
+ >Download .conf</dees-button>
217
+ <dees-button
218
+ @click=${() => appstate.vpnStatePart.dispatchAction(appstate.clearNewClientConfigAction, null)}
219
+ >Dismiss</dees-button>
220
+ </div>
221
+ ` : ''}
222
+
223
+ <dees-statsgrid .statsTiles=${statsTiles}></dees-statsgrid>
224
+
225
+ ${status ? html`
226
+ <div class="serverInfo">
227
+ <div class="infoItem">
228
+ <span class="infoLabel">Subnet</span>
229
+ <span class="infoValue">${status.subnet}</span>
230
+ </div>
231
+ <div class="infoItem">
232
+ <span class="infoLabel">WireGuard Port</span>
233
+ <span class="infoValue">${status.wgListenPort}</span>
234
+ </div>
235
+ <div class="infoItem">
236
+ <span class="infoLabel">Forwarding Mode</span>
237
+ <span class="infoValue">${status.forwardingMode}</span>
238
+ </div>
239
+ ${status.serverPublicKeys ? html`
240
+ <div class="infoItem">
241
+ <span class="infoLabel">WG Public Key</span>
242
+ <span class="infoValue" style="font-size: 11px; word-break: break-all;">${status.serverPublicKeys.wgPublicKey}</span>
243
+ </div>
244
+ ` : ''}
245
+ </div>
246
+ ` : ''}
247
+
248
+ <dees-table
249
+ .heading1=${'VPN Clients'}
250
+ .heading2=${'Manage WireGuard and SmartVPN client registrations'}
251
+ .data=${clients}
252
+ .displayFunction=${(client: interfaces.data.IVpnClient) => ({
253
+ 'Client ID': client.clientId,
254
+ 'Status': client.enabled
255
+ ? html`<span class="statusBadge enabled">enabled</span>`
256
+ : html`<span class="statusBadge disabled">disabled</span>`,
257
+ 'VPN IP': client.assignedIp || '-',
258
+ 'Tags': client.tags?.length
259
+ ? html`${client.tags.map(t => html`<span class="tagBadge">${t}</span>`)}`
260
+ : '-',
261
+ 'Description': client.description || '-',
262
+ 'Created': new Date(client.createdAt).toLocaleDateString(),
263
+ })}
264
+ .dataActions=${[
265
+ {
266
+ name: 'Toggle',
267
+ iconName: 'lucide:power',
268
+ action: async (client: interfaces.data.IVpnClient) => {
269
+ await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
270
+ clientId: client.clientId,
271
+ enabled: !client.enabled,
272
+ });
273
+ },
274
+ },
275
+ {
276
+ name: 'Delete',
277
+ iconName: 'lucide:trash2',
278
+ action: async (client: interfaces.data.IVpnClient) => {
279
+ const { DeesModal } = await import('@design.estate/dees-catalog');
280
+ DeesModal.createAndShow({
281
+ heading: 'Delete VPN Client',
282
+ content: html`<p>Are you sure you want to delete client "${client.clientId}"?</p>`,
283
+ menuOptions: [
284
+ { name: 'Cancel', action: async (modal: any) => modal.destroy() },
285
+ {
286
+ name: 'Delete',
287
+ action: async (modal: any) => {
288
+ await appstate.vpnStatePart.dispatchAction(appstate.deleteVpnClientAction, client.clientId);
289
+ modal.destroy();
290
+ },
291
+ },
292
+ ],
293
+ });
294
+ },
295
+ },
296
+ ]}
297
+ .createNewItem=${async () => {
298
+ const { DeesModal, DeesForm, DeesInputText } = await import('@design.estate/dees-catalog');
299
+ DeesModal.createAndShow({
300
+ heading: 'Create VPN Client',
301
+ content: html`
302
+ <dees-form>
303
+ <dees-input-text id="clientId" .label=${'Client ID'} .key=${'clientId'} required></dees-input-text>
304
+ <dees-input-text id="description" .label=${'Description'} .key=${'description'}></dees-input-text>
305
+ <dees-input-text id="tags" .label=${'Tags (comma-separated)'} .key=${'tags'}></dees-input-text>
306
+ </dees-form>
307
+ `,
308
+ menuOptions: [
309
+ { name: 'Cancel', action: async (modal: any) => modal.destroy() },
310
+ {
311
+ name: 'Create',
312
+ action: async (modal: any) => {
313
+ const form = modal.shadowRoot!.querySelector('dees-form') as any;
314
+ const data = await form.collectFormData();
315
+ const tags = data.tags ? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined;
316
+ await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
317
+ clientId: data.clientId,
318
+ description: data.description || undefined,
319
+ tags,
320
+ });
321
+ modal.destroy();
322
+ },
323
+ },
324
+ ],
325
+ });
326
+ }}
327
+ ></dees-table>
328
+ `;
329
+ }
330
+ }