Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c0e46ff4e | |||
| f62bc4a526 | |||
| 8f23600ec1 | |||
| 141f185fbf | |||
| 6f4a5f19e7 | |||
| 9d8354e58f | |||
| 947637eed7 | |||
| 5202c2ea27 | |||
| 6684dc43da | |||
| 04ec387ce5 |
36
changelog.md
36
changelog.md
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-02 - 12.5.0 - feat(ops-view-routes)
|
||||
add priority support and list-based domain editing for routes
|
||||
|
||||
- Adds a priority field to route create and edit forms so route matching order can be configured.
|
||||
- Replaces comma-separated domain text input with a list-based domain editor and updates form handling to persist domains as arrays.
|
||||
|
||||
## 2026-04-02 - 12.4.0 - feat(routes)
|
||||
add route edit and delete actions to the ops routes view
|
||||
|
||||
- introduces an update route action in web app state and refreshes merged routes after changes
|
||||
- adds edit and delete handlers with modal-based confirmation and route form inputs for programmatic routes
|
||||
- enables realtime chart window configuration in network and overview dashboards
|
||||
- bumps @serve.zone/catalog to ^2.11.0
|
||||
|
||||
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
|
||||
document unified database and reusable security profile and network target management
|
||||
|
||||
- Update project and interface documentation to replace separate storage/cache configuration with a unified database model
|
||||
- Document new security profile and network target APIs, data models, and dashboard capabilities
|
||||
- Add a global dashboard warning when the database is disabled so unavailable management features are clearly indicated
|
||||
- Bump @design.estate/dees-catalog and @serve.zone/catalog to support the updated dashboard experience
|
||||
|
||||
## 2026-04-02 - 12.2.6 - fix(ops-ui)
|
||||
improve operations table actions and modal form handling for profiles and network targets
|
||||
|
||||
- adds section headings for the Security Profiles and Network Targets views
|
||||
- updates edit and delete actions to support in-row table actions in addition to context menus
|
||||
- makes create and edit dialogs query forms safely from modal content and adds early returns when forms are unavailable
|
||||
- enables the database configuration in the development watch server
|
||||
|
||||
## 2026-04-02 - 12.2.5 - fix(dcrouter)
|
||||
sync allowed tunnel edges when merged routes change
|
||||
|
||||
- Triggers tunnelManager.syncAllowedEdges() after route updates are applied
|
||||
- Keeps derived ports in the Rust hub binary aligned with merged route changes
|
||||
|
||||
## 2026-04-02 - 12.2.4 - fix(routes)
|
||||
support profile and target metadata in route creation and refresh remote ingress routes after config initialization
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "12.2.4",
|
||||
"version": "12.5.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@api.global/typedsocket": "^4.1.2",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.49.2",
|
||||
"@design.estate/dees-catalog": "^3.50.2",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@push.rocks/lik": "^6.4.0",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartvpn": "1.19.1",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.9.1",
|
||||
"@serve.zone/catalog": "^2.11.0",
|
||||
"@serve.zone/interfaces": "^5.3.0",
|
||||
"@serve.zone/remoteingress": "^4.15.3",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.49.2
|
||||
version: 3.49.2(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.50.2
|
||||
version: 3.50.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -102,8 +102,8 @@ importers:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
'@serve.zone/catalog':
|
||||
specifier: ^2.9.1
|
||||
version: 2.9.1(@tiptap/pm@2.27.2)
|
||||
specifier: ^2.11.0
|
||||
version: 2.11.0(@tiptap/pm@2.27.2)
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -350,8 +350,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.49.2':
|
||||
resolution: {integrity: sha512-ChVf5IW/w1WSsfuI3BA1SX2QJFjZljnAvnyPDXnbzTXuOdTgs054p66JwlDca9KM8yBlndwibgAYJfD6/4sONw==}
|
||||
'@design.estate/dees-catalog@3.50.2':
|
||||
resolution: {integrity: sha512-oxB1kB3IxEwHgf+DjytTBilkDVVb8hryq465OhhzgBiJiHaNLPyBASAQaNTVp6eaORQGzyCmy/ac/GdQglZiIg==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -1583,8 +1583,8 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@serve.zone/catalog@2.9.1':
|
||||
resolution: {integrity: sha512-W+4x5O834DiEtcqfVNFrP6qYlj/6c9UTR9oEl2Wthf0R2SN0zC6Hbs16US2kP+mmQBKAIDMQYvTUI9oaXqvcog==}
|
||||
'@serve.zone/catalog@2.11.0':
|
||||
resolution: {integrity: sha512-4DFDewp1PFRhw5P+yQAoAw+i6gG2lfR3h+uPgbNxB5jCfW14eNDXi3nuwTMBQWRHL9jv8o0BokASjV9A0+q66g==}
|
||||
|
||||
'@serve.zone/interfaces@5.3.0':
|
||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||
@@ -4260,13 +4260,11 @@ packages:
|
||||
|
||||
xterm-addon-fit@0.8.0:
|
||||
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
|
||||
peerDependencies:
|
||||
xterm: ^5.0.0
|
||||
|
||||
xterm@5.3.0:
|
||||
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
|
||||
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
@@ -4341,7 +4339,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
||||
'@cloudflare/workers-types': 4.20260317.1
|
||||
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.0
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
@@ -4870,7 +4868,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.49.2(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.50.2(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
@@ -4978,7 +4976,7 @@ snapshots:
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.3
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
lit: 3.3.2
|
||||
@@ -6906,9 +6904,9 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@serve.zone/catalog@2.9.1(@tiptap/pm@2.27.2)':
|
||||
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.49.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.8.0
|
||||
|
||||
140
readme.md
140
readme.md
@@ -93,10 +93,11 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Socket-handler mode** — direct socket passing eliminates internal port hops
|
||||
- **Real-time metrics** via SmartMetrics (CPU, memory, connections, throughput)
|
||||
|
||||
### 💾 Persistent Storage & Caching
|
||||
- **Multiple storage backends**: filesystem, custom functions, or in-memory
|
||||
- **Embedded cache database** via smartdata + smartdb (MongoDB-compatible)
|
||||
### 💾 Unified Database
|
||||
- **Two deployment modes**: embedded LocalSmartDb (zero-config) or external MongoDB
|
||||
- **15 document classes** covering routes, certs, VPN, RADIUS, security profiles, network targets, and caches
|
||||
- **Automatic TTL-based cleanup** for cached emails and IP reputation data
|
||||
- **Reusable references** — security profiles and network targets that propagate changes to all referencing routes
|
||||
|
||||
### 🖥️ OpsServer Dashboard
|
||||
- **Web-based management interface** with real-time monitoring
|
||||
@@ -104,7 +105,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, remote ingress edges, VPN clients, and security events
|
||||
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
||||
- **Remote ingress management** with connection token generation and one-click copy
|
||||
- **Read-only configuration display** — DcRouter is configured through code
|
||||
- **Security profiles & network targets** — reusable security configurations and host:port targets with propagation to referencing routes
|
||||
- **Global warning banners** when database is disabled (management features unavailable)
|
||||
- **Read-only configuration display** for system overview
|
||||
- **Smart tab visibility handling** — auto-pauses all polling, WebSocket connections, and chart updates when the browser tab is hidden, preventing resource waste and tab freezing
|
||||
|
||||
### 🔧 Programmatic API Client
|
||||
@@ -269,11 +272,8 @@ const router = new DcRouter({
|
||||
],
|
||||
},
|
||||
|
||||
// Persistent storage
|
||||
storage: { fsPath: '/var/lib/dcrouter/data' },
|
||||
|
||||
// Cache database
|
||||
cacheConfig: { enabled: true, storagePath: '~/.serve.zone/dcrouter/tsmdb' },
|
||||
// Unified database (embedded LocalSmartDb or external MongoDB)
|
||||
dbConfig: { enabled: true },
|
||||
|
||||
// TLS & ACME
|
||||
tls: { contactEmail: 'admin@example.com' },
|
||||
@@ -311,8 +311,7 @@ graph TB
|
||||
CM[Certificate Manager<br/><i>smartacme v9</i>]
|
||||
OS[OpsServer Dashboard]
|
||||
MM[Metrics Manager]
|
||||
SM[Storage Manager]
|
||||
CD[Cache Database]
|
||||
DB2[DcRouterDb<br/><i>smartdata + smartdb</i>]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
@@ -339,8 +338,7 @@ graph TB
|
||||
DC --> CM
|
||||
DC --> OS
|
||||
DC --> MM
|
||||
DC --> SM
|
||||
DC --> CD
|
||||
DC --> DB2
|
||||
|
||||
SP --> WEB
|
||||
SP --> API
|
||||
@@ -365,8 +363,7 @@ graph TB
|
||||
| **RemoteIngress** | `@serve.zone/remoteingress` | Distributed edge tunneling with Rust data plane and TS management |
|
||||
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
||||
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
||||
| **StorageManager** | built-in | Pluggable key-value storage (filesystem, custom, or in-memory) |
|
||||
| **CacheDb** | `@push.rocks/smartdb` | Embedded MongoDB-compatible database (LocalSmartDb) for persistent caching |
|
||||
| **DcRouterDb** | `@push.rocks/smartdata` + `@push.rocks/smartdb` | Unified database — embedded LocalSmartDb or external MongoDB for all persistence |
|
||||
|
||||
### How It Works
|
||||
|
||||
@@ -509,24 +506,16 @@ interface IDcRouterOptions {
|
||||
};
|
||||
dnsChallenge?: { cloudflareApiKey?: string };
|
||||
|
||||
// ── Storage & Caching ─────────────────────────────────────────
|
||||
storage?: {
|
||||
fsPath?: string;
|
||||
readFunction?: (key: string) => Promise<string>;
|
||||
writeFunction?: (key: string, value: string) => Promise<void>;
|
||||
};
|
||||
cacheConfig?: {
|
||||
// ── Database ────────────────────────────────────────────────────
|
||||
/** Unified database for all persistence (routes, certs, VPN, RADIUS, etc.) */
|
||||
dbConfig?: {
|
||||
enabled?: boolean; // default: true
|
||||
mongoDbUrl?: string; // External MongoDB URL (omit for embedded LocalSmartDb)
|
||||
storagePath?: string; // default: '~/.serve.zone/dcrouter/tsmdb'
|
||||
dbName?: string; // default: 'dcrouter'
|
||||
cleanupIntervalHours?: number; // default: 1
|
||||
ttlConfig?: {
|
||||
emails?: number; // default: 30 days
|
||||
ipReputation?: number; // default: 1 day
|
||||
bounces?: number; // default: 30 days
|
||||
dkimKeys?: number; // default: 90 days
|
||||
suppression?: number; // default: 30 days
|
||||
};
|
||||
seedOnEmpty?: boolean; // Seed default profiles/targets if DB is empty
|
||||
seedData?: object; // Custom seed data
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1213,49 +1202,55 @@ The OpsServer includes a **Certificates** view showing:
|
||||
- One-click reprovisioning per domain
|
||||
- Certificate import and export
|
||||
|
||||
## Storage & Caching
|
||||
## Storage & Database
|
||||
|
||||
### StorageManager
|
||||
DcRouter uses a **unified database** (`DcRouterDb`) powered by [`@push.rocks/smartdata`](https://code.foss.global/push.rocks/smartdata) + [`@push.rocks/smartdb`](https://code.foss.global/push.rocks/smartdb) for all persistence. It supports two modes:
|
||||
|
||||
Provides a unified key-value interface with three backends:
|
||||
### Embedded LocalSmartDb (Default)
|
||||
|
||||
Zero-config, file-based MongoDB-compatible database — no external services needed:
|
||||
|
||||
```typescript
|
||||
// Filesystem backend
|
||||
storage: { fsPath: '/var/lib/dcrouter/data' }
|
||||
|
||||
// Custom backend (Redis, S3, etc.)
|
||||
storage: {
|
||||
readFunction: async (key) => await redis.get(key),
|
||||
writeFunction: async (key, value) => await redis.set(key, value)
|
||||
}
|
||||
|
||||
// In-memory (development only — data lost on restart)
|
||||
// Simply omit the storage config
|
||||
dbConfig: { enabled: true }
|
||||
// Data stored at ~/.serve.zone/dcrouter/tsmdb by default
|
||||
```
|
||||
|
||||
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state, remote ingress edge registrations.
|
||||
### External MongoDB
|
||||
|
||||
### Cache Database
|
||||
|
||||
An embedded MongoDB-compatible database (via smartdata + smartdb) for persistent caching with automatic TTL cleanup:
|
||||
Connect to an existing MongoDB instance:
|
||||
|
||||
```typescript
|
||||
cacheConfig: {
|
||||
dbConfig: {
|
||||
enabled: true,
|
||||
storagePath: '~/.serve.zone/dcrouter/tsmdb',
|
||||
mongoDbUrl: 'mongodb://localhost:27017',
|
||||
dbName: 'dcrouter',
|
||||
cleanupIntervalHours: 1,
|
||||
ttlConfig: {
|
||||
emails: 30, // days
|
||||
ipReputation: 1, // days
|
||||
bounces: 30, // days
|
||||
dkimKeys: 90, // days
|
||||
suppression: 30 // days
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cached document types: `CachedEmail`, `CachedIPReputation`.
|
||||
### Disabling the Database
|
||||
|
||||
For static, constructor-only deployments where no runtime management is needed:
|
||||
|
||||
```typescript
|
||||
dbConfig: { enabled: false }
|
||||
// Routes come exclusively from constructor config — no CRUD, no persistence
|
||||
// OpsServer still runs but management features are disabled
|
||||
```
|
||||
|
||||
### What's Stored
|
||||
|
||||
DcRouterDb persists all runtime state across 15 document classes:
|
||||
|
||||
| Category | Documents | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| **Routes** | `StoredRouteDoc`, `RouteOverrideDoc` | Programmatic routes and hardcoded route overrides |
|
||||
| **Certificates** | `ProxyCertDoc`, `AcmeCertDoc`, `CertBackoffDoc` | TLS certs, ACME state, per-domain backoff |
|
||||
| **Auth** | `ApiTokenDoc` | API token storage |
|
||||
| **Remote Ingress** | `RemoteIngressEdgeDoc` | Edge node registrations |
|
||||
| **VPN** | `VpnServerKeysDoc`, `VpnClientDoc` | Server keys and client registrations |
|
||||
| **RADIUS** | `VlanMappingsDoc`, `AccountingSessionDoc` | VLAN mappings and accounting sessions |
|
||||
| **References** | `SecurityProfileDoc`, `NetworkTargetDoc` | Reusable security profiles and network targets |
|
||||
| **Cache** | `CachedEmailDoc`, `CachedIpReputationDoc` | TTL-based caches with automatic cleanup |
|
||||
|
||||
## Security Features
|
||||
|
||||
@@ -1324,6 +1319,8 @@ The OpsServer provides a web-based management interface served on port 3000 by d
|
||||
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning, import/export |
|
||||
| 🌍 **RemoteIngress** | Edge node management, connection status, token generation, enable/disable |
|
||||
| 🔐 **VPN** | VPN client management, server status, create/toggle/export/rotate/delete clients |
|
||||
| 🛡️ **Security Profiles** | Reusable security configurations (IP allow/block lists, rate limits) |
|
||||
| 🎯 **Network Targets** | Reusable host:port destinations for route references |
|
||||
| 📡 **RADIUS** | NAS client management, VLAN mappings, session monitoring, accounting |
|
||||
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
||||
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
||||
@@ -1410,6 +1407,22 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
||||
'setVlanMapping' // Add/update VLAN mapping
|
||||
'removeVlanMapping' // Remove VLAN mapping
|
||||
'testVlanAssignment' // Test what VLAN a MAC gets
|
||||
|
||||
// Security Profiles
|
||||
'getSecurityProfiles' // List all security profiles
|
||||
'getSecurityProfile' // Get a single profile by ID
|
||||
'createSecurityProfile' // Create a reusable security profile
|
||||
'updateSecurityProfile' // Update a profile (propagates to referencing routes)
|
||||
'deleteSecurityProfile' // Delete a profile (with optional force)
|
||||
'getSecurityProfileUsage' // Get routes referencing a profile
|
||||
|
||||
// Network Targets
|
||||
'getNetworkTargets' // List all network targets
|
||||
'getNetworkTarget' // Get a single target by ID
|
||||
'createNetworkTarget' // Create a reusable host:port target
|
||||
'updateNetworkTarget' // Update a target (propagates to referencing routes)
|
||||
'deleteNetworkTarget' // Delete a target (with optional force)
|
||||
'getNetworkTargetUsage' // Get routes referencing a target
|
||||
```
|
||||
|
||||
## API Client
|
||||
@@ -1518,12 +1531,12 @@ const router = new DcRouter(options: IDcRouterOptions);
|
||||
| `remoteIngressManager` | `RemoteIngressManager` | Edge registration CRUD manager |
|
||||
| `tunnelManager` | `TunnelManager` | Tunnel lifecycle and status manager |
|
||||
| `vpnManager` | `VpnManager` | VPN server lifecycle and client CRUD manager |
|
||||
| `storageManager` | `StorageManager` | Storage backend |
|
||||
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
||||
| `metricsManager` | `MetricsManager` | Metrics collector |
|
||||
| `cacheDb` | `CacheDb` | Cache database instance |
|
||||
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
|
||||
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
|
||||
| `dcRouterDb` | `DcRouterDb` | Unified database instance (smartdata + smartdb) |
|
||||
| `routeConfigManager` | `RouteConfigManager` | Programmatic route CRUD manager |
|
||||
| `apiTokenManager` | `ApiTokenManager` | API token management |
|
||||
| `referenceResolver` | `ReferenceResolver` | Security profile and network target resolver |
|
||||
|
||||
### Re-exported Types
|
||||
|
||||
@@ -1589,7 +1602,8 @@ tstest test/test.opsserver-api.ts --verbose --timeout 60
|
||||
| `test.jwt-auth.ts` | JWT login, verification, logout, invalid credentials | 8 |
|
||||
| `test.opsserver-api.ts` | Health, statistics, configuration, log APIs | 8 |
|
||||
| `test.protected-endpoint.ts` | Admin auth, identity verification, public endpoints | 8 |
|
||||
| `test.storagemanager.ts` | Memory, filesystem, custom backends, concurrency | 8 |
|
||||
| `test.reference-resolver.ts` | Security profiles, network targets, route resolution | 20 |
|
||||
| `test.security-profiles-api.ts` | Profile/target API endpoints, auth enforcement | 13 |
|
||||
|
||||
## Docker / OCI Container Deployment
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ const devRouter = new DcRouter({
|
||||
{ clientId: 'admin-desktop', serverDefinedClientTags: ['admin'], description: 'Admin workstation' },
|
||||
],
|
||||
},
|
||||
// Disable db/mongo for dev
|
||||
dbConfig: { enabled: false },
|
||||
dbConfig: { enabled: true },
|
||||
});
|
||||
|
||||
console.log('Starting DcRouter in development mode...');
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.2.4',
|
||||
version: '12.5.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -478,11 +478,15 @@ export class DcRouter {
|
||||
}
|
||||
: undefined,
|
||||
this.referenceResolver,
|
||||
// Sync merged routes to RemoteIngressManager whenever routes change
|
||||
// Sync merged routes to RemoteIngressManager whenever routes change,
|
||||
// then push updated derived ports to the Rust hub binary
|
||||
(routes) => {
|
||||
if (this.remoteIngressManager) {
|
||||
this.remoteIngressManager.setRoutes(routes as any[]);
|
||||
}
|
||||
if (this.tunnelManager) {
|
||||
this.tunnelManager.syncAllowedEdges();
|
||||
}
|
||||
},
|
||||
);
|
||||
this.apiTokenManager = new ApiTokenManager();
|
||||
|
||||
@@ -90,6 +90,13 @@ interface IIdentity {
|
||||
| `IApiTokenInfo` | Token info: id, name, scopes, createdAt, expiresAt, enabled |
|
||||
| `TApiTokenScope` | Token scopes: `routes:read`, `routes:write`, `config:read`, `tokens:read`, `tokens:manage` |
|
||||
|
||||
#### Security & Reference Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
| `ISecurityProfile` | Reusable security config: id, name, description, security (ipAllowList, ipBlockList, maxConnections, rateLimit, etc.), extendsProfiles |
|
||||
| `INetworkTarget` | Reusable host:port destination: id, name, description, host (string or string[]), port |
|
||||
| `IRouteMetadata` | Route-to-reference links: securityProfileRef, networkTargetRef, snapshot names, lastResolvedAt |
|
||||
|
||||
#### Remote Ingress Interfaces
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
@@ -241,6 +248,26 @@ interface ICertificateInfo {
|
||||
| `IReq_GetRadiusStatistics` | `getRadiusStatistics` | RADIUS stats |
|
||||
| `IReq_GetRadiusAccountingSummary` | `getRadiusAccountingSummary` | Accounting summary |
|
||||
|
||||
#### 🛡️ Security Profiles
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetSecurityProfiles` | `getSecurityProfiles` | List all security profiles |
|
||||
| `IReq_GetSecurityProfile` | `getSecurityProfile` | Get a single profile by ID |
|
||||
| `IReq_CreateSecurityProfile` | `createSecurityProfile` | Create a reusable security profile |
|
||||
| `IReq_UpdateSecurityProfile` | `updateSecurityProfile` | Update a profile (propagates to routes) |
|
||||
| `IReq_DeleteSecurityProfile` | `deleteSecurityProfile` | Delete a profile (with optional force) |
|
||||
| `IReq_GetSecurityProfileUsage` | `getSecurityProfileUsage` | Get routes referencing a profile |
|
||||
|
||||
#### 🎯 Network Targets
|
||||
| Interface | Method | Description |
|
||||
|-----------|--------|-------------|
|
||||
| `IReq_GetNetworkTargets` | `getNetworkTargets` | List all network targets |
|
||||
| `IReq_GetNetworkTarget` | `getNetworkTarget` | Get a single target by ID |
|
||||
| `IReq_CreateNetworkTarget` | `createNetworkTarget` | Create a reusable host:port target |
|
||||
| `IReq_UpdateNetworkTarget` | `updateNetworkTarget` | Update a target (propagates to routes) |
|
||||
| `IReq_DeleteNetworkTarget` | `deleteNetworkTarget` | Delete a target (with optional force) |
|
||||
| `IReq_GetNetworkTargetUsage` | `getNetworkTargetUsage` | Get routes referencing a target |
|
||||
|
||||
## Example: Full API Integration
|
||||
|
||||
> 💡 **Tip:** For a higher-level, object-oriented API, use [`@serve.zone/dcrouter-apiclient`](https://www.npmjs.com/package/@serve.zone/dcrouter-apiclient) which wraps these interfaces with resource classes and builder patterns.
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '12.2.4',
|
||||
version: '12.5.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -1441,6 +1441,37 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
||||
}
|
||||
});
|
||||
|
||||
export const updateRouteAction = routeManagementStatePart.createAction<{
|
||||
id: string;
|
||||
route?: any;
|
||||
enabled?: boolean;
|
||||
metadata?: any;
|
||||
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState()!;
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateRoute
|
||||
>('/typedrequest', 'updateRoute');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity!,
|
||||
id: dataArg.id,
|
||||
route: dataArg.route,
|
||||
enabled: dataArg.enabled,
|
||||
metadata: dataArg.metadata,
|
||||
});
|
||||
|
||||
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to update route',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
@@ -43,6 +42,12 @@ export class OpsDashboard extends DeesElement {
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
@state() accessor configState: appstate.IConfigState = {
|
||||
config: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// Store viewTabs as a property to maintain object references
|
||||
private viewTabs = [
|
||||
{
|
||||
@@ -112,6 +117,20 @@ export class OpsDashboard extends DeesElement {
|
||||
},
|
||||
];
|
||||
|
||||
private get globalMessages() {
|
||||
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
|
||||
const config = this.configState.config;
|
||||
if (config && !config.cache.enabled) {
|
||||
messages.push({
|
||||
id: 'db-disabled',
|
||||
type: 'warning',
|
||||
message: 'Database is disabled. Creating and editing routes, profiles, targets, and API tokens is not available.',
|
||||
dismissible: false,
|
||||
});
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -137,6 +156,14 @@ export class OpsDashboard extends DeesElement {
|
||||
});
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
// Subscribe to config state (for global warnings)
|
||||
const configSubscription = appstate.configStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((configState) => {
|
||||
this.configState = configState;
|
||||
});
|
||||
this.rxSubscriptions.push(configSubscription);
|
||||
|
||||
// Subscribe to UI state
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
@@ -205,6 +232,7 @@ export class OpsDashboard extends DeesElement {
|
||||
name="DCRouter OpsServer"
|
||||
.viewTabs=${this.viewTabs}
|
||||
.selectedView=${this.currentViewTab}
|
||||
.globalMessages=${this.globalMessages}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
|
||||
@@ -287,27 +287,17 @@ export class OpsViewNetwork extends DeesElement {
|
||||
{
|
||||
name: 'Inbound',
|
||||
data: this.trafficDataIn,
|
||||
color: '#22c55e', // Green for download
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
name: 'Outbound',
|
||||
data: this.trafficDataOut,
|
||||
color: '#8b5cf6', // Purple for upload
|
||||
color: '#8b5cf6',
|
||||
}
|
||||
]}
|
||||
.stacked=${false}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${300000}
|
||||
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
||||
.tooltipFormatter=${(point: any) => {
|
||||
const mbps = point.y || 0;
|
||||
const seriesName = point.series?.name || 'Throughput';
|
||||
const timestamp = new Date(point.x).toLocaleTimeString();
|
||||
return `
|
||||
<div style="padding: 8px;">
|
||||
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
|
||||
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
|
||||
</div>
|
||||
`;
|
||||
}}
|
||||
></dees-chart-area>
|
||||
|
||||
<!-- Top IPs Section -->
|
||||
|
||||
@@ -64,6 +64,7 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
];
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Network Targets</ops-sectionheading>
|
||||
<div class="targetsContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
@@ -81,8 +82,8 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
name: 'Create Target',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header' as const],
|
||||
actionFunc: async (_: any, table: any) => {
|
||||
await this.showCreateTargetDialog(table);
|
||||
actionFunc: async () => {
|
||||
await this.showCreateTargetDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -96,16 +97,18 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['contextmenu' as const],
|
||||
actionFunc: async (target: interfaces.data.INetworkTarget, table: any) => {
|
||||
await this.showEditTargetDialog(target, table);
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const target = actionData.item as interfaces.data.INetworkTarget;
|
||||
await this.showEditTargetDialog(target);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['contextmenu' as const],
|
||||
actionFunc: async (target: interfaces.data.INetworkTarget) => {
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const target = actionData.item as interfaces.data.INetworkTarget;
|
||||
await this.deleteTarget(target);
|
||||
},
|
||||
},
|
||||
@@ -115,7 +118,7 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async showCreateTargetDialog(table: any) {
|
||||
private async showCreateTargetDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Network Target',
|
||||
@@ -128,10 +131,12 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Create',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createTargetAction, {
|
||||
@@ -143,12 +148,11 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditTargetDialog(target: interfaces.data.INetworkTarget, table: any) {
|
||||
private async showEditTargetDialog(target: interfaces.data.INetworkTarget) {
|
||||
const hostStr = Array.isArray(target.host) ? target.host.join(', ') : target.host;
|
||||
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
@@ -163,10 +167,12 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Save',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateTargetAction, {
|
||||
@@ -179,7 +185,6 @@ export class OpsViewNetworkTargets extends DeesElement {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,11 +121,15 @@ export class OpsViewOverview extends DeesElement {
|
||||
<dees-chart-area
|
||||
.label=${'Email Traffic (24h)'}
|
||||
.series=${this.getEmailTrafficSeries()}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${86400000}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-area
|
||||
.label=${'DNS Queries (24h)'}
|
||||
.series=${this.getDnsQuerySeries()}
|
||||
.realtimeMode=${true}
|
||||
.rollingWindow=${86400000}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
<dees-chart-log
|
||||
|
||||
@@ -204,7 +204,10 @@ export class OpsViewRoutes extends DeesElement {
|
||||
? html`
|
||||
<sz-route-list-view
|
||||
.routes=${szRoutes}
|
||||
.showActionsFilter=${(route: any) => route.tags?.includes('programmatic') ?? false}
|
||||
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
||||
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
||||
></sz-route-list-view>
|
||||
`
|
||||
: html`
|
||||
@@ -337,6 +340,165 @@ export class OpsViewRoutes extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRouteEdit(e: CustomEvent) {
|
||||
const clickedRoute = e.detail;
|
||||
if (!clickedRoute) return;
|
||||
|
||||
const merged = this.routeState.mergedRoutes.find(
|
||||
(mr) => mr.route.name === clickedRoute.name,
|
||||
);
|
||||
if (!merged || !merged.storedRouteId) return;
|
||||
|
||||
this.showEditRouteDialog(merged);
|
||||
}
|
||||
|
||||
private async handleRouteDelete(e: CustomEvent) {
|
||||
const clickedRoute = e.detail;
|
||||
if (!clickedRoute) return;
|
||||
|
||||
const merged = this.routeState.mergedRoutes.find(
|
||||
(mr) => mr.route.name === clickedRoute.name,
|
||||
);
|
||||
if (!merged || !merged.storedRouteId) return;
|
||||
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Delete Route: ${merged.route.name}`,
|
||||
content: html`
|
||||
<div style="color: #ccc; padding: 8px 0;">
|
||||
<p>Are you sure you want to delete this route? This action cannot be undone.</p>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash-2',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
appstate.deleteRouteAction,
|
||||
merged.storedRouteId!,
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditRouteDialog(merged: interfaces.data.IMergedRoute) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
const targets = this.profilesTargetsState.targets;
|
||||
|
||||
const profileOptions = [
|
||||
{ key: '', option: '(none — inline security)' },
|
||||
...profiles.map((p) => ({
|
||||
key: p.id,
|
||||
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
|
||||
})),
|
||||
];
|
||||
const targetOptions = [
|
||||
{ key: '', option: '(none — inline target)' },
|
||||
...targets.map((t) => ({
|
||||
key: t.id,
|
||||
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
|
||||
})),
|
||||
];
|
||||
|
||||
const route = merged.route;
|
||||
const currentPorts = Array.isArray(route.match.ports)
|
||||
? route.match.ports.map((p: any) => typeof p === 'number' ? String(p) : `${p.from}-${p.to}`).join(', ')
|
||||
: String(route.match.ports);
|
||||
const currentDomains: string[] = route.match.domains
|
||||
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
||||
: [];
|
||||
const firstTarget = route.action.targets?.[0];
|
||||
const currentTargetHost = firstTarget
|
||||
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
|
||||
: '';
|
||||
const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: `Edit Route: ${route.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${merged.metadata?.securityProfileRef || ''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${merged.metadata?.networkTargetRef || ''}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'lucide:check',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
if (!formData.name || !formData.ports) return;
|
||||
|
||||
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||
const domains: string[] = Array.isArray(formData.domains)
|
||||
? formData.domains.filter(Boolean)
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const updatedRoute: any = {
|
||||
name: formData.name,
|
||||
match: {
|
||||
ports,
|
||||
...(domains.length > 0 ? { domains } : {}),
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [
|
||||
{
|
||||
host: formData.targetHost || 'localhost',
|
||||
port: parseInt(formData.targetPort, 10) || 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
const metadata: any = {};
|
||||
if (formData.securityProfileRef) {
|
||||
metadata.securityProfileRef = formData.securityProfileRef;
|
||||
}
|
||||
if (formData.networkTargetRef) {
|
||||
metadata.networkTargetRef = formData.networkTargetRef;
|
||||
}
|
||||
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
appstate.updateRouteAction,
|
||||
{
|
||||
id: merged.storedRouteId!,
|
||||
route: updatedRoute,
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||
},
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showCreateRouteDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
@@ -364,7 +526,8 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (higher = matched first)'}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${''}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${''}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
|
||||
@@ -387,15 +550,16 @@ export class OpsViewRoutes extends DeesElement {
|
||||
if (!formData.name || !formData.ports) return;
|
||||
|
||||
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||
const domains = formData.domains
|
||||
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const domains: string[] = Array.isArray(formData.domains)
|
||||
? formData.domains.filter(Boolean)
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const route: any = {
|
||||
name: formData.name,
|
||||
match: {
|
||||
ports,
|
||||
...(domains && domains.length > 0 ? { domains } : {}),
|
||||
...(domains.length > 0 ? { domains } : {}),
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
@@ -406,6 +570,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
},
|
||||
],
|
||||
},
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
|
||||
@@ -64,6 +64,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
];
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Security Profiles</ops-sectionheading>
|
||||
<div class="profilesContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
@@ -89,8 +90,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
name: 'Create Profile',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header' as const],
|
||||
actionFunc: async (_: any, table: any) => {
|
||||
await this.showCreateProfileDialog(table);
|
||||
actionFunc: async () => {
|
||||
await this.showCreateProfileDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -104,16 +105,18 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['contextmenu' as const],
|
||||
actionFunc: async (profile: interfaces.data.ISecurityProfile, table: any) => {
|
||||
await this.showEditProfileDialog(profile, table);
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ISecurityProfile;
|
||||
await this.showEditProfileDialog(profile);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['contextmenu' as const],
|
||||
actionFunc: async (profile: interfaces.data.ISecurityProfile) => {
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ISecurityProfile;
|
||||
await this.deleteProfile(profile);
|
||||
},
|
||||
},
|
||||
@@ -123,7 +126,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async showCreateProfileDialog(table: any) {
|
||||
private async showCreateProfileDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Security Profile',
|
||||
@@ -137,10 +140,12 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Create',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
||||
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
||||
@@ -158,12 +163,11 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile, table: any) {
|
||||
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit Profile: ${profile.name}`,
|
||||
@@ -177,10 +181,12 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Save',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
|
||||
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
||||
@@ -199,7 +205,6 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,6 +68,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
- API token creation, revocation, and scope management
|
||||
- Routes tab and API Tokens tab in unified view
|
||||
|
||||
### 🛡️ Security Profiles & Network Targets
|
||||
- Create, edit, and delete reusable security profiles (IP allow/block lists, rate limits, max connections)
|
||||
- Create, edit, and delete reusable network targets (host:port destinations)
|
||||
- In-row and context menu actions for quick editing
|
||||
- Changes propagate automatically to all referencing routes
|
||||
|
||||
### ⚙️ Configuration
|
||||
- Read-only display of current system configuration
|
||||
- Status badges for boolean values (enabled/disabled)
|
||||
|
||||
Reference in New Issue
Block a user