Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de3344905 | |||
| ae34314f54 | |||
| 5b473de354 | |||
| 1a108fa8b7 |
BIN
.playwright-mcp/dcrouter-scrollbar-issue.png
Normal file
BIN
.playwright-mcp/dcrouter-scrollbar-issue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-10-23-737Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-10-23-737Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-11-19-449Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-11-19-449Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-12-03-126Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-12-03-126Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
.playwright-mcp/page-2026-02-01T23-12-15-576Z.png
Normal file
BIN
.playwright-mcp/page-2026-02-01T23-12-15-576Z.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-02 - 3.1.0 - feat(web)
|
||||
determine initial UI view from URL and wire selected view to appdash; add interface and web README files; bump various dependencies
|
||||
|
||||
- UI: derive initial active view from window.location.pathname so the dashboard supports deep linking and bookmarks (ts_web/appstate.ts)
|
||||
- UI: pass selectedView to dees-simple-appdash by adding a currentViewTab getter in ops-dashboard (ts_web/elements/ops-dashboard.ts)
|
||||
- Docs: add TypeScript interfaces README for @serve.zone/dcrouter-interfaces (ts_interfaces/readme.md)
|
||||
- Docs: add/update web module README detailing features, routing, and build instructions (ts_web/readme.md) and expand main project README
|
||||
- Deps: bump multiple dependencies in package.json (notable bumps: @api.global/typedrequest -> ^3.2.5, @design.estate/dees-catalog -> ^3.42.0, @design.estate/dees-element -> ^2.1.6, @push.rocks/projectinfo -> ^5.0.2, @push.rocks/smartdata -> ^5.16.7, @push.rocks/smartpromise -> ^4.2.3, @push.rocks/smartradius -> ^1.1.0, @push.rocks/smartstate -> ^2.0.30, mailauth -> ^4.12.1)
|
||||
|
||||
## 2026-02-01 - 3.0.0 - BREAKING CHANGE(deps)
|
||||
upgrade major dependencies, migrate action.target to action.targets (array), adapt to SmartRequest API changes, and add RADIUS server support
|
||||
|
||||
- Bumped many major dependencies: @api.global/typedserver 3.x → 8.3.0, @api.global/typedsocket 3.x → 4.1.0, @apiclient.xyz/cloudflare 6.x → 7.1.0, @design.estate/dees-catalog 1.x → 3.41.4, @push.rocks/smartpath 5.x → 6.x, @push.rocks/smartproxy 19.x → 22.x, @push.rocks/smartrequest 2.x → 5.x, uuid 11.x → 13.x, @types/node 25.1.0 → 25.2.0
|
||||
|
||||
## 2026-02-01 - 2.13.0 - feat(radius)
|
||||
add RADIUS server with MAC authentication (MAB), VLAN assignment, accounting and OpsServer API handlers
|
||||
|
||||
|
||||
36
package.json
36
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "2.13.0",
|
||||
"version": "3.1.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@@ -22,21 +22,21 @@
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.0.1",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/node": "^25.2.0",
|
||||
"node-forge": "^1.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^3.0.19",
|
||||
"@api.global/typedrequest": "^3.2.5",
|
||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||
"@design.estate/dees-catalog": "^1.10.10",
|
||||
"@design.estate/dees-element": "^2.0.45",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@api.global/typedserver": "^8.3.0",
|
||||
"@api.global/typedsocket": "^4.1.0",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.42.0",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartacme": "^8.0.0",
|
||||
"@push.rocks/smartdata": "^5.15.1",
|
||||
"@push.rocks/smartdata": "^5.16.7",
|
||||
"@push.rocks/smartdns": "^7.6.1",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartguard": "^3.1.0",
|
||||
@@ -45,23 +45,23 @@
|
||||
"@push.rocks/smartmail": "^2.2.0",
|
||||
"@push.rocks/smartmetrics": "^2.0.10",
|
||||
"@push.rocks/smartnetwork": "^4.4.0",
|
||||
"@push.rocks/smartpath": "^5.0.5",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartproxy": "^19.6.15",
|
||||
"@push.rocks/smartradius": "^1.0.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^22.4.2",
|
||||
"@push.rocks/smartradius": "^1.1.0",
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"@push.rocks/smartrule": "^2.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.0.27",
|
||||
"@push.rocks/smartstate": "^2.0.30",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"ip": "^2.0.1",
|
||||
"lru-cache": "^11.2.5",
|
||||
"mailauth": "^4.12.0",
|
||||
"mailauth": "^4.12.1",
|
||||
"mailparser": "^3.9.3",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"mail service",
|
||||
|
||||
299
pnpm-lock.yaml
generated
299
pnpm-lock.yaml
generated
@@ -9,37 +9,37 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@api.global/typedrequest':
|
||||
specifier: ^3.0.19
|
||||
specifier: ^3.2.5
|
||||
version: 3.2.5
|
||||
'@api.global/typedrequest-interfaces':
|
||||
specifier: ^3.0.19
|
||||
version: 3.0.19
|
||||
'@api.global/typedserver':
|
||||
specifier: ^3.0.74
|
||||
version: 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(@tiptap/pm@2.27.2)
|
||||
'@api.global/typedsocket':
|
||||
specifier: ^3.0.0
|
||||
version: 3.1.1(@push.rocks/smartserve@2.0.1)
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@apiclient.xyz/cloudflare':
|
||||
specifier: ^6.4.1
|
||||
version: 6.4.3
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^1.10.10
|
||||
version: 1.12.4(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.42.0
|
||||
version: 3.42.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.0.45
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6
|
||||
'@push.rocks/projectinfo':
|
||||
specifier: ^5.0.1
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@push.rocks/qenv':
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
version: 8.0.0(socks@2.8.7)
|
||||
'@push.rocks/smartdata':
|
||||
specifier: ^5.15.1
|
||||
specifier: ^5.16.7
|
||||
version: 5.16.7(socks@2.8.7)
|
||||
'@push.rocks/smartdns':
|
||||
specifier: ^7.6.1
|
||||
@@ -66,20 +66,20 @@ importers:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
'@push.rocks/smartpath':
|
||||
specifier: ^5.0.5
|
||||
version: 5.1.0
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@push.rocks/smartpromise':
|
||||
specifier: ^4.0.3
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^19.6.15
|
||||
version: 19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
specifier: ^22.4.2
|
||||
version: 22.4.2(socks@2.8.7)
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.0.3
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
'@push.rocks/smartrequest':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
'@push.rocks/smartrule':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
@@ -87,8 +87,8 @@ importers:
|
||||
specifier: ^3.0.10
|
||||
version: 3.0.10
|
||||
'@push.rocks/smartstate':
|
||||
specifier: ^2.0.27
|
||||
version: 2.0.27
|
||||
specifier: ^2.0.30
|
||||
version: 2.0.30
|
||||
'@push.rocks/smartunique':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
@@ -108,14 +108,14 @@ importers:
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
mailauth:
|
||||
specifier: ^4.12.0
|
||||
version: 4.12.0
|
||||
specifier: ^4.12.1
|
||||
version: 4.12.1
|
||||
mailparser:
|
||||
specifier: ^3.9.3
|
||||
version: 3.9.3
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^4.1.2
|
||||
@@ -133,8 +133,8 @@ importers:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@tiptap/pm@2.27.2)
|
||||
'@types/node':
|
||||
specifier: ^25.1.0
|
||||
version: 25.1.0
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0
|
||||
node-forge:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
@@ -172,6 +172,9 @@ packages:
|
||||
'@apiclient.xyz/cloudflare@6.4.3':
|
||||
resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
|
||||
|
||||
'@apiclient.xyz/cloudflare@7.1.0':
|
||||
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -359,11 +362,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@1.12.4':
|
||||
resolution: {integrity: sha512-tzNW3b1BQkbE7W2DcwFqXHy+igHzxMHoR+awCAE4/EzHl8kOJc4jF1RNyCe34LsuNaELgZ95Hde/HGgEC8JasA==}
|
||||
|
||||
'@design.estate/dees-catalog@3.41.4':
|
||||
resolution: {integrity: sha512-tVut61OuMF+o/1dzcKEvfom6sl8dMC5WzxIevN9jVq4sQgMO9DOrFd+UfKwRL9FfLZeqAFyxJDzU8389e4Pg0Q==}
|
||||
'@design.estate/dees-catalog@3.42.0':
|
||||
resolution: {integrity: sha512-pArkafnrhRsHsSxKUMUM2YP5ei/AbcchPEKZY2PyHHAdXcNxyT3pE2Oh1FPcs1pqF2LpEgJRq8KFQbFhvhp8Nw==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -374,9 +374,6 @@ packages:
|
||||
'@design.estate/dees-element@2.1.6':
|
||||
resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==}
|
||||
|
||||
'@design.estate/dees-wcctools@1.3.0':
|
||||
resolution: {integrity: sha512-+yd8c1gTIKNRQYCvG0xu6Am8dHsRm7ymluX2gnoBQN4aFOpZgIBi/v9CvGyPhTD1p/VRouIBz1wsUCejnwrFCA==}
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
||||
|
||||
@@ -994,6 +991,9 @@ packages:
|
||||
'@push.rocks/smartjson@5.2.0':
|
||||
resolution: {integrity: sha512-710e8UwovRfPgUtaBHcd6unaODUjV5fjxtGcGCqtaTcmvOV6VpasdVfT66xMDzQmWH2E9ZfHDJeso9HdDQzNQA==}
|
||||
|
||||
'@push.rocks/smartjson@6.0.0':
|
||||
resolution: {integrity: sha512-FYfJnmukt66WePn6xrVZ3BLmRQl9W82LcsICK3VU9sGW7kasig090jKXPm+yX8ibQcZAO/KyR/Q8tMIYZNxGew==}
|
||||
|
||||
'@push.rocks/smartjwt@2.2.1':
|
||||
resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==}
|
||||
|
||||
@@ -1066,8 +1066,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@19.6.17':
|
||||
resolution: {integrity: sha512-5y6lVxlHXoVQXAQLr5S+2ifxZf9EID32twyeuZTS9tDyof0wJKppLzKQepwB7hfQXS2J06JBN7oa9n0mguELBg==}
|
||||
'@push.rocks/smartproxy@22.4.2':
|
||||
resolution: {integrity: sha512-JdDa1VxGOnWfF5HuJRvkX3/zHuIKz+IV9n/XOsNZQA9zMZdLVlWPqjGio9GLWsPOWA2l1YZKymjMH4ybPbGQtA==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@@ -1111,8 +1111,8 @@ packages:
|
||||
'@push.rocks/smartspawn@3.0.3':
|
||||
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
|
||||
|
||||
'@push.rocks/smartstate@2.0.27':
|
||||
resolution: {integrity: sha512-q4UKir7GV3hakJWXQR4DoA4tUVwT5GRkJ/MtanHYF0wZLHfS19+nGmyO9y974zk3eT9hmy3+Lq5cKtU2W6+Y3w==}
|
||||
'@push.rocks/smartstate@2.0.30':
|
||||
resolution: {integrity: sha512-IuNW8XtSumXIr7g7MIFyWg5PBwLF2mwsymTJbSEycK2Pa9ZLk4yjRHnR907xCilxgiMU9ixQZyNdpa5MMF999A==}
|
||||
|
||||
'@push.rocks/smartstream@2.0.8':
|
||||
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
|
||||
@@ -1876,6 +1876,10 @@ packages:
|
||||
'@types/minimatch@5.1.2':
|
||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||
|
||||
'@types/minimatch@6.0.0':
|
||||
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
||||
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
@@ -1894,8 +1898,8 @@ packages:
|
||||
'@types/node@22.19.7':
|
||||
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
|
||||
|
||||
'@types/node@25.1.0':
|
||||
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||
'@types/node@25.2.0':
|
||||
resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
|
||||
|
||||
'@types/pidusage@2.0.5':
|
||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||
@@ -1969,9 +1973,6 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@webcontainer/api@1.2.0':
|
||||
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3':
|
||||
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||
|
||||
@@ -2572,10 +2573,6 @@ packages:
|
||||
fast-json-stable-stringify@2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
|
||||
fast-xml-parser@4.5.2:
|
||||
resolution: {integrity: sha512-xmnYV9o0StIz/0ArdzmWTxn9oDy0lH8Z80/8X/TD2EUQKXY4DHxoT9mYBqgGIG17DgddCJtH1M6DriMbalNsAA==}
|
||||
hasBin: true
|
||||
|
||||
fast-xml-parser@4.5.3:
|
||||
resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==}
|
||||
hasBin: true
|
||||
@@ -3097,14 +3094,11 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lucide@0.544.0:
|
||||
resolution: {integrity: sha512-U5ORwr5z9Sx7bNTDFaW55RbjVdQEnAcT3vws9uz3vRT1G4XXJUDAhRZdxhFoIyHEvjmTkzzlEhjSLYM5n4mb5w==}
|
||||
|
||||
lucide@0.563.0:
|
||||
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==}
|
||||
|
||||
mailauth@4.12.0:
|
||||
resolution: {integrity: sha512-2fMtvJBbXV32NlD6f6BSRTLNPsC1UgA+oj4mtAIUzzZlTQF2oNUgWmRQFKf1Ui9ad6KcadF9oqYrl7sCTHBNag==}
|
||||
mailauth@4.12.1:
|
||||
resolution: {integrity: sha512-mSbMST+YUKj4WAfVvVszw/lnqxYA9AsYX6jYdl9vQdgz1vP5gIMwK6/RcqY+CkMkfkhFzea5+72asj620eAHmQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3343,9 +3337,6 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||
|
||||
@@ -3438,10 +3429,6 @@ packages:
|
||||
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
|
||||
nodemailer@7.0.11:
|
||||
resolution: {integrity: sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nodemailer@7.0.13:
|
||||
resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -4064,8 +4051,8 @@ packages:
|
||||
tldts-core@7.0.21:
|
||||
resolution: {integrity: sha512-oVOMdHvgjqyzUZH1rOESgJP1uNe2bVrfK0jUHHmiM2rpEiRbf3j4BrsIc6JigJRbHGanQwuZv/R+LTcHsw+bLA==}
|
||||
|
||||
tldts@7.0.19:
|
||||
resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==}
|
||||
tldts@7.0.21:
|
||||
resolution: {integrity: sha512-Plu6V8fF/XU6d2k8jPtlQf5F4Xx2hAin4r2C2ca7wR8NK5MbRTo9huLUWRe28f3Uk8bYZfg74tit/dSjc18xnw==}
|
||||
hasBin: true
|
||||
|
||||
tmp@0.0.33:
|
||||
@@ -4163,8 +4150,8 @@ packages:
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
undici@7.16.0:
|
||||
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
|
||||
undici@7.19.2:
|
||||
resolution: {integrity: sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unified@11.0.5:
|
||||
@@ -4206,8 +4193,8 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
@@ -4438,7 +4425,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@cloudflare/workers-types': 4.20260131.0
|
||||
'@design.estate/dees-catalog': 3.41.4(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.42.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4485,7 +4472,7 @@ snapshots:
|
||||
'@push.rocks/isohash': 2.0.1
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartsocket': 2.1.0(@push.rocks/smartserve@2.0.1)
|
||||
'@push.rocks/smartsocket': 2.1.0
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
optionalDependencies:
|
||||
@@ -4523,6 +4510,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@apiclient.xyz/cloudflare@7.1.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 5.0.1
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
cloudflare: 5.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@aws-crypto/crc32@5.2.0':
|
||||
dependencies:
|
||||
'@aws-crypto/util': 5.2.0
|
||||
@@ -5028,43 +5027,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@1.12.4(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
'@design.estate/dees-element': 2.1.6
|
||||
'@design.estate/dees-wcctools': 1.3.0
|
||||
'@fortawesome/fontawesome-svg-core': 7.1.0
|
||||
'@fortawesome/free-brands-svg-icons': 7.1.0
|
||||
'@fortawesome/free-regular-svg-icons': 7.1.0
|
||||
'@fortawesome/free-solid-svg-icons': 7.1.0
|
||||
'@push.rocks/smarti18n': 1.0.4
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
|
||||
'@tiptap/extension-link': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
|
||||
'@tiptap/extension-text-align': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
'@tiptap/extension-typography': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
|
||||
'@tiptap/starter-kit': 2.27.2
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
'@webcontainer/api': 1.2.0
|
||||
apexcharts: 5.3.6
|
||||
highlight.js: 11.11.1
|
||||
ibantools: 4.5.1
|
||||
lit: 3.3.2
|
||||
lucide: 0.544.0
|
||||
monaco-editor: 0.52.2
|
||||
pdfjs-dist: 4.10.38
|
||||
xterm: 5.3.0
|
||||
xterm-addon-fit: 0.8.0(xterm@5.3.0)
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- '@tiptap/pm'
|
||||
- react
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-catalog@3.41.4(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.42.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
'@design.estate/dees-element': 2.1.6
|
||||
@@ -5117,7 +5080,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrouter': 1.3.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstate': 2.0.27
|
||||
'@push.rocks/smartstate': 2.0.30
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
@@ -5144,18 +5107,6 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-wcctools@1.3.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
'@design.estate/dees-element': 2.1.6
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
lit: 3.3.2
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- react
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.8
|
||||
@@ -5920,7 +5871,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
|
||||
'@push.rocks/smartacme@8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
'@push.rocks/smartacme@8.0.0(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
'@apiclient.xyz/cloudflare': 6.4.3
|
||||
@@ -5942,7 +5893,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
@@ -6248,6 +6198,13 @@ snapshots:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
|
||||
'@push.rocks/smartjson@6.0.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartenv': 6.0.0
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
|
||||
'@push.rocks/smartjwt@2.2.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
@@ -6454,22 +6411,22 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.3': {}
|
||||
|
||||
'@push.rocks/smartproxy@19.6.17(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
||||
'@push.rocks/smartproxy@22.4.2(socks@2.8.7)':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
||||
'@push.rocks/smartacme': 8.0.0(socks@2.8.7)
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfile': 11.2.7
|
||||
'@push.rocks/smartfile': 13.1.2
|
||||
'@push.rocks/smartlog': 3.1.10
|
||||
'@push.rocks/smartnetwork': 4.4.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartrequest': 5.0.1
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/taskbuffer': 3.5.0
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/minimatch': 6.0.0
|
||||
'@types/ws': 8.18.1
|
||||
minimatch: 10.1.1
|
||||
pretty-ms: 9.3.0
|
||||
@@ -6478,7 +6435,6 @@ snapshots:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bare-abort-controller
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -6592,7 +6548,7 @@ snapshots:
|
||||
'@push.rocks/webrequest': 4.0.1
|
||||
'@tsclass/tsclass': 9.3.0
|
||||
|
||||
'@push.rocks/smartsocket@2.1.0(@push.rocks/smartserve@2.0.1)':
|
||||
'@push.rocks/smartsocket@2.1.0':
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||
@@ -6611,7 +6567,6 @@ snapshots:
|
||||
socket.io-client: 4.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- '@push.rocks/smartserve'
|
||||
- bufferutil
|
||||
- react
|
||||
- supports-color
|
||||
@@ -6627,11 +6582,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartstate@2.0.27':
|
||||
'@push.rocks/smartstate@2.0.30':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smarthash': 3.2.6
|
||||
'@push.rocks/smartjson': 5.2.0
|
||||
'@push.rocks/smartjson': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
'@push.rocks/webstore': 2.0.20
|
||||
@@ -7462,27 +7417,27 @@ snapshots:
|
||||
|
||||
'@types/bn.js@5.2.0':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/body-parser@1.19.6':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/buffer-json@2.0.3': {}
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/cors@2.8.19':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
@@ -7490,7 +7445,7 @@ snapshots:
|
||||
|
||||
'@types/dns-packet@5.6.5':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/elliptic@6.4.18':
|
||||
dependencies:
|
||||
@@ -7498,7 +7453,7 @@ snapshots:
|
||||
|
||||
'@types/express-serve-static-core@5.1.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 1.2.1
|
||||
@@ -7511,17 +7466,17 @@ snapshots:
|
||||
|
||||
'@types/from2@2.3.6':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7543,18 +7498,18 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/mailparser@3.4.6':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
@@ -7572,20 +7527,24 @@ snapshots:
|
||||
|
||||
'@types/minimatch@5.1.2': {}
|
||||
|
||||
'@types/minimatch@6.0.0':
|
||||
dependencies:
|
||||
minimatch: 10.1.1
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/node-fetch@2.6.13':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
form-data: 4.0.5
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/node@18.19.130':
|
||||
dependencies:
|
||||
@@ -7595,7 +7554,7 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.1.0':
|
||||
'@types/node@25.2.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
@@ -7615,22 +7574,22 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/symbol-tree@3.2.5': {}
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -7656,17 +7615,15 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@webcontainer/api@1.2.0': {}
|
||||
|
||||
'@yr/monotone-cubic-spline@1.0.3': {}
|
||||
|
||||
'@zone-eu/mailsplit@5.4.8':
|
||||
@@ -8146,7 +8103,7 @@ snapshots:
|
||||
engine.io@6.6.4:
|
||||
dependencies:
|
||||
'@types/cors': 2.8.19
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.7.2
|
||||
@@ -8312,10 +8269,6 @@ snapshots:
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
|
||||
fast-xml-parser@4.5.2:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
|
||||
fast-xml-parser@4.5.3:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
@@ -8918,21 +8871,19 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lucide@0.544.0: {}
|
||||
|
||||
lucide@0.563.0: {}
|
||||
|
||||
mailauth@4.12.0:
|
||||
mailauth@4.12.1:
|
||||
dependencies:
|
||||
'@postalsys/vmc': 1.1.2
|
||||
fast-xml-parser: 4.5.2
|
||||
fast-xml-parser: 5.3.4
|
||||
ipaddr.js: 2.3.0
|
||||
joi: 18.0.2
|
||||
libmime: 5.3.7
|
||||
nodemailer: 7.0.11
|
||||
nodemailer: 7.0.13
|
||||
punycode.js: 2.3.1
|
||||
tldts: 7.0.19
|
||||
undici: 7.16.0
|
||||
tldts: 7.0.21
|
||||
undici: 7.19.2
|
||||
yargs: 17.7.2
|
||||
|
||||
mailparser@3.9.3:
|
||||
@@ -9352,8 +9303,6 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.2.7
|
||||
@@ -9446,8 +9395,6 @@ snapshots:
|
||||
|
||||
node-forge@1.3.3: {}
|
||||
|
||||
nodemailer@7.0.11: {}
|
||||
|
||||
nodemailer@7.0.13: {}
|
||||
|
||||
normalize-newline@4.1.0:
|
||||
@@ -10245,7 +10192,7 @@ snapshots:
|
||||
|
||||
tldts-core@7.0.21: {}
|
||||
|
||||
tldts@7.0.19:
|
||||
tldts@7.0.21:
|
||||
dependencies:
|
||||
tldts-core: 7.0.21
|
||||
|
||||
@@ -10322,7 +10269,7 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici@7.16.0: {}
|
||||
undici@7.19.2: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
@@ -10372,7 +10319,7 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
uuid@13.0.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
|
||||
134
readme.hints.md
134
readme.hints.md
@@ -1,5 +1,71 @@
|
||||
# Implementation Hints and Learnings
|
||||
|
||||
## Dependency Upgrade (2026-02-01)
|
||||
|
||||
### Major Upgrades Completed
|
||||
- `@api.global/typedserver`: 3.0.80 → 8.3.0
|
||||
- `@api.global/typedsocket`: 3.1.1 → 4.1.0
|
||||
- `@apiclient.xyz/cloudflare`: 6.4.3 → 7.1.0
|
||||
- `@design.estate/dees-catalog`: 1.12.4 → 3.41.4
|
||||
- `@push.rocks/smartpath`: 5.1.0 → 6.0.0
|
||||
- `@push.rocks/smartproxy`: 19.6.17 → 22.4.2
|
||||
- `@push.rocks/smartrequest`: 2.1.0 → 5.0.1
|
||||
- `uuid`: 11.1.0 → 13.0.0
|
||||
|
||||
### Breaking Changes Fixed
|
||||
|
||||
1. **SmartProxy v22**: `target` → `targets` (array)
|
||||
```typescript
|
||||
// Old
|
||||
action: { type: 'forward', target: { host: 'x', port: 25 } }
|
||||
// New
|
||||
action: { type: 'forward', targets: [{ host: 'x', port: 25 }] }
|
||||
```
|
||||
|
||||
2. **SmartRequest v5**: `SmartRequestClient` → `SmartRequest`, `.body` → `.json()`
|
||||
```typescript
|
||||
// Old
|
||||
const resp = await plugins.smartrequest.SmartRequestClient.create()...post();
|
||||
const json = resp.body;
|
||||
// New
|
||||
const resp = await plugins.smartrequest.SmartRequest.create()...post();
|
||||
const json = await resp.json();
|
||||
```
|
||||
|
||||
3. **dees-catalog v3**: Icon naming changed to library-prefixed format
|
||||
```typescript
|
||||
// Old (deprecated but supported)
|
||||
<dees-icon iconFA="check"></dees-icon>
|
||||
// New
|
||||
<dees-icon icon="fa:check"></dees-icon>
|
||||
<dees-icon icon="lucide:menu"></dees-icon>
|
||||
```
|
||||
|
||||
### TC39 Decorators
|
||||
- ts_web components updated to use `accessor` keyword for `@state()` decorators
|
||||
- Required for TC39 standard decorator support
|
||||
|
||||
### tswatch Configuration
|
||||
The project now uses tswatch for development:
|
||||
```bash
|
||||
pnpm run watch
|
||||
```
|
||||
Configuration in `npmextra.json`:
|
||||
```json
|
||||
{
|
||||
"@git.zone/tswatch": {
|
||||
"watchers": [{
|
||||
"name": "dcrouter-dev",
|
||||
"watch": ["ts/**/*.ts", "ts_*/**/*.ts", "test_watch/devserver.ts"],
|
||||
"command": "pnpm run build && tsrun test_watch/devserver.ts",
|
||||
"restart": true,
|
||||
"debounce": 500,
|
||||
"runOnStart": true
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## RADIUS Server Integration (2026-02-01)
|
||||
|
||||
### Overview
|
||||
@@ -1131,3 +1197,71 @@ The throughput was showing 0 because:
|
||||
4. Updated frontend to call the new endpoint for complete network metrics
|
||||
|
||||
The throughput data now flows correctly from SmartProxy → MetricsManager → API → UI.
|
||||
|
||||
## Email Operations Dashboard (2026-02-01)
|
||||
|
||||
### Overview
|
||||
Replaced mock data in the email UI with real backend data from the delivery queue and security logger.
|
||||
|
||||
### New Files Created
|
||||
- `ts_interfaces/requests/email-ops.ts` - TypedRequest interfaces for email operations
|
||||
- `ts/opsserver/handlers/email-ops.handler.ts` - Backend handler for email operations
|
||||
|
||||
### Key Interfaces
|
||||
- `IReq_GetQueuedEmails` - Fetch emails from delivery queue by status
|
||||
- `IReq_GetSentEmails` - Fetch delivered emails
|
||||
- `IReq_GetFailedEmails` - Fetch failed emails
|
||||
- `IReq_ResendEmail` - Re-queue a failed email for retry
|
||||
- `IReq_GetSecurityIncidents` - Fetch security events from SecurityLogger
|
||||
- `IReq_GetBounceRecords` - Fetch bounce records and suppression list
|
||||
- `IReq_RemoveFromSuppressionList` - Remove email from suppression list
|
||||
|
||||
### UI Changes (ops-view-emails.ts)
|
||||
- Replaced mock folders (inbox/sent/draft/trash) with operations views:
|
||||
- **Queued**: Emails pending delivery
|
||||
- **Sent**: Successfully delivered emails
|
||||
- **Failed**: Failed emails with resend capability
|
||||
- **Security**: Security incidents from SecurityLogger
|
||||
- Removed `generateMockEmails()` method
|
||||
- Added state management via `emailOpsStatePart` in appstate.ts
|
||||
- Added resend button for failed emails
|
||||
- Added security incident detail view
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
UnifiedDeliveryQueue → EmailOpsHandler → TypedRequest → Frontend State → UI
|
||||
SecurityLogger → EmailOpsHandler → TypedRequest → Frontend State → UI
|
||||
BounceManager → EmailOpsHandler → TypedRequest → Frontend State → UI
|
||||
```
|
||||
|
||||
### Backend Data Access
|
||||
The handler accesses data from:
|
||||
- `dcRouter.emailServer.deliveryQueue` - Email queue items (IQueueItem)
|
||||
- `SecurityLogger.getInstance()` - Security events (ISecurityEvent)
|
||||
- `emailServer.bounceManager` - Bounce records and suppression list
|
||||
|
||||
## OpsServer UI Fixes (2026-02-02)
|
||||
|
||||
### Configuration Page Fix
|
||||
The configuration page had field name mismatches between frontend and backend:
|
||||
- Frontend expected `server` and `storage` sections
|
||||
- Backend returns `proxy` section (not `server`)
|
||||
- Backend has no `storage` section
|
||||
|
||||
**Fix**: Updated `ops-view-config.ts` to use correct section names:
|
||||
- `proxy` instead of `server`
|
||||
- Removed non-existent `storage` section
|
||||
- Added optional chaining (`?.`) for safety
|
||||
|
||||
### Auth Persistence Fix
|
||||
Login state was using `'soft'` mode in Smartstate which is memory-only:
|
||||
- User login was lost on page refresh
|
||||
- State reset to logged out after browser restart
|
||||
|
||||
**Changes**:
|
||||
1. `ts_web/appstate.ts`: Changed loginStatePart from `'soft'` to `'persistent'`
|
||||
- Now uses IndexedDB to persist across browser sessions
|
||||
2. `ts/opsserver/handlers/admin.handler.ts`: JWT expiry changed from 7 days to 24 hours
|
||||
3. `ts_web/elements/ops-dashboard.ts`: Added JWT expiry check on session restore
|
||||
- Validates stored JWT hasn't expired before auto-logging in
|
||||
- Clears expired sessions and shows login form
|
||||
546
readme.md
546
readme.md
@@ -1,10 +1,14 @@
|
||||
# dcrouter
|
||||
# @serve.zone/dcrouter
|
||||
|
||||

|
||||
|
||||
**dcrouter: a traffic router intended to be gating your datacenter.**
|
||||
**dcrouter: A powerful traffic router designed to be the gateway for your datacenter.** 🚀
|
||||
|
||||
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), and DNS protocols. Designed for enterprises requiring robust traffic management, automatic certificate provisioning, and enterprise-grade email infrastructure.
|
||||
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), DNS protocols, and RADIUS authentication. Designed for enterprises requiring robust traffic management, automatic certificate provisioning, and enterprise-grade email infrastructure.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -13,11 +17,16 @@ A comprehensive traffic routing solution that provides unified gateway capabilit
|
||||
- [Quick Start](#quick-start)
|
||||
- [Architecture](#architecture)
|
||||
- [Configuration](#configuration)
|
||||
- [Socket-Handler Mode](#socket-handler-mode)
|
||||
- [Email System](#email-system)
|
||||
- [SmartProxy Routing](#smartproxy-routing)
|
||||
- [RADIUS Server](#radius-server)
|
||||
- [Storage System](#storage-system)
|
||||
- [Security Features](#security-features)
|
||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||
- [API Reference](#api-reference)
|
||||
- [Examples](#examples)
|
||||
- [Testing](#testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Features
|
||||
@@ -36,9 +45,16 @@ A comprehensive traffic routing solution that provides unified gateway capabilit
|
||||
|
||||
### 📧 **Complete Email Infrastructure**
|
||||
- **Multi-domain SMTP server** on standard ports (25, 587, 465)
|
||||
- **Pattern-based email routing** with three processing modes
|
||||
- **Pattern-based email routing** with four processing modes (forward, process, deliver, reject)
|
||||
- **DKIM, SPF, DMARC** authentication and verification
|
||||
- **Enterprise deliverability** with IP warmup and reputation management
|
||||
- **Bounce handling** with suppression lists
|
||||
|
||||
### 📡 **RADIUS Server**
|
||||
- **MAC Authentication Bypass (MAB)** for network device authentication
|
||||
- **VLAN assignment** based on MAC address or OUI patterns
|
||||
- **RADIUS accounting** for session tracking and billing
|
||||
- **OpsServer API integration** for real-time management
|
||||
|
||||
### ⚡ **High Performance**
|
||||
- **Connection pooling** and efficient resource management
|
||||
@@ -52,10 +68,18 @@ A comprehensive traffic routing solution that provides unified gateway capabilit
|
||||
- **Automatic data migration** between backends
|
||||
- **Persistent configuration** for domains, routes, and security data
|
||||
|
||||
### 🖥️ **OpsServer Dashboard**
|
||||
- **Web-based management interface** for real-time monitoring
|
||||
- **JWT authentication** with secure admin access
|
||||
- **Live statistics** for connections, email, DNS, and RADIUS
|
||||
- **Configuration management** via TypedRequest API
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @serve.zone/dcrouter --save
|
||||
# or
|
||||
pnpm add @serve.zone/dcrouter
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
@@ -79,7 +103,7 @@ const router = new DcRouter({
|
||||
match: { domains: ['example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.10', port: 8080 },
|
||||
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
}
|
||||
@@ -129,6 +153,30 @@ const router = new DcRouter({
|
||||
await router.start();
|
||||
```
|
||||
|
||||
### With OpsServer Dashboard
|
||||
|
||||
```typescript
|
||||
import { DcRouter } from '@serve.zone/dcrouter';
|
||||
|
||||
const router = new DcRouter({
|
||||
// Enable OpsServer for web dashboard
|
||||
opsServerConfig: {
|
||||
port: 3000,
|
||||
admin: {
|
||||
username: 'admin',
|
||||
password: 'your-secure-password'
|
||||
}
|
||||
},
|
||||
|
||||
// Your routing configuration...
|
||||
smartProxyConfig: { /* ... */ },
|
||||
emailConfig: { /* ... */ }
|
||||
});
|
||||
|
||||
await router.start();
|
||||
// Dashboard available at http://localhost:3000
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### System Overview
|
||||
@@ -140,6 +188,7 @@ graph TB
|
||||
SMTP[SMTP Clients]
|
||||
TCP[TCP Clients]
|
||||
DNS[DNS Queries]
|
||||
RADIUS[RADIUS Clients]
|
||||
end
|
||||
|
||||
subgraph "DcRouter Core"
|
||||
@@ -147,7 +196,9 @@ graph TB
|
||||
SmartProxy[SmartProxy Engine]
|
||||
EmailServer[Unified Email Server]
|
||||
DnsServer[DNS Server]
|
||||
RadiusServer[RADIUS Server]
|
||||
CertManager[Certificate Manager]
|
||||
OpsServer[OpsServer Dashboard]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
@@ -161,11 +212,14 @@ graph TB
|
||||
TCP --> SmartProxy
|
||||
SMTP --> EmailServer
|
||||
DNS --> DnsServer
|
||||
RADIUS --> RadiusServer
|
||||
|
||||
DcRouter --> SmartProxy
|
||||
DcRouter --> EmailServer
|
||||
DcRouter --> DnsServer
|
||||
DcRouter --> RadiusServer
|
||||
DcRouter --> CertManager
|
||||
DcRouter --> OpsServer
|
||||
|
||||
SmartProxy --> WebServices
|
||||
SmartProxy --> APIs
|
||||
@@ -192,12 +246,30 @@ High-performance HTTP/HTTPS and TCP/SNI proxy with:
|
||||
Enterprise-grade SMTP server with:
|
||||
- Multi-domain support
|
||||
- Pattern-based routing
|
||||
- Three processing modes
|
||||
- Complete authentication stack
|
||||
- Four processing modes (forward, process, deliver, reject)
|
||||
- Complete authentication stack (DKIM, SPF, DMARC)
|
||||
|
||||
#### **DNS Server**
|
||||
Authoritative DNS server with:
|
||||
- Dynamic record management
|
||||
- DNS-over-HTTPS (DoH) support
|
||||
- ACME DNS-01 challenge handling
|
||||
|
||||
#### **RADIUS Server**
|
||||
Network authentication server with:
|
||||
- MAC Authentication Bypass (MAB)
|
||||
- VLAN assignment
|
||||
- Accounting support
|
||||
|
||||
#### **Certificate Manager**
|
||||
Automatic TLS certificate provisioning via ACME with DNS-01 challenges.
|
||||
|
||||
#### **OpsServer Dashboard**
|
||||
Web-based management interface with:
|
||||
- JWT-secured API
|
||||
- Real-time statistics
|
||||
- Configuration management
|
||||
|
||||
## Configuration
|
||||
|
||||
### Complete Configuration Interface
|
||||
@@ -239,6 +311,27 @@ interface IDcRouterOptions {
|
||||
// DNS domain for automatic DNS-over-HTTPS setup
|
||||
dnsDomain?: string; // e.g., 'dns.example.com'
|
||||
|
||||
// DNS nameserver domains (enables authoritative DNS)
|
||||
dnsNsDomains?: string[]; // e.g., ['ns1.example.com', 'ns2.example.com']
|
||||
|
||||
// RADIUS server configuration
|
||||
radiusConfig?: {
|
||||
port?: number;
|
||||
secret: string;
|
||||
clients?: IRadiusClient[];
|
||||
macAuth?: IMacAuthConfig;
|
||||
vlanAssignment?: IVlanAssignment[];
|
||||
};
|
||||
|
||||
// OpsServer configuration
|
||||
opsServerConfig?: {
|
||||
port?: number;
|
||||
admin: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
|
||||
// TLS and certificate configuration
|
||||
tls?: {
|
||||
contactEmail: string;
|
||||
@@ -271,10 +364,10 @@ interface IRouteConfig {
|
||||
};
|
||||
action: {
|
||||
type: 'forward' | 'redirect' | 'serve';
|
||||
target?: {
|
||||
targets?: Array<{
|
||||
host: string;
|
||||
port: number | 'preserve' | ((context: any) => number);
|
||||
};
|
||||
}>;
|
||||
tls?: {
|
||||
mode: 'terminate' | 'passthrough';
|
||||
certificate?: 'auto' | string;
|
||||
@@ -548,30 +641,6 @@ Different handling for authenticated vs unauthenticated senders:
|
||||
}
|
||||
```
|
||||
|
||||
#### **Content-Based Filtering**
|
||||
Filter based on size, subject, or headers:
|
||||
```typescript
|
||||
{
|
||||
name: 'large-email-reject',
|
||||
match: { sizeRange: { min: 25000000 } }, // > 25MB
|
||||
action: {
|
||||
type: 'reject',
|
||||
reject: { code: 552, message: 'Message too large' }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'priority-emails',
|
||||
match: {
|
||||
headers: { 'X-Priority': 'high' },
|
||||
subject: /urgent|emergency/i
|
||||
},
|
||||
action: {
|
||||
type: 'process',
|
||||
process: { queue: 'priority' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Email Security Features
|
||||
|
||||
#### **Route Matching Patterns**
|
||||
@@ -640,15 +709,7 @@ const routes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '192.168.1.20',
|
||||
port: (context) => {
|
||||
// Route based on path
|
||||
if (context.path.startsWith('/v1/')) return 8080;
|
||||
if (context.path.startsWith('/v2/')) return 8081;
|
||||
return 8080;
|
||||
}
|
||||
},
|
||||
targets: [{ host: '192.168.1.20', port: 8080 }],
|
||||
tls: {
|
||||
mode: 'terminate',
|
||||
certificate: 'auto'
|
||||
@@ -687,10 +748,7 @@ const tcpRoutes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '192.168.1.30',
|
||||
port: 'preserve'
|
||||
},
|
||||
targets: [{ host: '192.168.1.30', port: 'preserve' }],
|
||||
security: {
|
||||
ipAllowList: ['192.168.1.0/24']
|
||||
}
|
||||
@@ -706,10 +764,7 @@ const tcpRoutes = [
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: '192.168.1.40',
|
||||
port: 8443
|
||||
},
|
||||
targets: [{ host: '192.168.1.40', port: 8443 }],
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
@@ -718,6 +773,72 @@ const tcpRoutes = [
|
||||
];
|
||||
```
|
||||
|
||||
## RADIUS Server
|
||||
|
||||
DcRouter includes a RADIUS server for network access control:
|
||||
|
||||
### Basic RADIUS Configuration
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
radiusConfig: {
|
||||
port: 1812,
|
||||
secret: 'your-radius-secret',
|
||||
clients: [
|
||||
{
|
||||
name: 'switch-1',
|
||||
ip: '192.168.1.1',
|
||||
secret: 'client-secret'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### MAC Authentication Bypass (MAB)
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
radiusConfig: {
|
||||
port: 1812,
|
||||
secret: 'radius-secret',
|
||||
macAuth: {
|
||||
enabled: true,
|
||||
allowedMacs: [
|
||||
'aa:bb:cc:dd:ee:ff',
|
||||
'aa:bb:cc:*' // Wildcard for OUI matching
|
||||
],
|
||||
defaultVlan: 100,
|
||||
guestVlan: 999
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### VLAN Assignment
|
||||
|
||||
```typescript
|
||||
const router = new DcRouter({
|
||||
radiusConfig: {
|
||||
secret: 'radius-secret',
|
||||
vlanAssignment: [
|
||||
{
|
||||
match: { mac: 'aa:bb:cc:*' }, // Vendor OUI match
|
||||
vlan: 100
|
||||
},
|
||||
{
|
||||
match: { mac: 'dd:ee:ff:*' },
|
||||
vlan: 200
|
||||
},
|
||||
{
|
||||
match: { default: true },
|
||||
vlan: 999 // Guest VLAN
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Storage System
|
||||
|
||||
### StorageManager
|
||||
@@ -765,31 +886,6 @@ The storage system is used for:
|
||||
- **IP Reputation**: `/security/ip-reputation/{ip}.json`
|
||||
- **Domain Configs**: `/email/domains/{domain}.json`
|
||||
|
||||
### Data Migration
|
||||
|
||||
Migrate data between storage backends:
|
||||
|
||||
```typescript
|
||||
import { StorageManager } from '@serve.zone/dcrouter';
|
||||
|
||||
// Export from filesystem
|
||||
const fsStorage = new StorageManager({ fsPath: './data' });
|
||||
const keys = await fsStorage.list('/');
|
||||
const data = {};
|
||||
for (const key of keys) {
|
||||
data[key] = await fsStorage.get(key);
|
||||
}
|
||||
|
||||
// Import to cloud storage
|
||||
const cloudStorage = new StorageManager({
|
||||
readFunction: cloudRead,
|
||||
writeFunction: cloudWrite
|
||||
});
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
await cloudStorage.set(key, value);
|
||||
}
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### IP Reputation Checking
|
||||
@@ -810,21 +906,58 @@ if (result.isBlocked) {
|
||||
}
|
||||
```
|
||||
|
||||
### Content Security Scanner
|
||||
### Rate Limiting
|
||||
|
||||
```typescript
|
||||
import { ContentScanner } from '@serve.zone/dcrouter';
|
||||
|
||||
const scanner = new ContentScanner({
|
||||
spamThreshold: 5.0,
|
||||
virusScanning: true,
|
||||
attachmentFiltering: {
|
||||
maxSize: 25 * 1024 * 1024,
|
||||
blockedTypes: ['.exe', '.bat', '.scr']
|
||||
const router = new DcRouter({
|
||||
emailConfig: {
|
||||
rateLimits: {
|
||||
inbound: {
|
||||
messagesPerMinute: 100,
|
||||
connectionsPerIp: 10,
|
||||
recipientsPerMessage: 50
|
||||
},
|
||||
outbound: {
|
||||
messagesPerHour: 1000,
|
||||
messagesPerDay: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
const scanResult = await scanner.scanEmail(email);
|
||||
## OpsServer Dashboard
|
||||
|
||||
The OpsServer provides a web-based management interface:
|
||||
|
||||
### Features
|
||||
|
||||
- **Real-time Statistics**: View connections, email throughput, DNS queries, RADIUS sessions
|
||||
- **Configuration Management**: Update routes and settings via API
|
||||
- **Log Viewer**: Access system logs with filtering
|
||||
- **Security Dashboard**: Monitor threats and blocked connections
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The OpsServer exposes TypedRequest endpoints:
|
||||
|
||||
```typescript
|
||||
// Health check
|
||||
POST /typedrequest { method: 'getHealthStatus' }
|
||||
|
||||
// Server statistics
|
||||
POST /typedrequest { method: 'getServerStatistics' }
|
||||
|
||||
// Configuration
|
||||
POST /typedrequest { method: 'getConfiguration' }
|
||||
POST /typedrequest { method: 'updateConfiguration', data: { ... } }
|
||||
|
||||
// Logs
|
||||
POST /typedrequest { method: 'getLogs', data: { level: 'info', limit: 100 } }
|
||||
|
||||
// RADIUS
|
||||
POST /typedrequest { method: 'getRadiusSessions' }
|
||||
POST /typedrequest { method: 'getRadiusClients' }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
@@ -839,7 +972,7 @@ constructor(options: IDcRouterOptions)
|
||||
#### Methods
|
||||
|
||||
##### `start(): Promise<void>`
|
||||
Starts all configured services (SmartProxy, email server, DNS server).
|
||||
Starts all configured services (SmartProxy, email server, DNS server, RADIUS server, OpsServer).
|
||||
|
||||
##### `stop(): Promise<void>`
|
||||
Gracefully stops all services.
|
||||
@@ -872,9 +1005,6 @@ const status = router.emailService.getEmailStatus(emailId);
|
||||
console.log(status.status); // 'pending', 'sent', 'delivered', 'bounced'
|
||||
```
|
||||
|
||||
#### `getDeliveryReport(emailId: string): IDeliveryReport`
|
||||
Detailed delivery information including bounce reasons and tracking data.
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Enterprise Setup
|
||||
@@ -883,6 +1013,15 @@ Detailed delivery information including bounce reasons and tracking data.
|
||||
import { DcRouter } from '@serve.zone/dcrouter';
|
||||
|
||||
const router = new DcRouter({
|
||||
// OpsServer dashboard
|
||||
opsServerConfig: {
|
||||
port: 3000,
|
||||
admin: {
|
||||
username: 'admin',
|
||||
password: process.env.ADMIN_PASSWORD
|
||||
}
|
||||
},
|
||||
|
||||
// HTTP/HTTPS routing
|
||||
smartProxyConfig: {
|
||||
routes: [
|
||||
@@ -893,7 +1032,7 @@ const router = new DcRouter({
|
||||
match: { domains: ['example.com', 'www.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.10', port: 80 },
|
||||
targets: [{ host: '192.168.1.10', port: 80 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
},
|
||||
@@ -905,20 +1044,9 @@ const router = new DcRouter({
|
||||
match: { domains: ['api.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.20', port: 8080 },
|
||||
targets: [{ host: '192.168.1.20', port: 8080 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' }
|
||||
}
|
||||
},
|
||||
|
||||
// Internal services
|
||||
{
|
||||
name: 'internal',
|
||||
match: { ports: [{ from: 8000, to: 8999 }] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: '192.168.1.30', port: 'preserve' },
|
||||
security: { ipAllowList: ['192.168.0.0/16'] }
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -945,28 +1073,9 @@ const router = new DcRouter({
|
||||
selector: 'mail',
|
||||
rotateKeys: true
|
||||
}
|
||||
},
|
||||
{
|
||||
domain: 'notifications.example.com',
|
||||
dnsMode: 'internal-dns',
|
||||
rateLimits: {
|
||||
outbound: { messagesPerHour: 10000 }
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Authentication configuration
|
||||
auth: {
|
||||
required: true,
|
||||
methods: ['PLAIN', 'LOGIN']
|
||||
},
|
||||
|
||||
// TLS configuration
|
||||
tls: {
|
||||
keyPath: './certs/mail-key.pem',
|
||||
certPath: './certs/mail-cert.pem'
|
||||
},
|
||||
|
||||
// Email routing rules
|
||||
routes: [
|
||||
// Relay from office network
|
||||
@@ -976,39 +1085,18 @@ const router = new DcRouter({
|
||||
match: { clientIp: '192.168.0.0/16' },
|
||||
action: {
|
||||
type: 'forward',
|
||||
forward: {
|
||||
host: 'internal-mail.example.com',
|
||||
port: 25
|
||||
}
|
||||
forward: { host: 'internal-mail.example.com', port: 25 }
|
||||
}
|
||||
},
|
||||
|
||||
// Transactional emails via processing
|
||||
// Process transactional emails
|
||||
{
|
||||
name: 'notifications',
|
||||
priority: 50,
|
||||
match: { recipients: '*@notifications.example.com' },
|
||||
action: {
|
||||
type: 'process',
|
||||
process: {
|
||||
scan: true,
|
||||
dkim: true,
|
||||
queue: 'priority'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Internal emails forwarded to Exchange
|
||||
{
|
||||
name: 'internal-mail',
|
||||
priority: 25,
|
||||
match: { recipients: '*@example.com' },
|
||||
action: {
|
||||
type: 'forward',
|
||||
forward: {
|
||||
host: 'exchange.internal.example.com',
|
||||
port: 25
|
||||
}
|
||||
process: { scan: true, dkim: true, queue: 'priority' }
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1018,15 +1106,23 @@ const router = new DcRouter({
|
||||
match: { recipients: '*' },
|
||||
action: {
|
||||
type: 'reject',
|
||||
reject: {
|
||||
code: 550,
|
||||
message: 'Relay denied'
|
||||
}
|
||||
reject: { code: 550, message: 'Relay denied' }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// RADIUS for network devices
|
||||
radiusConfig: {
|
||||
port: 1812,
|
||||
secret: process.env.RADIUS_SECRET,
|
||||
macAuth: {
|
||||
enabled: true,
|
||||
defaultVlan: 100,
|
||||
guestVlan: 999
|
||||
}
|
||||
},
|
||||
|
||||
// DNS server for ACME challenges
|
||||
dnsServerConfig: {
|
||||
port: 53,
|
||||
@@ -1056,33 +1152,33 @@ setInterval(() => {
|
||||
}, 60000);
|
||||
```
|
||||
|
||||
### Email Template System
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { EmailService, TemplateManager } from '@serve.zone/dcrouter';
|
||||
### Comprehensive Test Suite
|
||||
|
||||
// Setup email templates
|
||||
const templateManager = new TemplateManager();
|
||||
templateManager.addTemplate('welcome', {
|
||||
subject: 'Welcome to {{company}}!',
|
||||
html: `
|
||||
<h1>Welcome {{name}}!</h1>
|
||||
<p>Thank you for joining {{company}}.</p>
|
||||
<p>Your account: {{email}}</p>
|
||||
`,
|
||||
text: 'Welcome {{name}}! Thank you for joining {{company}}.'
|
||||
});
|
||||
DcRouter includes a comprehensive test suite with 195 test files covering all aspects of the system:
|
||||
|
||||
// Send templated email
|
||||
const emailService = new EmailService(router);
|
||||
await emailService.sendTemplatedEmail('welcome', {
|
||||
to: 'user@example.com',
|
||||
templateData: {
|
||||
name: 'John Doe',
|
||||
company: 'Example Corp',
|
||||
email: 'user@example.com'
|
||||
}
|
||||
});
|
||||
#### SMTP Protocol Tests
|
||||
- **Commands**: EHLO, HELO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT, VRFY, EXPN, HELP
|
||||
- **Extensions**: SIZE, PIPELINING, STARTTLS
|
||||
- **Connection Management**: TLS/plain connections, timeouts, limits, rejection handling
|
||||
- **Error Handling**: Syntax errors, invalid sequences, temporary/permanent failures
|
||||
- **Email Processing**: Basic sending, multiple recipients, large emails, invalid addresses
|
||||
- **Security**: Authentication, rate limiting
|
||||
- **Performance**: Throughput testing
|
||||
- **Edge Cases**: Very large emails, special characters
|
||||
|
||||
#### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run specific test categories
|
||||
tsx test/suite/smtpserver_commands/test.cmd-01.ehlo-command.ts
|
||||
|
||||
# Run with verbose output
|
||||
tstest test/test.integration.ts --verbose
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -1118,25 +1214,6 @@ dig TXT your-domain.com
|
||||
- Test CIDR notation: `192.168.0.0/16` includes all 192.168.x.x addresses
|
||||
- Confirm authentication state matches your expectations
|
||||
|
||||
**Common Route Patterns**
|
||||
```typescript
|
||||
// Debug route to log all traffic
|
||||
{
|
||||
name: 'debug-all',
|
||||
priority: 1000,
|
||||
match: { recipients: '*' },
|
||||
action: { type: 'process', process: { scan: false } }
|
||||
}
|
||||
|
||||
// Catch-all reject (should be lowest priority)
|
||||
{
|
||||
name: 'default-reject',
|
||||
priority: 0,
|
||||
match: { recipients: '*' },
|
||||
action: { type: 'reject', reject: { code: 550, message: 'No route' } }
|
||||
}
|
||||
```
|
||||
|
||||
#### DNS Issues
|
||||
```bash
|
||||
# Test DNS server
|
||||
@@ -1146,32 +1223,6 @@ dig @your-server.com your-domain.com
|
||||
dig your-domain.com @8.8.8.8
|
||||
```
|
||||
|
||||
### Logging and Monitoring
|
||||
|
||||
```typescript
|
||||
import { SmartLog } from '@push.rocks/smartlog';
|
||||
|
||||
// Configure logging
|
||||
const logger = new SmartLog({
|
||||
level: 'info',
|
||||
transport: 'console'
|
||||
});
|
||||
|
||||
// Monitor email events
|
||||
router.emailServer.on('emailReceived', (email) => {
|
||||
logger.log('info', `Email received: ${email.from} -> ${email.to}`);
|
||||
});
|
||||
|
||||
router.emailServer.on('emailSent', (result) => {
|
||||
logger.log('info', `Email sent: ${result.messageId} (${result.status})`);
|
||||
});
|
||||
|
||||
// Monitor proxy events
|
||||
router.smartProxy.on('connectionEstablished', (connection) => {
|
||||
logger.log('info', `Connection: ${connection.clientIp} -> ${connection.target}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
```typescript
|
||||
@@ -1196,60 +1247,23 @@ const performanceConfig = {
|
||||
};
|
||||
```
|
||||
|
||||
## License
|
||||
## License and Legal Information
|
||||
|
||||
MIT License - see LICENSE file for details.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
## Testing
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Comprehensive Test Suite
|
||||
### Trademarks
|
||||
|
||||
DcRouter includes a comprehensive test suite covering all aspects of the system:
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
#### SMTP Protocol Tests
|
||||
- **Commands**: EHLO, HELO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT, VRFY, EXPN, HELP
|
||||
- **Extensions**: SIZE, PIPELINING, STARTTLS
|
||||
- **Connection Management**: TLS/plain connections, timeouts, limits, rejection handling
|
||||
- **Error Handling**: Syntax errors, invalid sequences, temporary/permanent failures
|
||||
- **Email Processing**: Basic sending, multiple recipients, large emails, invalid addresses
|
||||
- **Security**: Authentication, rate limiting
|
||||
- **Performance**: Throughput testing
|
||||
- **Edge Cases**: Very large emails, special characters
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
#### Storage and Configuration Tests
|
||||
- **Storage Manager**: All backend types (filesystem, custom, memory)
|
||||
- **Integration**: Component storage usage and persistence
|
||||
- **DNS Validation**: All DNS modes (forward, internal, external)
|
||||
- **DNS Mode Switching**: Dynamic configuration changes
|
||||
- **Data Migration**: Moving data between storage backends
|
||||
### Company Information
|
||||
|
||||
#### Running Tests
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
# Run specific test categories
|
||||
tsx test/suite/commands/test.ehlo-command.ts
|
||||
tsx test/suite/connection/test.tls-connection.ts
|
||||
tsx test/suite/email-processing/test.basic-email.ts
|
||||
|
||||
# Run with verbose output
|
||||
tstest test/suite/security/test.authentication.ts --verbose
|
||||
```
|
||||
|
||||
### Test Infrastructure
|
||||
|
||||
The test suite uses a self-contained pattern where each test:
|
||||
1. Starts its own SMTP server instance
|
||||
2. Runs comprehensive test scenarios
|
||||
3. Cleans up all resources
|
||||
4. Provides detailed logging for debugging
|
||||
|
||||
This ensures tests are isolated, reliable, and can run in parallel.
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: [https://docs.serve.zone/dcrouter](https://docs.serve.zone/dcrouter)
|
||||
- Issues: [https://github.com/serve-zone/dcrouter/issues](https://github.com/serve-zone/dcrouter/issues)
|
||||
- Community: [https://community.serve.zone](https://community.serve.zone)
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '2.13.0',
|
||||
version: '3.1.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -517,10 +517,10 @@ export class DcRouter {
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: route.action.type === 'forward' && route.action.forward ? {
|
||||
targets: route.action.type === 'forward' && route.action.forward ? [{
|
||||
host: route.action.forward.host,
|
||||
port: route.action.forward.port || 25
|
||||
} : undefined,
|
||||
}] : undefined,
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export class OpsServer {
|
||||
private securityHandler: handlers.SecurityHandler;
|
||||
private statsHandler: handlers.StatsHandler;
|
||||
private radiusHandler: handlers.RadiusHandler;
|
||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
@@ -55,6 +56,7 @@ export class OpsServer {
|
||||
this.securityHandler = new handlers.SecurityHandler(this);
|
||||
this.statsHandler = new handlers.StatsHandler(this);
|
||||
this.radiusHandler = new handlers.RadiusHandler(this);
|
||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||
|
||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class AdminHandler {
|
||||
throw new plugins.typedrequest.TypedResponseError('login failed');
|
||||
}
|
||||
|
||||
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24 * 7; // 7 days
|
||||
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours
|
||||
|
||||
const jwt = await this.smartjwtInstance.createJWT({
|
||||
userId: user.id,
|
||||
|
||||
325
ts/opsserver/handlers/email-ops.handler.ts
Normal file
325
ts/opsserver/handlers/email-ops.handler.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { SecurityLogger } from '../../security/index.js';
|
||||
|
||||
export class EmailOpsHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
// Add this handler's router to the parent
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Get Queued Emails Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueuedEmails>(
|
||||
'getQueuedEmails',
|
||||
async (dataArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
if (!emailServer?.deliveryQueue) {
|
||||
return { items: [], total: 0 };
|
||||
}
|
||||
|
||||
const queue = emailServer.deliveryQueue;
|
||||
const stats = queue.getStats();
|
||||
|
||||
// Get all queue items and filter by status if provided
|
||||
const items = this.getQueueItems(
|
||||
dataArg.status,
|
||||
dataArg.limit || 50,
|
||||
dataArg.offset || 0
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: stats.queueSize,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get Sent Emails Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSentEmails>(
|
||||
'getSentEmails',
|
||||
async (dataArg) => {
|
||||
const items = this.getQueueItems(
|
||||
'delivered',
|
||||
dataArg.limit || 50,
|
||||
dataArg.offset || 0
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: items.length, // Note: total would ideally come from a counter
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get Failed Emails Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetFailedEmails>(
|
||||
'getFailedEmails',
|
||||
async (dataArg) => {
|
||||
const items = this.getQueueItems(
|
||||
'failed',
|
||||
dataArg.limit || 50,
|
||||
dataArg.offset || 0
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: items.length,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Resend Failed Email Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ResendEmail>(
|
||||
'resendEmail',
|
||||
async (dataArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
if (!emailServer?.deliveryQueue) {
|
||||
return { success: false, error: 'Email server not available' };
|
||||
}
|
||||
|
||||
const queue = emailServer.deliveryQueue;
|
||||
const item = queue.getItem(dataArg.emailId);
|
||||
|
||||
if (!item) {
|
||||
return { success: false, error: 'Email not found in queue' };
|
||||
}
|
||||
|
||||
if (item.status !== 'failed') {
|
||||
return { success: false, error: `Email is not in failed state (current: ${item.status})` };
|
||||
}
|
||||
|
||||
try {
|
||||
// Re-enqueue the failed email by creating a new queue entry
|
||||
// with the same data but reset attempt count
|
||||
const newQueueId = await queue.enqueue(
|
||||
item.processingResult,
|
||||
item.processingMode,
|
||||
item.route
|
||||
);
|
||||
|
||||
// Optionally remove the old failed entry
|
||||
await queue.removeItem(dataArg.emailId);
|
||||
|
||||
return { success: true, newQueueId };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to resend email'
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get Security Incidents Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityIncidents>(
|
||||
'getSecurityIncidents',
|
||||
async (dataArg) => {
|
||||
const securityLogger = SecurityLogger.getInstance();
|
||||
|
||||
const filter: {
|
||||
level?: any;
|
||||
type?: any;
|
||||
} = {};
|
||||
|
||||
if (dataArg.level) {
|
||||
filter.level = dataArg.level;
|
||||
}
|
||||
|
||||
if (dataArg.type) {
|
||||
filter.type = dataArg.type;
|
||||
}
|
||||
|
||||
const incidents = securityLogger.getRecentEvents(
|
||||
dataArg.limit || 100,
|
||||
Object.keys(filter).length > 0 ? filter : undefined
|
||||
);
|
||||
|
||||
return {
|
||||
incidents: incidents.map(event => ({
|
||||
timestamp: event.timestamp,
|
||||
level: event.level as interfaces.requests.TSecurityLogLevel,
|
||||
type: event.type as interfaces.requests.TSecurityEventType,
|
||||
message: event.message,
|
||||
details: event.details,
|
||||
ipAddress: event.ipAddress,
|
||||
userId: event.userId,
|
||||
sessionId: event.sessionId,
|
||||
emailId: event.emailId,
|
||||
domain: event.domain,
|
||||
action: event.action,
|
||||
result: event.result,
|
||||
success: event.success,
|
||||
})),
|
||||
total: incidents.length,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get Bounce Records Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetBounceRecords>(
|
||||
'getBounceRecords',
|
||||
async (dataArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
|
||||
// Get bounce manager from email server via reflection
|
||||
// BounceManager is private but we need to access it
|
||||
const bounceManager = (emailServer as any)?.bounceManager;
|
||||
|
||||
if (!bounceManager) {
|
||||
return { records: [], suppressionList: [], total: 0 };
|
||||
}
|
||||
|
||||
// Get suppression list
|
||||
const suppressionList = bounceManager.getSuppressionList();
|
||||
|
||||
// Get hard bounced addresses and convert to records
|
||||
const hardBouncedAddresses = bounceManager.getHardBouncedAddresses();
|
||||
|
||||
// Create bounce records from the available data
|
||||
const records: interfaces.requests.IBounceRecord[] = [];
|
||||
|
||||
for (const email of hardBouncedAddresses) {
|
||||
const bounceInfo = bounceManager.getBounceInfo(email);
|
||||
if (bounceInfo) {
|
||||
records.push({
|
||||
id: `bounce-${email}`,
|
||||
recipient: email,
|
||||
sender: '',
|
||||
domain: email.split('@')[1] || '',
|
||||
bounceType: bounceInfo.type as interfaces.requests.TBounceType,
|
||||
bounceCategory: bounceInfo.category as interfaces.requests.TBounceCategory,
|
||||
timestamp: bounceInfo.lastBounce,
|
||||
processed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply limit and offset
|
||||
const limit = dataArg.limit || 50;
|
||||
const offset = dataArg.offset || 0;
|
||||
const paginatedRecords = records.slice(offset, offset + limit);
|
||||
|
||||
return {
|
||||
records: paginatedRecords,
|
||||
suppressionList,
|
||||
total: records.length,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Remove from Suppression List Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RemoveFromSuppressionList>(
|
||||
'removeFromSuppressionList',
|
||||
async (dataArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
const bounceManager = (emailServer as any)?.bounceManager;
|
||||
|
||||
if (!bounceManager) {
|
||||
return { success: false, error: 'Bounce manager not available' };
|
||||
}
|
||||
|
||||
try {
|
||||
bounceManager.removeFromSuppressionList(dataArg.email);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to remove from suppression list'
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get queue items with filtering and pagination
|
||||
*/
|
||||
private getQueueItems(
|
||||
status?: interfaces.requests.TEmailQueueStatus,
|
||||
limit: number = 50,
|
||||
offset: number = 0
|
||||
): interfaces.requests.IEmailQueueItem[] {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
if (!emailServer?.deliveryQueue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queue = emailServer.deliveryQueue;
|
||||
const items: interfaces.requests.IEmailQueueItem[] = [];
|
||||
|
||||
// Access the internal queue map via reflection
|
||||
// This is necessary because the queue doesn't expose iteration methods
|
||||
const queueMap = (queue as any).queue as Map<string, any>;
|
||||
|
||||
if (!queueMap) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter and convert items
|
||||
for (const [id, item] of queueMap.entries()) {
|
||||
// Apply status filter if provided
|
||||
if (status && item.status !== status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract email details from processingResult if available
|
||||
const processingResult = item.processingResult;
|
||||
let from = '';
|
||||
let to: string[] = [];
|
||||
let subject = '';
|
||||
|
||||
if (processingResult) {
|
||||
// Check if it's an Email object or raw email data
|
||||
if (processingResult.email) {
|
||||
from = processingResult.email.from || '';
|
||||
to = processingResult.email.to || [];
|
||||
subject = processingResult.email.subject || '';
|
||||
} else if (processingResult.from) {
|
||||
from = processingResult.from;
|
||||
to = processingResult.to || [];
|
||||
subject = processingResult.subject || '';
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: item.id,
|
||||
processingMode: item.processingMode,
|
||||
status: item.status,
|
||||
attempts: item.attempts,
|
||||
nextAttempt: item.nextAttempt instanceof Date ? item.nextAttempt.getTime() : item.nextAttempt,
|
||||
lastError: item.lastError,
|
||||
createdAt: item.createdAt instanceof Date ? item.createdAt.getTime() : item.createdAt,
|
||||
updatedAt: item.updatedAt instanceof Date ? item.updatedAt.getTime() : item.updatedAt,
|
||||
deliveredAt: item.deliveredAt instanceof Date ? item.deliveredAt.getTime() : item.deliveredAt,
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by createdAt descending (newest first)
|
||||
items.sort((a, b) => b.createdAt - a.createdAt);
|
||||
|
||||
// Apply pagination
|
||||
return items.slice(offset, offset + limit);
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './logs.handler.js';
|
||||
export * from './security.handler.js';
|
||||
export * from './stats.handler.js';
|
||||
export * from './radius.handler.js';
|
||||
export * from './email-ops.handler.js';
|
||||
@@ -68,13 +68,13 @@ export class SmsService {
|
||||
recipients: [{ msisdn: toNumber }],
|
||||
};
|
||||
|
||||
const resp = await plugins.smartrequest.SmartRequestClient.create()
|
||||
const resp = await plugins.smartrequest.SmartRequest.create()
|
||||
.url('https://gatewayapi.com/rest/mtsms')
|
||||
.header('Authorization', `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`)
|
||||
.header('Content-Type', 'application/json')
|
||||
.json(payload)
|
||||
.post();
|
||||
const json = resp.body;
|
||||
const json = await resp.json();
|
||||
logger.log('info', `sent an sms to ${toNumber} with text '${messageText}'`, {
|
||||
eventType: 'sentSms',
|
||||
sms: {
|
||||
|
||||
175
ts_interfaces/readme.md
Normal file
175
ts_interfaces/readme.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# @serve.zone/dcrouter-interfaces
|
||||
|
||||
TypeScript interfaces and type definitions for the DCRouter OpsServer API. 📡
|
||||
|
||||
This module provides strongly-typed interfaces for communicating with the DCRouter OpsServer via TypedRequest. Use these interfaces for type-safe API interactions in your frontend applications or integration code.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @serve.zone/dcrouter-interfaces --save
|
||||
# or
|
||||
pnpm add @serve.zone/dcrouter-interfaces
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
||||
|
||||
// Use data interfaces for type definitions
|
||||
const identity: data.IIdentity = {
|
||||
jwt: 'your-jwt-token',
|
||||
userId: 'user-123',
|
||||
name: 'Admin User',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
// Use request interfaces for API calls
|
||||
const statsRequest: requests.IReq_GetServerStatistics = {
|
||||
method: 'getServerStatistics',
|
||||
request: {
|
||||
identity,
|
||||
includeHistory: true,
|
||||
timeRange: '24h'
|
||||
},
|
||||
response: null // Will be populated by the response
|
||||
};
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
### Data Interfaces (`data`)
|
||||
|
||||
Core data types used throughout the DCRouter system:
|
||||
|
||||
#### `IIdentity`
|
||||
Authentication identity for API requests:
|
||||
```typescript
|
||||
interface IIdentity {
|
||||
jwt: string; // JWT token for authentication
|
||||
userId: string; // Unique user identifier
|
||||
name: string; // Display name
|
||||
expiresAt: number; // Token expiration timestamp
|
||||
role?: string; // User role (e.g., 'admin')
|
||||
type?: string; // Identity type
|
||||
}
|
||||
```
|
||||
|
||||
#### Statistics Interfaces
|
||||
- `IServerStats` - Overall server statistics
|
||||
- `IEmailStats` - Email throughput and delivery metrics
|
||||
- `IDnsStats` - DNS query statistics
|
||||
- `IRateLimitInfo` - Rate limiting status
|
||||
- `ISecurityMetrics` - Security event metrics
|
||||
- `IConnectionInfo` - Active connection details
|
||||
- `IQueueStatus` - Email queue status
|
||||
- `IHealthStatus` - System health information
|
||||
|
||||
### Request Interfaces (`requests`)
|
||||
|
||||
TypedRequest interfaces for the OpsServer API:
|
||||
|
||||
#### Statistics Requests
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetServerStatistics` | `getServerStatistics` | Get overall server stats |
|
||||
| `IReq_GetEmailStatistics` | `getEmailStatistics` | Get email throughput stats |
|
||||
| `IReq_GetDnsStatistics` | `getDnsStatistics` | Get DNS query stats |
|
||||
| `IReq_GetRateLimitStatus` | `getRateLimitStatus` | Check rate limit status |
|
||||
| `IReq_GetSecurityMetrics` | `getSecurityMetrics` | Get security event metrics |
|
||||
| `IReq_GetActiveConnections` | `getActiveConnections` | List active connections |
|
||||
| `IReq_GetQueueStatus` | `getQueueStatus` | Get email queue status |
|
||||
| `IReq_GetHealthStatus` | `getHealthStatus` | System health check |
|
||||
|
||||
#### Admin Requests
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_AdminLogin` | `adminLogin` | Authenticate as admin |
|
||||
| `IReq_AdminLogout` | `adminLogout` | End admin session |
|
||||
| `IReq_VerifyIdentity` | `verifyIdentity` | Verify JWT token |
|
||||
|
||||
#### Configuration Requests
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetConfiguration` | `getConfiguration` | Get current config |
|
||||
| `IReq_UpdateConfiguration` | `updateConfiguration` | Update system config |
|
||||
|
||||
#### Log Requests
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetLogs` | `getLogs` | Retrieve system logs |
|
||||
|
||||
#### RADIUS Requests
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetRadiusSessions` | `getRadiusSessions` | List RADIUS sessions |
|
||||
| `IReq_GetRadiusClients` | `getRadiusClients` | List RADIUS clients |
|
||||
|
||||
#### Email Operations
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetEmailQueues` | `getEmailQueues` | Get email queue details |
|
||||
| `IReq_RetryEmail` | `retryEmail` | Retry failed email |
|
||||
|
||||
## Example: Complete API Integration
|
||||
|
||||
```typescript
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import { data, requests } from '@serve.zone/dcrouter-interfaces';
|
||||
|
||||
// Create typed request client
|
||||
const client = new typedrequest.TypedRequest<requests.IReq_AdminLogin>(
|
||||
'https://your-dcrouter:3000/typedrequest',
|
||||
'adminLogin'
|
||||
);
|
||||
|
||||
// Login to get identity
|
||||
const loginResponse = await client.fire({
|
||||
username: 'admin',
|
||||
password: 'your-password'
|
||||
});
|
||||
|
||||
const identity = loginResponse.identity;
|
||||
|
||||
// Now use identity for authenticated requests
|
||||
const statsClient = new typedrequest.TypedRequest<requests.IReq_GetServerStatistics>(
|
||||
'https://your-dcrouter:3000/typedrequest',
|
||||
'getServerStatistics'
|
||||
);
|
||||
|
||||
const stats = await statsClient.fire({
|
||||
identity,
|
||||
includeHistory: true,
|
||||
timeRange: '24h'
|
||||
});
|
||||
|
||||
console.log('Server stats:', stats.stats);
|
||||
console.log('History:', stats.history);
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
239
ts_interfaces/requests/email-ops.ts
Normal file
239
ts_interfaces/requests/email-ops.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as authInterfaces from '../data/auth.js';
|
||||
|
||||
// ============================================================================
|
||||
// Email Queue Item Interface (matches backend IQueueItem)
|
||||
// ============================================================================
|
||||
export type TEmailQueueStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
|
||||
|
||||
export interface IEmailQueueItem {
|
||||
id: string;
|
||||
processingMode: 'forward' | 'mta' | 'process';
|
||||
status: TEmailQueueStatus;
|
||||
attempts: number;
|
||||
nextAttempt: number; // timestamp
|
||||
lastError?: string;
|
||||
createdAt: number; // timestamp
|
||||
updatedAt: number; // timestamp
|
||||
deliveredAt?: number; // timestamp
|
||||
// Email details extracted from processingResult
|
||||
from?: string;
|
||||
to?: string[];
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Bounce Record Interface (matches backend BounceRecord)
|
||||
// ============================================================================
|
||||
export type TBounceType =
|
||||
| 'invalid_recipient'
|
||||
| 'domain_not_found'
|
||||
| 'mailbox_full'
|
||||
| 'mailbox_inactive'
|
||||
| 'blocked'
|
||||
| 'spam_related'
|
||||
| 'policy_related'
|
||||
| 'server_unavailable'
|
||||
| 'temporary_failure'
|
||||
| 'quota_exceeded'
|
||||
| 'network_error'
|
||||
| 'timeout'
|
||||
| 'auto_response'
|
||||
| 'challenge_response'
|
||||
| 'unknown';
|
||||
|
||||
export type TBounceCategory = 'hard' | 'soft' | 'auto_response' | 'unknown';
|
||||
|
||||
export interface IBounceRecord {
|
||||
id: string;
|
||||
originalEmailId?: string;
|
||||
recipient: string;
|
||||
sender: string;
|
||||
domain: string;
|
||||
subject?: string;
|
||||
bounceType: TBounceType;
|
||||
bounceCategory: TBounceCategory;
|
||||
timestamp: number;
|
||||
smtpResponse?: string;
|
||||
diagnosticCode?: string;
|
||||
statusCode?: string;
|
||||
processed: boolean;
|
||||
retryCount?: number;
|
||||
nextRetryTime?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Security Incident Interface (matches backend ISecurityEvent)
|
||||
// ============================================================================
|
||||
export type TSecurityLogLevel = 'info' | 'warn' | 'error' | 'critical';
|
||||
|
||||
export type TSecurityEventType =
|
||||
| 'authentication'
|
||||
| 'access_control'
|
||||
| 'email_validation'
|
||||
| 'email_processing'
|
||||
| 'email_forwarding'
|
||||
| 'email_delivery'
|
||||
| 'dkim'
|
||||
| 'spf'
|
||||
| 'dmarc'
|
||||
| 'rate_limit'
|
||||
| 'rate_limiting'
|
||||
| 'spam'
|
||||
| 'malware'
|
||||
| 'connection'
|
||||
| 'data_exposure'
|
||||
| 'configuration'
|
||||
| 'ip_reputation'
|
||||
| 'rejected_connection';
|
||||
|
||||
export interface ISecurityIncident {
|
||||
timestamp: number;
|
||||
level: TSecurityLogLevel;
|
||||
type: TSecurityEventType;
|
||||
message: string;
|
||||
details?: any;
|
||||
ipAddress?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
emailId?: string;
|
||||
domain?: string;
|
||||
action?: string;
|
||||
result?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Queued Emails Request
|
||||
// ============================================================================
|
||||
export interface IReq_GetQueuedEmails extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetQueuedEmails
|
||||
> {
|
||||
method: 'getQueuedEmails';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
status?: TEmailQueueStatus;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
response: {
|
||||
items: IEmailQueueItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Sent Emails Request
|
||||
// ============================================================================
|
||||
export interface IReq_GetSentEmails extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetSentEmails
|
||||
> {
|
||||
method: 'getSentEmails';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
response: {
|
||||
items: IEmailQueueItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Failed Emails Request
|
||||
// ============================================================================
|
||||
export interface IReq_GetFailedEmails extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetFailedEmails
|
||||
> {
|
||||
method: 'getFailedEmails';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
response: {
|
||||
items: IEmailQueueItem[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Resend Failed Email Request
|
||||
// ============================================================================
|
||||
export interface IReq_ResendEmail extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ResendEmail
|
||||
> {
|
||||
method: 'resendEmail';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
emailId: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
newQueueId?: string;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Security Incidents Request
|
||||
// ============================================================================
|
||||
export interface IReq_GetSecurityIncidents extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetSecurityIncidents
|
||||
> {
|
||||
method: 'getSecurityIncidents';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
type?: TSecurityEventType;
|
||||
level?: TSecurityLogLevel;
|
||||
limit?: number;
|
||||
};
|
||||
response: {
|
||||
incidents: ISecurityIncident[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Get Bounce Records Request
|
||||
// ============================================================================
|
||||
export interface IReq_GetBounceRecords extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetBounceRecords
|
||||
> {
|
||||
method: 'getBounceRecords';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
response: {
|
||||
records: IBounceRecord[];
|
||||
suppressionList: string[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Remove from Suppression List Request
|
||||
// ============================================================================
|
||||
export interface IReq_RemoveFromSuppressionList extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_RemoveFromSuppressionList
|
||||
> {
|
||||
method: 'removeFromSuppressionList';
|
||||
request: {
|
||||
identity?: authInterfaces.IIdentity;
|
||||
email: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './logs.js';
|
||||
export * from './stats.js';
|
||||
export * from './combined.stats.js';
|
||||
export * from './radius.js';
|
||||
export * from './email-ops.js';
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '2.13.0',
|
||||
version: '3.1.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -53,6 +53,20 @@ export interface INetworkState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface IEmailOpsState {
|
||||
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
||||
queuedEmails: interfaces.requests.IEmailQueueItem[];
|
||||
sentEmails: interfaces.requests.IEmailQueueItem[];
|
||||
failedEmails: interfaces.requests.IEmailQueueItem[];
|
||||
securityIncidents: interfaces.requests.ISecurityIncident[];
|
||||
bounceRecords: interfaces.requests.IBounceRecord[];
|
||||
suppressionList: string[];
|
||||
selectedEmailId: string | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
// Create state parts with appropriate persistence
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
@@ -60,7 +74,7 @@ export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'soft' // Login state persists across sessions
|
||||
'persistent' // Login state persists across browser sessions
|
||||
);
|
||||
|
||||
export const statsStatePart = await appState.getStatePart<IStatsState>(
|
||||
@@ -86,10 +100,19 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
||||
}
|
||||
);
|
||||
|
||||
// Determine initial view from URL path
|
||||
const getInitialView = (): string => {
|
||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'];
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
return validViews.includes(view) ? view : 'overview';
|
||||
};
|
||||
|
||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
'ui',
|
||||
{
|
||||
activeView: 'overview',
|
||||
activeView: getInitialView(),
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 1000, // 1 second
|
||||
@@ -121,6 +144,24 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
'soft'
|
||||
);
|
||||
|
||||
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||
'emailOps',
|
||||
{
|
||||
currentView: 'queued',
|
||||
queuedEmails: [],
|
||||
sentEmails: [],
|
||||
failedEmails: [],
|
||||
securityIncidents: [],
|
||||
bounceRecords: [],
|
||||
suppressionList: [],
|
||||
selectedEmailId: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: 0,
|
||||
},
|
||||
'soft'
|
||||
);
|
||||
|
||||
// Actions for state management
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
@@ -397,6 +438,238 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Email Operations Actions
|
||||
// ============================================================================
|
||||
|
||||
// Set Email Ops View Action
|
||||
export const setEmailOpsViewAction = emailOpsStatePart.createAction<IEmailOpsState['currentView']>(
|
||||
async (statePartArg, view) => {
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
currentView: view,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch Queued Emails Action
|
||||
export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetQueuedEmails
|
||||
>('/typedrequest', 'getQueuedEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
status: 'pending',
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
queuedEmails: response.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch queued emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Sent Emails Action
|
||||
export const fetchSentEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSentEmails
|
||||
>('/typedrequest', 'getSentEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
sentEmails: response.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch sent emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Failed Emails Action
|
||||
export const fetchFailedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetFailedEmails
|
||||
>('/typedrequest', 'getFailedEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
failedEmails: response.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch failed emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Security Incidents Action
|
||||
export const fetchSecurityIncidentsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSecurityIncidents
|
||||
>('/typedrequest', 'getSecurityIncidents');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
securityIncidents: response.incidents,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch security incidents',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Bounce Records Action
|
||||
export const fetchBounceRecordsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBounceRecords
|
||||
>('/typedrequest', 'getBounceRecords');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
bounceRecords: response.records,
|
||||
suppressionList: response.suppressionList,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch bounce records',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Resend Failed Email Action
|
||||
export const resendEmailAction = emailOpsStatePart.createAction<string>(async (statePartArg, emailId) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ResendEmail
|
||||
>('/typedrequest', 'resendEmail');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
emailId,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Refresh failed emails list
|
||||
await emailOpsStatePart.dispatchAction(fetchFailedEmailsAction, null);
|
||||
await emailOpsStatePart.dispatchAction(fetchQueuedEmailsAction, null);
|
||||
}
|
||||
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to resend email',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Remove from Suppression List Action
|
||||
export const removeFromSuppressionListAction = emailOpsStatePart.createAction<string>(
|
||||
async (statePartArg, email) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RemoveFromSuppressionList
|
||||
>('/typedrequest', 'removeFromSuppressionList');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
email,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Refresh bounce records
|
||||
await emailOpsStatePart.dispatchAction(fetchBounceRecordsAction, null);
|
||||
}
|
||||
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to remove from suppression list',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -62,6 +63,14 @@ export class OpsDashboard extends DeesElement {
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the current view tab based on the UI state's activeView.
|
||||
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
|
||||
*/
|
||||
private get currentViewTab() {
|
||||
return this.viewTabs.find(t => t.name.toLowerCase() === this.uiState.activeView) || this.viewTabs[0];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
document.title = 'DCRouter OpsServer';
|
||||
@@ -84,17 +93,55 @@ export class OpsDashboard extends DeesElement {
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
// Sync appdash view when state changes (e.g., from URL navigation)
|
||||
this.syncAppdashView(uiState.activeView);
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the dees-simple-appdash view selection with the current state.
|
||||
* This is needed when the URL changes and we need to update the UI.
|
||||
*/
|
||||
private syncAppdashView(viewName: string): void {
|
||||
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||
if (!appDash) return;
|
||||
|
||||
const targetTab = this.viewTabs.find(t => t.name.toLowerCase() === viewName);
|
||||
if (!targetTab) return;
|
||||
|
||||
// Check if we need to switch (avoid unnecessary updates)
|
||||
if (appDash.selectedView === targetTab) return;
|
||||
|
||||
// Update the selected view programmatically
|
||||
appDash.selectedView = targetTab;
|
||||
|
||||
// Update the displayed content
|
||||
const content = appDash.shadowRoot?.querySelector('.appcontent');
|
||||
if (content) {
|
||||
if (appDash.currentView) {
|
||||
appDash.currentView.remove();
|
||||
}
|
||||
const view = new targetTab.element();
|
||||
content.appendChild(view);
|
||||
appDash.currentView = view;
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -108,6 +155,7 @@ export class OpsDashboard extends DeesElement {
|
||||
<dees-simple-appdash
|
||||
name="DCRouter OpsServer"
|
||||
.viewTabs=${this.viewTabs}
|
||||
.selectedView=${this.currentViewTab}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
@@ -126,8 +174,9 @@ export class OpsDashboard extends DeesElement {
|
||||
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
|
||||
if (appDash) {
|
||||
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||
const viewName = e.detail.view.name;
|
||||
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
|
||||
const viewName = e.detail.view.name.toLowerCase();
|
||||
// Use router for navigation instead of direct state update
|
||||
appRouter.navigateToView(viewName);
|
||||
});
|
||||
|
||||
// Handle logout event
|
||||
@@ -136,14 +185,20 @@ export class OpsDashboard extends DeesElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle initial state
|
||||
// Handle initial state - check if we have a stored session that's still valid
|
||||
const loginState = appstate.loginStatePart.getState();
|
||||
// Check initial login state
|
||||
if (loginState.identity) {
|
||||
if (loginState.identity?.jwt) {
|
||||
// Verify JWT hasn't expired
|
||||
if (loginState.identity.expiresAt > Date.now()) {
|
||||
// JWT still valid, restore logged-in state
|
||||
this.loginState = loginState;
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
} else {
|
||||
// JWT expired, clear the stored state
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,11 +155,10 @@ export class OpsViewConfig extends DeesElement {
|
||||
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
|
||||
</div>
|
||||
|
||||
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)}
|
||||
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)}
|
||||
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)}
|
||||
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)}
|
||||
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
|
||||
${this.renderConfigSection('email', 'Email Configuration', this.configState.config?.email)}
|
||||
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config?.dns)}
|
||||
${this.renderConfigSection('proxy', 'Proxy Configuration', this.configState.config?.proxy)}
|
||||
${this.renderConfigSection('security', 'Security Configuration', this.configState.config?.security)}
|
||||
` : html`
|
||||
<div class="errorMessage">No configuration loaded</div>
|
||||
`}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@ import * as plugins from './plugins.js';
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
import './elements/index.js';
|
||||
import { appRouter } from './router.js';
|
||||
|
||||
// Initialize router before rendering
|
||||
appRouter.init();
|
||||
|
||||
plugins.deesElement.render(html`
|
||||
<ops-dashboard></ops-dashboard>
|
||||
|
||||
231
ts_web/readme.md
Normal file
231
ts_web/readme.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# @serve.zone/dcrouter-web
|
||||
|
||||
Web-based Operations Dashboard for DCRouter. 🖥️
|
||||
|
||||
This module provides the frontend web application for the DCRouter OpsServer, built with modern web components using the `@design.estate/dees-element` library. It offers a comprehensive dashboard for monitoring and managing your DCRouter instance in real-time.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Features
|
||||
|
||||
### 🔐 **Secure Authentication**
|
||||
- JWT-based authentication flow
|
||||
- Automatic session management
|
||||
- Secure login with username/password
|
||||
|
||||
### 📊 **Overview Dashboard**
|
||||
- Real-time server statistics
|
||||
- Connection monitoring
|
||||
- Email throughput visualization
|
||||
- DNS query metrics
|
||||
- RADIUS session tracking
|
||||
|
||||
### 🌐 **Network View**
|
||||
- Active connection monitoring
|
||||
- SmartProxy route visualization
|
||||
- TCP/HTTP connection details
|
||||
- TLS certificate status
|
||||
|
||||
### 📧 **Email Management**
|
||||
- Email queue monitoring
|
||||
- Delivery status tracking
|
||||
- Bounce management
|
||||
- DKIM key status
|
||||
- Domain configuration overview
|
||||
|
||||
### 📜 **Log Viewer**
|
||||
- Real-time log streaming
|
||||
- Log level filtering (error, warning, info, debug)
|
||||
- Search and filter capabilities
|
||||
- Time-range selection
|
||||
|
||||
### ⚙️ **Configuration**
|
||||
- View current system configuration
|
||||
- Update settings via TypedRequest API
|
||||
- Route management
|
||||
- Domain management
|
||||
|
||||
### 🛡️ **Security Dashboard**
|
||||
- IP reputation monitoring
|
||||
- Rate limit status
|
||||
- Blocked connections
|
||||
- Security event tracking
|
||||
|
||||
## Architecture
|
||||
|
||||
The web application is built using:
|
||||
|
||||
- **@design.estate/dees-element** - Modern web component framework with lit-element
|
||||
- **@design.estate/dees-catalog** - Pre-built UI components (appdash, login, forms)
|
||||
- **@push.rocks/smartstate** - Reactive state management
|
||||
- **@api.global/typedrequest** - Type-safe API communication
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
ts_web/
|
||||
├── index.ts # Application entry point
|
||||
├── plugins.ts # External dependency imports
|
||||
├── router.ts # Client-side routing
|
||||
├── appstate.ts # State management
|
||||
└── elements/
|
||||
├── index.ts # Component barrel export
|
||||
├── ops-dashboard.ts # Main dashboard container
|
||||
├── ops-view-overview.ts # Overview statistics
|
||||
├── ops-view-network.ts # Network monitoring
|
||||
├── ops-view-emails.ts # Email management
|
||||
├── ops-view-logs.ts # Log viewer
|
||||
├── ops-view-config.ts # Configuration
|
||||
├── ops-view-security.ts # Security dashboard
|
||||
└── shared/
|
||||
├── index.ts # Shared component exports
|
||||
├── css.ts # Shared styles
|
||||
└── ops-sectionheading.ts # Section heading component
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
The application uses `@push.rocks/smartstate` for reactive state management:
|
||||
|
||||
### State Parts
|
||||
|
||||
```typescript
|
||||
// Login state
|
||||
interface ILoginState {
|
||||
identity: IIdentity | null;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
// UI state
|
||||
interface IUiState {
|
||||
activeView: string;
|
||||
sidebarCollapsed: boolean;
|
||||
autoRefresh: boolean;
|
||||
refreshInterval: number;
|
||||
theme: 'light' | 'dark';
|
||||
}
|
||||
|
||||
// Statistics state
|
||||
interface IStatsState {
|
||||
server: IServerStats;
|
||||
email: IEmailStats;
|
||||
dns: IDnsStats;
|
||||
connections: IConnectionInfo[];
|
||||
health: IHealthStatus;
|
||||
}
|
||||
|
||||
// Configuration state
|
||||
interface IConfigState {
|
||||
configuration: IDcRouterConfig;
|
||||
loading: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
- `loginAction` - Authenticate user
|
||||
- `logoutAction` - End session
|
||||
- `fetchAllStatsAction` - Refresh all statistics
|
||||
- `fetchConfigurationAction` - Load configuration
|
||||
- `updateConfigurationAction` - Save configuration changes
|
||||
|
||||
## Client-Side Routing
|
||||
|
||||
The application includes client-side routing for deep linking:
|
||||
|
||||
```typescript
|
||||
// Routes
|
||||
/overview → Overview dashboard
|
||||
/network → Network monitoring
|
||||
/emails → Email management
|
||||
/logs → Log viewer
|
||||
/configuration → System configuration
|
||||
/security → Security dashboard
|
||||
```
|
||||
|
||||
URL state is synchronized with the UI, allowing bookmarking and sharing of specific views.
|
||||
|
||||
## Building
|
||||
|
||||
The web application is built using `@git.zone/tsbundle`:
|
||||
|
||||
```bash
|
||||
# Build the bundle
|
||||
pnpm run bundle
|
||||
|
||||
# Watch for development
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
The bundle is output to `./dist_serve/bundle.js` and served by the OpsServer.
|
||||
|
||||
## Development
|
||||
|
||||
### Local Development
|
||||
|
||||
```typescript
|
||||
// Start DCRouter with OpsServer enabled
|
||||
import { DcRouter } from '@serve.zone/dcrouter';
|
||||
|
||||
const router = new DcRouter({
|
||||
opsServerConfig: {
|
||||
port: 3000,
|
||||
admin: {
|
||||
username: 'admin',
|
||||
password: 'dev-password'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await router.start();
|
||||
// Dashboard at http://localhost:3000
|
||||
```
|
||||
|
||||
### Adding New Views
|
||||
|
||||
1. Create a new view component in `elements/`:
|
||||
```typescript
|
||||
import { DeesElement, customElement, html } from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-myview')
|
||||
export class OpsViewMyView extends DeesElement {
|
||||
public render() {
|
||||
return html`<div>My custom view</div>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Import and add to `ops-dashboard.ts`:
|
||||
```typescript
|
||||
import { OpsViewMyView } from './ops-view-myview.js';
|
||||
|
||||
private viewTabs = [
|
||||
// ... existing tabs
|
||||
{ name: 'MyView', element: OpsViewMyView }
|
||||
];
|
||||
```
|
||||
|
||||
3. Add route in `router.ts` if needed.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
181
ts_web/router.ts
Normal file
181
ts_web/router.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as appstate from './appstate.js';
|
||||
|
||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||
|
||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security'] as const;
|
||||
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
||||
|
||||
export type TValidView = typeof validViews[number];
|
||||
export type TValidEmailFolder = typeof validEmailFolders[number];
|
||||
|
||||
class AppRouter {
|
||||
private router: InstanceType<typeof SmartRouter>;
|
||||
private initialized = false;
|
||||
private suppressStateUpdate = false;
|
||||
|
||||
constructor() {
|
||||
this.router = new SmartRouter({ debug: false });
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
if (this.initialized) return;
|
||||
this.setupRoutes();
|
||||
this.setupStateSync();
|
||||
this.handleInitialRoute();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Main views
|
||||
for (const view of validViews) {
|
||||
if (view === 'emails') {
|
||||
// Email root - default to queued
|
||||
this.router.on('/emails', async () => {
|
||||
this.updateViewState('emails');
|
||||
this.updateEmailFolder('queued');
|
||||
});
|
||||
|
||||
// Email with folder parameter
|
||||
this.router.on('/emails/:folder', async (routeInfo) => {
|
||||
const folder = routeInfo.params.folder as string;
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.updateViewState('emails');
|
||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
||||
} else {
|
||||
// Invalid folder, redirect to queued
|
||||
this.navigateTo('/emails/queued');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.router.on(`/${view}`, async () => {
|
||||
this.updateViewState(view);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Root redirect
|
||||
this.router.on('/', async () => {
|
||||
this.navigateTo('/overview');
|
||||
});
|
||||
}
|
||||
|
||||
private setupStateSync(): void {
|
||||
// Sync URL when state changes programmatically (not from router)
|
||||
appstate.uiStatePart.state.subscribe((uiState) => {
|
||||
if (this.suppressStateUpdate) return;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const expectedPath = this.getExpectedPath(uiState.activeView);
|
||||
|
||||
// Only update URL if it doesn't match current state
|
||||
if (!currentPath.startsWith(expectedPath)) {
|
||||
this.suppressStateUpdate = true;
|
||||
if (uiState.activeView === 'emails') {
|
||||
const emailState = appstate.emailOpsStatePart.getState();
|
||||
this.router.pushUrl(`/emails/${emailState.currentView}`);
|
||||
} else {
|
||||
this.router.pushUrl(`/${uiState.activeView}`);
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getExpectedPath(view: string): string {
|
||||
if (view === 'emails') {
|
||||
return '/emails';
|
||||
}
|
||||
return `/${view}`;
|
||||
}
|
||||
|
||||
private handleInitialRoute(): void {
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (!path || path === '/') {
|
||||
// Redirect root to overview
|
||||
this.router.pushUrl('/overview');
|
||||
} else {
|
||||
// Parse current path and update state
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.updateViewState(view as TValidView);
|
||||
|
||||
if (view === 'emails' && segments[1]) {
|
||||
const folder = segments[1];
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
||||
} else {
|
||||
this.updateEmailFolder('queued');
|
||||
}
|
||||
} else if (view === 'emails') {
|
||||
this.updateEmailFolder('queued');
|
||||
}
|
||||
} else {
|
||||
// Invalid view, redirect to overview
|
||||
this.router.pushUrl('/overview');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateViewState(view: string): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.uiStatePart.getState();
|
||||
if (currentState.activeView !== view) {
|
||||
appstate.uiStatePart.setState({
|
||||
...currentState,
|
||||
activeView: view,
|
||||
});
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
|
||||
private updateEmailFolder(folder: TValidEmailFolder): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.emailOpsStatePart.getState();
|
||||
if (currentState.currentView !== folder) {
|
||||
appstate.emailOpsStatePart.setState({
|
||||
...currentState,
|
||||
currentView: folder as appstate.IEmailOpsState['currentView'],
|
||||
});
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
|
||||
public navigateTo(path: string): void {
|
||||
this.router.pushUrl(path);
|
||||
}
|
||||
|
||||
public navigateToView(view: string): void {
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.navigateTo(`/${view}`);
|
||||
} else {
|
||||
this.navigateTo('/overview');
|
||||
}
|
||||
}
|
||||
|
||||
public navigateToEmailFolder(folder: string): void {
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.navigateTo(`/emails/${folder}`);
|
||||
} else {
|
||||
this.navigateTo('/emails/queued');
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentView(): string {
|
||||
return appstate.uiStatePart.getState().activeView;
|
||||
}
|
||||
|
||||
public getCurrentEmailFolder(): string {
|
||||
return appstate.emailOpsStatePart.getState().currentView;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.router.destroy();
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const appRouter = new AppRouter();
|
||||
Reference in New Issue
Block a user