Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b7426f1e6 | |||
| 3c9c865841 | |||
| 8421c9fe46 | |||
| 907e3df156 | |||
| aaa0956148 | |||
| 118019fcf5 | |||
| deb80f4fd0 | |||
| 7d28cea937 | |||
| 2bd5e5c7c5 | |||
| 4d6ac81c59 | |||
| 2ebe0de92d | |||
| f5028ffb60 |
7
.playwright-mcp/console-2026-02-23T10-44-24-024Z.log
Normal file
7
.playwright-mcp/console-2026-02-23T10-44-24-024Z.log
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[ 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
|
||||||
12
.playwright-mcp/console-2026-02-23T11-19-21-255Z.log
Normal file
12
.playwright-mcp/console-2026-02-23T11-19-21-255Z.log
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[ 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
|
||||||
6
.playwright-mcp/console-2026-02-23T11-20-31-682Z.log
Normal file
6
.playwright-mcp/console-2026-02-23T11-20-31-682Z.log
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[ 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
|
||||||
50
.playwright-mcp/console-2026-02-23T11-21-09-382Z.log
Normal file
50
.playwright-mcp/console-2026-02-23T11-21-09-382Z.log
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[ 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
|
||||||
23
.playwright-mcp/console-2026-02-23T11-23-44-606Z.log
Normal file
23
.playwright-mcp/console-2026-02-23T11-23-44-606Z.log
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[ 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
|
||||||
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
31
.playwright-mcp/console-2026-02-23T12-47-06-007Z.log
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[ 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
|
||||||
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
25
.playwright-mcp/console-2026-02-23T12-48-31-563Z.log
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[ 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
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
1
.playwright-mcp/console-2026-02-23T12-53-33-702Z.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[ 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
|
||||||
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
24
.playwright-mcp/console-2026-02-23T12-55-40-311Z.log
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[ 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
|
||||||
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
30
.playwright-mcp/console-2026-02-23T12-57-47-953Z.log
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[ 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
|
||||||
BIN
.playwright-mcp/page-2026-02-23T11-25-39-255Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-25-39-255Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
.playwright-mcp/page-2026-02-23T11-26-10-952Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-26-10-952Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
.playwright-mcp/page-2026-02-23T11-26-15-885Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-23T11-26-15-885Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
48
changelog.md
48
changelog.md
@@ -1,5 +1,53 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-24 - 9.1.3 - fix(deps)
|
||||||
|
bump @api.global/typedserver to ^8.4.0 and @push.rocks/smartproxy to ^25.8.0
|
||||||
|
|
||||||
|
- Updated @api.global/typedserver from ^8.3.1 to ^8.4.0
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.7.9 to ^25.8.0
|
||||||
|
|
||||||
|
## 2026-02-24 - 9.1.2 - fix(deps)
|
||||||
|
bump dependency versions for build and runtime packages
|
||||||
|
|
||||||
|
- @git.zone/tsbundle: ^2.8.3 -> ^2.9.0
|
||||||
|
- @git.zone/tswatch: ^3.1.0 -> ^3.2.0
|
||||||
|
- @api.global/typedserver: ^8.3.0 -> ^8.3.1
|
||||||
|
- @design.estate/dees-catalog: ^3.43.2 -> ^3.43.3
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.1.1 - fix(dcrouter)
|
||||||
|
no changes detected — no files modified, no release necessary
|
||||||
|
|
||||||
|
- Git diff contained no changes
|
||||||
|
- No files added, modified, or deleted
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.1.0 - feat(ops-dashboard)
|
||||||
|
add lucide icons to Ops dashboard view tabs
|
||||||
|
|
||||||
|
- Added iconName property to 10 view tabs in ts_web/elements/ops-dashboard.ts to enable icons in the UI
|
||||||
|
- Icon mappings: Overview -> lucide:layoutDashboard, Configuration -> lucide:settings, Network -> lucide:network, Emails -> lucide:mail, Logs -> lucide:scrollText, Routes -> lucide:route, ApiTokens -> lucide:key, Security -> lucide:shield, Certificates -> lucide:badgeCheck, RemoteIngress -> lucide:globe
|
||||||
|
- Improves visual clarity of dashboard navigation
|
||||||
|
|
||||||
|
## 2026-02-23 - 9.0.0 - BREAKING CHANGE(opsserver)
|
||||||
|
Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
|
||||||
|
|
||||||
|
- Introduce IConfigData interface with typed sections: system, smartProxy, email, dns, tls, cache, radius, remoteIngress.
|
||||||
|
- Replace ConfigHandler.getConfiguration implementation to assemble and return IConfigData (changes API response shape for getConfiguration).
|
||||||
|
- Refactor frontend: update appstate types and ops-view-config to render the new config sections, use @serve.zone/catalog IConfigField/IConfigSectionAction, add uptime formatting and remote ingress UI.
|
||||||
|
- Fix ops-view-apitokens form handling to correctly read dees-input-tags values.
|
||||||
|
- Update tests to expect new configuration fields.
|
||||||
|
- Bump dependency @serve.zone/catalog to ^2.5.0.
|
||||||
|
|
||||||
|
## 2026-02-23 - 8.1.0 - feat(route-management)
|
||||||
|
add programmatic route management API with API tokens and admin UI
|
||||||
|
|
||||||
|
- Introduce RouteConfigManager to persist and manage programmatic routes and hardcoded-route overrides
|
||||||
|
- Add ApiTokenManager to create, validate, list, toggle and revoke API tokens (stored hashed)
|
||||||
|
- New OpsServer TypedRequest handlers: RouteManagementHandler (getMergedRoutes, create/update/delete/toggle routes, set/remove overrides) and ApiTokenHandler (create/list/revoke/toggle tokens)
|
||||||
|
- DcRouter integration: initialize routeConfigManager and apiTokenManager, expose getConstructorRoutes and re-apply programmatic routes after SmartProxy restarts
|
||||||
|
- Front-end additions: new 'Routes' and 'ApiTokens' views and UI components (ops-view-routes, ops-view-apitokens), router and appstate actions to fetch/manage routes and tokens
|
||||||
|
- New TS interfaces and request types for route-management and API tokens, plus storage schemas for persisted routes, overrides and tokens
|
||||||
|
- Bump dependency @serve.zone/catalog to ^2.3.0
|
||||||
|
|
||||||
## 2026-02-22 - 8.0.0 - BREAKING CHANGE(email-ops)
|
## 2026-02-22 - 8.0.0 - BREAKING CHANGE(email-ops)
|
||||||
migrate email operations to catalog-compatible email model and simplify UI/router
|
migrate email operations to catalog-compatible email model and simplify UI/router
|
||||||
|
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "8.0.0",
|
"version": "9.1.3",
|
||||||
"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": {
|
||||||
@@ -20,19 +20,19 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.1.2",
|
||||||
"@git.zone/tsbundle": "^2.8.3",
|
"@git.zone/tsbundle": "^2.9.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@git.zone/tswatch": "^3.1.0",
|
"@git.zone/tswatch": "^3.2.0",
|
||||||
"@types/node": "^25.3.0"
|
"@types/node": "^25.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.6",
|
"@api.global/typedrequest": "^3.2.6",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.3.0",
|
"@api.global/typedserver": "^8.4.0",
|
||||||
"@api.global/typedsocket": "^4.1.0",
|
"@api.global/typedsocket": "^4.1.0",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.43.2",
|
"@design.estate/dees-catalog": "^3.43.3",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
"@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.7.9",
|
"@push.rocks/smartproxy": "^25.8.0",
|
||||||
"@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.0.30",
|
"@push.rocks/smartstate": "^2.0.30",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/catalog": "^2.2.0",
|
"@serve.zone/catalog": "^2.5.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.0.0",
|
"@serve.zone/remoteingress": "^4.0.0",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
|
|||||||
217
pnpm-lock.yaml
generated
217
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: ^3.0.19
|
specifier: ^3.0.19
|
||||||
version: 3.0.19
|
version: 3.0.19
|
||||||
'@api.global/typedserver':
|
'@api.global/typedserver':
|
||||||
specifier: ^8.3.0
|
specifier: ^8.4.0
|
||||||
version: 8.3.0(@tiptap/pm@2.27.2)
|
version: 8.4.0(@tiptap/pm@2.27.2)
|
||||||
'@api.global/typedsocket':
|
'@api.global/typedsocket':
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||||
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.43.2
|
specifier: ^3.43.3
|
||||||
version: 3.43.2(@tiptap/pm@2.27.2)
|
version: 3.43.3(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.1.6
|
specifier: ^2.1.6
|
||||||
version: 2.1.6
|
version: 2.1.6
|
||||||
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^25.7.9
|
specifier: ^25.8.0
|
||||||
version: 25.7.9
|
version: 25.8.0
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -93,8 +93,8 @@ importers:
|
|||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
'@serve.zone/catalog':
|
'@serve.zone/catalog':
|
||||||
specifier: ^2.2.0
|
specifier: ^2.5.0
|
||||||
version: 2.2.0(@tiptap/pm@2.27.2)
|
version: 2.5.0(@tiptap/pm@2.27.2)
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
@@ -115,8 +115,8 @@ importers:
|
|||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
'@git.zone/tsbundle':
|
'@git.zone/tsbundle':
|
||||||
specifier: ^2.8.3
|
specifier: ^2.9.0
|
||||||
version: 2.8.3
|
version: 2.9.0
|
||||||
'@git.zone/tsrun':
|
'@git.zone/tsrun':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
@@ -124,8 +124,8 @@ importers:
|
|||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.2.0
|
||||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
version: 3.2.0(@tiptap/pm@2.27.2)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.3.0
|
specifier: ^25.3.0
|
||||||
version: 25.3.0
|
version: 25.3.0
|
||||||
@@ -144,8 +144,8 @@ packages:
|
|||||||
'@api.global/typedserver@3.0.80':
|
'@api.global/typedserver@3.0.80':
|
||||||
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
|
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
|
||||||
|
|
||||||
'@api.global/typedserver@8.3.0':
|
'@api.global/typedserver@8.4.0':
|
||||||
resolution: {integrity: sha512-Uh2sQkoQXbsKFb/fhSm7P9oCCEnawGY7R5/9VgCLQUuFV30G0FL0oBTKZNqFli0CNNDDs0nQHE+dpdf4VHhlXQ==}
|
resolution: {integrity: sha512-qOa5jUwiuHEoY1ZuLZiuU1unPl5JNd99Vv+hNAwgEIgAZd4TTy/mjdTp7lyviBzkGf2pROeCmAbDJJF8YQFCSA==}
|
||||||
|
|
||||||
'@api.global/typedsocket@3.1.1':
|
'@api.global/typedsocket@3.1.1':
|
||||||
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
|
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
|
||||||
@@ -351,11 +351,14 @@ packages:
|
|||||||
'@cloudflare/workers-types@4.20260210.0':
|
'@cloudflare/workers-types@4.20260210.0':
|
||||||
resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==}
|
resolution: {integrity: sha512-zHaF0RZVYUQwNCJCECnNAJdMur72Lk3FMiD6wU78Dx3Bv7DQRcuXNmPNuJmsGnosVZCcWintHlPTQ/4BEiDG5w==}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260303.0':
|
||||||
|
resolution: {integrity: sha512-soUlr4NJVkh5dR09RwtziTMbBQ+lbdoEesTGw8WUlvmnQ2M4h7CmJzAjC6a7IivUodiiCSjbLcGV/8PyZpvZkA==}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.43.2':
|
'@design.estate/dees-catalog@3.43.3':
|
||||||
resolution: {integrity: sha512-7pU+K+B70SxqR4DrBkpc/xvQGLDkxAV2jH7Qyh0TYgkCoxxjsxCuTMKM9JguA38wm6bEgBJVTvyg5S3wCwxm4Q==}
|
resolution: {integrity: sha512-GjTePdwqNBL4isMOx4Ibei6pgK55H+DccbtgyNqjHRBz3LL14mo809ebjY2IZOVobswyzuTcNFvhfiqFP4/HLg==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -558,8 +561,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==}
|
resolution: {integrity: sha512-S518ulKveO76pS6jrAELrnFaCw5nDAIZD9j6QzVmLYDiZuJmlRwPK3/2E8ugQ+b7ffpkwJ9MT685ooEGDcWQ4Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.8.3':
|
'@git.zone/tsbundle@2.9.0':
|
||||||
resolution: {integrity: sha512-9q+KbVGKUTDNND+jDiJuk4bPH/mtiA2B0EWtV+/NyvgZfIbpe/ItHemyIvXB4RAqncMdBhzXquCFCvGjAhwVIQ==}
|
resolution: {integrity: sha512-itXX/oiJjrRHUlIGTHUEqSwPuGwsG4Cq8kh7aqFOm8mYzJwtXYE1gBqLJTWZma6gI5n+xAk5qTxTyfikuPgWQA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.0':
|
'@git.zone/tspublish@1.11.0':
|
||||||
@@ -574,8 +577,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
|
resolution: {integrity: sha512-nmiLGeOkKMkLDyIk5BUBLx5ExskFbKHKlPdrWCARPVFkU4cAAiuIyJWVfLwISoS0TO/zSInLqArPwIc76yvaNw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tswatch@3.1.0':
|
'@git.zone/tswatch@3.2.0':
|
||||||
resolution: {integrity: sha512-R2ZI+j1OKVgd0zTbtGtJjyt7r2kF0Z4nl8neolHuQL+jpr16i2NHVfVK92uIeeZDnJSqo5vf7Syt0XeQ4rz2HA==}
|
resolution: {integrity: sha512-AlV2HPGPy1s91LwQ8x3a0UwHqe5/P2SRSXfHco5yo+D59KjkV7FaHLWwU+Dk03VNgmdkdUusPiB6/Cfkn1tPzw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@happy-dom/global-registrator@15.11.7':
|
'@happy-dom/global-registrator@15.11.7':
|
||||||
@@ -684,74 +687,74 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.4.6':
|
'@mongodb-js/saslprep@1.4.6':
|
||||||
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.93':
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
resolution: {integrity: sha512-xRIoOPFvneR29Dtq5d9p2AJbijDCFeV4jQ+5Ms/xVAXJVb8R0Jlu+pPr/SkhrG+Mouaml4roPSXugTIeRl6CMA==}
|
resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
resolution: {integrity: sha512-daNDi76HN5grC6GXDmpxdfP+N2mQPd3sCfg62VyHwUuvbZh32P7R/IUjkzAxtYMtTza+Zvx9hfLJ3J7ENL6WMA==}
|
resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.93':
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
resolution: {integrity: sha512-1YfuNPIQLawsg/gSNdJRk4kQWUy9M/Gy8FGsOI79nhQEJ2PZdqpSPl5UNzf4elfuNXuVbEbmmjP68EQdUunDuQ==}
|
resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
resolution: {integrity: sha512-8kEkOQPZjuyHjupvXExuJZiuiVNecdABGq3DLI7aO1EvQFOOlWMm2d/8Q5qXdV73Tn+nu3m16+kPajsN1oJefQ==}
|
resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-qIKLKkBkYSyWSYAoDThoxf5y1gr4X0g7W8rDU7d2HDeAAcotdVHUwuKkMeNe6+5VNk7/95EIhbslQjSxiCu32g==}
|
resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
resolution: {integrity: sha512-mAwQBGM3qArS9XEO21AK4E1uGvCuUCXjhIZk0dlVvs49MQ6wAAuCkYKNFpSKeSicKrLWwBMfgWX4qZoPh+M00A==}
|
resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-kaIH5MpPzOZfkM+QMsBxGdM9jlJT+N+fwz2IEaju/S+DL65E5TgPOx4QcD5dQ8vsMxlak6uDrudBc4ns5xzZCw==}
|
resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
resolution: {integrity: sha512-KtMZJqYWvOSeW5w3VSV2f5iGnwNdKJm4gwgVid4xNy1NFi+NJSyuglA1lX1u4wIPxizyxh8OW5c5Usf6oSOMNQ==}
|
resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
resolution: {integrity: sha512-qRZhOvlDBooRLX6V3/t9X9B+plZK+OrPLgfFixu0A1RO/3VHbubOknfnMnocSDAqk/L6cRyKI83VP2ciR9UO7w==}
|
resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
resolution: {integrity: sha512-um5XE44vF8bjkQEsH2iRSUP9fDeQGYbn/qjM/v4whXG83qsqapAXlOPOQqSARZB1SiNvPUAuXoRsJLlKFmAEFw==}
|
resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
resolution: {integrity: sha512-maHlizZgmKsAPJwjwBZMnsWfq3Ca9QutoteQwKe7YqsmbECoylrLCCOGCDOredstW4BRWqRTfCl6NJaVVeAQvQ==}
|
resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.93':
|
'@napi-rs/canvas@0.1.95':
|
||||||
resolution: {integrity: sha512-unVFo8CUlUeJCCxt50+j4yy91NF4x6n9zdGcvEsOFAWzowtZm3mgx8X2D7xjwV0cFSfxmpGPoe+JS77uzeFsxg==}
|
resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -1033,8 +1036,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.7.9':
|
'@push.rocks/smartproxy@25.8.0':
|
||||||
resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==}
|
resolution: {integrity: sha512-wf/eMleuYf50/sgJ6yMK/4FakMeijV2NwGbI8AS8gJj0DvCCqMdz5tbtupMJUsf8KFtvRePpSG9Qys+u9j3Eow==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1121,8 +1124,8 @@ packages:
|
|||||||
'@push.rocks/taskbuffer@3.5.0':
|
'@push.rocks/taskbuffer@3.5.0':
|
||||||
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@4.2.0':
|
'@push.rocks/taskbuffer@4.2.1':
|
||||||
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
|
resolution: {integrity: sha512-F3aizWLGWdAz7wSJqOzjwVgo1VQJcxTbHUjDN/Pqxw0WMQUwODRGbhgy4zLag7bOyE4tc8Jv7yid7Bjmn5hKdg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@6.1.2':
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
||||||
@@ -1333,8 +1336,8 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
'@serve.zone/catalog@2.2.0':
|
'@serve.zone/catalog@2.5.0':
|
||||||
resolution: {integrity: sha512-FxRGjuz8PdOXnfjHAGuPWP4jUTVGl5r9rsnxZlGSgTT+dHAm6Ue9AoTCkwVTKV9hP/Ac4yy8KKeNtNYIlidfJQ==}
|
resolution: {integrity: sha512-bRwk7pbDxUB471wUAS7p22MTOOBCHlMWijsE43K9tDAPcxlRarhtf2Dgx0Y25s/dFXqj2JHwe6jjE84S80jFzg==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||||
@@ -2006,6 +2009,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
balanced-match@4.0.4:
|
||||||
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
bare-events@2.8.2:
|
bare-events@2.8.2:
|
||||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2072,6 +2079,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
brace-expansion@5.0.3:
|
||||||
|
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
broadcast-channel@7.3.0:
|
broadcast-channel@7.3.0:
|
||||||
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
||||||
|
|
||||||
@@ -3231,8 +3242,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
|
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@10.2.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==}
|
||||||
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
minimatch@3.1.3:
|
||||||
|
resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==}
|
||||||
|
|
||||||
minimatch@9.0.5:
|
minimatch@9.0.5:
|
||||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||||
@@ -4334,13 +4349,13 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@api.global/typedserver@8.3.0(@tiptap/pm@2.27.2)':
|
'@api.global/typedserver@8.4.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.6
|
'@api.global/typedrequest': 3.2.6
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||||
'@cloudflare/workers-types': 4.20260210.0
|
'@cloudflare/workers-types': 4.20260303.0
|
||||||
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4364,7 +4379,7 @@ snapshots:
|
|||||||
'@push.rocks/smartserve': 2.0.1
|
'@push.rocks/smartserve': 2.0.1
|
||||||
'@push.rocks/smartsitemap': 2.0.4
|
'@push.rocks/smartsitemap': 2.0.4
|
||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
'@push.rocks/webrequest': 4.0.2
|
'@push.rocks/webrequest': 4.0.2
|
||||||
@@ -4934,11 +4949,13 @@ snapshots:
|
|||||||
|
|
||||||
'@cloudflare/workers-types@4.20260210.0': {}
|
'@cloudflare/workers-types@4.20260210.0': {}
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260303.0': {}
|
||||||
|
|
||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.43.2(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.43.3(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.8
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
@@ -5163,7 +5180,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.8.3':
|
'@git.zone/tsbundle@2.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/npmextra': 5.3.3
|
'@push.rocks/npmextra': 5.3.3
|
||||||
@@ -5220,7 +5237,7 @@ snapshots:
|
|||||||
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.9.0
|
||||||
'@git.zone/tsrun': 2.0.1
|
'@git.zone/tsrun': 2.0.1
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
@@ -5266,10 +5283,10 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tswatch@3.1.0(@tiptap/pm@2.27.2)':
|
'@git.zone/tswatch@3.2.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 8.3.0(@tiptap/pm@2.27.2)
|
'@api.global/typedserver': 8.4.0(@tiptap/pm@2.27.2)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.9.0
|
||||||
'@git.zone/tsrun': 2.0.1
|
'@git.zone/tsrun': 2.0.1
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -5282,7 +5299,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
'@push.rocks/taskbuffer': 4.2.0
|
'@push.rocks/taskbuffer': 4.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
@@ -5447,52 +5464,52 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.93':
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.93':
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.93':
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.93':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.93':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.93':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.93':
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.93':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.93':
|
'@napi-rs/canvas@0.1.95':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.93
|
'@napi-rs/canvas-android-arm64': 0.1.95
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.93
|
'@napi-rs/canvas-darwin-arm64': 0.1.95
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.93
|
'@napi-rs/canvas-darwin-x64': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.93
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.93
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.95
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.93
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.95
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.93
|
'@napi-rs/canvas-linux-x64-musl': 0.1.95
|
||||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.93
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.95
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.93
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.95
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
@@ -6321,13 +6338,13 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.7.9':
|
'@push.rocks/smartproxy@25.8.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
minimatch: 10.2.1
|
minimatch: 10.2.2
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6566,7 +6583,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@4.2.0':
|
'@push.rocks/taskbuffer@4.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -6574,7 +6591,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -6785,9 +6802,9 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
'@serve.zone/catalog@2.2.0(@tiptap/pm@2.27.2)':
|
'@serve.zone/catalog@2.5.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-catalog': 3.43.2(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-domtools': 2.3.8
|
'@design.estate/dees-domtools': 2.3.8
|
||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@design.estate/dees-wcctools': 3.8.0
|
'@design.estate/dees-wcctools': 3.8.0
|
||||||
@@ -7621,6 +7638,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jackspeak: 4.2.3
|
jackspeak: 4.2.3
|
||||||
|
|
||||||
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
bare-events@2.8.2: {}
|
bare-events@2.8.2: {}
|
||||||
|
|
||||||
bare-fs@4.5.3:
|
bare-fs@4.5.3:
|
||||||
@@ -7693,6 +7712,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 4.0.2
|
balanced-match: 4.0.2
|
||||||
|
|
||||||
|
brace-expansion@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.4
|
||||||
|
|
||||||
broadcast-channel@7.3.0:
|
broadcast-channel@7.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
@@ -8340,7 +8363,7 @@ snapshots:
|
|||||||
fs.realpath: 1.0.0
|
fs.realpath: 1.0.0
|
||||||
inflight: 1.0.6
|
inflight: 1.0.6
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.3
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
|
|
||||||
@@ -9137,7 +9160,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.2
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@10.2.2:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.3
|
||||||
|
|
||||||
|
minimatch@3.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
|
|
||||||
@@ -9397,7 +9424,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.93
|
'@napi-rs/canvas': 0.1.95
|
||||||
|
|
||||||
peberminta@0.9.0: {}
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ tap.test('should respond to configuration request', async () => {
|
|||||||
const response = await configRequest.fire({});
|
const response = await configRequest.fire({});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
|
expect(response.config).toHaveProperty('system');
|
||||||
|
expect(response.config).toHaveProperty('smartProxy');
|
||||||
expect(response.config).toHaveProperty('email');
|
expect(response.config).toHaveProperty('email');
|
||||||
expect(response.config).toHaveProperty('dns');
|
expect(response.config).toHaveProperty('dns');
|
||||||
expect(response.config).toHaveProperty('proxy');
|
expect(response.config).toHaveProperty('tls');
|
||||||
expect(response.config).toHaveProperty('security');
|
expect(response.config).toHaveProperty('cache');
|
||||||
|
expect(response.config).toHaveProperty('radius');
|
||||||
|
expect(response.config).toHaveProperty('remoteIngress');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should handle log retrieval request', async () => {
|
tap.test('should handle log retrieval request', async () => {
|
||||||
|
|||||||
@@ -106,10 +106,14 @@ tap.test('should allow read-only config access', async () => {
|
|||||||
const response = await configRequest.fire({});
|
const response = await configRequest.fire({});
|
||||||
|
|
||||||
expect(response).toHaveProperty('config');
|
expect(response).toHaveProperty('config');
|
||||||
|
expect(response.config).toHaveProperty('system');
|
||||||
|
expect(response.config).toHaveProperty('smartProxy');
|
||||||
expect(response.config).toHaveProperty('email');
|
expect(response.config).toHaveProperty('email');
|
||||||
expect(response.config).toHaveProperty('dns');
|
expect(response.config).toHaveProperty('dns');
|
||||||
expect(response.config).toHaveProperty('proxy');
|
expect(response.config).toHaveProperty('tls');
|
||||||
expect(response.config).toHaveProperty('security');
|
expect(response.config).toHaveProperty('cache');
|
||||||
|
expect(response.config).toHaveProperty('radius');
|
||||||
|
expect(response.config).toHaveProperty('remoteIngress');
|
||||||
console.log('Configuration read successfully');
|
console.log('Configuration read successfully');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import { DcRouter } from '../ts/index.js';
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
|
||||||
const devRouter = new DcRouter({
|
const devRouter = new DcRouter({
|
||||||
// Configure services as needed for development
|
// SmartProxy routes for development/demo
|
||||||
// OpsServer always starts on port 3000
|
smartProxyConfig: {
|
||||||
|
routes: [
|
||||||
// Example: Add SmartProxy routes
|
{
|
||||||
// smartProxyConfig: {
|
name: 'web-traffic',
|
||||||
// routes: [...]
|
match: { ports: [18080], domains: ['example.com', '*.example.com'] },
|
||||||
// },
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] },
|
||||||
|
},
|
||||||
// Example: Add email configuration
|
{
|
||||||
// emailConfig: {
|
name: 'api-gateway',
|
||||||
// ports: [2525],
|
match: { ports: [18080], domains: ['api.example.com'], path: '/v1/*' },
|
||||||
// hostname: 'localhost',
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 4000 }] },
|
||||||
// domains: [],
|
},
|
||||||
// routes: []
|
{
|
||||||
// },
|
name: 'tls-passthrough',
|
||||||
|
match: { ports: [18443], domains: ['secure.example.com'] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'localhost', port: 4443 }],
|
||||||
|
tls: { mode: 'passthrough' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Disable cache/mongo for dev
|
||||||
|
cacheConfig: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Starting DcRouter in development mode...');
|
console.log('Starting DcRouter in development mode...');
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '8.0.0',
|
version: '9.1.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { OpsServer } from './opsserver/index.js';
|
|||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||||
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
|
import { RouteConfigManager, ApiTokenManager } from './config/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
@@ -212,6 +213,10 @@ export class DcRouter {
|
|||||||
public remoteIngressManager?: RemoteIngressManager;
|
public remoteIngressManager?: RemoteIngressManager;
|
||||||
public tunnelManager?: TunnelManager;
|
public tunnelManager?: TunnelManager;
|
||||||
|
|
||||||
|
// Programmatic config API
|
||||||
|
public routeConfigManager?: RouteConfigManager;
|
||||||
|
public apiTokenManager?: ApiTokenManager;
|
||||||
|
|
||||||
// DNS query logging rate limiter state
|
// DNS query logging rate limiter state
|
||||||
private dnsLogWindow: number[] = [];
|
private dnsLogWindow: number[] = [];
|
||||||
private dnsBatchCount: number = 0;
|
private dnsBatchCount: number = 0;
|
||||||
@@ -233,6 +238,9 @@ export class DcRouter {
|
|||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
// Cached constructor routes (computed once during setupSmartProxy, used by RouteConfigManager)
|
||||||
|
private constructorRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
// Environment access
|
// Environment access
|
||||||
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
||||||
|
|
||||||
@@ -276,6 +284,16 @@ export class DcRouter {
|
|||||||
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
||||||
await this.setupSmartProxy();
|
await this.setupSmartProxy();
|
||||||
|
|
||||||
|
// Initialize programmatic config API managers
|
||||||
|
this.routeConfigManager = new RouteConfigManager(
|
||||||
|
this.storageManager,
|
||||||
|
() => this.getConstructorRoutes(),
|
||||||
|
() => this.smartProxy,
|
||||||
|
);
|
||||||
|
this.apiTokenManager = new ApiTokenManager(this.storageManager);
|
||||||
|
await this.apiTokenManager.initialize();
|
||||||
|
await this.routeConfigManager.initialize();
|
||||||
|
|
||||||
// Set up unified email handling if configured
|
// Set up unified email handling if configured
|
||||||
if (this.options.emailConfig) {
|
if (this.options.emailConfig) {
|
||||||
await this.setupUnifiedEmailHandling();
|
await this.setupUnifiedEmailHandling();
|
||||||
@@ -443,6 +461,9 @@ export class DcRouter {
|
|||||||
challengeHandlers.push(dns01Handler);
|
challengeHandlers.push(dns01Handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache constructor routes for RouteConfigManager
|
||||||
|
this.constructorRoutes = [...routes];
|
||||||
|
|
||||||
// If we have routes or need a basic SmartProxy instance, create it
|
// If we have routes or need a basic SmartProxy instance, create it
|
||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
||||||
@@ -857,6 +878,14 @@ export class DcRouter {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the routes derived from constructor config (smartProxy + email + DNS).
|
||||||
|
* Used by RouteConfigManager as the "hardcoded" base.
|
||||||
|
*/
|
||||||
|
public getConstructorRoutes(): plugins.smartproxy.IRouteConfig[] {
|
||||||
|
return this.constructorRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
logger.log('info', 'Stopping DcRouter services...');
|
logger.log('info', 'Stopping DcRouter services...');
|
||||||
|
|
||||||
@@ -929,6 +958,8 @@ export class DcRouter {
|
|||||||
this.smartAcme = undefined;
|
this.smartAcme = undefined;
|
||||||
this.certProvisionScheduler = undefined;
|
this.certProvisionScheduler = undefined;
|
||||||
this.remoteIngressManager = undefined;
|
this.remoteIngressManager = undefined;
|
||||||
|
this.routeConfigManager = undefined;
|
||||||
|
this.apiTokenManager = undefined;
|
||||||
this.certificateStatusMap.clear();
|
this.certificateStatusMap.clear();
|
||||||
|
|
||||||
logger.log('info', 'All DcRouter services stopped');
|
logger.log('info', 'All DcRouter services stopped');
|
||||||
@@ -960,6 +991,11 @@ export class DcRouter {
|
|||||||
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
||||||
await this.setupSmartProxy();
|
await this.setupSmartProxy();
|
||||||
|
|
||||||
|
// Re-apply programmatic routes and overrides after SmartProxy restart
|
||||||
|
if (this.routeConfigManager) {
|
||||||
|
await this.routeConfigManager.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', 'SmartProxy configuration updated');
|
logger.log('info', 'SmartProxy configuration updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
155
ts/config/classes.api-token-manager.ts
Normal file
155
ts/config/classes.api-token-manager.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
import type {
|
||||||
|
IStoredApiToken,
|
||||||
|
IApiTokenInfo,
|
||||||
|
TApiTokenScope,
|
||||||
|
} from '../../ts_interfaces/data/route-management.js';
|
||||||
|
|
||||||
|
const TOKENS_PREFIX = '/config-api/tokens/';
|
||||||
|
const TOKEN_PREFIX_STR = 'dcr_';
|
||||||
|
|
||||||
|
export class ApiTokenManager {
|
||||||
|
private tokens = new Map<string, IStoredApiToken>();
|
||||||
|
|
||||||
|
constructor(private storageManager: StorageManager) {}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.loadTokens();
|
||||||
|
if (this.tokens.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.tokens.size} API token(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Token lifecycle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API token. Returns the raw token value (shown once).
|
||||||
|
*/
|
||||||
|
public async createToken(
|
||||||
|
name: string,
|
||||||
|
scopes: TApiTokenScope[],
|
||||||
|
expiresInDays: number | null,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<{ id: string; rawToken: string }> {
|
||||||
|
const id = plugins.uuid.v4();
|
||||||
|
const randomBytes = plugins.crypto.randomBytes(32);
|
||||||
|
const rawPayload = `${id}:${randomBytes.toString('base64url')}`;
|
||||||
|
const rawToken = `${TOKEN_PREFIX_STR}${rawPayload}`;
|
||||||
|
|
||||||
|
const tokenHash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const stored: IStoredApiToken = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
tokenHash,
|
||||||
|
scopes,
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
createdBy,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tokens.set(id, stored);
|
||||||
|
await this.persistToken(stored);
|
||||||
|
logger.log('info', `API token '${name}' created (id: ${id})`);
|
||||||
|
return { id, rawToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a raw token string. Returns the stored token if valid, null otherwise.
|
||||||
|
* Also updates lastUsedAt.
|
||||||
|
*/
|
||||||
|
public async validateToken(rawToken: string): Promise<IStoredApiToken | null> {
|
||||||
|
if (!rawToken.startsWith(TOKEN_PREFIX_STR)) return null;
|
||||||
|
|
||||||
|
const hash = plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
|
||||||
|
|
||||||
|
for (const stored of this.tokens.values()) {
|
||||||
|
if (stored.tokenHash === hash) {
|
||||||
|
if (!stored.enabled) return null;
|
||||||
|
if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;
|
||||||
|
|
||||||
|
// Update lastUsedAt (fire and forget)
|
||||||
|
stored.lastUsedAt = Date.now();
|
||||||
|
this.persistToken(stored).catch(() => {});
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a token has a specific scope.
|
||||||
|
*/
|
||||||
|
public hasScope(token: IStoredApiToken, scope: TApiTokenScope): boolean {
|
||||||
|
return token.scopes.includes(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all tokens (safe info only, no hashes).
|
||||||
|
*/
|
||||||
|
public listTokens(): IApiTokenInfo[] {
|
||||||
|
const result: IApiTokenInfo[] = [];
|
||||||
|
for (const stored of this.tokens.values()) {
|
||||||
|
result.push({
|
||||||
|
id: stored.id,
|
||||||
|
name: stored.name,
|
||||||
|
scopes: stored.scopes,
|
||||||
|
createdAt: stored.createdAt,
|
||||||
|
expiresAt: stored.expiresAt,
|
||||||
|
lastUsedAt: stored.lastUsedAt,
|
||||||
|
enabled: stored.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke (delete) a token.
|
||||||
|
*/
|
||||||
|
public async revokeToken(id: string): Promise<boolean> {
|
||||||
|
if (!this.tokens.has(id)) return false;
|
||||||
|
const token = this.tokens.get(id)!;
|
||||||
|
this.tokens.delete(id);
|
||||||
|
await this.storageManager.delete(`${TOKENS_PREFIX}${id}.json`);
|
||||||
|
logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable a token.
|
||||||
|
*/
|
||||||
|
public async toggleToken(id: string, enabled: boolean): Promise<boolean> {
|
||||||
|
const stored = this.tokens.get(id);
|
||||||
|
if (!stored) return false;
|
||||||
|
stored.enabled = enabled;
|
||||||
|
await this.persistToken(stored);
|
||||||
|
logger.log('info', `API token '${stored.name}' ${enabled ? 'enabled' : 'disabled'} (id: ${id})`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async loadTokens(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(TOKENS_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const stored = await this.storageManager.getJSON<IStoredApiToken>(key);
|
||||||
|
if (stored?.id) {
|
||||||
|
this.tokens.set(stored.id, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistToken(stored: IStoredApiToken): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(`${TOKENS_PREFIX}${stored.id}.json`, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
271
ts/config/classes.route-config-manager.ts
Normal file
271
ts/config/classes.route-config-manager.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import type { StorageManager } from '../storage/index.js';
|
||||||
|
import type {
|
||||||
|
IStoredRoute,
|
||||||
|
IRouteOverride,
|
||||||
|
IMergedRoute,
|
||||||
|
IRouteWarning,
|
||||||
|
} from '../../ts_interfaces/data/route-management.js';
|
||||||
|
|
||||||
|
const ROUTES_PREFIX = '/config-api/routes/';
|
||||||
|
const OVERRIDES_PREFIX = '/config-api/overrides/';
|
||||||
|
|
||||||
|
export class RouteConfigManager {
|
||||||
|
private storedRoutes = new Map<string, IStoredRoute>();
|
||||||
|
private overrides = new Map<string, IRouteOverride>();
|
||||||
|
private warnings: IRouteWarning[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private storageManager: StorageManager,
|
||||||
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load persisted routes and overrides, compute warnings, apply to SmartProxy.
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.loadStoredRoutes();
|
||||||
|
await this.loadOverrides();
|
||||||
|
this.computeWarnings();
|
||||||
|
this.logWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Merged view
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public getMergedRoutes(): { routes: IMergedRoute[]; warnings: IRouteWarning[] } {
|
||||||
|
const merged: IMergedRoute[] = [];
|
||||||
|
|
||||||
|
// Hardcoded routes
|
||||||
|
for (const route of this.getHardcodedRoutes()) {
|
||||||
|
const name = route.name || '';
|
||||||
|
const override = this.overrides.get(name);
|
||||||
|
merged.push({
|
||||||
|
route,
|
||||||
|
source: 'hardcoded',
|
||||||
|
enabled: override ? override.enabled : true,
|
||||||
|
overridden: !!override,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
merged.push({
|
||||||
|
route: stored.route,
|
||||||
|
source: 'programmatic',
|
||||||
|
enabled: stored.enabled,
|
||||||
|
overridden: false,
|
||||||
|
storedRouteId: stored.id,
|
||||||
|
createdAt: stored.createdAt,
|
||||||
|
updatedAt: stored.updatedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { routes: merged, warnings: [...this.warnings] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Programmatic route CRUD
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public async createRoute(
|
||||||
|
route: plugins.smartproxy.IRouteConfig,
|
||||||
|
createdBy: string,
|
||||||
|
enabled = true,
|
||||||
|
): Promise<string> {
|
||||||
|
const id = plugins.uuid.v4();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Ensure route has a name
|
||||||
|
if (!route.name) {
|
||||||
|
route.name = `programmatic-${id.slice(0, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored: IStoredRoute = {
|
||||||
|
id,
|
||||||
|
route,
|
||||||
|
enabled,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
createdBy,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.storedRoutes.set(id, stored);
|
||||||
|
await this.persistRoute(stored);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateRoute(
|
||||||
|
id: string,
|
||||||
|
patch: { route?: Partial<plugins.smartproxy.IRouteConfig>; enabled?: boolean },
|
||||||
|
): Promise<boolean> {
|
||||||
|
const stored = this.storedRoutes.get(id);
|
||||||
|
if (!stored) return false;
|
||||||
|
|
||||||
|
if (patch.route) {
|
||||||
|
stored.route = { ...stored.route, ...patch.route } as plugins.smartproxy.IRouteConfig;
|
||||||
|
}
|
||||||
|
if (patch.enabled !== undefined) {
|
||||||
|
stored.enabled = patch.enabled;
|
||||||
|
}
|
||||||
|
stored.updatedAt = Date.now();
|
||||||
|
|
||||||
|
await this.persistRoute(stored);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRoute(id: string): Promise<boolean> {
|
||||||
|
if (!this.storedRoutes.has(id)) return false;
|
||||||
|
this.storedRoutes.delete(id);
|
||||||
|
await this.storageManager.delete(`${ROUTES_PREFIX}${id}.json`);
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async toggleRoute(id: string, enabled: boolean): Promise<boolean> {
|
||||||
|
return this.updateRoute(id, { enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hardcoded route overrides
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public async setOverride(routeName: string, enabled: boolean, updatedBy: string): Promise<void> {
|
||||||
|
const override: IRouteOverride = {
|
||||||
|
routeName,
|
||||||
|
enabled,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
updatedBy,
|
||||||
|
};
|
||||||
|
this.overrides.set(routeName, override);
|
||||||
|
await this.storageManager.setJSON(`${OVERRIDES_PREFIX}${routeName}.json`, override);
|
||||||
|
this.computeWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeOverride(routeName: string): Promise<boolean> {
|
||||||
|
if (!this.overrides.has(routeName)) return false;
|
||||||
|
this.overrides.delete(routeName);
|
||||||
|
await this.storageManager.delete(`${OVERRIDES_PREFIX}${routeName}.json`);
|
||||||
|
this.computeWarnings();
|
||||||
|
await this.applyRoutes();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: persistence
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async loadStoredRoutes(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(ROUTES_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const stored = await this.storageManager.getJSON<IStoredRoute>(key);
|
||||||
|
if (stored?.id) {
|
||||||
|
this.storedRoutes.set(stored.id, stored);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.storedRoutes.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.storedRoutes.size} programmatic route(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadOverrides(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(OVERRIDES_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!key.endsWith('.json')) continue;
|
||||||
|
const override = await this.storageManager.getJSON<IRouteOverride>(key);
|
||||||
|
if (override?.routeName) {
|
||||||
|
this.overrides.set(override.routeName, override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.overrides.size > 0) {
|
||||||
|
logger.log('info', `Loaded ${this.overrides.size} route override(s) from storage`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistRoute(stored: IStoredRoute): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(`${ROUTES_PREFIX}${stored.id}.json`, stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: warnings
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private computeWarnings(): void {
|
||||||
|
this.warnings = [];
|
||||||
|
const hardcodedNames = new Set(this.getHardcodedRoutes().map((r) => r.name || ''));
|
||||||
|
|
||||||
|
// Check overrides
|
||||||
|
for (const [routeName, override] of this.overrides) {
|
||||||
|
if (!hardcodedNames.has(routeName)) {
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'orphaned-override',
|
||||||
|
routeName,
|
||||||
|
message: `Orphaned override for route '${routeName}' — hardcoded route no longer exists`,
|
||||||
|
});
|
||||||
|
} else if (!override.enabled) {
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'disabled-hardcoded',
|
||||||
|
routeName,
|
||||||
|
message: `Route '${routeName}' is disabled via API override`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check disabled programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
if (!stored.enabled) {
|
||||||
|
const name = stored.route.name || stored.id;
|
||||||
|
this.warnings.push({
|
||||||
|
type: 'disabled-programmatic',
|
||||||
|
routeName: name,
|
||||||
|
message: `Programmatic route '${name}' (id: ${stored.id}) is disabled`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logWarnings(): void {
|
||||||
|
for (const w of this.warnings) {
|
||||||
|
logger.log('warn', w.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: apply merged routes to SmartProxy
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private async applyRoutes(): Promise<void> {
|
||||||
|
const smartProxy = this.getSmartProxy();
|
||||||
|
if (!smartProxy) return;
|
||||||
|
|
||||||
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
|
// Add enabled hardcoded routes (respecting overrides)
|
||||||
|
for (const route of this.getHardcodedRoutes()) {
|
||||||
|
const name = route.name || '';
|
||||||
|
const override = this.overrides.get(name);
|
||||||
|
if (override && !override.enabled) {
|
||||||
|
continue; // Skip disabled hardcoded route
|
||||||
|
}
|
||||||
|
enabledRoutes.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enabled programmatic routes
|
||||||
|
for (const stored of this.storedRoutes.values()) {
|
||||||
|
if (stored.enabled) {
|
||||||
|
enabledRoutes.push(stored.route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await smartProxy.updateRoutes(enabledRoutes);
|
||||||
|
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.storedRoutes.size} programmatic, ${this.overrides.size} overrides)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
// Export validation tools only
|
// Export validation tools only
|
||||||
export * from './validator.js';
|
export * from './validator.js';
|
||||||
|
export { RouteConfigManager } from './classes.route-config-manager.js';
|
||||||
|
export { ApiTokenManager } from './classes.api-token-manager.js';
|
||||||
@@ -20,6 +20,8 @@ export class OpsServer {
|
|||||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||||
private certificateHandler: handlers.CertificateHandler;
|
private certificateHandler: handlers.CertificateHandler;
|
||||||
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
||||||
|
private routeManagementHandler: handlers.RouteManagementHandler;
|
||||||
|
private apiTokenHandler: handlers.ApiTokenHandler;
|
||||||
|
|
||||||
constructor(dcRouterRefArg: DcRouter) {
|
constructor(dcRouterRefArg: DcRouter) {
|
||||||
this.dcRouterRef = dcRouterRefArg;
|
this.dcRouterRef = dcRouterRefArg;
|
||||||
@@ -61,6 +63,8 @@ export class OpsServer {
|
|||||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||||
this.certificateHandler = new handlers.CertificateHandler(this);
|
this.certificateHandler = new handlers.CertificateHandler(this);
|
||||||
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
||||||
|
this.routeManagementHandler = new handlers.RouteManagementHandler(this);
|
||||||
|
this.apiTokenHandler = new handlers.ApiTokenHandler(this);
|
||||||
|
|
||||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|||||||
96
ts/opsserver/handlers/api-token.handler.ts
Normal file
96
ts/opsserver/handlers/api-token.handler.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class ApiTokenHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
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 {
|
||||||
|
// Create API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateApiToken>(
|
||||||
|
'createApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const result = await manager.createToken(
|
||||||
|
dataArg.name,
|
||||||
|
dataArg.scopes,
|
||||||
|
dataArg.expiresInDays ?? null,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// List API tokens
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListApiTokens>(
|
||||||
|
'listApiTokens',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { tokens: [] };
|
||||||
|
}
|
||||||
|
return { tokens: manager.listTokens() };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Revoke API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeApiToken>(
|
||||||
|
'revokeApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.revokeToken(dataArg.id);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Token not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle API token
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleApiToken>(
|
||||||
|
'toggleApiToken',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAdmin(dataArg.identity);
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Token management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.toggleToken(dataArg.id, dataArg.enabled);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Token not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
|
import * as paths from '../../paths.js';
|
||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export class ConfigHandler {
|
|||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
||||||
'getConfiguration',
|
'getConfiguration',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
const config = await this.getConfiguration(dataArg.section);
|
const config = await this.getConfiguration();
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
section: dataArg.section,
|
section: dataArg.section,
|
||||||
@@ -27,82 +28,163 @@ export class ConfigHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getConfiguration(section?: string): Promise<{
|
private async getConfiguration(): Promise<interfaces.requests.IConfigData> {
|
||||||
email: {
|
|
||||||
enabled: boolean;
|
|
||||||
ports: number[];
|
|
||||||
maxMessageSize: number;
|
|
||||||
rateLimits: {
|
|
||||||
perMinute: number;
|
|
||||||
perHour: number;
|
|
||||||
perDay: number;
|
|
||||||
};
|
|
||||||
domains?: string[];
|
|
||||||
};
|
|
||||||
dns: {
|
|
||||||
enabled: boolean;
|
|
||||||
port: number;
|
|
||||||
nameservers: string[];
|
|
||||||
caching: boolean;
|
|
||||||
ttl: number;
|
|
||||||
};
|
|
||||||
proxy: {
|
|
||||||
enabled: boolean;
|
|
||||||
httpPort: number;
|
|
||||||
httpsPort: number;
|
|
||||||
maxConnections: number;
|
|
||||||
};
|
|
||||||
security: {
|
|
||||||
blockList: string[];
|
|
||||||
rateLimit: boolean;
|
|
||||||
spamDetection: boolean;
|
|
||||||
tlsRequired: boolean;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const opts = dcRouter.options;
|
||||||
|
const resolvedPaths = dcRouter.resolvedPaths;
|
||||||
|
|
||||||
// Get email domains if email server is configured
|
// --- System ---
|
||||||
|
const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.storage?.readFunction
|
||||||
|
? 'custom'
|
||||||
|
: opts.storage?.fsPath
|
||||||
|
? 'filesystem'
|
||||||
|
: 'memory';
|
||||||
|
|
||||||
|
const system: interfaces.requests.IConfigData['system'] = {
|
||||||
|
baseDir: resolvedPaths.dcrouterHomeDir,
|
||||||
|
dataDir: resolvedPaths.dataDir,
|
||||||
|
publicIp: opts.publicIp || null,
|
||||||
|
proxyIps: opts.proxyIps || [],
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
storageBackend,
|
||||||
|
storagePath: opts.storage?.fsPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SmartProxy ---
|
||||||
|
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
|
||||||
|
if (opts.smartProxyConfig?.acme) {
|
||||||
|
const acme = opts.smartProxyConfig.acme;
|
||||||
|
acmeInfo = {
|
||||||
|
enabled: acme.enabled !== false,
|
||||||
|
accountEmail: acme.accountEmail || '',
|
||||||
|
useProduction: acme.useProduction !== false,
|
||||||
|
autoRenew: acme.autoRenew !== false,
|
||||||
|
renewThresholdDays: acme.renewThresholdDays || 30,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let routeCount = 0;
|
||||||
|
if (dcRouter.routeConfigManager) {
|
||||||
|
try {
|
||||||
|
const merged = await dcRouter.routeConfigManager.getMergedRoutes();
|
||||||
|
routeCount = merged.routes.length;
|
||||||
|
} catch {
|
||||||
|
routeCount = opts.smartProxyConfig?.routes?.length || 0;
|
||||||
|
}
|
||||||
|
} else if (opts.smartProxyConfig?.routes) {
|
||||||
|
routeCount = opts.smartProxyConfig.routes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartProxy: interfaces.requests.IConfigData['smartProxy'] = {
|
||||||
|
enabled: !!dcRouter.smartProxy,
|
||||||
|
routeCount,
|
||||||
|
acme: acmeInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Email ---
|
||||||
let emailDomains: string[] = [];
|
let emailDomains: string[] = [];
|
||||||
if (dcRouter.emailServer && dcRouter.emailServer.domainRegistry) {
|
if (dcRouter.emailServer && (dcRouter.emailServer as any).domainRegistry) {
|
||||||
emailDomains = dcRouter.emailServer.domainRegistry.getAllDomains();
|
emailDomains = (dcRouter.emailServer as any).domainRegistry.getAllDomains();
|
||||||
} else if (dcRouter.options.emailConfig?.domains) {
|
} else if (opts.emailConfig?.domains) {
|
||||||
// Fallback: get domains from email config options
|
emailDomains = opts.emailConfig.domains.map((d: any) =>
|
||||||
emailDomains = dcRouter.options.emailConfig.domains.map(d =>
|
|
||||||
typeof d === 'string' ? d : d.domain
|
typeof d === 'string' ? d : d.domain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let portMapping: Record<string, number> | null = null;
|
||||||
email: {
|
if (opts.emailPortConfig?.portMapping) {
|
||||||
|
portMapping = {};
|
||||||
|
for (const [ext, int] of Object.entries(opts.emailPortConfig.portMapping)) {
|
||||||
|
portMapping[String(ext)] = int as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const email: interfaces.requests.IConfigData['email'] = {
|
||||||
enabled: !!dcRouter.emailServer,
|
enabled: !!dcRouter.emailServer,
|
||||||
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [],
|
ports: opts.emailConfig?.ports || [],
|
||||||
maxMessageSize: 10 * 1024 * 1024, // 10MB default
|
portMapping,
|
||||||
rateLimits: {
|
hostname: opts.emailConfig?.hostname || null,
|
||||||
perMinute: 10,
|
|
||||||
perHour: 100,
|
|
||||||
perDay: 1000,
|
|
||||||
},
|
|
||||||
domains: emailDomains,
|
domains: emailDomains,
|
||||||
},
|
emailRouteCount: opts.emailConfig?.routes?.length || 0,
|
||||||
dns: {
|
receivedEmailsPath: opts.emailPortConfig?.receivedEmailsPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- DNS ---
|
||||||
|
const dnsRecords = (opts.dnsRecords || []).map(r => ({
|
||||||
|
name: r.name,
|
||||||
|
type: r.type,
|
||||||
|
value: r.value,
|
||||||
|
ttl: r.ttl,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dns: interfaces.requests.IConfigData['dns'] = {
|
||||||
enabled: !!dcRouter.dnsServer,
|
enabled: !!dcRouter.dnsServer,
|
||||||
port: 53,
|
port: 53,
|
||||||
nameservers: dcRouter.options.dnsNsDomains || [],
|
nsDomains: opts.dnsNsDomains || [],
|
||||||
caching: true,
|
scopes: opts.dnsScopes || [],
|
||||||
ttl: 300,
|
recordCount: dnsRecords.length,
|
||||||
},
|
records: dnsRecords,
|
||||||
proxy: {
|
dnsChallenge: !!opts.dnsChallenge?.cloudflareApiKey,
|
||||||
enabled: !!dcRouter.smartProxy,
|
};
|
||||||
httpPort: 80,
|
|
||||||
httpsPort: 443,
|
// --- TLS ---
|
||||||
maxConnections: 1000,
|
let tlsSource: 'acme' | 'static' | 'none' = 'none';
|
||||||
},
|
if (opts.tls?.certPath && opts.tls?.keyPath) {
|
||||||
security: {
|
tlsSource = 'static';
|
||||||
blockList: [],
|
} else if (opts.smartProxyConfig?.acme?.enabled !== false && opts.smartProxyConfig?.acme) {
|
||||||
rateLimit: true,
|
tlsSource = 'acme';
|
||||||
spamDetection: true,
|
}
|
||||||
tlsRequired: false,
|
|
||||||
},
|
const tls: interfaces.requests.IConfigData['tls'] = {
|
||||||
|
contactEmail: opts.tls?.contactEmail || opts.smartProxyConfig?.acme?.accountEmail || null,
|
||||||
|
domain: opts.tls?.domain || null,
|
||||||
|
source: tlsSource,
|
||||||
|
certPath: opts.tls?.certPath || null,
|
||||||
|
keyPath: opts.tls?.keyPath || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Cache ---
|
||||||
|
const cacheConfig = opts.cacheConfig;
|
||||||
|
const cache: interfaces.requests.IConfigData['cache'] = {
|
||||||
|
enabled: cacheConfig?.enabled !== false,
|
||||||
|
storagePath: cacheConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
|
||||||
|
dbName: cacheConfig?.dbName || 'dcrouter',
|
||||||
|
defaultTTLDays: cacheConfig?.defaultTTLDays || 30,
|
||||||
|
cleanupIntervalHours: cacheConfig?.cleanupIntervalHours || 1,
|
||||||
|
ttlConfig: cacheConfig?.ttlConfig ? { ...cacheConfig.ttlConfig } as Record<string, number> : {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- RADIUS ---
|
||||||
|
const radiusCfg = opts.radiusConfig;
|
||||||
|
const radius: interfaces.requests.IConfigData['radius'] = {
|
||||||
|
enabled: !!dcRouter.radiusServer,
|
||||||
|
authPort: radiusCfg?.authPort || null,
|
||||||
|
acctPort: radiusCfg?.acctPort || null,
|
||||||
|
bindAddress: radiusCfg?.bindAddress || null,
|
||||||
|
clientCount: radiusCfg?.clients?.length || 0,
|
||||||
|
vlanDefaultVlan: radiusCfg?.vlanAssignment?.defaultVlan ?? null,
|
||||||
|
vlanAllowUnknownMacs: radiusCfg?.vlanAssignment?.allowUnknownMacs ?? null,
|
||||||
|
vlanMappingCount: radiusCfg?.vlanAssignment?.mappings?.length || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Remote Ingress ---
|
||||||
|
const riCfg = opts.remoteIngressConfig;
|
||||||
|
const remoteIngress: interfaces.requests.IConfigData['remoteIngress'] = {
|
||||||
|
enabled: !!dcRouter.remoteIngressManager,
|
||||||
|
tunnelPort: riCfg?.tunnelPort || null,
|
||||||
|
hubDomain: riCfg?.hubDomain || null,
|
||||||
|
tlsConfigured: !!(riCfg?.tls?.certPath && riCfg?.tls?.keyPath),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
system,
|
||||||
|
smartProxy,
|
||||||
|
email,
|
||||||
|
dns,
|
||||||
|
tls,
|
||||||
|
cache,
|
||||||
|
radius,
|
||||||
|
remoteIngress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,3 +7,5 @@ export * from './radius.handler.js';
|
|||||||
export * from './email-ops.handler.js';
|
export * from './email-ops.handler.js';
|
||||||
export * from './certificate.handler.js';
|
export * from './certificate.handler.js';
|
||||||
export * from './remoteingress.handler.js';
|
export * from './remoteingress.handler.js';
|
||||||
|
export * from './route-management.handler.js';
|
||||||
|
export * from './api-token.handler.js';
|
||||||
163
ts/opsserver/handlers/route-management.handler.ts
Normal file
163
ts/opsserver/handlers/route-management.handler.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class RouteManagementHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate auth: JWT identity OR API token with required scope.
|
||||||
|
* Returns a userId string on success, throws on failure.
|
||||||
|
*/
|
||||||
|
private async requireAuth(
|
||||||
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
||||||
|
requiredScope?: interfaces.data.TApiTokenScope,
|
||||||
|
): Promise<string> {
|
||||||
|
// Try JWT identity first
|
||||||
|
if (request.identity?.jwt) {
|
||||||
|
try {
|
||||||
|
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
||||||
|
identity: request.identity,
|
||||||
|
});
|
||||||
|
if (isAdmin) return request.identity.userId;
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try API token
|
||||||
|
if (request.apiToken) {
|
||||||
|
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
||||||
|
if (tokenManager) {
|
||||||
|
const token = await tokenManager.validateToken(request.apiToken);
|
||||||
|
if (token) {
|
||||||
|
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
||||||
|
return token.createdBy;
|
||||||
|
}
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// Get merged routes
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetMergedRoutes>(
|
||||||
|
'getMergedRoutes',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:read');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { routes: [], warnings: [] };
|
||||||
|
}
|
||||||
|
return manager.getMergedRoutes();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRoute>(
|
||||||
|
'createRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const id = await manager.createRoute(dataArg.route, userId, dataArg.enabled ?? true);
|
||||||
|
return { success: true, storedRouteId: id };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRoute>(
|
||||||
|
'updateRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.updateRoute(dataArg.id, {
|
||||||
|
route: dataArg.route as any,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRoute>(
|
||||||
|
'deleteRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.deleteRoute(dataArg.id);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set override on a hardcoded route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SetRouteOverride>(
|
||||||
|
'setRouteOverride',
|
||||||
|
async (dataArg) => {
|
||||||
|
const userId = await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
await manager.setOverride(dataArg.routeName, dataArg.enabled, userId);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove override from a hardcoded route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveRouteOverride>(
|
||||||
|
'removeRouteOverride',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.removeOverride(dataArg.routeName);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Override not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle programmatic route
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ToggleRoute>(
|
||||||
|
'toggleRoute',
|
||||||
|
async (dataArg) => {
|
||||||
|
await this.requireAuth(dataArg, 'routes:write');
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'Route management not initialized' };
|
||||||
|
}
|
||||||
|
const ok = await manager.toggleRoute(dataArg.id, dataArg.enabled);
|
||||||
|
return { success: ok, message: ok ? undefined : 'Route not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './auth.js';
|
export * from './auth.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
export * from './remoteingress.js';
|
export * from './remoteingress.js';
|
||||||
|
export * from './route-management.js';
|
||||||
83
ts_interfaces/data/route-management.ts
Normal file
83
ts_interfaces/data/route-management.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Data Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export type TApiTokenScope = 'routes:read' | 'routes:write' | 'config:read' | 'tokens:read' | 'tokens:manage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A merged route combining hardcoded and programmatic sources.
|
||||||
|
*/
|
||||||
|
export interface IMergedRoute {
|
||||||
|
route: IRouteConfig;
|
||||||
|
source: 'hardcoded' | 'programmatic';
|
||||||
|
enabled: boolean;
|
||||||
|
overridden: boolean;
|
||||||
|
storedRouteId?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A warning generated during route merge/startup.
|
||||||
|
*/
|
||||||
|
export interface IRouteWarning {
|
||||||
|
type: 'disabled-hardcoded' | 'disabled-programmatic' | 'orphaned-override';
|
||||||
|
routeName: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public info about an API token (never includes the hash).
|
||||||
|
*/
|
||||||
|
export interface IApiTokenInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number | null;
|
||||||
|
lastUsedAt: number | null;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Storage Schemas (persisted via StorageManager)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A programmatic route stored in /config-api/routes/{id}.json
|
||||||
|
*/
|
||||||
|
export interface IStoredRoute {
|
||||||
|
id: string;
|
||||||
|
route: IRouteConfig;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An override for a hardcoded route, stored in /config-api/overrides/{routeName}.json
|
||||||
|
*/
|
||||||
|
export interface IRouteOverride {
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
updatedAt: number;
|
||||||
|
updatedBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stored API token, stored in /config-api/tokens/{id}.json
|
||||||
|
*/
|
||||||
|
export interface IStoredApiToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tokenHash: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
createdAt: number;
|
||||||
|
expiresAt: number | null;
|
||||||
|
lastUsedAt: number | null;
|
||||||
|
createdBy: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
83
ts_interfaces/requests/api-tokens.ts
Normal file
83
ts_interfaces/requests/api-tokens.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type { IApiTokenInfo, TApiTokenScope } from '../data/route-management.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Token Management Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new API token. Returns the raw token value once (never shown again).
|
||||||
|
* Admin JWT only — tokens cannot create tokens.
|
||||||
|
*/
|
||||||
|
export interface IReq_CreateApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateApiToken
|
||||||
|
> {
|
||||||
|
method: 'createApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
name: string;
|
||||||
|
scopes: TApiTokenScope[];
|
||||||
|
expiresInDays?: number | null;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
tokenId?: string;
|
||||||
|
tokenValue?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all API tokens (without hashes).
|
||||||
|
*/
|
||||||
|
export interface IReq_ListApiTokens extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListApiTokens
|
||||||
|
> {
|
||||||
|
method: 'listApiTokens';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
tokens: IApiTokenInfo[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke (delete) an API token.
|
||||||
|
*/
|
||||||
|
export interface IReq_RevokeApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RevokeApiToken
|
||||||
|
> {
|
||||||
|
method: 'revokeApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable an API token.
|
||||||
|
*/
|
||||||
|
export interface IReq_ToggleApiToken extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ToggleApiToken
|
||||||
|
> {
|
||||||
|
method: 'toggleApiToken';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,78 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as authInterfaces from '../data/auth.js';
|
import * as authInterfaces from '../data/auth.js';
|
||||||
|
|
||||||
|
export interface IConfigData {
|
||||||
|
system: {
|
||||||
|
baseDir: string;
|
||||||
|
dataDir: string;
|
||||||
|
publicIp: string | null;
|
||||||
|
proxyIps: string[];
|
||||||
|
uptime: number;
|
||||||
|
storageBackend: 'filesystem' | 'custom' | 'memory';
|
||||||
|
storagePath: string | null;
|
||||||
|
};
|
||||||
|
smartProxy: {
|
||||||
|
enabled: boolean;
|
||||||
|
routeCount: number;
|
||||||
|
acme: {
|
||||||
|
enabled: boolean;
|
||||||
|
accountEmail: string;
|
||||||
|
useProduction: boolean;
|
||||||
|
autoRenew: boolean;
|
||||||
|
renewThresholdDays: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
email: {
|
||||||
|
enabled: boolean;
|
||||||
|
ports: number[];
|
||||||
|
portMapping: Record<string, number> | null;
|
||||||
|
hostname: string | null;
|
||||||
|
domains: string[];
|
||||||
|
emailRouteCount: number;
|
||||||
|
receivedEmailsPath: string | null;
|
||||||
|
};
|
||||||
|
dns: {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
nsDomains: string[];
|
||||||
|
scopes: string[];
|
||||||
|
recordCount: number;
|
||||||
|
records: Array<{ name: string; type: string; value: string; ttl?: number }>;
|
||||||
|
dnsChallenge: boolean;
|
||||||
|
};
|
||||||
|
tls: {
|
||||||
|
contactEmail: string | null;
|
||||||
|
domain: string | null;
|
||||||
|
source: 'acme' | 'static' | 'none';
|
||||||
|
certPath: string | null;
|
||||||
|
keyPath: string | null;
|
||||||
|
};
|
||||||
|
cache: {
|
||||||
|
enabled: boolean;
|
||||||
|
storagePath: string | null;
|
||||||
|
dbName: string | null;
|
||||||
|
defaultTTLDays: number;
|
||||||
|
cleanupIntervalHours: number;
|
||||||
|
ttlConfig: Record<string, number>;
|
||||||
|
};
|
||||||
|
radius: {
|
||||||
|
enabled: boolean;
|
||||||
|
authPort: number | null;
|
||||||
|
acctPort: number | null;
|
||||||
|
bindAddress: string | null;
|
||||||
|
clientCount: number;
|
||||||
|
vlanDefaultVlan: number | null;
|
||||||
|
vlanAllowUnknownMacs: boolean | null;
|
||||||
|
vlanMappingCount: number;
|
||||||
|
};
|
||||||
|
remoteIngress: {
|
||||||
|
enabled: boolean;
|
||||||
|
tunnelPort: number | null;
|
||||||
|
hubDomain: string | null;
|
||||||
|
tlsConfigured: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Get Configuration (read-only)
|
// Get Configuration (read-only)
|
||||||
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
@@ -12,7 +84,7 @@ export interface IReq_GetConfiguration extends plugins.typedrequestInterfaces.im
|
|||||||
section?: string;
|
section?: string;
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
config: any;
|
config: IConfigData;
|
||||||
section?: string;
|
section?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7,3 +7,5 @@ export * from './radius.js';
|
|||||||
export * from './email-ops.js';
|
export * from './email-ops.js';
|
||||||
export * from './certificate.js';
|
export * from './certificate.js';
|
||||||
export * from './remoteingress.js';
|
export * from './remoteingress.js';
|
||||||
|
export * from './route-management.js';
|
||||||
|
export * from './api-tokens.js';
|
||||||
146
ts_interfaces/requests/route-management.ts
Normal file
146
ts_interfaces/requests/route-management.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type { IMergedRoute, IRouteWarning } from '../data/route-management.js';
|
||||||
|
import type { IRouteConfig } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Endpoints
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all merged routes (hardcoded + programmatic) with warnings.
|
||||||
|
*/
|
||||||
|
export interface IReq_GetMergedRoutes extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetMergedRoutes
|
||||||
|
> {
|
||||||
|
method: 'getMergedRoutes';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
routes: IMergedRoute[];
|
||||||
|
warnings: IRouteWarning[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_CreateRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateRoute
|
||||||
|
> {
|
||||||
|
method: 'createRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
route: IRouteConfig;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
storedRouteId?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_UpdateRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateRoute
|
||||||
|
> {
|
||||||
|
method: 'updateRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
route?: Partial<IRouteConfig>;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a programmatic route.
|
||||||
|
*/
|
||||||
|
export interface IReq_DeleteRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteRoute
|
||||||
|
> {
|
||||||
|
method: 'deleteRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an override on a hardcoded route (disable/enable by name).
|
||||||
|
*/
|
||||||
|
export interface IReq_SetRouteOverride extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_SetRouteOverride
|
||||||
|
> {
|
||||||
|
method: 'setRouteOverride';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an override from a hardcoded route (restore default behavior).
|
||||||
|
*/
|
||||||
|
export interface IReq_RemoveRouteOverride extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RemoveRouteOverride
|
||||||
|
> {
|
||||||
|
method: 'removeRouteOverride';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
routeName: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a programmatic route on/off by id.
|
||||||
|
*/
|
||||||
|
export interface IReq_ToggleRoute extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ToggleRoute
|
||||||
|
> {
|
||||||
|
method: 'toggleRoute';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
apiToken?: string;
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '8.0.0',
|
version: '9.1.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface IStatsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigState {
|
export interface IConfigState {
|
||||||
config: any | null;
|
config: interfaces.requests.IConfigData | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
// Determine initial view from URL path
|
// Determine initial view from URL path
|
||||||
const getInitialView = (): string => {
|
const getInitialView = (): string => {
|
||||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'];
|
||||||
const segments = path.split('/').filter(Boolean);
|
const segments = path.split('/').filter(Boolean);
|
||||||
const view = segments[0];
|
const view = segments[0];
|
||||||
return validViews.includes(view) ? view : 'overview';
|
return validViews.includes(view) ? view : 'overview';
|
||||||
@@ -206,6 +206,32 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface IRouteManagementState {
|
||||||
|
mergedRoutes: interfaces.data.IMergedRoute[];
|
||||||
|
warnings: interfaces.data.IRouteWarning[];
|
||||||
|
apiTokens: interfaces.data.IApiTokenInfo[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeManagementStatePart = await appState.getStatePart<IRouteManagementState>(
|
||||||
|
'routeManagement',
|
||||||
|
{
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
|
'soft'
|
||||||
|
);
|
||||||
|
|
||||||
// Actions for state management
|
// Actions for state management
|
||||||
interface IActionContext {
|
interface IActionContext {
|
||||||
identity: interfaces.data.IIdentity | null;
|
identity: interfaces.data.IIdentity | null;
|
||||||
@@ -392,6 +418,20 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If switching to routes view, ensure we fetch route data
|
||||||
|
if (viewName === 'routes' && currentState.activeView !== 'routes') {
|
||||||
|
setTimeout(() => {
|
||||||
|
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If switching to apitokens view, ensure we fetch token data
|
||||||
|
if (viewName === 'apitokens' && currentState.activeView !== 'apitokens') {
|
||||||
|
setTimeout(() => {
|
||||||
|
routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// If switching to remoteingress view, ensure we fetch edge data
|
// If switching to remoteingress view, ensure we fetch edge data
|
||||||
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -862,6 +902,273 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Route Management Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetMergedRoutes
|
||||||
|
>('/typedrequest', 'getMergedRoutes');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
mergedRoutes: response.routes,
|
||||||
|
warnings: response.warnings,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch routes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRouteAction = routeManagementStatePart.createAction<{
|
||||||
|
route: any;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateRoute
|
||||||
|
>('/typedrequest', 'createRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
route: dataArg.route,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, routeId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_DeleteRoute
|
||||||
|
>('/typedrequest', 'deleteRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: routeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const toggleRouteAction = routeManagementStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ToggleRoute
|
||||||
|
>('/typedrequest', 'toggleRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
|
||||||
|
routeName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_SetRouteOverride
|
||||||
|
>('/typedrequest', 'setRouteOverride');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
routeName: dataArg.routeName,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to set override',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, routeName) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RemoveRouteOverride
|
||||||
|
>('/typedrequest', 'removeRouteOverride');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
routeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to remove override',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Token Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListApiTokens
|
||||||
|
>('/typedrequest', 'listApiTokens');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
apiTokens: response.tokens,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch tokens',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createApiToken(name: string, scopes: interfaces.data.TApiTokenScope[], expiresInDays?: number | null) {
|
||||||
|
const context = getActionContext();
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateApiToken
|
||||||
|
>('/typedrequest', 'createApiToken');
|
||||||
|
|
||||||
|
return request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
name,
|
||||||
|
scopes,
|
||||||
|
expiresInDays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
|
||||||
|
async (statePartArg, tokenId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RevokeApiToken
|
||||||
|
>('/typedrequest', 'revokeApiToken');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: tokenId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to revoke token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ToggleApiToken
|
||||||
|
>('/typedrequest', 'toggleApiToken');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to toggle token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TypedSocket Client for Real-time Log Streaming
|
// TypedSocket Client for Real-time Log Streaming
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export * from './ops-view-network.js';
|
|||||||
export * from './ops-view-emails.js';
|
export * from './ops-view-emails.js';
|
||||||
export * from './ops-view-logs.js';
|
export * from './ops-view-logs.js';
|
||||||
export * from './ops-view-config.js';
|
export * from './ops-view-config.js';
|
||||||
|
export * from './ops-view-routes.js';
|
||||||
|
export * from './ops-view-apitokens.js';
|
||||||
export * from './ops-view-security.js';
|
export * from './ops-view-security.js';
|
||||||
export * from './ops-view-certificates.js';
|
export * from './ops-view-certificates.js';
|
||||||
export * from './ops-view-remoteingress.js';
|
export * from './ops-view-remoteingress.js';
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { OpsViewNetwork } from './ops-view-network.js';
|
|||||||
import { OpsViewEmails } from './ops-view-emails.js';
|
import { OpsViewEmails } from './ops-view-emails.js';
|
||||||
import { OpsViewLogs } from './ops-view-logs.js';
|
import { OpsViewLogs } from './ops-view-logs.js';
|
||||||
import { OpsViewConfig } from './ops-view-config.js';
|
import { OpsViewConfig } from './ops-view-config.js';
|
||||||
|
import { OpsViewRoutes } from './ops-view-routes.js';
|
||||||
|
import { OpsViewApiTokens } from './ops-view-apitokens.js';
|
||||||
import { OpsViewSecurity } from './ops-view-security.js';
|
import { OpsViewSecurity } from './ops-view-security.js';
|
||||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||||
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
||||||
@@ -41,34 +43,52 @@ export class OpsDashboard extends DeesElement {
|
|||||||
private viewTabs = [
|
private viewTabs = [
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
|
iconName: 'lucide:layoutDashboard',
|
||||||
element: OpsViewOverview,
|
element: OpsViewOverview,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Configuration',
|
||||||
|
iconName: 'lucide:settings',
|
||||||
|
element: OpsViewConfig,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Network',
|
name: 'Network',
|
||||||
|
iconName: 'lucide:network',
|
||||||
element: OpsViewNetwork,
|
element: OpsViewNetwork,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Emails',
|
name: 'Emails',
|
||||||
|
iconName: 'lucide:mail',
|
||||||
element: OpsViewEmails,
|
element: OpsViewEmails,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
|
iconName: 'lucide:scrollText',
|
||||||
element: OpsViewLogs,
|
element: OpsViewLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Configuration',
|
name: 'Routes',
|
||||||
element: OpsViewConfig,
|
iconName: 'lucide:route',
|
||||||
|
element: OpsViewRoutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ApiTokens',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
element: OpsViewApiTokens,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Security',
|
name: 'Security',
|
||||||
|
iconName: 'lucide:shield',
|
||||||
element: OpsViewSecurity,
|
element: OpsViewSecurity,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Certificates',
|
name: 'Certificates',
|
||||||
|
iconName: 'lucide:badgeCheck',
|
||||||
element: OpsViewCertificates,
|
element: OpsViewCertificates,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RemoteIngress',
|
name: 'RemoteIngress',
|
||||||
|
iconName: 'lucide:globe',
|
||||||
element: OpsViewRemoteIngress,
|
element: OpsViewRemoteIngress,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
285
ts_web/elements/ops-view-apitokens.ts
Normal file
285
ts_web/elements/ops-view-apitokens.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
type TApiTokenScope = interfaces.data.TApiTokenScope;
|
||||||
|
|
||||||
|
@customElement('ops-view-apitokens')
|
||||||
|
export class OpsViewApiTokens extends DeesElement {
|
||||||
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const sub = appstate.routeManagementStatePart
|
||||||
|
.select((s) => s)
|
||||||
|
.subscribe((routeState) => {
|
||||||
|
this.routeState = routeState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
|
||||||
|
// Re-fetch tokens when user logs in (fixes race condition where
|
||||||
|
// the view is created before authentication completes)
|
||||||
|
const loginSub = appstate.loginStatePart
|
||||||
|
.select((s) => s.isLoggedIn)
|
||||||
|
.subscribe((isLoggedIn) => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(loginSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.apiTokensContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopePill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 130, 200, 0.1)', 'rgba(0, 170, 255, 0.1)')};
|
||||||
|
color: ${cssManager.bdTheme('#0369a1', '#0af')};
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.active {
|
||||||
|
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||||
|
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||||
|
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.expired {
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
|
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const { apiTokens } = this.routeState;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>API Tokens</ops-sectionheading>
|
||||||
|
|
||||||
|
<div class="apiTokensContainer">
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'API Tokens'}
|
||||||
|
.heading2=${'Manage programmatic access tokens'}
|
||||||
|
.data=${apiTokens}
|
||||||
|
.dataName=${'token'}
|
||||||
|
.searchable=${true}
|
||||||
|
.displayFunction=${(token: interfaces.data.IApiTokenInfo) => ({
|
||||||
|
name: token.name,
|
||||||
|
scopes: this.renderScopePills(token.scopes),
|
||||||
|
status: this.renderStatusBadge(token),
|
||||||
|
created: new Date(token.createdAt).toLocaleDateString(),
|
||||||
|
expires: token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : 'Never',
|
||||||
|
lastUsed: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : 'Never',
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Create Token',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
type: ['header'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await this.showCreateTokenDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Enable',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleApiTokenAction,
|
||||||
|
{ id: token.id, enabled: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disable',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleApiTokenAction,
|
||||||
|
{ id: token.id, enabled: false },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Revoke',
|
||||||
|
iconName: 'lucide:trash2',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.revokeApiTokenAction,
|
||||||
|
token.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderScopePills(scopes: TApiTokenScope[]): TemplateResult {
|
||||||
|
return html`<div style="display: flex; flex-wrap: wrap; gap: 2px;">${scopes.map(
|
||||||
|
(s) => html`<span class="scopePill">${s}</span>`,
|
||||||
|
)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStatusBadge(token: interfaces.data.IApiTokenInfo): TemplateResult {
|
||||||
|
if (!token.enabled) {
|
||||||
|
return html`<span class="statusBadge disabled">Disabled</span>`;
|
||||||
|
}
|
||||||
|
if (token.expiresAt && token.expiresAt < Date.now()) {
|
||||||
|
return html`<span class="statusBadge expired">Expired</span>`;
|
||||||
|
}
|
||||||
|
return html`<span class="statusBadge active">Active</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showCreateTokenDialog() {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
const allScopes: TApiTokenScope[] = [
|
||||||
|
'routes:read',
|
||||||
|
'routes:write',
|
||||||
|
'config:read',
|
||||||
|
'tokens:read',
|
||||||
|
'tokens:manage',
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Create API Token',
|
||||||
|
content: html`
|
||||||
|
<div style="color: #888; margin-bottom: 12px; font-size: 13px;">
|
||||||
|
The token value will be shown once after creation. Copy it immediately.
|
||||||
|
</div>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Token Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-tags
|
||||||
|
.key=${'scopes'}
|
||||||
|
.label=${'Token Scopes'}
|
||||||
|
.value=${['routes:read', 'routes:write']}
|
||||||
|
.suggestions=${allScopes}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-tags>
|
||||||
|
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const contentEl = modalArg.shadowRoot?.querySelector('.content');
|
||||||
|
const form = contentEl?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
if (!formData.name) return;
|
||||||
|
|
||||||
|
// dees-input-tags is not in dees-form's FORM_INPUT_TYPES, so collectFormData() won't
|
||||||
|
// include it. Query the tags input directly and call getValue().
|
||||||
|
const tagsInput = form.querySelector('dees-input-tags') as any;
|
||||||
|
const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
|
||||||
|
const scopes = rawScopes
|
||||||
|
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
|
||||||
|
|
||||||
|
const expiresInDays = formData.expiresInDays
|
||||||
|
? parseInt(formData.expiresInDays, 10)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
|
||||||
|
if (response.success && response.tokenValue) {
|
||||||
|
// Refresh the list first so it's ready when user dismisses the modal
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
|
||||||
|
// Show the token value in a new modal
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Token Created',
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Copy this token now. It will not be shown again.</p>
|
||||||
|
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
|
||||||
|
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Done',
|
||||||
|
iconName: 'lucide:check',
|
||||||
|
action: async (m: any) => await m.destroy(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create token:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstUpdated() {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from './shared/index.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
import { appRouter } from '../router.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -12,6 +13,8 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
|
||||||
|
|
||||||
@customElement('ops-view-config')
|
@customElement('ops-view-config')
|
||||||
export class OpsViewConfig extends DeesElement {
|
export class OpsViewConfig extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.configSection {
|
|
||||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionHeader {
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionTitle dees-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#888')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.sectionContent {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configField {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configField:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldLabel {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue.empty {
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestedFields {
|
|
||||||
margin-left: 16px;
|
|
||||||
padding-left: 16px;
|
|
||||||
border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status badge styles */
|
|
||||||
.statusBadge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge.enabled {
|
|
||||||
background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')};
|
|
||||||
color: ${cssManager.bdTheme('#155724', '#66cc66')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge.disabled {
|
|
||||||
background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')};
|
|
||||||
color: ${cssManager.bdTheme('#721c24', '#cc6666')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBadge dees-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Array/list display */
|
|
||||||
.arrayItems {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrayItem {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
|
||||||
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrayCount {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Numeric value formatting */
|
|
||||||
.numericValue {
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#0066cc', '#66aaff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
|
||||||
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingMessage {
|
.loadingMessage {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoNote {
|
.errorMessage {
|
||||||
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
|
||||||
border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')};
|
border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 24px;
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||||
color: ${cssManager.bdTheme('#004085', '#88ccff')};
|
margin: 16px 0;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoNote dees-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -202,185 +59,266 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Configuration</ops-sectionheading>
|
<ops-sectionheading>Configuration</ops-sectionheading>
|
||||||
|
|
||||||
${this.configState.isLoading ? html`
|
${this.configState.isLoading
|
||||||
|
? html`
|
||||||
<div class="loadingMessage">
|
<div class="loadingMessage">
|
||||||
<dees-spinner></dees-spinner>
|
<dees-spinner></dees-spinner>
|
||||||
<p>Loading configuration...</p>
|
<p>Loading configuration...</p>
|
||||||
</div>
|
</div>
|
||||||
` : this.configState.error ? html`
|
`
|
||||||
|
: this.configState.error
|
||||||
|
? html`
|
||||||
<div class="errorMessage">
|
<div class="errorMessage">
|
||||||
Error loading configuration: ${this.configState.error}
|
Error loading configuration: ${this.configState.error}
|
||||||
</div>
|
</div>
|
||||||
` : this.configState.config ? html`
|
`
|
||||||
<div class="infoNote">
|
: this.configState.config
|
||||||
<dees-icon icon="lucide:info"></dees-icon>
|
? this.renderConfig()
|
||||||
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
|
: html`<div class="errorMessage">No configuration loaded</div>`}
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)}
|
|
||||||
${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)}
|
|
||||||
${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)}
|
|
||||||
${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)}
|
|
||||||
` : html`
|
|
||||||
<div class="errorMessage">No configuration loaded</div>
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderConfigSection(key: string, title: string, icon: string, config: any) {
|
private renderConfig(): TemplateResult {
|
||||||
const isEnabled = config?.enabled ?? false;
|
const cfg = this.configState.config!;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configSection">
|
<sz-config-overview
|
||||||
<div class="sectionHeader">
|
infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
|
||||||
<h3 class="sectionTitle">
|
@navigate=${(e: CustomEvent) => {
|
||||||
<dees-icon icon="${icon}"></dees-icon>
|
if (e.detail?.view) {
|
||||||
${title}
|
appRouter.navigateToView(e.detail.view);
|
||||||
</h3>
|
}
|
||||||
${this.renderStatusBadge(isEnabled)}
|
}}
|
||||||
</div>
|
>
|
||||||
<div class="sectionContent">
|
${this.renderSystemSection(cfg.system)}
|
||||||
${config ? this.renderConfigFields(config) : html`
|
${this.renderSmartProxySection(cfg.smartProxy)}
|
||||||
<div class="fieldValue empty">Not configured</div>
|
${this.renderEmailSection(cfg.email)}
|
||||||
`}
|
${this.renderDnsSection(cfg.dns)}
|
||||||
</div>
|
${this.renderTlsSection(cfg.tls)}
|
||||||
</div>
|
${this.renderCacheSection(cfg.cache)}
|
||||||
|
${this.renderRadiusSection(cfg.radius)}
|
||||||
|
${this.renderRemoteIngressSection(cfg.remoteIngress)}
|
||||||
|
</sz-config-overview>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatusBadge(enabled: boolean): TemplateResult {
|
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
|
||||||
return enabled
|
const fields: IConfigField[] = [
|
||||||
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
|
{ key: 'Base Directory', value: sys.baseDir },
|
||||||
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
|
{ key: 'Data Directory', value: sys.dataDir },
|
||||||
}
|
{ key: 'Public IP', value: sys.publicIp },
|
||||||
|
{ key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' },
|
||||||
|
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
|
||||||
|
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
|
||||||
|
{ key: 'Storage Path', value: sys.storagePath },
|
||||||
|
];
|
||||||
|
|
||||||
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
|
|
||||||
if (!config || typeof config !== 'object') {
|
|
||||||
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(config).map(([key, value]) => {
|
|
||||||
const fieldName = prefix ? `${prefix}.${key}` : key;
|
|
||||||
const displayName = this.formatFieldName(key);
|
|
||||||
|
|
||||||
// Handle boolean values with badges
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="System"
|
||||||
${this.renderStatusBadge(value)}
|
subtitle="Base paths and infrastructure"
|
||||||
</div>
|
icon="lucide:server"
|
||||||
|
status="enabled"
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle arrays
|
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
|
||||||
if (Array.isArray(value)) {
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Route Count', value: proxy.routeCount },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (proxy.acme) {
|
||||||
|
fields.push(
|
||||||
|
{ key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
|
||||||
|
{ key: 'Account Email', value: proxy.acme.accountEmail || null },
|
||||||
|
{ key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
|
||||||
|
{ key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
|
||||||
|
{ key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="SmartProxy"
|
||||||
${this.renderArrayValue(value, key)}
|
subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
|
||||||
</div>
|
icon="lucide:network"
|
||||||
|
.status=${proxy.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle nested objects
|
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
|
||||||
if (typeof value === 'object' && value !== null) {
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
|
||||||
|
{ key: 'Hostname', value: email.hostname },
|
||||||
|
{ key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
|
||||||
|
{ key: 'Email Routes', value: email.emailRouteCount },
|
||||||
|
{ key: 'Received Emails Path', value: email.receivedEmailsPath },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (email.portMapping) {
|
||||||
|
const mappingStr = Object.entries(email.portMapping)
|
||||||
|
.map(([ext, int]) => `${ext} → ${int}`)
|
||||||
|
.join(', ');
|
||||||
|
fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="Email Server"
|
||||||
<div class="nestedFields">
|
subtitle="SMTP email handling with smartmta"
|
||||||
${this.renderConfigFields(value, fieldName)}
|
icon="lucide:mail"
|
||||||
</div>
|
.status=${email.enabled ? 'enabled' : 'disabled'}
|
||||||
</div>
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle primitive values
|
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
|
||||||
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Port', value: dns.port },
|
||||||
|
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
|
||||||
|
{ key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
|
||||||
|
{ key: 'Record Count', value: dns.recordCount },
|
||||||
|
{ key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="configField">
|
<sz-config-section
|
||||||
<label class="fieldLabel">${displayName}</label>
|
title="DNS Server"
|
||||||
<div class="fieldValue">${this.formatValue(value, key)}</div>
|
subtitle="Authoritative DNS with smartdns"
|
||||||
</div>
|
icon="lucide:globe"
|
||||||
|
.status=${dns.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
|
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
|
||||||
if (arr.length === 0) {
|
const fields: IConfigField[] = [
|
||||||
return html`<div class="fieldValue empty">None configured</div>`;
|
{ key: 'Contact Email', value: tls.contactEmail },
|
||||||
}
|
{ key: 'Domain', value: tls.domain },
|
||||||
|
{ key: 'Source', value: tls.source, type: 'badge' },
|
||||||
|
{ key: 'Certificate Path', value: tls.certPath },
|
||||||
|
{ key: 'Key Path', value: tls.keyPath },
|
||||||
|
];
|
||||||
|
|
||||||
// Determine if we should show as pills/tags
|
const status = tls.source === 'none' ? 'not-configured' : 'enabled';
|
||||||
const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number');
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
|
||||||
|
];
|
||||||
|
|
||||||
if (showAsPills) {
|
|
||||||
const itemLabel = this.getArrayItemLabel(fieldKey, arr.length);
|
|
||||||
return html`
|
return html`
|
||||||
<div class="arrayCount">${arr.length} ${itemLabel}</div>
|
<sz-config-section
|
||||||
<div class="arrayItems">
|
title="TLS / Certificates"
|
||||||
${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
|
subtitle="Certificate management and ACME"
|
||||||
</div>
|
icon="lucide:shield-check"
|
||||||
|
.status=${status as any}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For complex arrays, show as JSON
|
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
|
||||||
|
const fields: IConfigField[] = [
|
||||||
|
{ key: 'Storage Path', value: cache.storagePath },
|
||||||
|
{ key: 'DB Name', value: cache.dbName },
|
||||||
|
{ key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
|
||||||
|
{ key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
|
||||||
|
for (const [key, val] of Object.entries(cache.ttlConfig)) {
|
||||||
|
fields.push({ key: `TTL: ${key}`, value: `${val} days` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="fieldValue">
|
<sz-config-section
|
||||||
${arr.length} items configured
|
title="Cache Database"
|
||||||
</div>
|
subtitle="Persistent caching with smartdata"
|
||||||
|
icon="lucide:database"
|
||||||
|
.status=${cache.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getArrayItemLabel(fieldKey: string, count: number): string {
|
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
|
||||||
const labels: Record<string, [string, string]> = {
|
const fields: IConfigField[] = [
|
||||||
ports: ['port', 'ports'],
|
{ key: 'Auth Port', value: radius.authPort },
|
||||||
domains: ['domain', 'domains'],
|
{ key: 'Accounting Port', value: radius.acctPort },
|
||||||
nameservers: ['nameserver', 'nameservers'],
|
{ key: 'Bind Address', value: radius.bindAddress },
|
||||||
blockList: ['IP', 'IPs'],
|
{ key: 'Client Count', value: radius.clientCount },
|
||||||
};
|
];
|
||||||
|
|
||||||
const label = labels[fieldKey] || ['item', 'items'];
|
if (radius.vlanDefaultVlan !== null) {
|
||||||
return count === 1 ? label[0] : label[1];
|
fields.push(
|
||||||
|
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
|
||||||
|
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
|
||||||
|
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatFieldName(key: string): string {
|
const status = radius.enabled ? 'enabled' : 'not-configured';
|
||||||
// Convert camelCase to readable format
|
|
||||||
return key
|
return html`
|
||||||
.replace(/([A-Z])/g, ' $1')
|
<sz-config-section
|
||||||
.replace(/^./, str => str.toUpperCase())
|
title="RADIUS Server"
|
||||||
.trim();
|
subtitle="Network authentication and VLAN assignment"
|
||||||
|
icon="lucide:wifi"
|
||||||
|
.status=${status as any}
|
||||||
|
.fields=${fields}
|
||||||
|
></sz-config-section>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatValue(value: any, fieldKey?: string): string | TemplateResult {
|
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
|
||||||
if (value === null || value === undefined) {
|
const fields: IConfigField[] = [
|
||||||
return html`<span class="empty">Not set</span>`;
|
{ key: 'Tunnel Port', value: ri.tunnelPort },
|
||||||
|
{ key: 'Hub Domain', value: ri.hubDomain },
|
||||||
|
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const actions: IConfigSectionAction[] = [
|
||||||
|
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<sz-config-section
|
||||||
|
title="Remote Ingress"
|
||||||
|
subtitle="Edge tunnel nodes"
|
||||||
|
icon="lucide:cloud"
|
||||||
|
.status=${ri.enabled ? 'enabled' : 'disabled'}
|
||||||
|
.fields=${fields}
|
||||||
|
.actions=${actions}
|
||||||
|
></sz-config-section>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'number') {
|
private formatUptime(seconds: number): string {
|
||||||
// Format bytes
|
const days = Math.floor(seconds / 86400);
|
||||||
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) {
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
}
|
|
||||||
// Format time values
|
|
||||||
if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
|
|
||||||
return html`<span class="numericValue">${value} seconds</span>`;
|
|
||||||
}
|
|
||||||
// Format port numbers
|
|
||||||
if (fieldKey?.toLowerCase().includes('port')) {
|
|
||||||
return html`<span class="numericValue">${value}</span>`;
|
|
||||||
}
|
|
||||||
// Format counts with separators
|
|
||||||
return html`<span class="numericValue">${value.toLocaleString()}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value);
|
const parts: string[] = [];
|
||||||
}
|
if (days > 0) parts.push(`${days}d`);
|
||||||
|
if (hours > 0) parts.push(`${hours}h`);
|
||||||
private formatBytes(bytes: number): string {
|
parts.push(`${mins}m`);
|
||||||
if (bytes === 0) return '0 B';
|
return parts.join(' ');
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
389
ts_web/elements/ops-view-routes.ts
Normal file
389
ts_web/elements/ops-view-routes.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
type TemplateResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
@customElement('ops-view-routes')
|
||||||
|
export class OpsViewRoutes extends DeesElement {
|
||||||
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
||||||
|
mergedRoutes: [],
|
||||||
|
warnings: [],
|
||||||
|
apiTokens: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const sub = appstate.routeManagementStatePart
|
||||||
|
.select((s) => s)
|
||||||
|
.subscribe((routeState) => {
|
||||||
|
this.routeState = routeState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
|
||||||
|
// Re-fetch routes when user logs in (fixes race condition where
|
||||||
|
// the view is created before authentication completes)
|
||||||
|
const loginSub = appstate.loginStatePart
|
||||||
|
.select((s) => s.isLoggedIn)
|
||||||
|
.subscribe((isLoggedIn) => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(loginSub);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.routesContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnings-bar {
|
||||||
|
background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#b45309', '#fa0')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: ${cssManager.bdTheme('#6b7280', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const { mergedRoutes, warnings } = this.routeState;
|
||||||
|
|
||||||
|
const hardcodedCount = mergedRoutes.filter((mr) => mr.source === 'hardcoded').length;
|
||||||
|
const programmaticCount = mergedRoutes.filter((mr) => mr.source === 'programmatic').length;
|
||||||
|
const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
|
||||||
|
|
||||||
|
const statsTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'totalRoutes',
|
||||||
|
title: 'Total Routes',
|
||||||
|
type: 'number',
|
||||||
|
value: mergedRoutes.length,
|
||||||
|
icon: 'lucide:route',
|
||||||
|
description: 'All configured routes',
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hardcoded',
|
||||||
|
title: 'Hardcoded',
|
||||||
|
type: 'number',
|
||||||
|
value: hardcodedCount,
|
||||||
|
icon: 'lucide:lock',
|
||||||
|
description: 'Routes from constructor config',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'programmatic',
|
||||||
|
title: 'Programmatic',
|
||||||
|
type: 'number',
|
||||||
|
value: programmaticCount,
|
||||||
|
icon: 'lucide:code',
|
||||||
|
description: 'Routes added via API',
|
||||||
|
color: '#0ea5e9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
title: 'Disabled',
|
||||||
|
type: 'number',
|
||||||
|
value: disabledCount,
|
||||||
|
icon: 'lucide:pauseCircle',
|
||||||
|
description: 'Currently disabled routes',
|
||||||
|
color: disabledCount > 0 ? '#ef4444' : '#6b7280',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map merged routes to sz-route-list-view format
|
||||||
|
const szRoutes = mergedRoutes.map((mr) => {
|
||||||
|
const tags = [...(mr.route.tags || [])];
|
||||||
|
tags.push(mr.source);
|
||||||
|
if (!mr.enabled) tags.push('disabled');
|
||||||
|
if (mr.overridden) tags.push('overridden');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mr.route,
|
||||||
|
enabled: mr.enabled,
|
||||||
|
tags,
|
||||||
|
id: mr.storedRouteId || mr.route.name || undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Route Management</ops-sectionheading>
|
||||||
|
|
||||||
|
<div class="routesContainer">
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${statsTiles}
|
||||||
|
.gridActions=${[
|
||||||
|
{
|
||||||
|
name: 'Add Route',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
action: () => this.showCreateRouteDialog(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Refresh',
|
||||||
|
iconName: 'lucide:refreshCw',
|
||||||
|
action: () => this.refreshData(),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
${warnings.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="warnings-bar">
|
||||||
|
${warnings.map(
|
||||||
|
(w) => html`
|
||||||
|
<div class="warning-item">
|
||||||
|
<span class="warning-icon">⚠</span>
|
||||||
|
<span>${w.message}</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
${szRoutes.length > 0
|
||||||
|
? html`
|
||||||
|
<sz-route-list-view
|
||||||
|
.routes=${szRoutes}
|
||||||
|
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||||
|
></sz-route-list-view>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No routes configured</p>
|
||||||
|
<p>Add a programmatic route or check your constructor configuration.</p>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRouteClick(e: CustomEvent) {
|
||||||
|
const clickedRoute = e.detail;
|
||||||
|
if (!clickedRoute) return;
|
||||||
|
|
||||||
|
// Find the corresponding merged route
|
||||||
|
const merged = this.routeState.mergedRoutes.find(
|
||||||
|
(mr) => mr.route.name === clickedRoute.name,
|
||||||
|
);
|
||||||
|
if (!merged) return;
|
||||||
|
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
if (merged.source === 'hardcoded') {
|
||||||
|
const menuOptions = merged.enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: 'Disable Route',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.setRouteOverrideAction,
|
||||||
|
{ routeName: merged.route.name!, enabled: false },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
name: 'Enable Route',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.setRouteOverrideAction,
|
||||||
|
{ routeName: merged.route.name!, enabled: true },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Remove Override',
|
||||||
|
iconName: 'lucide:undo',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.removeRouteOverrideAction,
|
||||||
|
merged.route.name!,
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Route: ${merged.route.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Source: <strong style="color: #88f;">hardcoded</strong></p>
|
||||||
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled (overridden)'}</strong></p>
|
||||||
|
<p style="color: #888; font-size: 13px;">Hardcoded routes cannot be edited or deleted, but they can be disabled via an override.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Programmatic route
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Route: ${merged.route.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
|
||||||
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
||||||
|
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: merged.enabled ? 'Disable' : 'Enable',
|
||||||
|
iconName: merged.enabled ? 'lucide:pause' : 'lucide:play',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.toggleRouteAction,
|
||||||
|
{ id: merged.storedRouteId!, enabled: !merged.enabled },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.deleteRouteAction,
|
||||||
|
merged.storedRouteId!,
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Close',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showCreateRouteDialog() {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Add Programmatic Route',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
if (!formData.name || !formData.ports) return;
|
||||||
|
|
||||||
|
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||||
|
const domains = formData.domains
|
||||||
|
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const route: any = {
|
||||||
|
name: formData.name,
|
||||||
|
match: {
|
||||||
|
ports,
|
||||||
|
...(domains && domains.length > 0 ? { domains } : {}),
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
host: formData.targetHost || 'localhost',
|
||||||
|
port: parseInt(formData.targetPort, 10),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.createRouteAction,
|
||||||
|
{ route },
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshData() {
|
||||||
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async firstUpdated() {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
|
|||||||
|
|
||||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
||||||
|
|
||||||
export type TValidView = typeof validViews[number];
|
export type TValidView = typeof validViews[number];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user