@serve.zone/dcrouter 15.0.1 → 15.0.3
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/deno.json +1 -1
- package/dist_serve/bundle.js +768 -768
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +21 -139
- package/dist_ts/classes.dcrouter.js +71 -1585
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
- package/dist_ts/email/classes.accepted-email-spool.js +345 -0
- package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
- package/dist_ts/email/classes.email-route-builder.js +260 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
- package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
- package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
- package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
- package/dist_ts/vpn/index.d.ts +1 -0
- package/dist_ts/vpn/index.js +2 -1
- package/dist_ts_migrations/index.js +92 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/acme.d.ts +17 -0
- package/dist_ts_web/appstate/acme.js +64 -0
- package/dist_ts_web/appstate/certificates.d.ts +37 -0
- package/dist_ts_web/appstate/certificates.js +107 -0
- package/dist_ts_web/appstate/config.d.ts +9 -0
- package/dist_ts_web/appstate/config.js +35 -0
- package/dist_ts_web/appstate/domains.d.ts +80 -0
- package/dist_ts_web/appstate/domains.js +324 -0
- package/dist_ts_web/appstate/email-domains.d.ts +25 -0
- package/dist_ts_web/appstate/email-domains.js +104 -0
- package/dist_ts_web/appstate/email-ops.d.ts +10 -0
- package/dist_ts_web/appstate/email-ops.js +40 -0
- package/dist_ts_web/appstate/login.d.ts +30 -0
- package/dist_ts_web/appstate/login.js +83 -0
- package/dist_ts_web/appstate/logs.d.ts +16 -0
- package/dist_ts_web/appstate/logs.js +27 -0
- package/dist_ts_web/appstate/network.d.ts +50 -0
- package/dist_ts_web/appstate/network.js +122 -0
- package/dist_ts_web/appstate/profiles-targets.d.ts +45 -0
- package/dist_ts_web/appstate/profiles-targets.js +173 -0
- package/dist_ts_web/appstate/remoteingress.d.ts +47 -0
- package/dist_ts_web/appstate/remoteingress.js +204 -0
- package/dist_ts_web/appstate/routes.d.ts +76 -0
- package/dist_ts_web/appstate/routes.js +316 -0
- package/dist_ts_web/appstate/runtime.d.ts +1 -0
- package/dist_ts_web/appstate/runtime.js +276 -0
- package/dist_ts_web/appstate/security.d.ts +29 -0
- package/dist_ts_web/appstate/security.js +167 -0
- package/dist_ts_web/appstate/shared.d.ts +3 -0
- package/dist_ts_web/appstate/shared.js +13 -0
- package/dist_ts_web/appstate/stats.d.ts +15 -0
- package/dist_ts_web/appstate/stats.js +59 -0
- package/dist_ts_web/appstate/target-profiles.d.ts +37 -0
- package/dist_ts_web/appstate/target-profiles.js +118 -0
- package/dist_ts_web/appstate/ui.d.ts +11 -0
- package/dist_ts_web/appstate/ui.js +55 -0
- package/dist_ts_web/appstate/users.d.ts +27 -0
- package/dist_ts_web/appstate/users.js +85 -0
- package/dist_ts_web/appstate/vpn.d.ts +44 -0
- package/dist_ts_web/appstate/vpn.js +148 -0
- package/dist_ts_web/appstate.d.ts +20 -568
- package/dist_ts_web/appstate.js +24 -2418
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +118 -1919
- package/ts/dns/classes.dns-server-runtime.ts +525 -0
- package/ts/dns/index.ts +1 -0
- package/ts/email/classes.accepted-email-spool.ts +434 -0
- package/ts/email/classes.email-route-builder.ts +312 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
- package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
- package/ts/remoteingress/index.ts +1 -0
- package/ts/security/classes.route-policy-augmenter.ts +140 -0
- package/ts/security/index.ts +1 -0
- package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
- package/ts/vpn/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/acme.ts +93 -0
- package/ts_web/appstate/certificates.ts +159 -0
- package/ts_web/appstate/config.ts +49 -0
- package/ts_web/appstate/domains.ts +429 -0
- package/ts_web/appstate/email-domains.ts +155 -0
- package/ts_web/appstate/email-ops.ts +57 -0
- package/ts_web/appstate/login.ts +128 -0
- package/ts_web/appstate/logs.ts +50 -0
- package/ts_web/appstate/network.ts +161 -0
- package/ts_web/appstate/profiles-targets.ts +240 -0
- package/ts_web/appstate/remoteingress.ts +300 -0
- package/ts_web/appstate/routes.ts +447 -0
- package/ts_web/appstate/runtime.ts +308 -0
- package/ts_web/appstate/security.ts +229 -0
- package/ts_web/appstate/shared.ts +15 -0
- package/ts_web/appstate/stats.ts +79 -0
- package/ts_web/appstate/target-profiles.ts +164 -0
- package/ts_web/appstate/ui.ts +75 -0
- package/ts_web/appstate/users.ts +133 -0
- package/ts_web/appstate/vpn.ts +234 -0
- package/ts_web/appstate.ts +24 -3403
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { runBackgroundRefresh } from './shared.js';
|
|
4
|
+
import { getActionContext, loginStatePart, logoutAction } from './login.js';
|
|
5
|
+
import { uiStatePart } from './ui.js';
|
|
6
|
+
import { statsStatePart } from './stats.js';
|
|
7
|
+
import { logStatePart, type ILogState } from './logs.js';
|
|
8
|
+
import { networkStatePart, refreshNetworkIpIntelligence } from './network.js';
|
|
9
|
+
import { securityPolicyStatePart, fetchSecurityPolicyAction } from './security.js';
|
|
10
|
+
import { certificateStatePart, fetchCertificateOverviewAction } from './certificates.js';
|
|
11
|
+
import { remoteIngressStatePart, fetchRemoteIngressAction } from './remoteingress.js';
|
|
12
|
+
import { vpnStatePart, fetchVpnAction } from './vpn.js';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// TypedSocket Client for Real-time Log Streaming
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
let socketClient: plugins.typedsocket.TypedSocket | null = null;
|
|
20
|
+
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
|
21
|
+
|
|
22
|
+
// Batched log entry handler — buffers incoming entries and flushes once per animation frame
|
|
23
|
+
let logEntryBuffer: interfaces.data.ILogEntry[] = [];
|
|
24
|
+
let logFlushScheduled = false;
|
|
25
|
+
|
|
26
|
+
function flushLogEntries() {
|
|
27
|
+
logFlushScheduled = false;
|
|
28
|
+
if (logEntryBuffer.length === 0) return;
|
|
29
|
+
const current = logStatePart.getState()!;
|
|
30
|
+
const updated = [...current.recentLogs, ...logEntryBuffer];
|
|
31
|
+
logEntryBuffer = [];
|
|
32
|
+
// Cap at 2000 entries
|
|
33
|
+
if (updated.length > 2000) {
|
|
34
|
+
updated.splice(0, updated.length - 2000);
|
|
35
|
+
}
|
|
36
|
+
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Register handler for pushed log entries from the server
|
|
40
|
+
socketRouter.addTypedHandler(
|
|
41
|
+
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
|
|
42
|
+
'pushLogEntry',
|
|
43
|
+
async (dataArg) => {
|
|
44
|
+
logEntryBuffer.push(dataArg.entry);
|
|
45
|
+
if (!logFlushScheduled) {
|
|
46
|
+
logFlushScheduled = true;
|
|
47
|
+
requestAnimationFrame(flushLogEntries);
|
|
48
|
+
}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
async function connectSocket() {
|
|
55
|
+
if (socketClient) return;
|
|
56
|
+
try {
|
|
57
|
+
socketClient = await plugins.typedsocket.TypedSocket.createClient(
|
|
58
|
+
socketRouter,
|
|
59
|
+
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
|
|
60
|
+
);
|
|
61
|
+
await socketClient.setTag('role', 'ops_dashboard');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('TypedSocket connection failed:', err);
|
|
64
|
+
socketClient = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function disconnectSocket() {
|
|
69
|
+
if (socketClient) {
|
|
70
|
+
try {
|
|
71
|
+
await socketClient.stop();
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore disconnect errors
|
|
74
|
+
}
|
|
75
|
+
socketClient = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// In-flight guard to prevent concurrent refresh requests
|
|
80
|
+
let isRefreshing = false;
|
|
81
|
+
|
|
82
|
+
// Combined refresh action for efficient polling
|
|
83
|
+
async function dispatchCombinedRefreshAction() {
|
|
84
|
+
if (isRefreshing) return;
|
|
85
|
+
isRefreshing = true;
|
|
86
|
+
try {
|
|
87
|
+
await dispatchCombinedRefreshActionInner();
|
|
88
|
+
} finally {
|
|
89
|
+
isRefreshing = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function dispatchCombinedRefreshActionInner() {
|
|
94
|
+
const context = getActionContext();
|
|
95
|
+
if (!context.identity) return;
|
|
96
|
+
const currentView = uiStatePart.getState()!.activeView;
|
|
97
|
+
const currentSubview = uiStatePart.getState()!.activeSubview;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Always fetch basic stats for dashboard widgets
|
|
101
|
+
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
102
|
+
interfaces.requests.IReq_GetCombinedMetrics
|
|
103
|
+
>('/typedrequest', 'getCombinedMetrics');
|
|
104
|
+
|
|
105
|
+
const combinedResponse = await combinedRequest.fire({
|
|
106
|
+
identity: context.identity,
|
|
107
|
+
sections: {
|
|
108
|
+
server: true,
|
|
109
|
+
email: true,
|
|
110
|
+
dns: true,
|
|
111
|
+
security: true,
|
|
112
|
+
network: currentView === 'network' && currentSubview === 'activity',
|
|
113
|
+
radius: true,
|
|
114
|
+
vpn: true,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Update all stats from combined response
|
|
119
|
+
const currentStatsState = statsStatePart.getState()!;
|
|
120
|
+
statsStatePart.setState({
|
|
121
|
+
...currentStatsState,
|
|
122
|
+
serverStats: combinedResponse.metrics.server || currentStatsState.serverStats,
|
|
123
|
+
emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
|
|
124
|
+
dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
|
|
125
|
+
securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
|
|
126
|
+
radiusStats: combinedResponse.metrics.radius || currentStatsState.radiusStats,
|
|
127
|
+
vpnStats: combinedResponse.metrics.vpn || currentStatsState.vpnStats,
|
|
128
|
+
lastUpdated: Date.now(),
|
|
129
|
+
isLoading: false,
|
|
130
|
+
error: null,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Update network stats if included
|
|
134
|
+
if (combinedResponse.metrics.network && currentView === 'network') {
|
|
135
|
+
const network = combinedResponse.metrics.network;
|
|
136
|
+
const connectionsByIP: { [ip: string]: number } = {};
|
|
137
|
+
|
|
138
|
+
// Build connectionsByIP from connectionDetails (now populated with real per-IP data)
|
|
139
|
+
network.connectionDetails.forEach(conn => {
|
|
140
|
+
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + (conn.connectionCount || 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Build connections from connectionDetails (real per-IP aggregates)
|
|
144
|
+
const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({
|
|
145
|
+
id: `ip-${conn.remoteAddress}`,
|
|
146
|
+
remoteAddress: conn.remoteAddress,
|
|
147
|
+
localAddress: 'server',
|
|
148
|
+
startTime: conn.startTime,
|
|
149
|
+
protocol: conn.protocol as any,
|
|
150
|
+
state: conn.state as any,
|
|
151
|
+
bytesReceived: conn.bytesIn,
|
|
152
|
+
bytesSent: conn.bytesOut,
|
|
153
|
+
connectionCount: conn.connectionCount,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
networkStatePart.setState({
|
|
157
|
+
...networkStatePart.getState()!,
|
|
158
|
+
connections,
|
|
159
|
+
connectionsByIP,
|
|
160
|
+
throughputRate: {
|
|
161
|
+
bytesInPerSecond: network.totalBandwidth.in,
|
|
162
|
+
bytesOutPerSecond: network.totalBandwidth.out,
|
|
163
|
+
},
|
|
164
|
+
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
|
165
|
+
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })),
|
|
166
|
+
topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({
|
|
167
|
+
ip: e.endpoint,
|
|
168
|
+
count: e.connections,
|
|
169
|
+
bwIn: e.bandwidth?.in || 0,
|
|
170
|
+
bwOut: e.bandwidth?.out || 0,
|
|
171
|
+
})),
|
|
172
|
+
topASNs: network.topASNs || [],
|
|
173
|
+
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
|
174
|
+
domainActivity: network.domainActivity || [],
|
|
175
|
+
throughputHistory: network.throughputHistory || [],
|
|
176
|
+
requestsPerSecond: network.requestsPerSecond || 0,
|
|
177
|
+
requestsTotal: network.requestsTotal || 0,
|
|
178
|
+
backends: network.backends || [],
|
|
179
|
+
frontendProtocols: network.frontendProtocols || null,
|
|
180
|
+
backendProtocols: network.backendProtocols || null,
|
|
181
|
+
lastUpdated: Date.now(),
|
|
182
|
+
isLoading: false,
|
|
183
|
+
error: null,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
refreshNetworkIpIntelligence(context.identity, [
|
|
187
|
+
...network.connectionDetails.map((conn) => conn.remoteAddress),
|
|
188
|
+
...network.topEndpoints.map((endpoint) => endpoint.endpoint),
|
|
189
|
+
...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (currentView === 'security') {
|
|
194
|
+
runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
|
|
195
|
+
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Refresh certificate data if on Domains > Certificates subview
|
|
200
|
+
if (currentView === 'domains' && currentSubview === 'certificates') {
|
|
201
|
+
runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
|
|
202
|
+
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Refresh remote ingress data if on the Network → Remote Ingress subview
|
|
207
|
+
if (currentView === 'network' && currentSubview === 'remoteingress') {
|
|
208
|
+
runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
|
|
209
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Refresh VPN data if on the Network → VPN subview
|
|
214
|
+
if (currentView === 'network' && currentSubview === 'vpn') {
|
|
215
|
+
runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
|
|
216
|
+
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Combined refresh failed:', error);
|
|
221
|
+
// If the error looks like an auth failure (invalid JWT), force re-login
|
|
222
|
+
const errMsg = String(error);
|
|
223
|
+
if (errMsg.includes('invalid') || errMsg.includes('unauthorized') || errMsg.includes('401')) {
|
|
224
|
+
await loginStatePart.dispatchAction(logoutAction, null);
|
|
225
|
+
window.location.reload();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create a proper action for the combined refresh so we can use createScheduledAction
|
|
231
|
+
const combinedRefreshAction = statsStatePart.createAction<void>(async (statePartArg) => {
|
|
232
|
+
await dispatchCombinedRefreshAction();
|
|
233
|
+
// Return current state — dispatchCombinedRefreshAction already updates all state parts directly
|
|
234
|
+
return statePartArg.getState()!;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Scheduled refresh process with autoPause: 'visibility' — automatically pauses when tab is hidden
|
|
238
|
+
let refreshProcess: ReturnType<typeof statsStatePart.createScheduledAction> | null = null;
|
|
239
|
+
|
|
240
|
+
const startAutoRefresh = () => {
|
|
241
|
+
const uiState = uiStatePart.getState()!;
|
|
242
|
+
const loginState = loginStatePart.getState()!;
|
|
243
|
+
|
|
244
|
+
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
|
245
|
+
// Dispose old process if interval changed or not running
|
|
246
|
+
if (refreshProcess) {
|
|
247
|
+
refreshProcess.dispose();
|
|
248
|
+
refreshProcess = null;
|
|
249
|
+
}
|
|
250
|
+
refreshProcess = statsStatePart.createScheduledAction({
|
|
251
|
+
action: combinedRefreshAction,
|
|
252
|
+
payload: undefined,
|
|
253
|
+
intervalMs: uiState.refreshInterval,
|
|
254
|
+
autoPause: 'visibility',
|
|
255
|
+
});
|
|
256
|
+
} else {
|
|
257
|
+
if (refreshProcess) {
|
|
258
|
+
refreshProcess.dispose();
|
|
259
|
+
refreshProcess = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Watch for relevant changes
|
|
265
|
+
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
|
|
266
|
+
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
|
|
267
|
+
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
|
|
268
|
+
|
|
269
|
+
uiStatePart.select((s) => ({ autoRefresh: s.autoRefresh, refreshInterval: s.refreshInterval }))
|
|
270
|
+
.subscribe((state) => {
|
|
271
|
+
if (state.autoRefresh !== previousAutoRefresh ||
|
|
272
|
+
state.refreshInterval !== previousRefreshInterval) {
|
|
273
|
+
previousAutoRefresh = state.autoRefresh;
|
|
274
|
+
previousRefreshInterval = state.refreshInterval;
|
|
275
|
+
startAutoRefresh();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
loginStatePart.select((s) => s.isLoggedIn).subscribe((isLoggedIn) => {
|
|
280
|
+
if (isLoggedIn !== previousIsLoggedIn) {
|
|
281
|
+
previousIsLoggedIn = isLoggedIn;
|
|
282
|
+
startAutoRefresh();
|
|
283
|
+
|
|
284
|
+
// Connect/disconnect TypedSocket based on login state
|
|
285
|
+
if (isLoggedIn) {
|
|
286
|
+
connectSocket();
|
|
287
|
+
} else {
|
|
288
|
+
disconnectSocket();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Pause/resume WebSocket when tab visibility changes
|
|
294
|
+
document.addEventListener('visibilitychange', () => {
|
|
295
|
+
if (document.hidden) {
|
|
296
|
+
disconnectSocket();
|
|
297
|
+
} else if (loginStatePart.getState()!.isLoggedIn) {
|
|
298
|
+
connectSocket();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Initial start
|
|
303
|
+
startAutoRefresh();
|
|
304
|
+
|
|
305
|
+
// Connect TypedSocket if already logged in (e.g., persistent session)
|
|
306
|
+
if (loginStatePart.getState()!.isLoggedIn) {
|
|
307
|
+
connectSocket();
|
|
308
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { appState } from './shared.js';
|
|
4
|
+
import { getActionContext } from './login.js';
|
|
5
|
+
import { runBackgroundRefresh } from './shared.js';
|
|
6
|
+
|
|
7
|
+
export interface ISecurityPolicyState {
|
|
8
|
+
rules: interfaces.data.ISecurityBlockRule[];
|
|
9
|
+
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
|
10
|
+
compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
|
|
11
|
+
auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
lastUpdated: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
|
|
18
|
+
'securityPolicy',
|
|
19
|
+
{
|
|
20
|
+
rules: [],
|
|
21
|
+
ipIntelligence: [],
|
|
22
|
+
compiledPolicy: null,
|
|
23
|
+
auditEvents: [],
|
|
24
|
+
isLoading: false,
|
|
25
|
+
error: null,
|
|
26
|
+
lastUpdated: 0,
|
|
27
|
+
},
|
|
28
|
+
'soft',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void {
|
|
32
|
+
runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => {
|
|
33
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
34
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
35
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
36
|
+
const intelligenceResponse = await intelligenceRequest.fire({
|
|
37
|
+
identity,
|
|
38
|
+
limit: 500,
|
|
39
|
+
});
|
|
40
|
+
securityPolicyStatePart.setState({
|
|
41
|
+
...securityPolicyStatePart.getState()!,
|
|
42
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// Security Policy Actions
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
52
|
+
async (statePartArg): Promise<ISecurityPolicyState> => {
|
|
53
|
+
const context = getActionContext();
|
|
54
|
+
const currentState = statePartArg.getState()!;
|
|
55
|
+
if (!context.identity) return currentState;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
59
|
+
interfaces.requests.IReq_ListSecurityBlockRules
|
|
60
|
+
>('/typedrequest', 'listSecurityBlockRules');
|
|
61
|
+
const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
62
|
+
interfaces.requests.IReq_GetCompiledSecurityPolicy
|
|
63
|
+
>('/typedrequest', 'getCompiledSecurityPolicy');
|
|
64
|
+
const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
65
|
+
interfaces.requests.IReq_ListSecurityPolicyAudit
|
|
66
|
+
>('/typedrequest', 'listSecurityPolicyAudit');
|
|
67
|
+
|
|
68
|
+
const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
|
|
69
|
+
rulesRequest.fire({ identity: context.identity }),
|
|
70
|
+
compiledPolicyRequest.fire({ identity: context.identity }),
|
|
71
|
+
auditRequest.fire({ identity: context.identity, limit: 100 }),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
refreshSecurityIpIntelligence(context.identity);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
rules: rulesResponse.rules || [],
|
|
78
|
+
ipIntelligence: currentState.ipIntelligence,
|
|
79
|
+
compiledPolicy: compiledPolicyResponse.policy,
|
|
80
|
+
auditEvents: auditResponse.events || [],
|
|
81
|
+
isLoading: false,
|
|
82
|
+
error: null,
|
|
83
|
+
lastUpdated: Date.now(),
|
|
84
|
+
};
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
return {
|
|
87
|
+
...currentState,
|
|
88
|
+
isLoading: false,
|
|
89
|
+
error: error instanceof Error ? error.message : 'Failed to fetch security policy',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
|
96
|
+
type: interfaces.data.TSecurityBlockRuleType;
|
|
97
|
+
value: string;
|
|
98
|
+
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
|
99
|
+
reason?: string;
|
|
100
|
+
enabled?: boolean;
|
|
101
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
|
102
|
+
const context = getActionContext();
|
|
103
|
+
const currentState = statePartArg.getState()!;
|
|
104
|
+
if (!context.identity) return currentState;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
108
|
+
interfaces.requests.IReq_CreateSecurityBlockRule
|
|
109
|
+
>('/typedrequest', 'createSecurityBlockRule');
|
|
110
|
+
|
|
111
|
+
const response = await request.fire({
|
|
112
|
+
identity: context.identity,
|
|
113
|
+
type: dataArg.type,
|
|
114
|
+
value: dataArg.value,
|
|
115
|
+
matchMode: dataArg.matchMode,
|
|
116
|
+
reason: dataArg.reason,
|
|
117
|
+
enabled: dataArg.enabled,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.success) {
|
|
121
|
+
return { ...currentState, error: response.message || 'Failed to create security block rule' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
125
|
+
} catch (error: unknown) {
|
|
126
|
+
return {
|
|
127
|
+
...currentState,
|
|
128
|
+
error: error instanceof Error ? error.message : 'Failed to create security block rule',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
|
134
|
+
id: string;
|
|
135
|
+
value?: string;
|
|
136
|
+
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
|
137
|
+
reason?: string;
|
|
138
|
+
enabled?: boolean;
|
|
139
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
|
140
|
+
const context = getActionContext();
|
|
141
|
+
const currentState = statePartArg.getState()!;
|
|
142
|
+
if (!context.identity) return currentState;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
146
|
+
interfaces.requests.IReq_UpdateSecurityBlockRule
|
|
147
|
+
>('/typedrequest', 'updateSecurityBlockRule');
|
|
148
|
+
|
|
149
|
+
const response = await request.fire({
|
|
150
|
+
identity: context.identity,
|
|
151
|
+
id: dataArg.id,
|
|
152
|
+
value: dataArg.value,
|
|
153
|
+
matchMode: dataArg.matchMode,
|
|
154
|
+
reason: dataArg.reason,
|
|
155
|
+
enabled: dataArg.enabled,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.success) {
|
|
159
|
+
return { ...currentState, error: response.message || 'Failed to update security block rule' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
163
|
+
} catch (error: unknown) {
|
|
164
|
+
return {
|
|
165
|
+
...currentState,
|
|
166
|
+
error: error instanceof Error ? error.message : 'Failed to update security block rule',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
|
|
172
|
+
async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
|
|
173
|
+
const context = getActionContext();
|
|
174
|
+
const currentState = statePartArg.getState()!;
|
|
175
|
+
if (!context.identity) return currentState;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
179
|
+
interfaces.requests.IReq_DeleteSecurityBlockRule
|
|
180
|
+
>('/typedrequest', 'deleteSecurityBlockRule');
|
|
181
|
+
|
|
182
|
+
const response = await request.fire({ identity: context.identity, id: ruleId });
|
|
183
|
+
if (!response.success) {
|
|
184
|
+
return { ...currentState, error: response.message || 'Failed to delete security block rule' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
188
|
+
} catch (error: unknown) {
|
|
189
|
+
return {
|
|
190
|
+
...currentState,
|
|
191
|
+
error: error instanceof Error ? error.message : 'Failed to delete security block rule',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
|
|
198
|
+
async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
|
|
199
|
+
const context = getActionContext();
|
|
200
|
+
const currentState = statePartArg.getState()!;
|
|
201
|
+
if (!context.identity) return currentState;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
205
|
+
interfaces.requests.IReq_RefreshIpIntelligence
|
|
206
|
+
>('/typedrequest', 'refreshIpIntelligence');
|
|
207
|
+
const response = await request.fire({ identity: context.identity, ipAddress });
|
|
208
|
+
if (!response.success) {
|
|
209
|
+
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
|
210
|
+
}
|
|
211
|
+
const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
212
|
+
if (!response.record) return refreshedState;
|
|
213
|
+
return {
|
|
214
|
+
...refreshedState,
|
|
215
|
+
ipIntelligence: [
|
|
216
|
+
response.record,
|
|
217
|
+
...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress),
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
} catch (error: unknown) {
|
|
221
|
+
return {
|
|
222
|
+
...currentState,
|
|
223
|
+
error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
|
|
3
|
+
// Create main app state instance
|
|
4
|
+
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
|
5
|
+
|
|
6
|
+
const backgroundRefreshesInFlight = new Set<string>();
|
|
7
|
+
|
|
8
|
+
export function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): void {
|
|
9
|
+
if (backgroundRefreshesInFlight.has(key)) return;
|
|
10
|
+
backgroundRefreshesInFlight.add(key);
|
|
11
|
+
void task()
|
|
12
|
+
.catch((error) => console.error(errorMessage, error))
|
|
13
|
+
.finally(() => backgroundRefreshesInFlight.delete(key));
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { appState } from './shared.js';
|
|
4
|
+
import { getActionContext } from './login.js';
|
|
5
|
+
|
|
6
|
+
export interface IStatsState {
|
|
7
|
+
serverStats: interfaces.data.IServerStats | null;
|
|
8
|
+
emailStats: interfaces.data.IEmailStats | null;
|
|
9
|
+
dnsStats: interfaces.data.IDnsStats | null;
|
|
10
|
+
securityMetrics: interfaces.data.ISecurityMetrics | null;
|
|
11
|
+
radiusStats: interfaces.data.IRadiusStats | null;
|
|
12
|
+
vpnStats: interfaces.data.IVpnStats | null;
|
|
13
|
+
lastUpdated: number;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
error: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const statsStatePart = await appState.getStatePart<IStatsState>(
|
|
19
|
+
'stats',
|
|
20
|
+
{
|
|
21
|
+
serverStats: null,
|
|
22
|
+
emailStats: null,
|
|
23
|
+
dnsStats: null,
|
|
24
|
+
securityMetrics: null,
|
|
25
|
+
radiusStats: null,
|
|
26
|
+
vpnStats: null,
|
|
27
|
+
lastUpdated: 0,
|
|
28
|
+
isLoading: false,
|
|
29
|
+
error: null,
|
|
30
|
+
},
|
|
31
|
+
'soft' // Stats are cached but not persisted
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Fetch All Stats Action - Using combined endpoint for efficiency
|
|
35
|
+
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg): Promise<IStatsState> => {
|
|
36
|
+
const context = getActionContext();
|
|
37
|
+
const currentState = statePartArg.getState()!;
|
|
38
|
+
if (!context.identity) return currentState;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Use combined metrics endpoint - single request instead of 4
|
|
42
|
+
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
43
|
+
interfaces.requests.IReq_GetCombinedMetrics
|
|
44
|
+
>('/typedrequest', 'getCombinedMetrics');
|
|
45
|
+
|
|
46
|
+
const combinedResponse = await combinedRequest.fire({
|
|
47
|
+
identity: context.identity,
|
|
48
|
+
sections: {
|
|
49
|
+
server: true,
|
|
50
|
+
email: true,
|
|
51
|
+
dns: true,
|
|
52
|
+
security: true,
|
|
53
|
+
network: false, // Network is fetched separately for the network view
|
|
54
|
+
radius: true,
|
|
55
|
+
vpn: true,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Update state with all stats from combined response
|
|
60
|
+
return {
|
|
61
|
+
serverStats: combinedResponse.metrics.server || currentState.serverStats,
|
|
62
|
+
emailStats: combinedResponse.metrics.email || currentState.emailStats,
|
|
63
|
+
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
|
|
64
|
+
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
|
|
65
|
+
radiusStats: combinedResponse.metrics.radius || currentState.radiusStats,
|
|
66
|
+
vpnStats: combinedResponse.metrics.vpn || currentState.vpnStats,
|
|
67
|
+
lastUpdated: Date.now(),
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null,
|
|
70
|
+
};
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
return {
|
|
73
|
+
...currentState,
|
|
74
|
+
isLoading: false,
|
|
75
|
+
error: (error as Error).message || 'Failed to fetch statistics',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|