Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec94b7dae | |||
| d5dfe439c7 | |||
| aaf3c9cb1c | |||
| abde872ab2 | |||
| ca2d2b09ad | |||
| fb7d4d988b | |||
| 26e6eea5d5 | |||
| 2458dd08d8 | |||
| dee648b3bc | |||
| f4ed32cee4 | |||
| e9c72952ab | |||
| 1bd485c43e | |||
| 421a0390ba | |||
| c7f87a7c22 | |||
| 390d5c648f | |||
| ec651c1cdb | |||
| 6f82c393e7 | |||
| afdb48367b | |||
| 53526ca3ba | |||
| 07e8f4489b | |||
| 14101a09d3 | |||
| 5344d53806 | |||
| 971535926c | |||
| c13a4ae4be | |||
| e7a03c48ae | |||
| a682329a3f | |||
| c4580f9874 | |||
| b331065b8c | |||
| 4675ca3e89 | |||
| 70e2c8e17d | |||
| db53d87cc5 | |||
| ff6244d3d1 | |||
| f0aafe9027 | |||
| 487f2acac8 | |||
| 0a5e35c58e | |||
| 34c0cab5dc | |||
| 3a666e9300 | |||
| cbe1b5d37d | |||
| 30f2044d9f | |||
| 593b000ca3 | |||
| 60c298c396 | |||
| d7f1c16454 | |||
| 4290d4be86 | |||
| bc34cb5eab | |||
| eda12f3ce3 | |||
| 65f19aac72 | |||
| 29a992a695 | |||
| dbb2166a8f | |||
| 22691329a5 | |||
| e098e1a2ad | |||
| 16d64ec988 | |||
| cb1332ff76 | |||
| 3e52060788 | |||
| f041891a3f | |||
| f902c2c1db | |||
| e1a9e1f997 | |||
| d7b39a3017 | |||
| 0f41b0d8c7 | |||
| 2d33c037ba | |||
| dca7b37eb8 | |||
| b56598ba00 | |||
| bbf550b183 | |||
| f4fc5eb1fd | |||
| d9e88cf5f9 | |||
| eccb9706f2 | |||
| 285e681413 | |||
| 4f3958d94d | |||
| d19f22255d | |||
| 87ec55619a | |||
| b91dab0f85 | |||
| df573d498e | |||
| da2b838019 | |||
| 107adeee1d | |||
| 45f933b473 | |||
| ad16bc44f1 | |||
| 96d5b7e01a | |||
| 93ffcf86b3 | |||
| de98b070db | |||
| d3d2bde440 | |||
| 0840b2b571 | |||
| fa2e784eaa | |||
| 64f2854023 | |||
| 03e3261755 | |||
| c724e68b8c | |||
| f8f66d1392 | |||
| c66bdc9f88 | |||
| 8d57547ace | |||
| 54eaf23298 | |||
| 7148306381 | |||
| d3aefef78d | |||
| ecd0cc0066 | |||
| eac490297a | |||
| de65641f6f | |||
| ffddc1a5f5 | |||
| 26152e0520 | |||
| f79ad07a57 | |||
| 76d5b9bf7c | |||
| 670b67eecf | |||
| 174af5cf86 | |||
| a1f5e45e94 | |||
| d06165bd0c | |||
| 8f3c6fdf23 | |||
| 106ef2919e | |||
| 3d7fd233cf | |||
| 34d40f7370 | |||
| 89b9d01628 | |||
| ed3964e892 | |||
| baab152fd3 | |||
| 9baf09ff61 | |||
| 71f23302d3 | |||
| ecbaab3000 | |||
| 8cb1f3c12d | |||
| c7d7f92759 | |||
| 02e1b9231f | |||
| 4ec4dd2bdb | |||
| aa543160e2 | |||
| 94fa0f04d8 | |||
| 17deb481e0 | |||
| e452ffd38e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ dist_*/
|
|||||||
**/.claude/settings.local.json
|
**/.claude/settings.local.json
|
||||||
.nogit/data/
|
.nogit/data/
|
||||||
readme.plan.md
|
readme.plan.md
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
[ 74ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
|
||||||
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
|
||||||
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
|
||||||
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
|
||||||
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
|
||||||
[ 587ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 697ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[ 669ms] [WARNING] Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information. @ http://localhost:3000/chunk-3L5NJTXF.js:13541
|
|
||||||
[ 729ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/favicon.ico:0
|
|
||||||
[ 27973ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
|
||||||
[ 27973ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
|
||||||
[ 29975ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
|
||||||
[ 29975ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
|
||||||
[ 33977ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
|
||||||
[ 33978ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
|
||||||
[ 41980ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
|
||||||
[ 41980ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
|
||||||
[ 51983ms] [ERROR] WebSocket connection to 'ws://localhost:3000/ws/reload' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/main.js:115
|
|
||||||
[ 51983ms] [ERROR] [ReloadService] WebSocket error: Event @ http://localhost:3000/main.js:141
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[ 55ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
|
||||||
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
|
||||||
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
|
||||||
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
|
||||||
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
|
||||||
[ 791ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
[ 272ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
|
||||||
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 272ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 274ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Pause-circle
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
|
||||||
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 274ms] [WARNING] Lucide icon 'Pause-circle' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 275ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 275ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078)
|
|
||||||
at async N._$EP (http://localhost:3000/bundle.js:1:9024) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 276ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for Refresh-cw
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 276ms] [WARNING] Lucide icon 'Refresh-cw' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 297ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 377ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
|
||||||
[ 78064ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 78237ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
|
||||||
[ 127969ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
|
||||||
[ 127969ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
|
||||||
[ 129695ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
|
||||||
[ 129695ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
|
||||||
[ 133309ms] [ERROR] WebSocket connection to 'ws://localhost:3000/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/typedserver/devtools:16227
|
|
||||||
[ 133309ms] [ERROR] TypedSocket WebSocket error: Event @ http://localhost:3000/typedserver/devtools:16251
|
|
||||||
[ 141762ms] [ERROR] method: >>getMergedRoutes<< got an ERROR: "unauthorized" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 141910ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/routes:0
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
[ 437ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
[ 38948ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 52895ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 52896ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 52896ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 52897ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 99401ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 99401ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
[ 75ms] TypeError: Cannot read properties of null (reading 'appendChild')
|
|
||||||
at TypedserverStatusPill.show (http://localhost:3000/typedserver/devtools:17607:21)
|
|
||||||
at TypedserverStatusPill.updateStatus (http://localhost:3000/typedserver/devtools:17567:10)
|
|
||||||
at ReloadChecker.checkReload (http://localhost:3000/typedserver/devtools:18137:23)
|
|
||||||
at async ReloadChecker.start (http://localhost:3000/typedserver/devtools:18224:9)
|
|
||||||
[ 763ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
[ 22315ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 22315ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 22316ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 22316ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 22321ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 22322ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 22322ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 22322ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 65371ms] [ERROR] method: >>createApiToken<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 65371ms] [ERROR] Failed to create token: zs @ http://localhost:3000/bundle.js:38142
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
[ 642ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
[ 114916ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 179731ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 179731ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 179732ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 179737ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 179738ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 179738ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 179738ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[ 603ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
[ 308ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 309ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 309ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 310ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 349ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 350ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 350ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 351ms] [ERROR] method: >>listApiTokens<< got an ERROR: "admin access required" with data undefined @ http://localhost:3000/bundle.js:13
|
|
||||||
[ 500ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/apitokens:0
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[ 427ms] [ERROR] Error while trying to use the following icon from the Manifest: http://localhost:3000/assetbroker/manifest/icon-144x144.png (Download error or resource isn't a valid image) @ http://localhost:3000/overview:0
|
|
||||||
[ 44124ms] [WARNING] FontAwesome icon not found: circle-check @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 59106ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 59106ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 59107ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 59107ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 59116ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 59116ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
[ 89192ms] [ERROR] Error rendering Lucide icon: Error: Could not create element for MagnifyingGlass
|
|
||||||
at N.updated (http://localhost:3000/bundle.js:1204:736)
|
|
||||||
at N._$AE (http://localhost:3000/bundle.js:1:9837)
|
|
||||||
at N.performUpdate (http://localhost:3000/bundle.js:1:9701)
|
|
||||||
at N.scheduleUpdate (http://localhost:3000/bundle.js:1:9170)
|
|
||||||
at N._$EP (http://localhost:3000/bundle.js:1:9078) @ http://localhost:3000/bundle.js:1203
|
|
||||||
[ 89192ms] [WARNING] Lucide icon 'MagnifyingGlass' not found in lucideIcons object @ http://localhost:3000/bundle.js:1174
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
371
changelog.md
371
changelog.md
@@ -1,5 +1,376 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-06 - 11.2.0 - feat(apiclient)
|
||||||
|
add typed, object-oriented API client documentation and interfaces; document builders, resource managers, and new programmatic endpoints
|
||||||
|
|
||||||
|
- Add new @serve.zone/dcrouter-apiclient documentation (ts_apiclient/readme.md) and publish ordering (ts_apiclient/tspublish.json).
|
||||||
|
- Document OO resource classes, fluent builders, auth modes, examples, and API surface for routes, certificates, apiTokens, remoteIngress, stats, config, logs, emails, and radius.
|
||||||
|
- Update main readme: add API Client section, list new client methods, add package entry for @serve.zone/dcrouter-apiclient, and add apiclient test coverage entry.
|
||||||
|
- Update interfaces readme: add Route Management and API Token Management request interfaces and email method changes (getAllEmails, getEmailDetail).
|
||||||
|
- API reference changes: consolidate email endpoints (getAllEmails/getEmailDetail), add route and api token management methods, rename getLogs to getRecentLogs and add getLogStream.
|
||||||
|
- Update web docs to include route & API token management pages and ops view (ops-view-routes)
|
||||||
|
|
||||||
|
## 2026-03-06 - 11.1.0 - feat(apiclient)
|
||||||
|
add TypeScript API client (ts_apiclient) with resource managers and package exports
|
||||||
|
|
||||||
|
- Add new ts_apiclient module providing DcRouterApiClient and resource managers: routes, certificates, api tokens, remote ingress, emails, stats, config, logs, and radius (with sub-managers).
|
||||||
|
- Add resource classes and builders (Route, RemoteIngress, ApiToken, Certificate, Email) and convenience manager APIs for common operations.
|
||||||
|
- Export apiclient in package.json (exports and files) and add ts_apiclient index and plugins wrapper for @api.global/typedrequest.
|
||||||
|
- Add comprehensive tests for the API client (test/test.apiclient.ts).
|
||||||
|
- Bump devDependencies: @git.zone/tsbuild -> ^4.3.0 and @types/node -> ^25.3.5
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.51 - fix(build)
|
||||||
|
include HTML files in tsbundle output and bump tsbuild/tsbundle devDependencies
|
||||||
|
|
||||||
|
- Add includeFiles: ["./html/**/*.html"] to bundler config in npmextra.json so HTML assets are included in the bundle
|
||||||
|
- Bump devDependencies: @git.zone/tsbuild ^4.2.4 -> ^4.2.6, @git.zone/tsbundle ^2.9.0 -> ^2.9.1 (non-breaking tooling updates)
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.50 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild to ^4.2.4
|
||||||
|
|
||||||
|
- updated devDependency @git.zone/tsbuild from ^4.2.3 to ^4.2.4
|
||||||
|
- no other package changes
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.49 - fix(dcrouter)
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- No files changed in this commit
|
||||||
|
- Working tree unchanged; no version bump required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.48 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild to ^4.2.3
|
||||||
|
|
||||||
|
- package.json: updated devDependency @git.zone/tsbuild from ^4.2.2 to ^4.2.3
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.47 - fix(dcrouter)
|
||||||
|
no code changes; nothing to release
|
||||||
|
|
||||||
|
- No files changed in this commit (git diff is empty)
|
||||||
|
- No version bump required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.46 - fix(none)
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- Git diff reported no changes
|
||||||
|
- No files were modified; no version bump required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.45 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild to ^4.2.2
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.2.1 to ^4.2.2 in package.json
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.44 - fix(dev-deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.2.1
|
||||||
|
|
||||||
|
- Updated package.json devDependency @git.zone/tsbuild from ^4.2.0 to ^4.2.1
|
||||||
|
- Non-breaking patch update for build tool dependency
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.43 - fix(dcrouter)
|
||||||
|
no changes detected; nothing to release
|
||||||
|
|
||||||
|
- Git diff reported no changes
|
||||||
|
- No files were modified, so no version bump is recommended
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.42 - fix(dcrouter)
|
||||||
|
empty commit — no changes
|
||||||
|
|
||||||
|
- No files were modified in this commit
|
||||||
|
- No version bump required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.41 - fix(deps)
|
||||||
|
bump devDependency @git.zone/tsbuild to ^4.2.0
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.26 to ^4.2.0
|
||||||
|
- Change made in package.json under devDependencies
|
||||||
|
- No source code changes — dev tooling dependency bump
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.40 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.26
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild: ^4.1.25 → ^4.1.26 in package.json
|
||||||
|
- Build tooling/dev dependency bump only; no runtime or API changes
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.39 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.25
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.24 to ^4.1.25 in package.json
|
||||||
|
- Only a devDependency was changed; no runtime dependencies or source files modified
|
||||||
|
- Current package version is 11.0.38; recommend a patch release
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.38 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild to ^4.1.24
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild in devDependencies from ^4.1.23 to ^4.1.24
|
||||||
|
- Dev tooling dependency bump; no runtime or API changes expected
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.37 - fix(dcrouter)
|
||||||
|
bump patch version (no changes detected)
|
||||||
|
|
||||||
|
- No files changed in the provided diff
|
||||||
|
- Current package version is 11.0.36 (package.json)
|
||||||
|
- Recommend a patch bump to record a new release if desired
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.36 - fix(repo)
|
||||||
|
no changes detected; no release necessary
|
||||||
|
|
||||||
|
- Diff contains no changes
|
||||||
|
- No files were modified — skip version bump
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.35 - fix(dev-deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.23
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.22 to ^4.1.23 in package.json
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.34 - fix(dcrouter)
|
||||||
|
empty diff — no changes detected; no version bump suggested
|
||||||
|
|
||||||
|
- No file changes in the provided git diff
|
||||||
|
- Current package.json version is 11.0.33 — keep unchanged
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.33 - fix(build)
|
||||||
|
bump @git.zone/tsbuild to ^4.1.22
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.21 to ^4.1.22
|
||||||
|
- Change affects build tooling only (devDependencies) — no runtime or API changes expected
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.32 - fix(dev-deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.21
|
||||||
|
|
||||||
|
- Updated package.json devDependency @git.zone/tsbuild from ^4.1.20 to ^4.1.21
|
||||||
|
- Change affects development tooling only (no runtime/source changes)
|
||||||
|
- Bump package patch version from 11.0.31 to 11.0.32 recommended
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.31 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.20
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.19 to ^4.1.20
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.30 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.19
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.18 to ^4.1.19 in package.json
|
||||||
|
- Change is limited to devDependencies (build toolchain) and should not affect runtime behavior
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.29 - fix(build)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.18
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.17 to ^4.1.18
|
||||||
|
- Change is a devDependency update only; no runtime behavior expected to change
|
||||||
|
- Recommend patch version bump
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.28 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.17
|
||||||
|
|
||||||
|
- package.json: updated @git.zone/tsbuild from ^4.1.16 to ^4.1.17 (devDependency)
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.27 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild to ^4.1.16
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.15 to ^4.1.16 in package.json
|
||||||
|
- No runtime code or dependency changes; only a dev/build tool bump
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.26 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.15
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.14 to ^4.1.15 in package.json
|
||||||
|
- No runtime changes; development tooling update only
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.25 - fix(logger)
|
||||||
|
remove build verification comment from logger export
|
||||||
|
|
||||||
|
- Removed parenthetical '(build verification)' from export comment in ts/logger.ts
|
||||||
|
- No functional changes — this is a comment-only cleanup
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.24 - fix(dcrouter)
|
||||||
|
no changes detected — no release necessary
|
||||||
|
|
||||||
|
- No files changed in the provided diff; no code, docs, or dependency updates to release.
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.23 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.14
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.13 to ^4.1.14 in package.json
|
||||||
|
- Change affects build tooling only (devDependencies); no production code changes
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.22 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.13
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.9 to ^4.1.13 in devDependencies
|
||||||
|
- No runtime code changes; build/dev dependency update only
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.21 - fix()
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- No files changed in this diff; no release required.
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.20 - fix(logger)
|
||||||
|
annotate singleton logger export comment for build verification
|
||||||
|
|
||||||
|
- Changed comment in ts/logger.ts to add '(build verification)'
|
||||||
|
- No functional code changes; only a comment update
|
||||||
|
- Intended to mark the export for build verification purposes
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.19 - fix(dcrouter)
|
||||||
|
no changes
|
||||||
|
|
||||||
|
- No files changed in this commit.
|
||||||
|
- Package version remains 11.0.18.
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.18 - fix(dcrouter)
|
||||||
|
no changes detected; no version bump required
|
||||||
|
|
||||||
|
- Git diff contains no changes — nothing to release
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.17 - fix(dcrouter)
|
||||||
|
no changes detected in diff; no code or documentation updates
|
||||||
|
|
||||||
|
- No files changed in this diff
|
||||||
|
- No code, tests, or documentation modified; no release required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.16 - fix(dcrouter)
|
||||||
|
noop commit: no changes detected
|
||||||
|
|
||||||
|
- No files changed in this diff.
|
||||||
|
- No code or configuration modifications detected.
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.15 - fix()
|
||||||
|
no changes detected; no version bump necessary
|
||||||
|
|
||||||
|
- Diff contains no changes; no files were modified
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.14 - fix(dcrouter)
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- Provided git diff contains no changes; nothing to release or bump
|
||||||
|
- Create a commit only if an empty/placeholder commit is intentionally required
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.13 - fix()
|
||||||
|
no code changes
|
||||||
|
|
||||||
|
- No files were changed in this commit.
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.12 - fix(dcrouter)
|
||||||
|
no changes detected — nothing to commit
|
||||||
|
|
||||||
|
- Diff reported: No changes
|
||||||
|
- No files were modified or staged; no functional or documentation changes to release
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.11 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.9
|
||||||
|
|
||||||
|
- Updated @git.zone/tsbuild from ^4.1.4 to ^4.1.9 in package.json
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.10 - fix(playwright-mcp)
|
||||||
|
remove committed Playwright artifacts and add .playwright-mcp/ to .gitignore
|
||||||
|
|
||||||
|
- Added .playwright-mcp/ to .gitignore to avoid committing transient Playwright outputs
|
||||||
|
- Removed many Playwright-generated logs, screenshots and console dumps under .playwright-mcp/ to reduce repository noise/size
|
||||||
|
- Prevents accidental check-in of large test artifacts generated by Playwright runs
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.9 - fix(devDependencies)
|
||||||
|
bump @git.zone/tsbuild devDependency to ^4.1.4
|
||||||
|
|
||||||
|
- package.json: Updated @git.zone/tsbuild from ^4.1.3 to ^4.1.4
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.8 - fix()
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- No files changed in this commit
|
||||||
|
- No version bump recommended
|
||||||
|
|
||||||
|
## 2026-03-05 - 11.0.7 - fix(deps)
|
||||||
|
bump @git.zone/tsbuild to ^4.1.3 and @push.rocks/lik to ^6.3.1
|
||||||
|
|
||||||
|
- Updated devDependency @git.zone/tsbuild from ^4.1.2 to ^4.1.3 in package.json
|
||||||
|
- Updated dependency @push.rocks/lik from ^6.2.2 to ^6.3.1 in package.json
|
||||||
|
- Changes are non-breaking dependency bumps; no source code changes
|
||||||
|
|
||||||
|
## 2026-03-04 - 11.0.5 - fix(none)
|
||||||
|
no changes detected; nothing to release
|
||||||
|
|
||||||
|
- Diff contained no changes
|
||||||
|
- No files modified; no version bump required
|
||||||
|
|
||||||
|
## 2026-03-04 - 11.0.4 - fix()
|
||||||
|
no changes
|
||||||
|
|
||||||
|
- No files changed in the provided diff; no release or version bump required.
|
||||||
|
|
||||||
|
## 2026-03-04 - 11.0.3 - fix()
|
||||||
|
no changes detected
|
||||||
|
|
||||||
|
- Diff shows no file changes; no code changes to release.
|
||||||
|
|
||||||
|
## 2026-03-04 - 11.0.2 - fix(dcrouter)
|
||||||
|
no changes detected; no files were modified
|
||||||
|
|
||||||
|
- diff was empty
|
||||||
|
- no source or package changes detected
|
||||||
|
|
||||||
|
## 2026-03-04 - 11.0.1 - fix(auth)
|
||||||
|
treat expired JWTs as no identity, improve logout and token verification flow, and bump deps
|
||||||
|
|
||||||
|
- App: getActionContext now treats expired JWTs as null to avoid using stale identities for requests.
|
||||||
|
- Logout action always clears local login state; server-side adminLogout is attempted only when a valid identity exists.
|
||||||
|
- Dashboard: verify persisted JWT with server (verifyIdentity) on startup; if verification fails, clear state and show login.
|
||||||
|
- Auto-refresh: on combined refresh failure, detect auth-related errors (invalid/unauthorized/401), dispatch logout and reload to force re-login.
|
||||||
|
- Deps: bumped devDependencies @git.zone/tstest (^3.2.0) and @git.zone/tswatch (^3.2.5); added runtime dependency @push.rocks/lik (^6.2.2).
|
||||||
|
- Tests/artifacts: added Playwright console logs and page screenshots (test artifacts) to the commit.
|
||||||
|
|
||||||
|
## 2026-03-03 - 11.0.0 - BREAKING CHANGE(opsserver)
|
||||||
|
Require authentication for OpsServer endpoints, split handlers into authenticated view/admin routers, and make identity required on many TypedRequest interfaces
|
||||||
|
|
||||||
|
- Added viewRouter and adminRouter to OpsServer and wired middleware to enforce identity/admin checks (requireValidIdentity, requireAdminIdentity).
|
||||||
|
- Moved handlers to appropriate routers (viewRouter for read endpoints, adminRouter for write/admin endpoints) instead of registering on the unauthenticated main typedrouter.
|
||||||
|
- Made identity a required field on numerous ts_interfaces request types (breaking change to request typings).
|
||||||
|
- Refactored ApiTokenHandler to register directly on adminRouter and use dataArg.identity.userId (no per-handler admin checks needed thanks to middleware).
|
||||||
|
- Updated tests: added admin login to obtain identity, adjusted protected endpoint tests to expect rejection when unauthenticated, and adapted other tests to pass identity where required.
|
||||||
|
- Added IReq_GetNetworkStats request/response typings to ts_interfaces/requests/stats.ts.
|
||||||
|
- Bumped dependencies: @api.global/typedrequest ^3.3.0 and @api.global/typedserver ^8.4.2.
|
||||||
|
|
||||||
|
## 2026-03-03 - 10.1.9 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.9.1
|
||||||
|
|
||||||
|
- Updated package.json dependency @push.rocks/smartproxy from ^25.9.0 to ^25.9.1
|
||||||
|
- No other code changes; current package version is 10.1.8, recommend a patch release
|
||||||
|
|
||||||
|
## 2026-03-03 - 10.1.8 - fix(deps)
|
||||||
|
bump dependencies: @push.rocks/smartmetrics to ^3.0.2, @push.rocks/smartproxy to ^25.9.0, @serve.zone/remoteingress to ^4.4.0
|
||||||
|
|
||||||
|
- @push.rocks/smartmetrics: 3.0.1 -> 3.0.2 (patch)
|
||||||
|
- @push.rocks/smartproxy: 25.8.5 -> 25.9.0 (minor)
|
||||||
|
- @serve.zone/remoteingress: 4.3.0 -> 4.4.0 (minor)
|
||||||
|
|
||||||
|
## 2026-03-03 - 10.1.7 - fix(ops-view-apitokens)
|
||||||
|
use correct lucide icon name for roll/rotate actions in API tokens view
|
||||||
|
|
||||||
|
- Updated iconName from 'lucide:rotate-cw' to 'lucide:rotateCw' in ts_web/elements/ops-view-apitokens.ts (two occurrences) to match lucide icon naming and ensure icons render correctly
|
||||||
|
- Non-functional UI fix; no API or behavior changes
|
||||||
|
|
||||||
|
## 2026-03-02 - 10.1.6 - fix(ts_web)
|
||||||
|
use actionContext for dispatches in web state actions and bump @push.rocks/smartstate to ^2.2.0
|
||||||
|
|
||||||
|
- Action handlers in ts_web/appstate.ts now accept an actionContext parameter and call await actionContext.dispatch(...) instead of using statePartArg.dispatchAction(...).
|
||||||
|
- Handlers return the awaited dispatch result (ensuring callers receive refreshed state) instead of returning the previous statePartArg.getState().
|
||||||
|
- Dependency bumped in package.json: @push.rocks/smartstate from ^2.1.1 to ^2.2.0.
|
||||||
|
- Playwright artifacts (logs and page screenshots) were added under .playwright-mcp.
|
||||||
|
|
||||||
|
## 2026-03-02 - 10.1.5 - fix(monitoring)
|
||||||
|
use a per-second ring buffer for DNS query metrics, improve DNS logging rate limiting and security event aggregation, and bump smartmta dependency
|
||||||
|
|
||||||
|
- Replace unbounded query timestamp array with a fixed-size per-second Int32Array ring buffer (300s) to calculate queries-per-second with O(1) updates and bounded memory
|
||||||
|
- Add incrementQueryRing and getQueryRingSum helpers to correctly zero stale slots and sum recent seconds
|
||||||
|
- Change metrics cache interval from 200ms to 1000ms to better match dashboard polling and reduce update frequency
|
||||||
|
- Refactor DNS adaptive logging to use per-second counters (dnsLogWindowSecond / dnsLogWindowCount) instead of timestamp arrays to avoid per-query array filtering and improve rate limiting accuracy; reset counters on flush
|
||||||
|
- Security logger: avoid mutating source when sorting/filtering, and implement single-pass aggregation with optional time-window filtering for byLevel/byType/top lists
|
||||||
|
- Bump dependency @push.rocks/smartmta from ^5.3.0 to ^5.3.1
|
||||||
|
|
||||||
|
## 2026-03-02 - 10.1.4 - fix(no-changes)
|
||||||
|
no changes detected; no version bump required
|
||||||
|
|
||||||
|
- package version is 10.1.3
|
||||||
|
- git diff contains no changes
|
||||||
|
|
||||||
## 2026-03-02 - 10.1.3 - fix(deps)
|
## 2026-03-02 - 10.1.3 - fix(deps)
|
||||||
bump @api.global/typedrequest to ^3.2.7
|
bump @api.global/typedrequest to ^3.2.7
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"to": "./dist_serve/bundle.js",
|
"to": "./dist_serve/bundle.js",
|
||||||
"outputMode": "bundle",
|
"outputMode": "bundle",
|
||||||
"bundler": "esbuild",
|
"bundler": "esbuild",
|
||||||
"production": true
|
"production": true,
|
||||||
|
"includeFiles": ["./html/**/*.html"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "10.1.3",
|
"version": "11.2.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist_ts/index.js",
|
".": "./dist_ts/index.js",
|
||||||
"./interfaces": "./dist_ts_interfaces/index.js"
|
"./interfaces": "./dist_ts_interfaces/index.js",
|
||||||
|
"./apiclient": "./dist_ts_apiclient/index.js"
|
||||||
},
|
},
|
||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -19,21 +20,22 @@
|
|||||||
"watch": "tswatch"
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@git.zone/tsbundle": "^2.9.0",
|
"@git.zone/tsbundle": "^2.9.1",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.2.0",
|
||||||
"@git.zone/tswatch": "^3.2.0",
|
"@git.zone/tswatch": "^3.2.5",
|
||||||
"@types/node": "^25.3.3"
|
"@types/node": "^25.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.7",
|
"@api.global/typedrequest": "^3.3.0",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.4.0",
|
"@api.global/typedserver": "^8.4.2",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.43.3",
|
"@design.estate/dees-catalog": "^3.43.3",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
|
"@push.rocks/lik": "^6.3.1",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^9.1.3",
|
"@push.rocks/smartacme": "^9.1.3",
|
||||||
@@ -43,21 +45,21 @@
|
|||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartmetrics": "^3.0.1",
|
"@push.rocks/smartmetrics": "^3.0.2",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartmta": "^5.3.0",
|
"@push.rocks/smartmta": "^5.3.1",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.8.5",
|
"@push.rocks/smartproxy": "^25.9.1",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.1.1",
|
"@push.rocks/smartstate": "^2.2.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/catalog": "^2.5.0",
|
"@serve.zone/catalog": "^2.5.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.3.0",
|
"@serve.zone/remoteingress": "^4.4.0",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
"lru-cache": "^11.2.6",
|
"lru-cache": "^11.2.6",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
@@ -99,10 +101,12 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
"ts_web/**/*",
|
"ts_web/**/*",
|
||||||
|
"ts_apiclient/**/*",
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"dist_*/**/*",
|
"dist_*/**/*",
|
||||||
"dist_ts/**/*",
|
"dist_ts/**/*",
|
||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
|
"dist_ts_apiclient/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
|
|||||||
2806
pnpm-lock.yaml
generated
2806
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
110
readme.md
110
readme.md
@@ -26,6 +26,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- [Storage & Caching](#storage--caching)
|
- [Storage & Caching](#storage--caching)
|
||||||
- [Security Features](#security-features)
|
- [Security Features](#security-features)
|
||||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||||
|
- [API Client](#api-client)
|
||||||
- [API Reference](#api-reference)
|
- [API Reference](#api-reference)
|
||||||
- [Sub-Modules](#sub-modules)
|
- [Sub-Modules](#sub-modules)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
@@ -90,6 +91,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Remote ingress management** with connection token generation and one-click copy
|
- **Remote ingress management** with connection token generation and one-click copy
|
||||||
- **Read-only configuration display** — DcRouter is configured through code
|
- **Read-only configuration display** — DcRouter is configured through code
|
||||||
|
|
||||||
|
### 🔧 Programmatic API Client
|
||||||
|
- **Object-oriented API** — resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) with instance methods
|
||||||
|
- **Builder pattern** — fluent `.setName().setMatch().save()` chains for creating routes, tokens, and edges
|
||||||
|
- **Auto-injected auth** — JWT identity and API tokens included automatically in every request
|
||||||
|
- **Dual auth modes** — login with credentials (JWT) or pass an API token for programmatic access
|
||||||
|
- **Full coverage** — wraps every OpsServer endpoint with typed request/response pairs
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1038,12 +1046,9 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
|||||||
'getCombinedMetrics' // All metrics in one call
|
'getCombinedMetrics' // All metrics in one call
|
||||||
|
|
||||||
// Email Operations
|
// Email Operations
|
||||||
'getQueuedEmails' // Emails pending delivery
|
'getAllEmails' // List all emails (queued/sent/failed)
|
||||||
'getSentEmails' // Successfully delivered emails
|
'getEmailDetail' // Full detail for a specific email
|
||||||
'getFailedEmails' // Failed emails
|
|
||||||
'resendEmail' // Re-queue a failed email
|
'resendEmail' // Re-queue a failed email
|
||||||
'getBounceRecords' // Bounce records
|
|
||||||
'removeFromSuppressionList' // Unsuppress an address
|
|
||||||
|
|
||||||
// Certificates
|
// Certificates
|
||||||
'getCertificateOverview' // Domain-centric certificate status
|
'getCertificateOverview' // Domain-centric certificate status
|
||||||
@@ -1062,11 +1067,28 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
|||||||
'getRemoteIngressStatus' // Runtime status of all edges
|
'getRemoteIngressStatus' // Runtime status of all edges
|
||||||
'getRemoteIngressConnectionToken' // Generate a connection token for an edge
|
'getRemoteIngressConnectionToken' // Generate a connection token for an edge
|
||||||
|
|
||||||
|
// Route Management (JWT or API token auth)
|
||||||
|
'getMergedRoutes' // List all routes (hardcoded + programmatic)
|
||||||
|
'createRoute' // Create a new programmatic route
|
||||||
|
'updateRoute' // Update a programmatic route
|
||||||
|
'deleteRoute' // Delete a programmatic route
|
||||||
|
'toggleRoute' // Enable/disable a programmatic route
|
||||||
|
'setRouteOverride' // Override a hardcoded route
|
||||||
|
'removeRouteOverride' // Remove a hardcoded route override
|
||||||
|
|
||||||
|
// API Token Management (admin JWT only)
|
||||||
|
'createApiToken' // Create API token → returns raw value once
|
||||||
|
'listApiTokens' // List all tokens (without secrets)
|
||||||
|
'revokeApiToken' // Delete an API token
|
||||||
|
'rollApiToken' // Regenerate token secret
|
||||||
|
'toggleApiToken' // Enable/disable a token
|
||||||
|
|
||||||
// Configuration (read-only)
|
// Configuration (read-only)
|
||||||
'getConfiguration' // Current system config
|
'getConfiguration' // Current system config
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
'getLogs' // Retrieve system logs
|
'getRecentLogs' // Retrieve system logs with filtering
|
||||||
|
'getLogStream' // Stream live logs
|
||||||
|
|
||||||
// RADIUS
|
// RADIUS
|
||||||
'getRadiusSessions' // Active RADIUS sessions
|
'getRadiusSessions' // Active RADIUS sessions
|
||||||
@@ -1080,6 +1102,77 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
|||||||
'testVlanAssignment' // Test what VLAN a MAC gets
|
'testVlanAssignment' // Test what VLAN a MAC gets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## API Client
|
||||||
|
|
||||||
|
DcRouter ships with a typed, object-oriented API client for programmatic management of a running instance. Install it separately or import from the main package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/dcrouter-apiclient
|
||||||
|
# or import from the main package:
|
||||||
|
# import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quick Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||||
|
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
|
||||||
|
await client.login('admin', 'password');
|
||||||
|
|
||||||
|
// OO resource instances with methods
|
||||||
|
const { routes } = await client.routes.list();
|
||||||
|
await routes[0].toggle(false);
|
||||||
|
|
||||||
|
// Builder pattern for creation
|
||||||
|
const newRoute = await client.routes.build()
|
||||||
|
.setName('api-gateway')
|
||||||
|
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
||||||
|
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||||
|
.setTls({ mode: 'terminate', certificate: 'auto' })
|
||||||
|
.save();
|
||||||
|
|
||||||
|
// Manage certificates
|
||||||
|
const { certificates, summary } = await client.certificates.list();
|
||||||
|
await certificates[0].reprovision();
|
||||||
|
|
||||||
|
// Create API tokens with builder
|
||||||
|
const token = await client.apiTokens.build()
|
||||||
|
.setName('ci-token')
|
||||||
|
.setScopes(['routes:read', 'routes:write'])
|
||||||
|
.setExpiresInDays(90)
|
||||||
|
.save();
|
||||||
|
console.log(token.tokenValue); // only available at creation
|
||||||
|
|
||||||
|
// Remote ingress edges
|
||||||
|
const edge = await client.remoteIngress.build()
|
||||||
|
.setName('edge-nyc-01')
|
||||||
|
.setListenPorts([80, 443])
|
||||||
|
.save();
|
||||||
|
const connToken = await edge.getConnectionToken();
|
||||||
|
|
||||||
|
// Read-only managers
|
||||||
|
const health = await client.stats.getHealth();
|
||||||
|
const config = await client.config.get();
|
||||||
|
const { logs } = await client.logs.getRecent({ level: 'error', limit: 50 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Managers
|
||||||
|
|
||||||
|
| Manager | Operations |
|
||||||
|
|---------|-----------|
|
||||||
|
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
|
||||||
|
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
|
||||||
|
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
|
||||||
|
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
|
||||||
|
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
|
||||||
|
| `client.config` | `get(section?)` |
|
||||||
|
| `client.logs` | `getRecent()`, `getStream()` |
|
||||||
|
| `client.emails` | `list()` → Email: `getDetail()`, `resend()` |
|
||||||
|
| `client.radius` | `.clients`, `.vlans`, `.sessions` sub-managers + `getStatistics()`, `getAccountingSummary()` |
|
||||||
|
|
||||||
|
See the [full API client documentation](./ts_apiclient/readme.md) for detailed usage of every manager, builder, and resource class.
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### DcRouter Class
|
### DcRouter Class
|
||||||
@@ -1144,12 +1237,14 @@ DcRouter is published as a monorepo with separately-installable interface and we
|
|||||||
|---------|-------------|---------|
|
|---------|-------------|---------|
|
||||||
| [`@serve.zone/dcrouter`](https://www.npmjs.com/package/@serve.zone/dcrouter) | Main package — the full router | `pnpm add @serve.zone/dcrouter` |
|
| [`@serve.zone/dcrouter`](https://www.npmjs.com/package/@serve.zone/dcrouter) | Main package — the full router | `pnpm add @serve.zone/dcrouter` |
|
||||||
| [`@serve.zone/dcrouter-interfaces`](https://www.npmjs.com/package/@serve.zone/dcrouter-interfaces) | TypedRequest interfaces for the OpsServer API | `pnpm add @serve.zone/dcrouter-interfaces` |
|
| [`@serve.zone/dcrouter-interfaces`](https://www.npmjs.com/package/@serve.zone/dcrouter-interfaces) | TypedRequest interfaces for the OpsServer API | `pnpm add @serve.zone/dcrouter-interfaces` |
|
||||||
|
| [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) | OO API client with builder pattern | `pnpm add @serve.zone/dcrouter-apiclient` |
|
||||||
| [`@serve.zone/dcrouter-web`](https://www.npmjs.com/package/@serve.zone/dcrouter-web) | Web dashboard components | `pnpm add @serve.zone/dcrouter-web` |
|
| [`@serve.zone/dcrouter-web`](https://www.npmjs.com/package/@serve.zone/dcrouter-web) | Web dashboard components | `pnpm add @serve.zone/dcrouter-web` |
|
||||||
|
|
||||||
You can also import interfaces directly from the main package:
|
You can also import directly from the main package:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { data, requests } from '@serve.zone/dcrouter/interfaces';
|
import { data, requests } from '@serve.zone/dcrouter/interfaces';
|
||||||
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -1171,6 +1266,7 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
|||||||
|
|
||||||
| Test File | Area | Tests |
|
| Test File | Area | Tests |
|
||||||
|-----------|------|-------|
|
|-----------|------|-------|
|
||||||
|
| `test.apiclient.ts` | API client instantiation, builders, resource hydration, exports | 18 |
|
||||||
| `test.contentscanner.ts` | Content scanning (spam, phishing, malware, attachments) | 13 |
|
| `test.contentscanner.ts` | Content scanning (spam, phishing, malware, attachments) | 13 |
|
||||||
| `test.dcrouter.email.ts` | Email config, domain and route setup | 4 |
|
| `test.dcrouter.email.ts` | Email config, domain and route setup | 4 |
|
||||||
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
| `test.dns-server-config.ts` | DNS record parsing, grouping, extraction | 5 |
|
||||||
|
|||||||
376
test/test.apiclient.ts
Normal file
376
test/test.apiclient.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import {
|
||||||
|
DcRouterApiClient,
|
||||||
|
Route,
|
||||||
|
RouteBuilder,
|
||||||
|
RouteManager,
|
||||||
|
Certificate,
|
||||||
|
CertificateManager,
|
||||||
|
ApiToken,
|
||||||
|
ApiTokenBuilder,
|
||||||
|
ApiTokenManager,
|
||||||
|
RemoteIngress,
|
||||||
|
RemoteIngressBuilder,
|
||||||
|
RemoteIngressManager,
|
||||||
|
Email,
|
||||||
|
EmailManager,
|
||||||
|
StatsManager,
|
||||||
|
ConfigManager,
|
||||||
|
LogManager,
|
||||||
|
RadiusManager,
|
||||||
|
RadiusClientManager,
|
||||||
|
RadiusVlanManager,
|
||||||
|
RadiusSessionManager,
|
||||||
|
} from '../ts_apiclient/index.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Instantiation & Structure
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - should instantiate with baseUrl', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
expect(client).toBeTruthy();
|
||||||
|
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||||
|
expect(client.identity).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - should strip trailing slashes from baseUrl', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000///' });
|
||||||
|
expect(client.baseUrl).toEqual('https://localhost:3000');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - should accept optional apiToken', async () => {
|
||||||
|
const client = new DcRouterApiClient({
|
||||||
|
baseUrl: 'https://localhost:3000',
|
||||||
|
apiToken: 'dcr_test_token',
|
||||||
|
});
|
||||||
|
expect(client.apiToken).toEqual('dcr_test_token');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - should have all resource managers', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
expect(client.routes).toBeInstanceOf(RouteManager);
|
||||||
|
expect(client.certificates).toBeInstanceOf(CertificateManager);
|
||||||
|
expect(client.apiTokens).toBeInstanceOf(ApiTokenManager);
|
||||||
|
expect(client.remoteIngress).toBeInstanceOf(RemoteIngressManager);
|
||||||
|
expect(client.stats).toBeInstanceOf(StatsManager);
|
||||||
|
expect(client.config).toBeInstanceOf(ConfigManager);
|
||||||
|
expect(client.logs).toBeInstanceOf(LogManager);
|
||||||
|
expect(client.emails).toBeInstanceOf(EmailManager);
|
||||||
|
expect(client.radius).toBeInstanceOf(RadiusManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// buildRequestPayload
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - buildRequestPayload includes identity when set', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const identity = {
|
||||||
|
jwt: 'test-jwt',
|
||||||
|
userId: 'user1',
|
||||||
|
name: 'Admin',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
};
|
||||||
|
client.identity = identity;
|
||||||
|
|
||||||
|
const payload = client.buildRequestPayload({ extra: 'data' });
|
||||||
|
expect(payload.identity).toEqual(identity);
|
||||||
|
expect(payload.extra).toEqual('data');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - buildRequestPayload includes apiToken when set', async () => {
|
||||||
|
const client = new DcRouterApiClient({
|
||||||
|
baseUrl: 'https://localhost:3000',
|
||||||
|
apiToken: 'dcr_abc123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = client.buildRequestPayload();
|
||||||
|
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('DcRouterApiClient - buildRequestPayload with both identity and apiToken', async () => {
|
||||||
|
const client = new DcRouterApiClient({
|
||||||
|
baseUrl: 'https://localhost:3000',
|
||||||
|
apiToken: 'dcr_abc123',
|
||||||
|
});
|
||||||
|
client.identity = {
|
||||||
|
jwt: 'test-jwt',
|
||||||
|
userId: 'user1',
|
||||||
|
name: 'Admin',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = client.buildRequestPayload({ foo: 'bar' });
|
||||||
|
expect(payload.identity).toBeTruthy();
|
||||||
|
expect(payload.apiToken).toEqual('dcr_abc123');
|
||||||
|
expect(payload.foo).toEqual('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Route Builder
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('RouteBuilder - should support fluent builder pattern', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const builder = client.routes.build();
|
||||||
|
expect(builder).toBeInstanceOf(RouteBuilder);
|
||||||
|
|
||||||
|
// Fluent methods return `this` (same reference)
|
||||||
|
const result = builder
|
||||||
|
.setName('test-route')
|
||||||
|
.setMatch({ ports: 443, domains: 'example.com' })
|
||||||
|
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||||
|
.setEnabled(true);
|
||||||
|
|
||||||
|
expect(result === builder).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ApiToken Builder
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('ApiTokenBuilder - should support fluent builder pattern', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const builder = client.apiTokens.build();
|
||||||
|
expect(builder).toBeInstanceOf(ApiTokenBuilder);
|
||||||
|
|
||||||
|
const result = builder
|
||||||
|
.setName('ci-token')
|
||||||
|
.setScopes(['routes:read', 'routes:write'])
|
||||||
|
.addScope('config:read')
|
||||||
|
.setExpiresInDays(30);
|
||||||
|
|
||||||
|
expect(result === builder).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RemoteIngress Builder
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('RemoteIngressBuilder - should support fluent builder pattern', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const builder = client.remoteIngress.build();
|
||||||
|
expect(builder).toBeInstanceOf(RemoteIngressBuilder);
|
||||||
|
|
||||||
|
const result = builder
|
||||||
|
.setName('edge-1')
|
||||||
|
.setListenPorts([80, 443])
|
||||||
|
.setAutoDerivePorts(true)
|
||||||
|
.setTags(['production']);
|
||||||
|
|
||||||
|
expect(result === builder).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Route resource class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('Route - should hydrate from IMergedRoute data', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const route = new Route(client, {
|
||||||
|
route: {
|
||||||
|
name: 'test-route',
|
||||||
|
match: { ports: 443, domains: 'example.com' },
|
||||||
|
action: { type: 'forward', targets: [{ host: 'backend', port: 8080 }] },
|
||||||
|
},
|
||||||
|
source: 'programmatic',
|
||||||
|
enabled: true,
|
||||||
|
overridden: false,
|
||||||
|
storedRouteId: 'route-123',
|
||||||
|
createdAt: 1000,
|
||||||
|
updatedAt: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(route.name).toEqual('test-route');
|
||||||
|
expect(route.source).toEqual('programmatic');
|
||||||
|
expect(route.enabled).toEqual(true);
|
||||||
|
expect(route.overridden).toEqual(false);
|
||||||
|
expect(route.storedRouteId).toEqual('route-123');
|
||||||
|
expect(route.routeConfig.match.ports).toEqual(443);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Route - should throw on update/delete/toggle for hardcoded routes', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const route = new Route(client, {
|
||||||
|
route: {
|
||||||
|
name: 'hardcoded-route',
|
||||||
|
match: { ports: 80 },
|
||||||
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
|
||||||
|
},
|
||||||
|
source: 'hardcoded',
|
||||||
|
enabled: true,
|
||||||
|
overridden: false,
|
||||||
|
// No storedRouteId for hardcoded routes
|
||||||
|
});
|
||||||
|
|
||||||
|
let updateError: Error | undefined;
|
||||||
|
try {
|
||||||
|
await route.update({ name: 'new-name' });
|
||||||
|
} catch (e) {
|
||||||
|
updateError = e as Error;
|
||||||
|
}
|
||||||
|
expect(updateError).toBeTruthy();
|
||||||
|
expect(updateError!.message).toInclude('hardcoded');
|
||||||
|
|
||||||
|
let deleteError: Error | undefined;
|
||||||
|
try {
|
||||||
|
await route.delete();
|
||||||
|
} catch (e) {
|
||||||
|
deleteError = e as Error;
|
||||||
|
}
|
||||||
|
expect(deleteError).toBeTruthy();
|
||||||
|
|
||||||
|
let toggleError: Error | undefined;
|
||||||
|
try {
|
||||||
|
await route.toggle(false);
|
||||||
|
} catch (e) {
|
||||||
|
toggleError = e as Error;
|
||||||
|
}
|
||||||
|
expect(toggleError).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Certificate resource class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('Certificate - should hydrate from ICertificateInfo data', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const cert = new Certificate(client, {
|
||||||
|
domain: 'example.com',
|
||||||
|
routeNames: ['main-route'],
|
||||||
|
status: 'valid',
|
||||||
|
source: 'acme',
|
||||||
|
tlsMode: 'terminate',
|
||||||
|
expiryDate: '2027-01-01T00:00:00Z',
|
||||||
|
issuer: "Let's Encrypt",
|
||||||
|
canReprovision: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cert.domain).toEqual('example.com');
|
||||||
|
expect(cert.status).toEqual('valid');
|
||||||
|
expect(cert.source).toEqual('acme');
|
||||||
|
expect(cert.canReprovision).toEqual(true);
|
||||||
|
expect(cert.routeNames.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ApiToken resource class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('ApiToken - should hydrate from IApiTokenInfo data', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const token = new ApiToken(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
id: 'token-1',
|
||||||
|
name: 'ci-token',
|
||||||
|
scopes: ['routes:read', 'routes:write'],
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
'dcr_secret_value',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(token.id).toEqual('token-1');
|
||||||
|
expect(token.name).toEqual('ci-token');
|
||||||
|
expect(token.scopes.length).toEqual(2);
|
||||||
|
expect(token.enabled).toEqual(true);
|
||||||
|
expect(token.tokenValue).toEqual('dcr_secret_value');
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RemoteIngress resource class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('RemoteIngress - should hydrate from IRemoteIngress data', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const edge = new RemoteIngress(client, {
|
||||||
|
id: 'edge-1',
|
||||||
|
name: 'test-edge',
|
||||||
|
secret: 'secret123',
|
||||||
|
listenPorts: [80, 443],
|
||||||
|
enabled: true,
|
||||||
|
autoDerivePorts: true,
|
||||||
|
tags: ['prod'],
|
||||||
|
createdAt: 1000,
|
||||||
|
updatedAt: 2000,
|
||||||
|
effectiveListenPorts: [80, 443, 8080],
|
||||||
|
manualPorts: [80, 443],
|
||||||
|
derivedPorts: [8080],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(edge.id).toEqual('edge-1');
|
||||||
|
expect(edge.name).toEqual('test-edge');
|
||||||
|
expect(edge.listenPorts.length).toEqual(2);
|
||||||
|
expect(edge.effectiveListenPorts!.length).toEqual(3);
|
||||||
|
expect(edge.autoDerivePorts).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Email resource class
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('Email - should hydrate from IEmail data', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
const email = new Email(client, {
|
||||||
|
id: 'email-1',
|
||||||
|
direction: 'inbound',
|
||||||
|
status: 'delivered',
|
||||||
|
from: 'sender@example.com',
|
||||||
|
to: 'recipient@example.com',
|
||||||
|
subject: 'Test email',
|
||||||
|
timestamp: '2026-03-06T00:00:00Z',
|
||||||
|
messageId: '<msg-1@example.com>',
|
||||||
|
size: '1234',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(email.id).toEqual('email-1');
|
||||||
|
expect(email.direction).toEqual('inbound');
|
||||||
|
expect(email.status).toEqual('delivered');
|
||||||
|
expect(email.from).toEqual('sender@example.com');
|
||||||
|
expect(email.subject).toEqual('Test email');
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RadiusManager structure
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('RadiusManager - should have sub-managers', async () => {
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://localhost:3000' });
|
||||||
|
expect(client.radius.clients).toBeInstanceOf(RadiusClientManager);
|
||||||
|
expect(client.radius.vlans).toBeInstanceOf(RadiusVlanManager);
|
||||||
|
expect(client.radius.sessions).toBeInstanceOf(RadiusSessionManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Exports verification
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
tap.test('Exports - all expected classes should be importable', async () => {
|
||||||
|
expect(DcRouterApiClient).toBeTruthy();
|
||||||
|
expect(Route).toBeTruthy();
|
||||||
|
expect(RouteBuilder).toBeTruthy();
|
||||||
|
expect(RouteManager).toBeTruthy();
|
||||||
|
expect(Certificate).toBeTruthy();
|
||||||
|
expect(CertificateManager).toBeTruthy();
|
||||||
|
expect(ApiToken).toBeTruthy();
|
||||||
|
expect(ApiTokenBuilder).toBeTruthy();
|
||||||
|
expect(ApiTokenManager).toBeTruthy();
|
||||||
|
expect(RemoteIngress).toBeTruthy();
|
||||||
|
expect(RemoteIngressBuilder).toBeTruthy();
|
||||||
|
expect(RemoteIngressManager).toBeTruthy();
|
||||||
|
expect(Email).toBeTruthy();
|
||||||
|
expect(EmailManager).toBeTruthy();
|
||||||
|
expect(StatsManager).toBeTruthy();
|
||||||
|
expect(ConfigManager).toBeTruthy();
|
||||||
|
expect(LogManager).toBeTruthy();
|
||||||
|
expect(RadiusManager).toBeTruthy();
|
||||||
|
expect(RadiusClientManager).toBeTruthy();
|
||||||
|
expect(RadiusVlanManager).toBeTruthy();
|
||||||
|
expect(RadiusSessionManager).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -4,6 +4,7 @@ import { TypedRequest } from '@api.global/typedrequest';
|
|||||||
import * as interfaces from '../ts_interfaces/index.js';
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
let testDcRouter: DcRouter;
|
let testDcRouter: DcRouter;
|
||||||
|
let adminIdentity: interfaces.data.IIdentity;
|
||||||
|
|
||||||
tap.test('should start DCRouter with OpsServer', async () => {
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
testDcRouter = new DcRouter({
|
testDcRouter = new DcRouter({
|
||||||
@@ -15,6 +16,21 @@ tap.test('should start DCRouter with OpsServer', async () => {
|
|||||||
expect(testDcRouter.opsServer).toBeInstanceOf(Object);
|
expect(testDcRouter.opsServer).toBeInstanceOf(Object);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should login as admin', async () => {
|
||||||
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'adminLoginWithUsernameAndPassword'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await loginRequest.fire({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('identity');
|
||||||
|
adminIdentity = response.identity;
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should respond to health status request', async () => {
|
tap.test('should respond to health status request', async () => {
|
||||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3000/typedrequest',
|
||||||
@@ -22,7 +38,8 @@ tap.test('should respond to health status request', async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await healthRequest.fire({
|
const response = await healthRequest.fire({
|
||||||
detailed: false
|
identity: adminIdentity,
|
||||||
|
detailed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('health');
|
expect(response).toHaveProperty('health');
|
||||||
@@ -37,7 +54,8 @@ tap.test('should respond to server statistics request', async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await statsRequest.fire({
|
const response = await statsRequest.fire({
|
||||||
includeHistory: false
|
identity: adminIdentity,
|
||||||
|
includeHistory: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('stats');
|
expect(response).toHaveProperty('stats');
|
||||||
@@ -52,7 +70,9 @@ tap.test('should respond to configuration request', async () => {
|
|||||||
'getConfiguration'
|
'getConfiguration'
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await configRequest.fire({});
|
const response = await configRequest.fire({
|
||||||
|
identity: adminIdentity,
|
||||||
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
expect(response.config).toHaveProperty('system');
|
expect(response.config).toHaveProperty('system');
|
||||||
@@ -72,7 +92,8 @@ tap.test('should handle log retrieval request', async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await logsRequest.fire({
|
const response = await logsRequest.fire({
|
||||||
limit: 10
|
identity: adminIdentity,
|
||||||
|
limit: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('logs');
|
expect(response).toHaveProperty('logs');
|
||||||
@@ -81,6 +102,20 @@ tap.test('should handle log retrieval request', async () => {
|
|||||||
expect(response.logs).toBeArray();
|
expect(response.logs).toBeArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('should reject unauthenticated requests', async () => {
|
||||||
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'getHealthStatus'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await healthRequest.fire({} as any);
|
||||||
|
expect(true).toBeFalse(); // Should not reach here
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should stop DCRouter', async () => {
|
tap.test('should stop DCRouter', async () => {
|
||||||
await testDcRouter.stop();
|
await testDcRouter.stop();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,28 +82,31 @@ tap.test('should reject verify identity with invalid JWT', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should allow access to public endpoints without auth', async () => {
|
tap.test('should reject protected endpoints without auth', async () => {
|
||||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3000/typedrequest',
|
||||||
'getHealthStatus'
|
'getHealthStatus'
|
||||||
);
|
);
|
||||||
|
|
||||||
// No identity provided
|
try {
|
||||||
const response = await healthRequest.fire({});
|
// No identity provided — should be rejected
|
||||||
|
await healthRequest.fire({} as any);
|
||||||
expect(response).toHaveProperty('health');
|
expect(true).toBeFalse(); // Should not reach here
|
||||||
expect(response.health.healthy).toBeTrue();
|
} catch (error) {
|
||||||
console.log('Public endpoint accessible without auth');
|
expect(error).toBeTruthy();
|
||||||
|
console.log('Protected endpoint correctly rejects unauthenticated request');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should allow read-only config access', async () => {
|
tap.test('should allow authenticated access to protected endpoints', async () => {
|
||||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'http://localhost:3000/typedrequest',
|
'http://localhost:3000/typedrequest',
|
||||||
'getConfiguration'
|
'getConfiguration'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Config is read-only and doesn't require auth
|
const response = await configRequest.fire({
|
||||||
const response = await configRequest.fire({});
|
identity: adminIdentity,
|
||||||
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
expect(response.config).toHaveProperty('system');
|
expect(response.config).toHaveProperty('system');
|
||||||
@@ -114,7 +117,7 @@ tap.test('should allow read-only config access', async () => {
|
|||||||
expect(response.config).toHaveProperty('cache');
|
expect(response.config).toHaveProperty('cache');
|
||||||
expect(response.config).toHaveProperty('radius');
|
expect(response.config).toHaveProperty('radius');
|
||||||
expect(response.config).toHaveProperty('remoteIngress');
|
expect(response.config).toHaveProperty('remoteIngress');
|
||||||
console.log('Configuration read successfully');
|
console.log('Authenticated access to config successful');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should stop DCRouter', async () => {
|
tap.test('should stop DCRouter', async () => {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '10.1.3',
|
version: '11.2.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ export class DcRouter {
|
|||||||
public detectedPublicIp: string | null = null;
|
public detectedPublicIp: string | null = null;
|
||||||
|
|
||||||
// DNS query logging rate limiter state
|
// DNS query logging rate limiter state
|
||||||
private dnsLogWindow: number[] = [];
|
private dnsLogWindowSecond: number = 0; // epoch second of current window
|
||||||
|
private dnsLogWindowCount: number = 0; // queries logged this second
|
||||||
private dnsBatchCount: number = 0;
|
private dnsBatchCount: number = 0;
|
||||||
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
|
private dnsBatchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
@@ -901,7 +902,8 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
this.dnsBatchTimer = null;
|
this.dnsBatchTimer = null;
|
||||||
this.dnsBatchCount = 0;
|
this.dnsBatchCount = 0;
|
||||||
this.dnsLogWindow = [];
|
this.dnsLogWindowSecond = 0;
|
||||||
|
this.dnsLogWindowCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.opsServer.stop();
|
await this.opsServer.stop();
|
||||||
@@ -1312,11 +1314,14 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive logging: individual logs up to 2/sec, then batch
|
// Adaptive logging: individual logs up to 2/sec, then batch
|
||||||
const now = Date.now();
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
this.dnsLogWindow = this.dnsLogWindow.filter(t => now - t < 1000);
|
if (nowSec !== this.dnsLogWindowSecond) {
|
||||||
|
this.dnsLogWindowSecond = nowSec;
|
||||||
|
this.dnsLogWindowCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.dnsLogWindow.length < 2) {
|
if (this.dnsLogWindowCount < 2) {
|
||||||
this.dnsLogWindow.push(now);
|
this.dnsLogWindowCount++;
|
||||||
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
const summary = event.questions.map(q => `${q.type} ${q.name}`).join(', ');
|
||||||
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
logger.log('info', `DNS query: ${summary} (${event.responseTimeMs}ms, ${event.answered ? 'answered' : 'unanswered'})`, { zone: 'dns' });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export class MetricsManager {
|
|||||||
queryTypes: {} as Record<string, number>,
|
queryTypes: {} as Record<string, number>,
|
||||||
topDomains: new Map<string, number>(),
|
topDomains: new Map<string, number>(),
|
||||||
lastResetDate: new Date().toDateString(),
|
lastResetDate: new Date().toDateString(),
|
||||||
queryTimestamps: [] as number[], // Track query timestamps for rate calculation
|
// Per-second query count ring buffer (300 entries = 5 minutes)
|
||||||
|
queryRing: new Int32Array(300),
|
||||||
|
queryRingLastSecond: 0, // last epoch second that was written
|
||||||
responseTimes: [] as number[], // Track response times in ms
|
responseTimes: [] as number[], // Track response times in ms
|
||||||
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
|
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
|
||||||
};
|
};
|
||||||
@@ -95,7 +97,8 @@ export class MetricsManager {
|
|||||||
this.dnsMetrics.cacheMisses = 0;
|
this.dnsMetrics.cacheMisses = 0;
|
||||||
this.dnsMetrics.queryTypes = {};
|
this.dnsMetrics.queryTypes = {};
|
||||||
this.dnsMetrics.topDomains.clear();
|
this.dnsMetrics.topDomains.clear();
|
||||||
this.dnsMetrics.queryTimestamps = [];
|
this.dnsMetrics.queryRing.fill(0);
|
||||||
|
this.dnsMetrics.queryRingLastSecond = 0;
|
||||||
this.dnsMetrics.responseTimes = [];
|
this.dnsMetrics.responseTimes = [];
|
||||||
this.dnsMetrics.recentQueries = [];
|
this.dnsMetrics.recentQueries = [];
|
||||||
this.dnsMetrics.lastResetDate = currentDate;
|
this.dnsMetrics.lastResetDate = currentDate;
|
||||||
@@ -111,15 +114,6 @@ export class MetricsManager {
|
|||||||
this.securityMetrics.lastResetDate = currentDate;
|
this.securityMetrics.lastResetDate = currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prune old query timestamps (keep last 5 minutes)
|
|
||||||
const fiveMinutesAgo = Date.now() - 300000;
|
|
||||||
const idx = this.dnsMetrics.queryTimestamps.findIndex(ts => ts >= fiveMinutesAgo);
|
|
||||||
if (idx > 0) {
|
|
||||||
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.slice(idx);
|
|
||||||
} else if (idx === -1) {
|
|
||||||
this.dnsMetrics.queryTimestamps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune old time-series buckets every minute (don't wait for lazy query)
|
// Prune old time-series buckets every minute (don't wait for lazy query)
|
||||||
this.pruneOldBuckets();
|
this.pruneOldBuckets();
|
||||||
}, 60000); // Check every minute
|
}, 60000); // Check every minute
|
||||||
@@ -150,16 +144,16 @@ export class MetricsManager {
|
|||||||
const smartMetricsData = await this.smartMetrics.getMetrics();
|
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
|
const proxyStats = this.dcRouter.smartProxy ? await this.dcRouter.smartProxy.getStatistics() : null;
|
||||||
|
const { heapUsed, heapTotal, external, rss } = process.memoryUsage();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
startTime: Date.now() - (process.uptime() * 1000),
|
startTime: Date.now() - (process.uptime() * 1000),
|
||||||
memoryUsage: {
|
memoryUsage: {
|
||||||
heapUsed: process.memoryUsage().heapUsed,
|
heapUsed,
|
||||||
heapTotal: process.memoryUsage().heapTotal,
|
heapTotal,
|
||||||
external: process.memoryUsage().external,
|
external,
|
||||||
rss: process.memoryUsage().rss,
|
rss,
|
||||||
// Add SmartMetrics memory data
|
|
||||||
maxMemoryMB: this.smartMetrics.maxMemoryMB,
|
maxMemoryMB: this.smartMetrics.maxMemoryMB,
|
||||||
actualUsageBytes: smartMetricsData.memoryUsageBytes,
|
actualUsageBytes: smartMetricsData.memoryUsageBytes,
|
||||||
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
||||||
@@ -228,11 +222,8 @@ export class MetricsManager {
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([domain, count]) => ({ domain, count }));
|
.map(([domain, count]) => ({ domain, count }));
|
||||||
|
|
||||||
// Calculate queries per second from recent timestamps
|
// Calculate queries per second from ring buffer (sum last 60 seconds)
|
||||||
const now = Date.now();
|
const queriesPerSecond = this.getQueryRingSum(60) / 60;
|
||||||
const oneMinuteAgo = now - 60000;
|
|
||||||
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
|
|
||||||
const queriesPerSecond = recentQueries.length / 60;
|
|
||||||
|
|
||||||
// Calculate average response time
|
// Calculate average response time
|
||||||
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
|
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
|
||||||
@@ -436,8 +427,8 @@ export class MetricsManager {
|
|||||||
this.dnsMetrics.cacheMisses++;
|
this.dnsMetrics.cacheMisses++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track query timestamp (pruning moved to resetInterval to avoid O(n) per query)
|
// Increment per-second query counter in ring buffer
|
||||||
this.dnsMetrics.queryTimestamps.push(Date.now());
|
this.incrementQueryRing();
|
||||||
|
|
||||||
// Track response time if provided
|
// Track response time if provided
|
||||||
if (responseTimeMs) {
|
if (responseTimeMs) {
|
||||||
@@ -609,7 +600,7 @@ export class MetricsManager {
|
|||||||
requestsPerSecond,
|
requestsPerSecond,
|
||||||
requestsTotal,
|
requestsTotal,
|
||||||
};
|
};
|
||||||
}, 200); // Use 200ms cache for more frequent updates
|
}, 1000); // 1s cache — matches typical dashboard poll interval
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Time-series helpers ---
|
// --- Time-series helpers ---
|
||||||
@@ -638,6 +629,63 @@ export class MetricsManager {
|
|||||||
bucket.queries++;
|
bucket.queries++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the per-second query counter in the ring buffer.
|
||||||
|
* Zeros any stale slots between the last write and the current second.
|
||||||
|
*/
|
||||||
|
private incrementQueryRing(): void {
|
||||||
|
const currentSecond = Math.floor(Date.now() / 1000);
|
||||||
|
const ring = this.dnsMetrics.queryRing;
|
||||||
|
const last = this.dnsMetrics.queryRingLastSecond;
|
||||||
|
|
||||||
|
if (last === 0) {
|
||||||
|
// First call — zero and anchor
|
||||||
|
ring.fill(0);
|
||||||
|
this.dnsMetrics.queryRingLastSecond = currentSecond;
|
||||||
|
ring[currentSecond % ring.length] = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gap = currentSecond - last;
|
||||||
|
if (gap >= ring.length) {
|
||||||
|
// Entire ring is stale — clear all
|
||||||
|
ring.fill(0);
|
||||||
|
} else if (gap > 0) {
|
||||||
|
// Zero slots from (last+1) to currentSecond (inclusive)
|
||||||
|
for (let s = last + 1; s <= currentSecond; s++) {
|
||||||
|
ring[s % ring.length] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dnsMetrics.queryRingLastSecond = currentSecond;
|
||||||
|
ring[currentSecond % ring.length]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sum query counts from the ring buffer for the last N seconds.
|
||||||
|
*/
|
||||||
|
private getQueryRingSum(seconds: number): number {
|
||||||
|
const currentSecond = Math.floor(Date.now() / 1000);
|
||||||
|
const ring = this.dnsMetrics.queryRing;
|
||||||
|
const last = this.dnsMetrics.queryRingLastSecond;
|
||||||
|
|
||||||
|
if (last === 0) return 0;
|
||||||
|
|
||||||
|
// First, zero stale slots so reads are accurate even without writes
|
||||||
|
const gap = currentSecond - last;
|
||||||
|
if (gap >= ring.length) return 0; // all data is stale
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
const limit = Math.min(seconds, ring.length);
|
||||||
|
for (let i = 0; i < limit; i++) {
|
||||||
|
const sec = currentSecond - i;
|
||||||
|
if (sec < last - (ring.length - 1)) break; // slot is from older cycle
|
||||||
|
if (sec > last) continue; // no writes yet for this second
|
||||||
|
sum += ring[sec % ring.length];
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
private pruneOldBuckets(): void {
|
private pruneOldBuckets(): void {
|
||||||
const cutoff = Date.now() - 86400000; // 24h
|
const cutoff = Date.now() - 86400000; // 24h
|
||||||
for (const key of this.emailMinuteBuckets.keys()) {
|
for (const key of this.emailMinuteBuckets.keys()) {
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import type DcRouter from '../classes.dcrouter.js';
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
import * as handlers from './handlers/index.js';
|
import * as handlers from './handlers/index.js';
|
||||||
|
import * as interfaces from '../../ts_interfaces/index.js';
|
||||||
|
import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js';
|
||||||
|
|
||||||
export class OpsServer {
|
export class OpsServer {
|
||||||
public dcRouterRef: DcRouter;
|
public dcRouterRef: DcRouter;
|
||||||
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
||||||
|
|
||||||
// TypedRouter for OpsServer-specific handlers
|
// Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
// Auth-enforced routers — middleware validates identity before any handler runs
|
||||||
|
public viewRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
|
||||||
|
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
|
||||||
|
|
||||||
// Handler instances
|
// Handler instances
|
||||||
public adminHandler: handlers.AdminHandler;
|
public adminHandler: handlers.AdminHandler;
|
||||||
private configHandler: handlers.ConfigHandler;
|
private configHandler: handlers.ConfigHandler;
|
||||||
@@ -51,10 +57,25 @@ export class OpsServer {
|
|||||||
* Set up all TypedRequest handlers
|
* Set up all TypedRequest handlers
|
||||||
*/
|
*/
|
||||||
private async setupHandlers(): Promise<void> {
|
private async setupHandlers(): Promise<void> {
|
||||||
// Instantiate all handlers - they self-register with the typedrouter
|
// AdminHandler must be initialized first (JWT setup needed for guards)
|
||||||
this.adminHandler = new handlers.AdminHandler(this);
|
this.adminHandler = new handlers.AdminHandler(this);
|
||||||
await this.adminHandler.initialize(); // JWT needs async initialization
|
await this.adminHandler.initialize();
|
||||||
|
|
||||||
|
// viewRouter middleware: requires valid identity (any logged-in user)
|
||||||
|
this.viewRouter.addMiddleware(async (typedRequest) => {
|
||||||
|
await requireValidIdentity(this.adminHandler, typedRequest.request);
|
||||||
|
});
|
||||||
|
|
||||||
|
// adminRouter middleware: requires admin identity
|
||||||
|
this.adminRouter.addMiddleware(async (typedRequest) => {
|
||||||
|
await requireAdminIdentity(this.adminHandler, typedRequest.request);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect auth routers to the main typedrouter
|
||||||
|
this.typedrouter.addTypedRouter(this.viewRouter);
|
||||||
|
this.typedrouter.addTypedRouter(this.adminRouter);
|
||||||
|
|
||||||
|
// Instantiate all handlers — they self-register with the appropriate router
|
||||||
this.configHandler = new handlers.ConfigHandler(this);
|
this.configHandler = new handlers.ConfigHandler(this);
|
||||||
this.logsHandler = new handlers.LogsHandler(this);
|
this.logsHandler = new handlers.LogsHandler(this);
|
||||||
this.securityHandler = new handlers.SecurityHandler(this);
|
this.securityHandler = new handlers.SecurityHandler(this);
|
||||||
|
|||||||
@@ -3,34 +3,20 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class ApiTokenHandler {
|
export class ApiTokenHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Token management requires admin JWT only (tokens cannot manage tokens).
|
|
||||||
*/
|
|
||||||
private async requireAdmin(identity?: interfaces.data.IIdentity): Promise<string> {
|
|
||||||
if (!identity?.jwt) {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
||||||
}
|
|
||||||
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({ identity });
|
|
||||||
if (!isAdmin) {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError('admin access required');
|
|
||||||
}
|
|
||||||
return identity.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// All token management endpoints register directly on adminRouter
|
||||||
|
// (middleware enforces admin JWT check, so no per-handler requireAdmin needed)
|
||||||
|
const router = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
// Create API token
|
// Create API token
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
||||||
'createApiToken',
|
'createApiToken',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const userId = await this.requireAdmin(dataArg.identity);
|
|
||||||
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { success: false, message: 'Token management not initialized' };
|
return { success: false, message: 'Token management not initialized' };
|
||||||
@@ -39,7 +25,7 @@ export class ApiTokenHandler {
|
|||||||
dataArg.name,
|
dataArg.name,
|
||||||
dataArg.scopes,
|
dataArg.scopes,
|
||||||
dataArg.expiresInDays ?? null,
|
dataArg.expiresInDays ?? null,
|
||||||
userId,
|
dataArg.identity.userId,
|
||||||
);
|
);
|
||||||
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
||||||
},
|
},
|
||||||
@@ -47,11 +33,10 @@ export class ApiTokenHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// List API tokens
|
// List API tokens
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
||||||
'listApiTokens',
|
'listApiTokens',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAdmin(dataArg.identity);
|
|
||||||
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { tokens: [] };
|
return { tokens: [] };
|
||||||
@@ -62,11 +47,10 @@ export class ApiTokenHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Revoke API token
|
// Revoke API token
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
||||||
'revokeApiToken',
|
'revokeApiToken',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAdmin(dataArg.identity);
|
|
||||||
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { success: false, message: 'Token management not initialized' };
|
return { success: false, message: 'Token management not initialized' };
|
||||||
@@ -78,11 +62,10 @@ export class ApiTokenHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Roll API token
|
// Roll API token
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RollApiToken>(
|
||||||
'rollApiToken',
|
'rollApiToken',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAdmin(dataArg.identity);
|
|
||||||
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { success: false, message: 'Token management not initialized' };
|
return { success: false, message: 'Token management not initialized' };
|
||||||
@@ -97,11 +80,10 @@ export class ApiTokenHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Toggle API token
|
// Toggle API token
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
||||||
'toggleApiToken',
|
'toggleApiToken',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await this.requireAdmin(dataArg.identity);
|
|
||||||
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
return { success: false, message: 'Token management not initialized' };
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class CertificateHandler {
|
export class CertificateHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
const viewRouter = this.opsServerRef.viewRouter;
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
|
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
|
||||||
|
|
||||||
// Get Certificate Overview
|
// Get Certificate Overview
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
||||||
'getCertificateOverview',
|
'getCertificateOverview',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -23,8 +25,10 @@ export class CertificateHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
|
||||||
|
|
||||||
// Legacy route-based reprovision (backward compat)
|
// Legacy route-based reprovision (backward compat)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||||
'reprovisionCertificate',
|
'reprovisionCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -34,7 +38,7 @@ export class CertificateHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Domain-based reprovision (preferred)
|
// Domain-based reprovision (preferred)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||||
'reprovisionCertificateDomain',
|
'reprovisionCertificateDomain',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -44,7 +48,7 @@ export class CertificateHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Delete certificate
|
// Delete certificate
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
||||||
'deleteCertificate',
|
'deleteCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -54,7 +58,7 @@ export class CertificateHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Export certificate
|
// Export certificate
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
||||||
'exportCertificate',
|
'exportCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -64,7 +68,7 @@ export class CertificateHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Import certificate
|
// Import certificate
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
||||||
'importCertificate',
|
'importCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class ConfigHandler {
|
export class ConfigHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// Config endpoint registers directly on viewRouter (valid identity required via middleware)
|
||||||
|
const router = this.opsServerRef.viewRouter;
|
||||||
|
|
||||||
// Get Configuration Handler (read-only)
|
// Get Configuration Handler (read-only)
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'getConfiguration',
|
'getConfiguration',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class EmailOpsHandler {
|
export class EmailOpsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
const viewRouter = this.opsServerRef.viewRouter;
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
|
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
|
||||||
|
|
||||||
// Get All Emails Handler
|
// Get All Emails Handler
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAllEmails>(
|
||||||
'getAllEmails',
|
'getAllEmails',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -24,7 +25,7 @@ export class EmailOpsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get Email Detail Handler
|
// Get Email Detail Handler
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailDetail>(
|
||||||
'getEmailDetail',
|
'getEmailDetail',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
@@ -34,8 +35,10 @@ export class EmailOpsHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---- Write endpoints (adminRouter) ----
|
||||||
|
|
||||||
// Resend Failed Email Handler
|
// Resend Failed Email Handler
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
|
||||||
'resendEmail',
|
'resendEmail',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ let logPushDestinationInstalled = false;
|
|||||||
let currentOpsServerRef: OpsServer | null = null;
|
let currentOpsServerRef: OpsServer | null = null;
|
||||||
|
|
||||||
export class LogsHandler {
|
export class LogsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
private activeStreamStops: Set<() => void> = new Set();
|
private activeStreamStops: Set<() => void> = new Set();
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
this.setupLogPushDestination();
|
this.setupLogPushDestination();
|
||||||
}
|
}
|
||||||
@@ -35,8 +32,11 @@ export class LogsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// All log endpoints register directly on viewRouter (valid identity required via middleware)
|
||||||
|
const router = this.opsServerRef.viewRouter;
|
||||||
|
|
||||||
// Get Recent Logs Handler
|
// Get Recent Logs Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
|
||||||
'getRecentLogs',
|
'getRecentLogs',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -59,7 +59,7 @@ export class LogsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get Log Stream Handler
|
// Get Log Stream Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
|
||||||
'getLogStream',
|
'getLogStream',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -3,21 +3,19 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class RadiusHandler {
|
export class RadiusHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
const viewRouter = this.opsServerRef.viewRouter;
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// RADIUS Client Management
|
// RADIUS Client Management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Get all RADIUS clients
|
// Get all RADIUS clients (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusClients>(
|
||||||
'getRadiusClients',
|
'getRadiusClients',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -40,8 +38,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add or update a RADIUS client
|
// Add or update a RADIUS client (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRadiusClient>(
|
||||||
'setRadiusClient',
|
'setRadiusClient',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -61,8 +59,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove a RADIUS client
|
// Remove a RADIUS client (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRadiusClient>(
|
||||||
'removeRadiusClient',
|
'removeRadiusClient',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -85,8 +83,8 @@ export class RadiusHandler {
|
|||||||
// VLAN Mapping Management
|
// VLAN Mapping Management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Get all VLAN mappings
|
// Get all VLAN mappings (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetVlanMappings>(
|
||||||
'getVlanMappings',
|
'getVlanMappings',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -121,8 +119,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add or update a VLAN mapping
|
// Add or update a VLAN mapping (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetVlanMapping>(
|
||||||
'setVlanMapping',
|
'setVlanMapping',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -153,8 +151,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove a VLAN mapping
|
// Remove a VLAN mapping (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveVlanMapping>(
|
||||||
'removeVlanMapping',
|
'removeVlanMapping',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -174,8 +172,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update VLAN configuration
|
// Update VLAN configuration (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateVlanConfig>(
|
||||||
'updateVlanConfig',
|
'updateVlanConfig',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -206,8 +204,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test VLAN assignment
|
// Test VLAN assignment (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_TestVlanAssignment>(
|
||||||
'testVlanAssignment',
|
'testVlanAssignment',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -240,8 +238,8 @@ export class RadiusHandler {
|
|||||||
// Accounting / Session Management
|
// Accounting / Session Management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Get active sessions
|
// Get active sessions (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusSessions>(
|
||||||
'getRadiusSessions',
|
'getRadiusSessions',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -289,8 +287,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Disconnect a session
|
// Disconnect a session (write)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DisconnectRadiusSession>(
|
||||||
'disconnectRadiusSession',
|
'disconnectRadiusSession',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -314,8 +312,8 @@ export class RadiusHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get accounting summary
|
// Get accounting summary (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
||||||
'getRadiusAccountingSummary',
|
'getRadiusAccountingSummary',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -351,8 +349,8 @@ export class RadiusHandler {
|
|||||||
// Statistics
|
// Statistics
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Get RADIUS statistics
|
// Get RADIUS statistics (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRadiusStatistics>(
|
||||||
'getRadiusStatistics',
|
'getRadiusStatistics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import type { OpsServer } from '../classes.opsserver.js';
|
|||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
export class RemoteIngressHandler {
|
export class RemoteIngressHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
const viewRouter = this.opsServerRef.viewRouter;
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
|
// ---- Read endpoints (viewRouter — valid identity required via middleware) ----
|
||||||
|
|
||||||
// Get all remote ingress edges
|
// Get all remote ingress edges
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
|
||||||
'getRemoteIngresses',
|
'getRemoteIngresses',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -36,8 +38,10 @@ export class RemoteIngressHandler {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---- Write endpoints (adminRouter) ----
|
||||||
|
|
||||||
// Create a new remote ingress edge
|
// Create a new remote ingress edge
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
|
||||||
'createRemoteIngress',
|
'createRemoteIngress',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -69,7 +73,7 @@ export class RemoteIngressHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Delete a remote ingress edge
|
// Delete a remote ingress edge
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
|
||||||
'deleteRemoteIngress',
|
'deleteRemoteIngress',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -94,7 +98,7 @@ export class RemoteIngressHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update a remote ingress edge
|
// Update a remote ingress edge
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
|
||||||
'updateRemoteIngress',
|
'updateRemoteIngress',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -138,7 +142,7 @@ export class RemoteIngressHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Regenerate secret for an edge
|
// Regenerate secret for an edge
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
||||||
'regenerateRemoteIngressSecret',
|
'regenerateRemoteIngressSecret',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -164,8 +168,8 @@ export class RemoteIngressHandler {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get runtime status of all edges
|
// Get runtime status of all edges (read)
|
||||||
this.typedrouter.addTypedHandler(
|
viewRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
||||||
'getRemoteIngressStatus',
|
'getRemoteIngressStatus',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -178,8 +182,8 @@ export class RemoteIngressHandler {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get a connection token for an edge
|
// Get a connection token for an edge (write — exposes secret)
|
||||||
this.typedrouter.addTypedHandler(
|
adminRouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||||
'getRemoteIngressConnectionToken',
|
'getRemoteIngressConnectionToken',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import * as interfaces from '../../../ts_interfaces/index.js';
|
|||||||
import { MetricsManager } from '../../monitoring/index.js';
|
import { MetricsManager } from '../../monitoring/index.js';
|
||||||
|
|
||||||
export class SecurityHandler {
|
export class SecurityHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// All security endpoints register directly on viewRouter (valid identity required via middleware)
|
||||||
|
const router = this.opsServerRef.viewRouter;
|
||||||
|
|
||||||
// Security Metrics Handler
|
// Security Metrics Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>(
|
||||||
'getSecurityMetrics',
|
'getSecurityMetrics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -40,7 +39,7 @@ export class SecurityHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Active Connections Handler
|
// Active Connections Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>(
|
||||||
'getActiveConnections',
|
'getActiveConnections',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -77,8 +76,8 @@ export class SecurityHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Network Stats Handler - provides comprehensive network metrics
|
// Network Stats Handler - provides comprehensive network metrics
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetNetworkStats>(
|
||||||
'getNetworkStats',
|
'getNetworkStats',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
// Get network stats from MetricsManager if available
|
// Get network stats from MetricsManager if available
|
||||||
@@ -121,7 +120,7 @@ export class SecurityHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Rate Limit Status Handler
|
// Rate Limit Status Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||||
'getRateLimitStatus',
|
'getRateLimitStatus',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import { MetricsManager } from '../../monitoring/index.js';
|
|||||||
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
||||||
|
|
||||||
export class StatsHandler {
|
export class StatsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
|
// All stats endpoints register directly on viewRouter (valid identity required via middleware)
|
||||||
|
const router = this.opsServerRef.viewRouter;
|
||||||
|
|
||||||
// Server Statistics Handler
|
// Server Statistics Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>(
|
||||||
'getServerStatistics',
|
'getServerStatistics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -38,7 +37,7 @@ export class StatsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Email Statistics Handler
|
// Email Statistics Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>(
|
||||||
'getEmailStatistics',
|
'getEmailStatistics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -77,7 +76,7 @@ export class StatsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// DNS Statistics Handler
|
// DNS Statistics Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>(
|
||||||
'getDnsStatistics',
|
'getDnsStatistics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -114,7 +113,7 @@ export class StatsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Queue Status Handler
|
// Queue Status Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>(
|
||||||
'getQueueStatus',
|
'getQueueStatus',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -142,7 +141,7 @@ export class StatsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Health Status Handler
|
// Health Status Handler
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
'getHealthStatus',
|
'getHealthStatus',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
@@ -167,7 +166,7 @@ export class StatsHandler {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Combined Metrics Handler - More efficient for frontend polling
|
// Combined Metrics Handler - More efficient for frontend polling
|
||||||
this.typedrouter.addTypedHandler(
|
router.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||||
'getCombinedMetrics',
|
'getCombinedMetrics',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ export async function passGuards<T extends { identity?: any }>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to check admin identity in handlers
|
* Helper to check admin identity in handlers and middleware.
|
||||||
|
* Accepts both optional and required identity for flexibility.
|
||||||
*/
|
*/
|
||||||
export async function requireAdminIdentity<T extends { identity?: interfaces.data.IIdentity }>(
|
export async function requireAdminIdentity(
|
||||||
adminHandler: AdminHandler,
|
adminHandler: AdminHandler,
|
||||||
dataArg: T
|
dataArg: { identity?: interfaces.data.IIdentity }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!dataArg.identity) {
|
if (!dataArg.identity) {
|
||||||
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
||||||
@@ -39,11 +40,12 @@ export async function requireAdminIdentity<T extends { identity?: interfaces.dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to check valid identity in handlers
|
* Helper to check valid identity in handlers and middleware.
|
||||||
|
* Accepts both optional and required identity for flexibility.
|
||||||
*/
|
*/
|
||||||
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>(
|
export async function requireValidIdentity(
|
||||||
adminHandler: AdminHandler,
|
adminHandler: AdminHandler,
|
||||||
dataArg: T
|
dataArg: { identity?: interfaces.data.IIdentity }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!dataArg.identity) {
|
if (!dataArg.identity) {
|
||||||
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
||||||
|
|||||||
@@ -162,8 +162,9 @@ export class SecurityLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return most recent events up to limit
|
// Return most recent events up to limit (slice first to avoid mutating source)
|
||||||
return filteredEvents
|
return filteredEvents
|
||||||
|
.slice()
|
||||||
.sort((a, b) => b.timestamp - a.timestamp)
|
.sort((a, b) => b.timestamp - a.timestamp)
|
||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
@@ -249,40 +250,34 @@ export class SecurityLogger {
|
|||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
topDomains: Array<{ domain: string; count: number }>;
|
topDomains: Array<{ domain: string; count: number }>;
|
||||||
} {
|
} {
|
||||||
// Filter by time window if provided
|
const cutoff = timeWindow ? Date.now() - timeWindow : 0;
|
||||||
let events = this.securityEvents;
|
|
||||||
if (timeWindow) {
|
// Initialize counters
|
||||||
const cutoff = Date.now() - timeWindow;
|
const byLevel = {} as Record<SecurityLogLevel, number>;
|
||||||
events = events.filter(e => e.timestamp >= cutoff);
|
for (const level of Object.values(SecurityLogLevel)) {
|
||||||
|
byLevel[level] = 0;
|
||||||
|
}
|
||||||
|
const byType = {} as Record<SecurityEventType, number>;
|
||||||
|
for (const type of Object.values(SecurityEventType)) {
|
||||||
|
byType[type] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count by level
|
|
||||||
const byLevel = Object.values(SecurityLogLevel).reduce((acc, level) => {
|
|
||||||
acc[level] = events.filter(e => e.level === level).length;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<SecurityLogLevel, number>);
|
|
||||||
|
|
||||||
// Count by type
|
|
||||||
const byType = Object.values(SecurityEventType).reduce((acc, type) => {
|
|
||||||
acc[type] = events.filter(e => e.type === type).length;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<SecurityEventType, number>);
|
|
||||||
|
|
||||||
// Count by IP
|
|
||||||
const ipCounts = new Map<string, number>();
|
const ipCounts = new Map<string, number>();
|
||||||
events.forEach(e => {
|
const domainCounts = new Map<string, number>();
|
||||||
|
|
||||||
|
// Single pass over all events
|
||||||
|
let total = 0;
|
||||||
|
for (const e of this.securityEvents) {
|
||||||
|
if (cutoff && e.timestamp < cutoff) continue;
|
||||||
|
total++;
|
||||||
|
byLevel[e.level]++;
|
||||||
|
byType[e.type]++;
|
||||||
if (e.ipAddress) {
|
if (e.ipAddress) {
|
||||||
ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1);
|
ipCounts.set(e.ipAddress, (ipCounts.get(e.ipAddress) || 0) + 1);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Count by domain
|
|
||||||
const domainCounts = new Map<string, number>();
|
|
||||||
events.forEach(e => {
|
|
||||||
if (e.domain) {
|
if (e.domain) {
|
||||||
domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
|
domainCounts.set(e.domain, (domainCounts.get(e.domain) || 0) + 1);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Sort and limit top entries
|
// Sort and limit top entries
|
||||||
const topIPs = Array.from(ipCounts.entries())
|
const topIPs = Array.from(ipCounts.entries())
|
||||||
@@ -295,12 +290,6 @@ export class SecurityLogger {
|
|||||||
.sort((a, b) => b.count - a.count)
|
.sort((a, b) => b.count - a.count)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
return {
|
return { total, byLevel, byType, topIPs, topDomains };
|
||||||
total: events.length,
|
|
||||||
byLevel,
|
|
||||||
byType,
|
|
||||||
topIPs,
|
|
||||||
topDomains
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
157
ts_apiclient/classes.apitoken.ts
Normal file
157
ts_apiclient/classes.apitoken.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class ApiToken {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
// Data from IApiTokenInfo
|
||||||
|
public id: string;
|
||||||
|
public name: string;
|
||||||
|
public scopes: interfaces.data.TApiTokenScope[];
|
||||||
|
public createdAt: number;
|
||||||
|
public expiresAt: number | null;
|
||||||
|
public lastUsedAt: number | null;
|
||||||
|
public enabled: boolean;
|
||||||
|
|
||||||
|
/** Only set on creation or roll. Not persisted on server side. */
|
||||||
|
public tokenValue?: string;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IApiTokenInfo, tokenValue?: string) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.id = data.id;
|
||||||
|
this.name = data.name;
|
||||||
|
this.scopes = data.scopes;
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
this.expiresAt = data.expiresAt;
|
||||||
|
this.lastUsedAt = data.lastUsedAt;
|
||||||
|
this.enabled = data.enabled;
|
||||||
|
this.tokenValue = tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async revoke(): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RevokeApiToken>(
|
||||||
|
'revokeApiToken',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to revoke token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async roll(): Promise<string> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RollApiToken>(
|
||||||
|
'rollApiToken',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to roll token');
|
||||||
|
}
|
||||||
|
this.tokenValue = response.tokenValue;
|
||||||
|
return response.tokenValue!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggle(enabled: boolean): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleApiToken>(
|
||||||
|
'toggleApiToken',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id, enabled }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to toggle token');
|
||||||
|
}
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiTokenBuilder {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
private tokenName: string = '';
|
||||||
|
private tokenScopes: interfaces.data.TApiTokenScope[] = [];
|
||||||
|
private tokenExpiresInDays?: number | null;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.tokenName = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setScopes(scopes: interfaces.data.TApiTokenScope[]): this {
|
||||||
|
this.tokenScopes = scopes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addScope(scope: interfaces.data.TApiTokenScope): this {
|
||||||
|
if (!this.tokenScopes.includes(scope)) {
|
||||||
|
this.tokenScopes.push(scope);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setExpiresInDays(days: number | null): this {
|
||||||
|
this.tokenExpiresInDays = days;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<ApiToken> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_CreateApiToken>(
|
||||||
|
'createApiToken',
|
||||||
|
this.clientRef.buildRequestPayload({
|
||||||
|
name: this.tokenName,
|
||||||
|
scopes: this.tokenScopes,
|
||||||
|
expiresInDays: this.tokenExpiresInDays,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to create API token');
|
||||||
|
}
|
||||||
|
return new ApiToken(
|
||||||
|
this.clientRef,
|
||||||
|
{
|
||||||
|
id: response.tokenId!,
|
||||||
|
name: this.tokenName,
|
||||||
|
scopes: this.tokenScopes,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresAt: this.tokenExpiresInDays
|
||||||
|
? Date.now() + this.tokenExpiresInDays * 24 * 60 * 60 * 1000
|
||||||
|
: null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
response.tokenValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiTokenManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<ApiToken[]> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ListApiTokens>(
|
||||||
|
'listApiTokens',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.tokens.map((t) => new ApiToken(this.clientRef, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(options: {
|
||||||
|
name: string;
|
||||||
|
scopes: interfaces.data.TApiTokenScope[];
|
||||||
|
expiresInDays?: number | null;
|
||||||
|
}): Promise<ApiToken> {
|
||||||
|
return this.build()
|
||||||
|
.setName(options.name)
|
||||||
|
.setScopes(options.scopes)
|
||||||
|
.setExpiresInDays(options.expiresInDays ?? null)
|
||||||
|
.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): ApiTokenBuilder {
|
||||||
|
return new ApiTokenBuilder(this.clientRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
ts_apiclient/classes.certificate.ts
Normal file
123
ts_apiclient/classes.certificate.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class Certificate {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
// Data from ICertificateInfo
|
||||||
|
public domain: string;
|
||||||
|
public routeNames: string[];
|
||||||
|
public status: interfaces.requests.TCertificateStatus;
|
||||||
|
public source: interfaces.requests.TCertificateSource;
|
||||||
|
public tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
public expiryDate?: string;
|
||||||
|
public issuer?: string;
|
||||||
|
public issuedAt?: string;
|
||||||
|
public error?: string;
|
||||||
|
public canReprovision: boolean;
|
||||||
|
public backoffInfo?: {
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string;
|
||||||
|
lastError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.ICertificateInfo) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.domain = data.domain;
|
||||||
|
this.routeNames = data.routeNames;
|
||||||
|
this.status = data.status;
|
||||||
|
this.source = data.source;
|
||||||
|
this.tlsMode = data.tlsMode;
|
||||||
|
this.expiryDate = data.expiryDate;
|
||||||
|
this.issuer = data.issuer;
|
||||||
|
this.issuedAt = data.issuedAt;
|
||||||
|
this.error = data.error;
|
||||||
|
this.canReprovision = data.canReprovision;
|
||||||
|
this.backoffInfo = data.backoffInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reprovision(): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||||
|
'reprovisionCertificateDomain',
|
||||||
|
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to reprovision certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteCertificate>(
|
||||||
|
'deleteCertificate',
|
||||||
|
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to delete certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async export(): Promise<{
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
} | undefined> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ExportCertificate>(
|
||||||
|
'exportCertificate',
|
||||||
|
this.clientRef.buildRequestPayload({ domain: this.domain }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to export certificate');
|
||||||
|
}
|
||||||
|
return response.cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICertificateSummary {
|
||||||
|
total: number;
|
||||||
|
valid: number;
|
||||||
|
expiring: number;
|
||||||
|
expired: number;
|
||||||
|
failed: number;
|
||||||
|
unknown: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<{ certificates: Certificate[]; summary: ICertificateSummary }> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetCertificateOverview>(
|
||||||
|
'getCertificateOverview',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
certificates: response.certificates.map((c) => new Certificate(this.clientRef, c)),
|
||||||
|
summary: response.summary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async import(cert: {
|
||||||
|
id: string;
|
||||||
|
domainName: string;
|
||||||
|
created: number;
|
||||||
|
validUntil: number;
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
csr: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ImportCertificate>(
|
||||||
|
'importCertificate',
|
||||||
|
this.clientRef.buildRequestPayload({ cert }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to import certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
ts_apiclient/classes.config.ts
Normal file
17
ts_apiclient/classes.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class ConfigManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(section?: string): Promise<interfaces.requests.IReq_GetConfiguration['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetConfiguration>(
|
||||||
|
'getConfiguration',
|
||||||
|
this.clientRef.buildRequestPayload({ section }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
112
ts_apiclient/classes.dcrouterapiclient.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
import { RouteManager } from './classes.route.js';
|
||||||
|
import { CertificateManager } from './classes.certificate.js';
|
||||||
|
import { ApiTokenManager } from './classes.apitoken.js';
|
||||||
|
import { RemoteIngressManager } from './classes.remoteingress.js';
|
||||||
|
import { StatsManager } from './classes.stats.js';
|
||||||
|
import { ConfigManager } from './classes.config.js';
|
||||||
|
import { LogManager } from './classes.logs.js';
|
||||||
|
import { EmailManager } from './classes.email.js';
|
||||||
|
import { RadiusManager } from './classes.radius.js';
|
||||||
|
|
||||||
|
export interface IDcRouterApiClientOptions {
|
||||||
|
baseUrl: string;
|
||||||
|
apiToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DcRouterApiClient {
|
||||||
|
public baseUrl: string;
|
||||||
|
public apiToken?: string;
|
||||||
|
public identity?: interfaces.data.IIdentity;
|
||||||
|
|
||||||
|
// Resource managers
|
||||||
|
public routes: RouteManager;
|
||||||
|
public certificates: CertificateManager;
|
||||||
|
public apiTokens: ApiTokenManager;
|
||||||
|
public remoteIngress: RemoteIngressManager;
|
||||||
|
public stats: StatsManager;
|
||||||
|
public config: ConfigManager;
|
||||||
|
public logs: LogManager;
|
||||||
|
public emails: EmailManager;
|
||||||
|
public radius: RadiusManager;
|
||||||
|
|
||||||
|
constructor(options: IDcRouterApiClientOptions) {
|
||||||
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
||||||
|
this.apiToken = options.apiToken;
|
||||||
|
|
||||||
|
this.routes = new RouteManager(this);
|
||||||
|
this.certificates = new CertificateManager(this);
|
||||||
|
this.apiTokens = new ApiTokenManager(this);
|
||||||
|
this.remoteIngress = new RemoteIngressManager(this);
|
||||||
|
this.stats = new StatsManager(this);
|
||||||
|
this.config = new ConfigManager(this);
|
||||||
|
this.logs = new LogManager(this);
|
||||||
|
this.emails = new EmailManager(this);
|
||||||
|
this.radius = new RadiusManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Auth
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
public async login(username: string, password: string): Promise<interfaces.data.IIdentity> {
|
||||||
|
const response = await this.request<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
|
'adminLoginWithUsernameAndPassword',
|
||||||
|
{ username, password },
|
||||||
|
);
|
||||||
|
if (response.identity) {
|
||||||
|
this.identity = response.identity;
|
||||||
|
}
|
||||||
|
return response.identity!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
await this.request<interfaces.requests.IReq_AdminLogout>(
|
||||||
|
'adminLogout',
|
||||||
|
{ identity: this.identity! },
|
||||||
|
);
|
||||||
|
this.identity = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verifyIdentity(): Promise<{ valid: boolean; identity?: interfaces.data.IIdentity }> {
|
||||||
|
const response = await this.request<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
|
'verifyIdentity',
|
||||||
|
{ identity: this.identity! },
|
||||||
|
);
|
||||||
|
if (response.identity) {
|
||||||
|
this.identity = response.identity;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Internal request helper
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
public async request<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||||
|
method: string,
|
||||||
|
requestData: T['request'],
|
||||||
|
): Promise<T['response']> {
|
||||||
|
const typedRequest = new plugins.typedrequest.TypedRequest<T>(
|
||||||
|
`${this.baseUrl}/typedrequest`,
|
||||||
|
method,
|
||||||
|
);
|
||||||
|
return typedRequest.fire(requestData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a request payload with identity and optional API token auto-injected.
|
||||||
|
*/
|
||||||
|
public buildRequestPayload(extra: Record<string, any> = {}): Record<string, any> {
|
||||||
|
const payload: Record<string, any> = { ...extra };
|
||||||
|
if (this.identity) {
|
||||||
|
payload.identity = this.identity;
|
||||||
|
}
|
||||||
|
if (this.apiToken) {
|
||||||
|
payload.apiToken = this.apiToken;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
ts_apiclient/classes.email.ts
Normal file
77
ts_apiclient/classes.email.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class Email {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
// Data from IEmail
|
||||||
|
public id: string;
|
||||||
|
public direction: interfaces.requests.TEmailDirection;
|
||||||
|
public status: interfaces.requests.TEmailStatus;
|
||||||
|
public from: string;
|
||||||
|
public to: string;
|
||||||
|
public subject: string;
|
||||||
|
public timestamp: string;
|
||||||
|
public messageId: string;
|
||||||
|
public size: string;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient, data: interfaces.requests.IEmail) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.id = data.id;
|
||||||
|
this.direction = data.direction;
|
||||||
|
this.status = data.status;
|
||||||
|
this.from = data.from;
|
||||||
|
this.to = data.to;
|
||||||
|
this.subject = data.subject;
|
||||||
|
this.timestamp = data.timestamp;
|
||||||
|
this.messageId = data.messageId;
|
||||||
|
this.size = data.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDetail(): Promise<interfaces.requests.IEmailDetail | null> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||||
|
'getEmailDetail',
|
||||||
|
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||||
|
);
|
||||||
|
return response.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resend(): Promise<{ success: boolean; newQueueId?: string }> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||||
|
'resendEmail',
|
||||||
|
this.clientRef.buildRequestPayload({ emailId: this.id }) as any,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmailManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<Email[]> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetAllEmails>(
|
||||||
|
'getAllEmails',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.emails.map((e) => new Email(this.clientRef, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDetail(emailId: string): Promise<interfaces.requests.IEmailDetail | null> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetEmailDetail>(
|
||||||
|
'getEmailDetail',
|
||||||
|
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||||
|
);
|
||||||
|
return response.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resend(emailId: string): Promise<{ success: boolean; newQueueId?: string }> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_ResendEmail>(
|
||||||
|
'resendEmail',
|
||||||
|
this.clientRef.buildRequestPayload({ emailId }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
ts_apiclient/classes.logs.ts
Normal file
37
ts_apiclient/classes.logs.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class LogManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRecent(options?: {
|
||||||
|
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
timeRange?: string;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetRecentLogs['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetRecentLogs>(
|
||||||
|
'getRecentLogs',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStream(options?: {
|
||||||
|
follow?: boolean;
|
||||||
|
filters?: {
|
||||||
|
level?: string[];
|
||||||
|
category?: string[];
|
||||||
|
};
|
||||||
|
}): Promise<interfaces.requests.IReq_GetLogStream['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetLogStream>(
|
||||||
|
'getLogStream',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
ts_apiclient/classes.radius.ts
Normal file
180
ts_apiclient/classes.radius.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Sub-managers
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
export class RadiusClientManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<Array<{
|
||||||
|
name: string;
|
||||||
|
ipRange: string;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusClients>(
|
||||||
|
'getRadiusClients',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(client: {
|
||||||
|
name: string;
|
||||||
|
ipRange: string;
|
||||||
|
secret: string;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_SetRadiusClient>(
|
||||||
|
'setRadiusClient',
|
||||||
|
this.clientRef.buildRequestPayload({ client }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to set RADIUS client');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(name: string): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRadiusClient>(
|
||||||
|
'removeRadiusClient',
|
||||||
|
this.clientRef.buildRequestPayload({ name }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to remove RADIUS client');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadiusVlanManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<interfaces.requests.IReq_GetVlanMappings['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetVlanMappings>(
|
||||||
|
'getVlanMappings',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(mapping: {
|
||||||
|
mac: string;
|
||||||
|
vlan: number;
|
||||||
|
description?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_SetVlanMapping>(
|
||||||
|
'setVlanMapping',
|
||||||
|
this.clientRef.buildRequestPayload({ mapping }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to set VLAN mapping');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(mac: string): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveVlanMapping>(
|
||||||
|
'removeVlanMapping',
|
||||||
|
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to remove VLAN mapping');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateConfig(options: {
|
||||||
|
defaultVlan?: number;
|
||||||
|
allowUnknownMacs?: boolean;
|
||||||
|
}): Promise<{ defaultVlan: number; allowUnknownMacs: boolean }> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateVlanConfig>(
|
||||||
|
'updateVlanConfig',
|
||||||
|
this.clientRef.buildRequestPayload(options) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error('Failed to update VLAN config');
|
||||||
|
}
|
||||||
|
return response.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testAssignment(mac: string): Promise<interfaces.requests.IReq_TestVlanAssignment['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_TestVlanAssignment>(
|
||||||
|
'testVlanAssignment',
|
||||||
|
this.clientRef.buildRequestPayload({ mac }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadiusSessionManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(filter?: {
|
||||||
|
username?: string;
|
||||||
|
nasIpAddress?: string;
|
||||||
|
vlanId?: number;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetRadiusSessions['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetRadiusSessions>(
|
||||||
|
'getRadiusSessions',
|
||||||
|
this.clientRef.buildRequestPayload({ filter }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnect(sessionId: string, reason?: string): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_DisconnectRadiusSession>(
|
||||||
|
'disconnectRadiusSession',
|
||||||
|
this.clientRef.buildRequestPayload({ sessionId, reason }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to disconnect session');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Main RADIUS Manager
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
export class RadiusManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
public clients: RadiusClientManager;
|
||||||
|
public vlans: RadiusVlanManager;
|
||||||
|
public sessions: RadiusSessionManager;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.clients = new RadiusClientManager(clientRef);
|
||||||
|
this.vlans = new RadiusVlanManager(clientRef);
|
||||||
|
this.sessions = new RadiusSessionManager(clientRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAccountingSummary(
|
||||||
|
startTime: number,
|
||||||
|
endTime: number,
|
||||||
|
): Promise<interfaces.requests.IReq_GetRadiusAccountingSummary['response']['summary']> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetRadiusAccountingSummary>(
|
||||||
|
'getRadiusAccountingSummary',
|
||||||
|
this.clientRef.buildRequestPayload({ startTime, endTime }) as any,
|
||||||
|
);
|
||||||
|
return response.summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStatistics(): Promise<interfaces.requests.IReq_GetRadiusStatistics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetRadiusStatistics>(
|
||||||
|
'getRadiusStatistics',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
ts_apiclient/classes.remoteingress.ts
Normal file
185
ts_apiclient/classes.remoteingress.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class RemoteIngress {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
// Data from IRemoteIngress
|
||||||
|
public id: string;
|
||||||
|
public name: string;
|
||||||
|
public secret: string;
|
||||||
|
public listenPorts: number[];
|
||||||
|
public enabled: boolean;
|
||||||
|
public autoDerivePorts: boolean;
|
||||||
|
public tags?: string[];
|
||||||
|
public createdAt: number;
|
||||||
|
public updatedAt: number;
|
||||||
|
public effectiveListenPorts?: number[];
|
||||||
|
public manualPorts?: number[];
|
||||||
|
public derivedPorts?: number[];
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IRemoteIngress) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.id = data.id;
|
||||||
|
this.name = data.name;
|
||||||
|
this.secret = data.secret;
|
||||||
|
this.listenPorts = data.listenPorts;
|
||||||
|
this.enabled = data.enabled;
|
||||||
|
this.autoDerivePorts = data.autoDerivePorts;
|
||||||
|
this.tags = data.tags;
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
this.updatedAt = data.updatedAt;
|
||||||
|
this.effectiveListenPorts = data.effectiveListenPorts;
|
||||||
|
this.manualPorts = data.manualPorts;
|
||||||
|
this.derivedPorts = data.derivedPorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(changes: {
|
||||||
|
name?: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRemoteIngress>(
|
||||||
|
'updateRemoteIngress',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id, ...changes }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error('Failed to update remote ingress');
|
||||||
|
}
|
||||||
|
// Update local state from response
|
||||||
|
const edge = response.edge;
|
||||||
|
this.name = edge.name;
|
||||||
|
this.listenPorts = edge.listenPorts;
|
||||||
|
this.enabled = edge.enabled;
|
||||||
|
this.autoDerivePorts = edge.autoDerivePorts;
|
||||||
|
this.tags = edge.tags;
|
||||||
|
this.updatedAt = edge.updatedAt;
|
||||||
|
this.effectiveListenPorts = edge.effectiveListenPorts;
|
||||||
|
this.manualPorts = edge.manualPorts;
|
||||||
|
this.derivedPorts = edge.derivedPorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRemoteIngress>(
|
||||||
|
'deleteRemoteIngress',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to delete remote ingress');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async regenerateSecret(): Promise<string> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
||||||
|
'regenerateRemoteIngressSecret',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.id }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error('Failed to regenerate secret');
|
||||||
|
}
|
||||||
|
this.secret = response.secret;
|
||||||
|
return response.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getConnectionToken(hubHost?: string): Promise<string | undefined> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
|
||||||
|
'getRemoteIngressConnectionToken',
|
||||||
|
this.clientRef.buildRequestPayload({ edgeId: this.id, hubHost }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to get connection token');
|
||||||
|
}
|
||||||
|
return response.token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteIngressBuilder {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
private edgeName: string = '';
|
||||||
|
private edgeListenPorts?: number[];
|
||||||
|
private edgeAutoDerivePorts?: boolean;
|
||||||
|
private edgeTags?: string[];
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.edgeName = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setListenPorts(ports: number[]): this {
|
||||||
|
this.edgeListenPorts = ports;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAutoDerivePorts(auto: boolean): this {
|
||||||
|
this.edgeAutoDerivePorts = auto;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTags(tags: string[]): this {
|
||||||
|
this.edgeTags = tags;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<RemoteIngress> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRemoteIngress>(
|
||||||
|
'createRemoteIngress',
|
||||||
|
this.clientRef.buildRequestPayload({
|
||||||
|
name: this.edgeName,
|
||||||
|
listenPorts: this.edgeListenPorts,
|
||||||
|
autoDerivePorts: this.edgeAutoDerivePorts,
|
||||||
|
tags: this.edgeTags,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error('Failed to create remote ingress');
|
||||||
|
}
|
||||||
|
return new RemoteIngress(this.clientRef, response.edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteIngressManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<RemoteIngress[]> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngresses>(
|
||||||
|
'getRemoteIngresses',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.edges.map((e) => new RemoteIngress(this.clientRef, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStatuses(): Promise<interfaces.data.IRemoteIngressStatus[]> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
||||||
|
'getRemoteIngressStatus',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return response.statuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(options: {
|
||||||
|
name: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
autoDerivePorts?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
}): Promise<RemoteIngress> {
|
||||||
|
const builder = this.build().setName(options.name);
|
||||||
|
if (options.listenPorts) builder.setListenPorts(options.listenPorts);
|
||||||
|
if (options.autoDerivePorts !== undefined) builder.setAutoDerivePorts(options.autoDerivePorts);
|
||||||
|
if (options.tags) builder.setTags(options.tags);
|
||||||
|
return builder.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): RemoteIngressBuilder {
|
||||||
|
return new RemoteIngressBuilder(this.clientRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
203
ts_apiclient/classes.route.ts
Normal file
203
ts_apiclient/classes.route.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
export class Route {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
// Data from IMergedRoute
|
||||||
|
public routeConfig: IRouteConfig;
|
||||||
|
public source: 'hardcoded' | 'programmatic';
|
||||||
|
public enabled: boolean;
|
||||||
|
public overridden: boolean;
|
||||||
|
public storedRouteId?: string;
|
||||||
|
public createdAt?: number;
|
||||||
|
public updatedAt?: number;
|
||||||
|
|
||||||
|
// Convenience accessors
|
||||||
|
public get name(): string {
|
||||||
|
return this.routeConfig.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient, data: interfaces.data.IMergedRoute) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
this.routeConfig = data.route;
|
||||||
|
this.source = data.source;
|
||||||
|
this.enabled = data.enabled;
|
||||||
|
this.overridden = data.overridden;
|
||||||
|
this.storedRouteId = data.storedRouteId;
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
this.updatedAt = data.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(changes: Partial<IRouteConfig>): Promise<void> {
|
||||||
|
if (!this.storedRouteId) {
|
||||||
|
throw new Error('Cannot update a hardcoded route. Use setOverride() instead.');
|
||||||
|
}
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_UpdateRoute>(
|
||||||
|
'updateRoute',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.storedRouteId, route: changes }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to update route');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
if (!this.storedRouteId) {
|
||||||
|
throw new Error('Cannot delete a hardcoded route. Use setOverride() instead.');
|
||||||
|
}
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_DeleteRoute>(
|
||||||
|
'deleteRoute',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.storedRouteId }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to delete route');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggle(enabled: boolean): Promise<void> {
|
||||||
|
if (!this.storedRouteId) {
|
||||||
|
throw new Error('Cannot toggle a hardcoded route. Use setOverride() instead.');
|
||||||
|
}
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_ToggleRoute>(
|
||||||
|
'toggleRoute',
|
||||||
|
this.clientRef.buildRequestPayload({ id: this.storedRouteId, enabled }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to toggle route');
|
||||||
|
}
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setOverride(enabled: boolean): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_SetRouteOverride>(
|
||||||
|
'setRouteOverride',
|
||||||
|
this.clientRef.buildRequestPayload({ routeName: this.name, enabled }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to set route override');
|
||||||
|
}
|
||||||
|
this.overridden = true;
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeOverride(): Promise<void> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_RemoveRouteOverride>(
|
||||||
|
'removeRouteOverride',
|
||||||
|
this.clientRef.buildRequestPayload({ routeName: this.name }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to remove route override');
|
||||||
|
}
|
||||||
|
this.overridden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RouteBuilder {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
private routeConfig: Partial<IRouteConfig> = {};
|
||||||
|
private isEnabled: boolean = true;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.routeConfig.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMatch(match: IRouteConfig['match']): this {
|
||||||
|
this.routeConfig.match = match;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAction(action: IRouteConfig['action']): this {
|
||||||
|
this.routeConfig.action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTls(tls: IRouteConfig['action']['tls']): this {
|
||||||
|
if (!this.routeConfig.action) {
|
||||||
|
this.routeConfig.action = { type: 'forward' } as IRouteConfig['action'];
|
||||||
|
}
|
||||||
|
this.routeConfig.action!.tls = tls;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setEnabled(enabled: boolean): this {
|
||||||
|
this.isEnabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<Route> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||||
|
'createRoute',
|
||||||
|
this.clientRef.buildRequestPayload({
|
||||||
|
route: this.routeConfig as IRouteConfig,
|
||||||
|
enabled: this.isEnabled,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to create route');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a Route instance by re-fetching the list
|
||||||
|
// The created route is programmatic, so we find it by storedRouteId
|
||||||
|
const { routes } = await new RouteManager(this.clientRef).list();
|
||||||
|
const created = routes.find((r) => r.storedRouteId === response.storedRouteId);
|
||||||
|
if (created) {
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: construct from known data
|
||||||
|
return new Route(this.clientRef, {
|
||||||
|
route: this.routeConfig as IRouteConfig,
|
||||||
|
source: 'programmatic',
|
||||||
|
enabled: this.isEnabled,
|
||||||
|
overridden: false,
|
||||||
|
storedRouteId: response.storedRouteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RouteManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async list(): Promise<{ routes: Route[]; warnings: interfaces.data.IRouteWarning[] }> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_GetMergedRoutes>(
|
||||||
|
'getMergedRoutes',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
routes: response.routes.map((r) => new Route(this.clientRef, r)),
|
||||||
|
warnings: response.warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(routeConfig: IRouteConfig, enabled?: boolean): Promise<Route> {
|
||||||
|
const response = await this.clientRef.request<interfaces.requests.IReq_CreateRoute>(
|
||||||
|
'createRoute',
|
||||||
|
this.clientRef.buildRequestPayload({ route: routeConfig, enabled: enabled ?? true }) as any,
|
||||||
|
);
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.message || 'Failed to create route');
|
||||||
|
}
|
||||||
|
return new Route(this.clientRef, {
|
||||||
|
route: routeConfig,
|
||||||
|
source: 'programmatic',
|
||||||
|
enabled: enabled ?? true,
|
||||||
|
overridden: false,
|
||||||
|
storedRouteId: response.storedRouteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): RouteBuilder {
|
||||||
|
return new RouteBuilder(this.clientRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
ts_apiclient/classes.stats.ts
Normal file
111
ts_apiclient/classes.stats.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
import type { DcRouterApiClient } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
type TTimeRange = '1h' | '6h' | '24h' | '7d' | '30d';
|
||||||
|
|
||||||
|
export class StatsManager {
|
||||||
|
private clientRef: DcRouterApiClient;
|
||||||
|
|
||||||
|
constructor(clientRef: DcRouterApiClient) {
|
||||||
|
this.clientRef = clientRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getServer(options?: {
|
||||||
|
timeRange?: TTimeRange;
|
||||||
|
includeHistory?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetServerStatistics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetServerStatistics>(
|
||||||
|
'getServerStatistics',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEmail(options?: {
|
||||||
|
timeRange?: TTimeRange;
|
||||||
|
domain?: string;
|
||||||
|
includeDetails?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetEmailStatistics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetEmailStatistics>(
|
||||||
|
'getEmailStatistics',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDns(options?: {
|
||||||
|
timeRange?: TTimeRange;
|
||||||
|
domain?: string;
|
||||||
|
includeQueryTypes?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetDnsStatistics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetDnsStatistics>(
|
||||||
|
'getDnsStatistics',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRateLimits(options?: {
|
||||||
|
domain?: string;
|
||||||
|
ip?: string;
|
||||||
|
includeBlocked?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetRateLimitStatus['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||||
|
'getRateLimitStatus',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSecurity(options?: {
|
||||||
|
timeRange?: TTimeRange;
|
||||||
|
includeDetails?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetSecurityMetrics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetSecurityMetrics>(
|
||||||
|
'getSecurityMetrics',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getConnections(options?: {
|
||||||
|
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
|
||||||
|
state?: string;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetActiveConnections['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetActiveConnections>(
|
||||||
|
'getActiveConnections',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQueues(options?: {
|
||||||
|
queueName?: string;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetQueueStatus['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetQueueStatus>(
|
||||||
|
'getQueueStatus',
|
||||||
|
this.clientRef.buildRequestPayload(options || {}) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHealth(detailed?: boolean): Promise<interfaces.requests.IReq_GetHealthStatus['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
|
'getHealthStatus',
|
||||||
|
this.clientRef.buildRequestPayload({ detailed }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNetwork(): Promise<interfaces.requests.IReq_GetNetworkStats['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetNetworkStats>(
|
||||||
|
'getNetworkStats',
|
||||||
|
this.clientRef.buildRequestPayload() as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCombined(sections?: {
|
||||||
|
server?: boolean;
|
||||||
|
email?: boolean;
|
||||||
|
dns?: boolean;
|
||||||
|
security?: boolean;
|
||||||
|
network?: boolean;
|
||||||
|
}): Promise<interfaces.requests.IReq_GetCombinedMetrics['response']> {
|
||||||
|
return this.clientRef.request<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||||
|
'getCombinedMetrics',
|
||||||
|
this.clientRef.buildRequestPayload({ sections }) as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ts_apiclient/index.ts
Normal file
15
ts_apiclient/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Main client
|
||||||
|
export { DcRouterApiClient, type IDcRouterApiClientOptions } from './classes.dcrouterapiclient.js';
|
||||||
|
|
||||||
|
// Resource classes
|
||||||
|
export { Route, RouteBuilder, RouteManager } from './classes.route.js';
|
||||||
|
export { Certificate, CertificateManager, type ICertificateSummary } from './classes.certificate.js';
|
||||||
|
export { ApiToken, ApiTokenBuilder, ApiTokenManager } from './classes.apitoken.js';
|
||||||
|
export { RemoteIngress, RemoteIngressBuilder, RemoteIngressManager } from './classes.remoteingress.js';
|
||||||
|
export { Email, EmailManager } from './classes.email.js';
|
||||||
|
|
||||||
|
// Read-only managers
|
||||||
|
export { StatsManager } from './classes.stats.js';
|
||||||
|
export { ConfigManager } from './classes.config.js';
|
||||||
|
export { LogManager } from './classes.logs.js';
|
||||||
|
export { RadiusManager, RadiusClientManager, RadiusVlanManager, RadiusSessionManager } from './classes.radius.js';
|
||||||
8
ts_apiclient/plugins.ts
Normal file
8
ts_apiclient/plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// @api.global scope
|
||||||
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
|
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
typedrequest,
|
||||||
|
typedrequestInterfaces,
|
||||||
|
};
|
||||||
279
ts_apiclient/readme.md
Normal file
279
ts_apiclient/readme.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# @serve.zone/dcrouter-apiclient
|
||||||
|
|
||||||
|
A typed, object-oriented API client for DcRouter with a fluent builder pattern. 🔧
|
||||||
|
|
||||||
|
Programmatically manage your DcRouter instance — routes, certificates, API tokens, remote ingress edges, RADIUS, email operations, and more — all with full TypeScript type safety and an intuitive OO interface.
|
||||||
|
|
||||||
|
## Issue Reporting and Security
|
||||||
|
|
||||||
|
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @serve.zone/dcrouter-apiclient
|
||||||
|
```
|
||||||
|
|
||||||
|
Or import directly from the main package:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DcRouterApiClient } from '@serve.zone/dcrouter/apiclient';
|
||||||
|
|
||||||
|
const client = new DcRouterApiClient({ baseUrl: 'https://dcrouter.example.com' });
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
await client.login('admin', 'password');
|
||||||
|
|
||||||
|
// List routes
|
||||||
|
const { routes, warnings } = await client.routes.list();
|
||||||
|
console.log(`${routes.length} routes, ${warnings.length} warnings`);
|
||||||
|
|
||||||
|
// Check health
|
||||||
|
const { health } = await client.stats.getHealth();
|
||||||
|
console.log(`Healthy: ${health.healthy}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 🔐 Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Login with credentials — identity is stored and auto-injected into all subsequent requests
|
||||||
|
const identity = await client.login('admin', 'password');
|
||||||
|
|
||||||
|
// Verify current session
|
||||||
|
const { valid } = await client.verifyIdentity();
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
await client.logout();
|
||||||
|
|
||||||
|
// Or use an API token for programmatic access (route management only)
|
||||||
|
const client = new DcRouterApiClient({
|
||||||
|
baseUrl: 'https://dcrouter.example.com',
|
||||||
|
apiToken: 'dcr_your_token_here',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 Routes — OO Resources + Builder
|
||||||
|
|
||||||
|
Routes are returned as `Route` instances with methods for update, delete, toggle, and overrides:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List all routes (hardcoded + programmatic)
|
||||||
|
const { routes, warnings } = await client.routes.list();
|
||||||
|
|
||||||
|
// Inspect a route
|
||||||
|
const route = routes[0];
|
||||||
|
console.log(route.name, route.source, route.enabled);
|
||||||
|
|
||||||
|
// Modify a programmatic route
|
||||||
|
await route.update({ name: 'renamed-route' });
|
||||||
|
await route.toggle(false);
|
||||||
|
await route.delete();
|
||||||
|
|
||||||
|
// Override a hardcoded route (disable it)
|
||||||
|
const hardcodedRoute = routes.find(r => r.source === 'hardcoded');
|
||||||
|
await hardcodedRoute.setOverride(false);
|
||||||
|
await hardcodedRoute.removeOverride();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Builder pattern** for creating new routes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const newRoute = await client.routes.build()
|
||||||
|
.setName('api-gateway')
|
||||||
|
.setMatch({ ports: 443, domains: ['api.example.com'] })
|
||||||
|
.setAction({ type: 'forward', targets: [{ host: 'backend', port: 8080 }] })
|
||||||
|
.setTls({ mode: 'terminate', certificate: 'auto' })
|
||||||
|
.setEnabled(true)
|
||||||
|
.save();
|
||||||
|
|
||||||
|
// Or use quick creation
|
||||||
|
const route = await client.routes.create(routeConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔑 API Tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List existing tokens
|
||||||
|
const tokens = await client.apiTokens.list();
|
||||||
|
|
||||||
|
// Create with builder
|
||||||
|
const token = await client.apiTokens.build()
|
||||||
|
.setName('ci-pipeline')
|
||||||
|
.setScopes(['routes:read', 'routes:write'])
|
||||||
|
.addScope('config:read')
|
||||||
|
.setExpiresInDays(90)
|
||||||
|
.save();
|
||||||
|
|
||||||
|
console.log(token.tokenValue); // Only available at creation time!
|
||||||
|
|
||||||
|
// Manage tokens
|
||||||
|
await token.toggle(false); // Disable
|
||||||
|
const newValue = await token.roll(); // Regenerate secret
|
||||||
|
await token.revoke(); // Delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Certificates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { certificates, summary } = await client.certificates.list();
|
||||||
|
console.log(`${summary.valid} valid, ${summary.expiring} expiring, ${summary.failed} failed`);
|
||||||
|
|
||||||
|
// Operate on individual certificates
|
||||||
|
const cert = certificates[0];
|
||||||
|
await cert.reprovision();
|
||||||
|
const exported = await cert.export();
|
||||||
|
await cert.delete();
|
||||||
|
|
||||||
|
// Import a certificate
|
||||||
|
await client.certificates.import({
|
||||||
|
id: 'cert-id',
|
||||||
|
domainName: 'example.com',
|
||||||
|
created: Date.now(),
|
||||||
|
validUntil: Date.now() + 90 * 24 * 3600 * 1000,
|
||||||
|
privateKey: '...',
|
||||||
|
publicKey: '...',
|
||||||
|
csr: '...',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌍 Remote Ingress
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List edges and their statuses
|
||||||
|
const edges = await client.remoteIngress.list();
|
||||||
|
const statuses = await client.remoteIngress.getStatuses();
|
||||||
|
|
||||||
|
// Create with builder
|
||||||
|
const edge = await client.remoteIngress.build()
|
||||||
|
.setName('edge-nyc-01')
|
||||||
|
.setListenPorts([80, 443])
|
||||||
|
.setAutoDerivePorts(true)
|
||||||
|
.setTags(['us-east'])
|
||||||
|
.save();
|
||||||
|
|
||||||
|
// Manage an edge
|
||||||
|
await edge.update({ name: 'edge-nyc-02' });
|
||||||
|
const newSecret = await edge.regenerateSecret();
|
||||||
|
const token = await edge.getConnectionToken();
|
||||||
|
await edge.delete();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Statistics (Read-Only)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const serverStats = await client.stats.getServer({ timeRange: '24h', includeHistory: true });
|
||||||
|
const emailStats = await client.stats.getEmail({ domain: 'example.com' });
|
||||||
|
const dnsStats = await client.stats.getDns();
|
||||||
|
const security = await client.stats.getSecurity({ includeDetails: true });
|
||||||
|
const connections = await client.stats.getConnections({ protocol: 'https' });
|
||||||
|
const queues = await client.stats.getQueues();
|
||||||
|
const health = await client.stats.getHealth(true);
|
||||||
|
const network = await client.stats.getNetwork();
|
||||||
|
const combined = await client.stats.getCombined({ server: true, email: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚙️ Configuration & Logs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Read-only configuration
|
||||||
|
const config = await client.config.get();
|
||||||
|
const emailSection = await client.config.get('email');
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
const { logs, total, hasMore } = await client.logs.getRecent({
|
||||||
|
level: 'error',
|
||||||
|
category: 'smtp',
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📧 Email Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const emails = await client.emails.list();
|
||||||
|
const email = emails[0];
|
||||||
|
const detail = await email.getDetail();
|
||||||
|
await email.resend();
|
||||||
|
|
||||||
|
// Or use the manager directly
|
||||||
|
const detail2 = await client.emails.getDetail('email-id');
|
||||||
|
await client.emails.resend('email-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📡 RADIUS
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Client management
|
||||||
|
const clients = await client.radius.clients.list();
|
||||||
|
await client.radius.clients.set({
|
||||||
|
name: 'switch-1',
|
||||||
|
ipRange: '192.168.1.0/24',
|
||||||
|
secret: 'shared-secret',
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
await client.radius.clients.remove('switch-1');
|
||||||
|
|
||||||
|
// VLAN management
|
||||||
|
const { mappings, config: vlanConfig } = await client.radius.vlans.list();
|
||||||
|
await client.radius.vlans.set({ mac: 'aa:bb:cc:dd:ee:ff', vlan: 10, enabled: true });
|
||||||
|
const result = await client.radius.vlans.testAssignment('aa:bb:cc:dd:ee:ff');
|
||||||
|
await client.radius.vlans.updateConfig({ defaultVlan: 200 });
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
const { sessions } = await client.radius.sessions.list({ vlanId: 10 });
|
||||||
|
await client.radius.sessions.disconnect('session-id', 'Admin disconnect');
|
||||||
|
|
||||||
|
// Statistics & Accounting
|
||||||
|
const stats = await client.radius.getStatistics();
|
||||||
|
const summary = await client.radius.getAccountingSummary(startTime, endTime);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Surface
|
||||||
|
|
||||||
|
| Manager | Methods |
|
||||||
|
|---------|---------|
|
||||||
|
| `client.login()` / `logout()` / `verifyIdentity()` | Authentication |
|
||||||
|
| `client.routes` | `list()`, `create()`, `build()` → Route: `update()`, `delete()`, `toggle()`, `setOverride()`, `removeOverride()` |
|
||||||
|
| `client.certificates` | `list()`, `import()` → Certificate: `reprovision()`, `delete()`, `export()` |
|
||||||
|
| `client.apiTokens` | `list()`, `create()`, `build()` → ApiToken: `revoke()`, `roll()`, `toggle()` |
|
||||||
|
| `client.remoteIngress` | `list()`, `getStatuses()`, `create()`, `build()` → RemoteIngress: `update()`, `delete()`, `regenerateSecret()`, `getConnectionToken()` |
|
||||||
|
| `client.stats` | `getServer()`, `getEmail()`, `getDns()`, `getRateLimits()`, `getSecurity()`, `getConnections()`, `getQueues()`, `getHealth()`, `getNetwork()`, `getCombined()` |
|
||||||
|
| `client.config` | `get(section?)` |
|
||||||
|
| `client.logs` | `getRecent()`, `getStream()` |
|
||||||
|
| `client.emails` | `list()`, `getDetail()`, `resend()` → Email: `getDetail()`, `resend()` |
|
||||||
|
| `client.radius` | `.clients.list/set/remove()`, `.vlans.list/set/remove/updateConfig/testAssignment()`, `.sessions.list/disconnect()`, `getStatistics()`, `getAccountingSummary()` |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The client uses HTTP-based [TypedRequest](https://code.foss.global/api.global/typedrequest) for transport. All requests are sent as POST to `{baseUrl}/typedrequest`. Authentication (JWT identity and/or API token) is automatically injected into every request payload via `buildRequestPayload()`.
|
||||||
|
|
||||||
|
Resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`) hold a reference to the client and provide instance methods that fire the appropriate TypedRequest operations. Builder classes (`RouteBuilder`, `ApiTokenBuilder`, `RemoteIngressBuilder`) use fluent chaining and a terminal `.save()` method.
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||||
|
|
||||||
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||||
3
ts_apiclient/tspublish.json
Normal file
3
ts_apiclient/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"order": 4
|
||||||
|
}
|
||||||
@@ -82,6 +82,14 @@ interface IIdentity {
|
|||||||
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
| `INetworkMetrics` | Bandwidth, connection counts, top endpoints |
|
||||||
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
| `ILogEntry` | Timestamp, level, category, message, metadata |
|
||||||
|
|
||||||
|
#### Route Management Interfaces
|
||||||
|
| Interface | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `IMergedRoute` | Combined route: routeConfig, source (hardcoded/programmatic), enabled, overridden |
|
||||||
|
| `IRouteWarning` | Merge warning: disabled-hardcoded, disabled-programmatic, orphaned-override |
|
||||||
|
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
|
||||||
|
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
|
||||||
|
|
||||||
#### Remote Ingress Interfaces
|
#### Remote Ingress Interfaces
|
||||||
| Interface | Description |
|
| Interface | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
@@ -128,13 +136,29 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
|||||||
#### 📧 Email Operations
|
#### 📧 Email Operations
|
||||||
| Interface | Method | Description |
|
| Interface | Method | Description |
|
||||||
|-----------|--------|-------------|
|
|-----------|--------|-------------|
|
||||||
| `IReq_GetQueuedEmails` | `getQueuedEmails` | List queued emails |
|
| `IReq_GetAllEmails` | `getAllEmails` | List all emails |
|
||||||
| `IReq_GetSentEmails` | `getSentEmails` | List delivered emails |
|
| `IReq_GetEmailDetail` | `getEmailDetail` | Full detail for a specific email |
|
||||||
| `IReq_GetFailedEmails` | `getFailedEmails` | List failed emails |
|
|
||||||
| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email |
|
| `IReq_ResendEmail` | `resendEmail` | Re-queue a failed email |
|
||||||
| `IReq_GetSecurityIncidents` | `getSecurityIncidents` | Security events |
|
|
||||||
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
|
#### 🛣️ Route Management
|
||||||
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
|
| Interface | Method | Description |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| `IReq_GetMergedRoutes` | `getMergedRoutes` | List all routes (hardcoded + programmatic) |
|
||||||
|
| `IReq_CreateRoute` | `createRoute` | Create a new programmatic route |
|
||||||
|
| `IReq_UpdateRoute` | `updateRoute` | Update a programmatic route |
|
||||||
|
| `IReq_DeleteRoute` | `deleteRoute` | Delete a programmatic route |
|
||||||
|
| `IReq_ToggleRoute` | `toggleRoute` | Enable/disable a programmatic route |
|
||||||
|
| `IReq_SetRouteOverride` | `setRouteOverride` | Override a hardcoded route |
|
||||||
|
| `IReq_RemoveRouteOverride` | `removeRouteOverride` | Remove a route override |
|
||||||
|
|
||||||
|
#### 🔑 API Token Management
|
||||||
|
| Interface | Method | Description |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| `IReq_CreateApiToken` | `createApiToken` | Create a new API token |
|
||||||
|
| `IReq_ListApiTokens` | `listApiTokens` | List all tokens |
|
||||||
|
| `IReq_RevokeApiToken` | `revokeApiToken` | Revoke (delete) a token |
|
||||||
|
| `IReq_RollApiToken` | `rollApiToken` | Regenerate token secret |
|
||||||
|
| `IReq_ToggleApiToken` | `toggleApiToken` | Enable/disable a token |
|
||||||
|
|
||||||
#### 🔐 Certificates
|
#### 🔐 Certificates
|
||||||
| Interface | Method | Description |
|
| Interface | Method | Description |
|
||||||
@@ -198,6 +222,8 @@ interface ICertificateInfo {
|
|||||||
|
|
||||||
## Example: Full API Integration
|
## Example: Full API Integration
|
||||||
|
|
||||||
|
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as typedrequest from '@api.global/typedrequest';
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'createApiToken';
|
method: 'createApiToken';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
name: string;
|
name: string;
|
||||||
scopes: TApiTokenScope[];
|
scopes: TApiTokenScope[];
|
||||||
expiresInDays?: number | null;
|
expiresInDays?: number | null;
|
||||||
@@ -38,7 +38,7 @@ export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.imple
|
|||||||
> {
|
> {
|
||||||
method: 'listApiTokens';
|
method: 'listApiTokens';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
tokens: IApiTokenInfo[];
|
tokens: IApiTokenInfo[];
|
||||||
@@ -54,7 +54,7 @@ export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'revokeApiToken';
|
method: 'revokeApiToken';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -73,7 +73,7 @@ export interface IReq_RollApiToken extends plugins.typedrequestInterfaces.implem
|
|||||||
> {
|
> {
|
||||||
method: 'rollApiToken';
|
method: 'rollApiToken';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -92,7 +92,7 @@ export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'toggleApiToken';
|
method: 'toggleApiToken';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
|
|||||||
> {
|
> {
|
||||||
method: 'getCertificateOverview';
|
method: 'getCertificateOverview';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
certificates: ICertificateInfo[];
|
certificates: ICertificateInfo[];
|
||||||
@@ -50,7 +50,7 @@ export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfa
|
|||||||
> {
|
> {
|
||||||
method: 'reprovisionCertificate';
|
method: 'reprovisionCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
routeName: string;
|
routeName: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -66,7 +66,7 @@ export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestI
|
|||||||
> {
|
> {
|
||||||
method: 'reprovisionCertificateDomain';
|
method: 'reprovisionCertificateDomain';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -82,7 +82,7 @@ export interface IReq_DeleteCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'deleteCertificate';
|
method: 'deleteCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -98,7 +98,7 @@ export interface IReq_ExportCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'exportCertificate';
|
method: 'exportCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -123,7 +123,7 @@ export interface IReq_ImportCertificate extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'importCertificate';
|
method: 'importCertificate';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
cert: {
|
cert: {
|
||||||
id: string;
|
id: string;
|
||||||
domainName: string;
|
domainName: string;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
|
|||||||
> {
|
> {
|
||||||
method: 'getConfiguration';
|
method: 'getConfiguration';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
section?: string;
|
section?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export interface IReq_GetAllEmails extends plugins.typedrequestInterfaces.implem
|
|||||||
> {
|
> {
|
||||||
method: 'getAllEmails';
|
method: 'getAllEmails';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
emails: IEmail[];
|
emails: IEmail[];
|
||||||
@@ -84,7 +84,7 @@ export interface IReq_GetEmailDetail extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'getEmailDetail';
|
method: 'getEmailDetail';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
emailId: string;
|
emailId: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -101,7 +101,7 @@ export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.impleme
|
|||||||
> {
|
> {
|
||||||
method: 'resendEmail';
|
method: 'resendEmail';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
emailId: string;
|
emailId: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface IReq_GetRecentLogs extends plugins.typedrequestInterfaces.imple
|
|||||||
> {
|
> {
|
||||||
method: 'getRecentLogs';
|
method: 'getRecentLogs';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -31,7 +31,7 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
|
|||||||
> {
|
> {
|
||||||
method: 'getLogStream';
|
method: 'getLogStream';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
follow?: boolean;
|
follow?: boolean;
|
||||||
filters?: {
|
filters?: {
|
||||||
level?: string[];
|
level?: string[];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface IReq_GetRadiusClients extends plugins.typedrequestInterfaces.im
|
|||||||
> {
|
> {
|
||||||
method: 'getRadiusClients';
|
method: 'getRadiusClients';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
clients: Array<{
|
clients: Array<{
|
||||||
@@ -35,7 +35,7 @@ export interface IReq_SetRadiusClient extends plugins.typedrequestInterfaces.imp
|
|||||||
> {
|
> {
|
||||||
method: 'setRadiusClient';
|
method: 'setRadiusClient';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
client: {
|
client: {
|
||||||
name: string;
|
name: string;
|
||||||
ipRange: string;
|
ipRange: string;
|
||||||
@@ -59,7 +59,7 @@ export interface IReq_RemoveRadiusClient extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'removeRadiusClient';
|
method: 'removeRadiusClient';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -81,7 +81,7 @@ export interface IReq_GetVlanMappings extends plugins.typedrequestInterfaces.imp
|
|||||||
> {
|
> {
|
||||||
method: 'getVlanMappings';
|
method: 'getVlanMappings';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
mappings: Array<{
|
mappings: Array<{
|
||||||
@@ -108,7 +108,7 @@ export interface IReq_SetVlanMapping extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'setVlanMapping';
|
method: 'setVlanMapping';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
mapping: {
|
mapping: {
|
||||||
mac: string;
|
mac: string;
|
||||||
vlan: number;
|
vlan: number;
|
||||||
@@ -139,7 +139,7 @@ export interface IReq_RemoveVlanMapping extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'removeVlanMapping';
|
method: 'removeVlanMapping';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
mac: string;
|
mac: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -157,7 +157,7 @@ export interface IReq_UpdateVlanConfig extends plugins.typedrequestInterfaces.im
|
|||||||
> {
|
> {
|
||||||
method: 'updateVlanConfig';
|
method: 'updateVlanConfig';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
defaultVlan?: number;
|
defaultVlan?: number;
|
||||||
allowUnknownMacs?: boolean;
|
allowUnknownMacs?: boolean;
|
||||||
};
|
};
|
||||||
@@ -179,7 +179,7 @@ export interface IReq_TestVlanAssignment extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'testVlanAssignment';
|
method: 'testVlanAssignment';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
mac: string;
|
mac: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -207,7 +207,7 @@ export interface IReq_GetRadiusSessions extends plugins.typedrequestInterfaces.i
|
|||||||
> {
|
> {
|
||||||
method: 'getRadiusSessions';
|
method: 'getRadiusSessions';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
filter?: {
|
filter?: {
|
||||||
username?: string;
|
username?: string;
|
||||||
nasIpAddress?: string;
|
nasIpAddress?: string;
|
||||||
@@ -243,7 +243,7 @@ export interface IReq_DisconnectRadiusSession extends plugins.typedrequestInterf
|
|||||||
> {
|
> {
|
||||||
method: 'disconnectRadiusSession';
|
method: 'disconnectRadiusSession';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
@@ -262,7 +262,7 @@ export interface IReq_GetRadiusAccountingSummary extends plugins.typedrequestInt
|
|||||||
> {
|
> {
|
||||||
method: 'getRadiusAccountingSummary';
|
method: 'getRadiusAccountingSummary';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
};
|
};
|
||||||
@@ -296,7 +296,7 @@ export interface IReq_GetRadiusStatistics extends plugins.typedrequestInterfaces
|
|||||||
> {
|
> {
|
||||||
method: 'getRadiusStatistics';
|
method: 'getRadiusStatistics';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
> {
|
> {
|
||||||
method: 'createRemoteIngress';
|
method: 'createRemoteIngress';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
name: string;
|
name: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
autoDerivePorts?: boolean;
|
autoDerivePorts?: boolean;
|
||||||
@@ -36,7 +36,7 @@ export interface IReq_DeleteRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
> {
|
> {
|
||||||
method: 'deleteRemoteIngress';
|
method: 'deleteRemoteIngress';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -54,7 +54,7 @@ export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces
|
|||||||
> {
|
> {
|
||||||
method: 'updateRemoteIngress';
|
method: 'updateRemoteIngress';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
@@ -77,7 +77,7 @@ export interface IReq_RegenerateRemoteIngressSecret extends plugins.typedrequest
|
|||||||
> {
|
> {
|
||||||
method: 'regenerateRemoteIngressSecret';
|
method: 'regenerateRemoteIngressSecret';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -95,7 +95,7 @@ export interface IReq_GetRemoteIngresses extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'getRemoteIngresses';
|
method: 'getRemoteIngresses';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
edges: IRemoteIngress[];
|
edges: IRemoteIngress[];
|
||||||
@@ -111,7 +111,7 @@ export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfa
|
|||||||
> {
|
> {
|
||||||
method: 'getRemoteIngressStatus';
|
method: 'getRemoteIngressStatus';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
statuses: IRemoteIngressStatus[];
|
statuses: IRemoteIngressStatus[];
|
||||||
@@ -128,7 +128,7 @@ export interface IReq_GetRemoteIngressConnectionToken extends plugins.typedreque
|
|||||||
> {
|
> {
|
||||||
method: 'getRemoteIngressConnectionToken';
|
method: 'getRemoteIngressConnectionToken';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
edgeId: string;
|
edgeId: string;
|
||||||
hubHost?: string;
|
hubHost?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface IReq_GetServerStatistics extends plugins.typedrequestInterfaces
|
|||||||
> {
|
> {
|
||||||
method: 'getServerStatistics';
|
method: 'getServerStatistics';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
includeHistory?: boolean;
|
includeHistory?: boolean;
|
||||||
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
||||||
};
|
};
|
||||||
@@ -29,7 +29,7 @@ export interface IReq_GetEmailStatistics extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'getEmailStatistics';
|
method: 'getEmailStatistics';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
includeDetails?: boolean;
|
includeDetails?: boolean;
|
||||||
@@ -49,7 +49,7 @@ export interface IReq_GetDnsStatistics extends plugins.typedrequestInterfaces.im
|
|||||||
> {
|
> {
|
||||||
method: 'getDnsStatistics';
|
method: 'getDnsStatistics';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
||||||
domain?: string;
|
domain?: string;
|
||||||
includeQueryTypes?: boolean;
|
includeQueryTypes?: boolean;
|
||||||
@@ -69,7 +69,7 @@ export interface IReq_GetRateLimitStatus extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'getRateLimitStatus';
|
method: 'getRateLimitStatus';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
includeBlocked?: boolean;
|
includeBlocked?: boolean;
|
||||||
@@ -91,7 +91,7 @@ export interface IReq_GetSecurityMetrics extends plugins.typedrequestInterfaces.
|
|||||||
> {
|
> {
|
||||||
method: 'getSecurityMetrics';
|
method: 'getSecurityMetrics';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d';
|
||||||
includeDetails?: boolean;
|
includeDetails?: boolean;
|
||||||
};
|
};
|
||||||
@@ -112,7 +112,7 @@ export interface IReq_GetActiveConnections extends plugins.typedrequestInterface
|
|||||||
> {
|
> {
|
||||||
method: 'getActiveConnections';
|
method: 'getActiveConnections';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
|
protocol?: 'smtp' | 'smtps' | 'http' | 'https';
|
||||||
state?: string;
|
state?: string;
|
||||||
};
|
};
|
||||||
@@ -137,7 +137,7 @@ export interface IReq_GetQueueStatus extends plugins.typedrequestInterfaces.impl
|
|||||||
> {
|
> {
|
||||||
method: 'getQueueStatus';
|
method: 'getQueueStatus';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
queueName?: string;
|
queueName?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -153,10 +153,31 @@ export interface IReq_GetHealthStatus extends plugins.typedrequestInterfaces.imp
|
|||||||
> {
|
> {
|
||||||
method: 'getHealthStatus';
|
method: 'getHealthStatus';
|
||||||
request: {
|
request: {
|
||||||
identity?: authInterfaces.IIdentity;
|
identity: authInterfaces.IIdentity;
|
||||||
detailed?: boolean;
|
detailed?: boolean;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
health: statsInterfaces.IHealthStatus;
|
health: statsInterfaces.IHealthStatus;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Network Stats (raw SmartProxy network data)
|
||||||
|
export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetNetworkStats
|
||||||
|
> {
|
||||||
|
method: 'getNetworkStats';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
connectionsByIP: Array<{ ip: string; count: number }>;
|
||||||
|
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||||
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
|
totalDataTransferred: { bytesIn: number; bytesOut: number };
|
||||||
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
|
requestsPerSecond: number;
|
||||||
|
requestsTotal: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '10.1.3',
|
version: '11.2.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,9 +238,12 @@ interface IActionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getActionContext = (): IActionContext => {
|
const getActionContext = (): IActionContext => {
|
||||||
return {
|
const identity = loginStatePart.getState().identity;
|
||||||
identity: loginStatePart.getState().identity,
|
// Treat expired JWTs as no identity — prevents stale persisted sessions from firing requests
|
||||||
};
|
if (identity && identity.expiresAt && identity.expiresAt < Date.now()) {
|
||||||
|
return { identity: null };
|
||||||
|
}
|
||||||
|
return { identity };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login Action
|
// Login Action
|
||||||
@@ -271,24 +274,23 @@ export const loginAction = loginStatePart.createAction<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logout Action
|
// Logout Action — always clears state, even if identity is expired/missing
|
||||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
if (!context.identity) return statePartArg.getState();
|
|
||||||
|
|
||||||
|
// Try to notify server, but don't block logout if identity is missing/expired
|
||||||
|
if (context.identity) {
|
||||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_AdminLogout
|
interfaces.requests.IReq_AdminLogout
|
||||||
>('/typedrequest', 'adminLogout');
|
>('/typedrequest', 'adminLogout');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await typedRequest.fire({
|
await typedRequest.fire({ identity: context.identity });
|
||||||
identity: context.identity,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear login state regardless
|
// Always clear login state
|
||||||
return {
|
return {
|
||||||
identity: null,
|
identity: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
@@ -298,8 +300,8 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
|
|||||||
// Fetch All Stats Action - Using combined endpoint for efficiency
|
// Fetch All Stats Action - Using combined endpoint for efficiency
|
||||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use combined metrics endpoint - single request instead of 4
|
// Use combined metrics endpoint - single request instead of 4
|
||||||
@@ -340,8 +342,8 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
|
|||||||
// Fetch Configuration Action (read-only)
|
// Fetch Configuration Action (read-only)
|
||||||
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
|
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -373,6 +375,7 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
|
|||||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
if (!context.identity) return statePartArg.getState();
|
||||||
|
|
||||||
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetRecentLogs
|
interfaces.requests.IReq_GetRecentLogs
|
||||||
@@ -448,8 +451,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|||||||
// Fetch Network Stats Action
|
// Fetch Network Stats Action
|
||||||
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch active connections using the existing endpoint
|
// Fetch active connections using the existing endpoint
|
||||||
@@ -522,6 +525,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -554,6 +558,7 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
|
|||||||
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
|
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -581,7 +586,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||||
async (statePartArg, domain) => {
|
async (statePartArg, domain, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -596,8 +601,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Re-fetch overview after reprovisioning
|
// Re-fetch overview after reprovisioning
|
||||||
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -608,7 +612,7 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const deleteCertificateAction = certificateStatePart.createAction<string>(
|
export const deleteCertificateAction = certificateStatePart.createAction<string>(
|
||||||
async (statePartArg, domain) => {
|
async (statePartArg, domain, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -623,8 +627,7 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Re-fetch overview after deletion
|
// Re-fetch overview after deletion
|
||||||
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -643,7 +646,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
|
|||||||
publicKey: string;
|
publicKey: string;
|
||||||
csr: string;
|
csr: string;
|
||||||
}>(
|
}>(
|
||||||
async (statePartArg, cert) => {
|
async (statePartArg, cert, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -658,8 +661,7 @@ export const importCertificateAction = certificateStatePart.createAction<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Re-fetch overview after import
|
// Re-fetch overview after import
|
||||||
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -700,6 +702,7 @@ export async function fetchConnectionToken(edgeId: string) {
|
|||||||
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -737,7 +740,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
autoDerivePorts?: boolean;
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -756,7 +759,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Refresh the list
|
// Refresh the list
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...statePartArg.getState(),
|
...statePartArg.getState(),
|
||||||
@@ -774,7 +777,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
||||||
async (statePartArg, edgeId) => {
|
async (statePartArg, edgeId, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -788,8 +791,7 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
|
|||||||
id: edgeId,
|
id: edgeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -805,7 +807,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
listenPorts?: number[];
|
listenPorts?: number[];
|
||||||
autoDerivePorts?: boolean;
|
autoDerivePorts?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -823,8 +825,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
tags: dataArg.tags,
|
tags: dataArg.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -877,7 +878,7 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
|
|||||||
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
id: string;
|
id: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -892,8 +893,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
return await actionContext.dispatch(fetchRemoteIngressAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -909,6 +909,7 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -939,7 +940,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
|
|||||||
export const createRouteAction = routeManagementStatePart.createAction<{
|
export const createRouteAction = routeManagementStatePart.createAction<{
|
||||||
route: any;
|
route: any;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -954,8 +955,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
|||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -965,7 +965,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||||
async (statePartArg, routeId) => {
|
async (statePartArg, routeId, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -979,8 +979,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
|||||||
id: routeId,
|
id: routeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -993,7 +992,7 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
|||||||
export const toggleRouteAction = routeManagementStatePart.createAction<{
|
export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||||
id: string;
|
id: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -1008,8 +1007,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
|
|||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -1021,7 +1019,7 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
|
|||||||
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||||
routeName: string;
|
routeName: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -1036,8 +1034,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
|||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -1047,7 +1044,7 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
|
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
|
||||||
async (statePartArg, routeName) => {
|
async (statePartArg, routeName, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -1061,8 +1058,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
|
|||||||
routeName,
|
routeName,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
return await actionContext.dispatch(fetchMergedRoutesAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -1079,6 +1075,7 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
|
|||||||
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
@@ -1128,7 +1125,7 @@ export async function rollApiToken(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
||||||
async (statePartArg, tokenId) => {
|
async (statePartArg, tokenId, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -1142,8 +1139,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
|
|||||||
id: tokenId,
|
id: tokenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
return await actionContext.dispatch(fetchApiTokensAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -1156,7 +1152,7 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
|
|||||||
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
||||||
id: string;
|
id: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}>(async (statePartArg, dataArg) => {
|
}>(async (statePartArg, dataArg, actionContext) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
@@ -1171,8 +1167,7 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
|||||||
enabled: dataArg.enabled,
|
enabled: dataArg.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
return await actionContext.dispatch(fetchApiTokensAction, null);
|
||||||
return statePartArg.getState();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -1233,6 +1228,7 @@ async function disconnectSocket() {
|
|||||||
// Combined refresh action for efficient polling
|
// Combined refresh action for efficient polling
|
||||||
async function dispatchCombinedRefreshAction() {
|
async function dispatchCombinedRefreshAction() {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
if (!context.identity) return;
|
||||||
const currentView = uiStatePart.getState().activeView;
|
const currentView = uiStatePart.getState().activeView;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1344,6 +1340,12 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Combined refresh failed:', error);
|
console.error('Combined refresh failed:', error);
|
||||||
|
// If the error looks like an auth failure (invalid JWT), force re-login
|
||||||
|
const errMsg = String(error);
|
||||||
|
if (errMsg.includes('invalid') || errMsg.includes('unauthorized') || errMsg.includes('401')) {
|
||||||
|
await loginStatePart.dispatchAction(logoutAction, null);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
import { appRouter } from '../router.js';
|
import { appRouter } from '../router.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -218,13 +219,27 @@ export class OpsDashboard extends DeesElement {
|
|||||||
// Handle initial state - check if we have a stored session that's still valid
|
// Handle initial state - check if we have a stored session that's still valid
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
if (loginState.identity?.jwt) {
|
if (loginState.identity?.jwt) {
|
||||||
// Verify JWT hasn't expired
|
|
||||||
if (loginState.identity.expiresAt > Date.now()) {
|
if (loginState.identity.expiresAt > Date.now()) {
|
||||||
// JWT still valid, restore logged-in state
|
// Client-side expiry looks valid — verify with server (keypair may have changed)
|
||||||
|
try {
|
||||||
|
const verifyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_VerifyIdentity
|
||||||
|
>('/typedrequest', 'verifyIdentity');
|
||||||
|
const response = await verifyRequest.fire({ identity: loginState.identity });
|
||||||
|
if (response.valid) {
|
||||||
|
// JWT confirmed valid by server
|
||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
await simpleLogin.switchToSlottedContent();
|
await simpleLogin.switchToSlottedContent();
|
||||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||||
|
} else {
|
||||||
|
// Server rejected the JWT — clear state, show login
|
||||||
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Server unreachable or error — clear state, show login
|
||||||
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// JWT expired, clear the stored state
|
// JWT expired, clear the stored state
|
||||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export class OpsViewApiTokens extends DeesElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Roll',
|
name: 'Roll',
|
||||||
iconName: 'lucide:rotate-cw',
|
iconName: 'lucide:rotateCw',
|
||||||
type: ['inRow', 'contextmenu'] as any,
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
actionFunc: async (actionData: any) => {
|
actionFunc: async (actionData: any) => {
|
||||||
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
@@ -306,7 +306,7 @@ export class OpsViewApiTokens extends DeesElement {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Roll Token',
|
name: 'Roll Token',
|
||||||
iconName: 'lucide:rotate-cw',
|
iconName: 'lucide:rotateCw',
|
||||||
action: async (modalArg: any) => {
|
action: async (modalArg: any) => {
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- Filter by log level (error, warning, info, debug)
|
- Filter by log level (error, warning, info, debug)
|
||||||
- Search and time-range selection
|
- Search and time-range selection
|
||||||
|
|
||||||
|
### 🛣️ Route & API Token Management
|
||||||
|
- Programmatic route CRUD with enable/disable and override controls
|
||||||
|
- API token creation, revocation, and scope management
|
||||||
|
- Routes tab and API Tokens tab in unified view
|
||||||
|
|
||||||
### ⚙️ Configuration
|
### ⚙️ Configuration
|
||||||
- Read-only display of current system configuration
|
- Read-only display of current system configuration
|
||||||
- Status badges for boolean values (enabled/disabled)
|
- Status badges for boolean values (enabled/disabled)
|
||||||
@@ -96,6 +101,7 @@ ts_web/
|
|||||||
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||||
├── ops-view-remoteingress.ts # Remote ingress edge management
|
├── ops-view-remoteingress.ts # Remote ingress edge management
|
||||||
├── ops-view-logs.ts # Log viewer
|
├── ops-view-logs.ts # Log viewer
|
||||||
|
├── ops-view-routes.ts # Route & API token management
|
||||||
├── ops-view-config.ts # Configuration display
|
├── ops-view-config.ts # Configuration display
|
||||||
├── ops-view-security.ts # Security dashboard
|
├── ops-view-security.ts # Security dashboard
|
||||||
└── shared/
|
└── shared/
|
||||||
@@ -171,6 +177,7 @@ fetchConnectionToken(edgeId) // Get connection token (standalone function)
|
|||||||
/emails/security → Security incidents
|
/emails/security → Security incidents
|
||||||
/certificates → Certificate management
|
/certificates → Certificate management
|
||||||
/remoteingress → Remote ingress edge management
|
/remoteingress → Remote ingress edge management
|
||||||
|
/routes → Route & API token management
|
||||||
/logs → Log viewer
|
/logs → Log viewer
|
||||||
/configuration → System configuration
|
/configuration → System configuration
|
||||||
/security → Security dashboard
|
/security → Security dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user