feat(vpn,target-profiles,migrations): add startup data migrations, support scoped VPN route allow entries, and rename target profile hosts to ips
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-07 - 13.1.0 - feat(vpn,target-profiles,migrations)
|
||||||
|
add startup data migrations, support scoped VPN route allow entries, and rename target profile hosts to ips
|
||||||
|
|
||||||
|
- runs smartmigration at startup before configuration is loaded and adds a migration for target profile targets from host to ip
|
||||||
|
- changes VPN client routing to always force traffic through SmartProxy while allowing direct target bypasses from target profiles
|
||||||
|
- supports domain-scoped VPN ipAllowList entries for vpnOnly routes based on matching target profile domains
|
||||||
|
- updates certificate reprovisioning to reapply routes so renewed certificates are loaded into the running proxy
|
||||||
|
- removes the forceDestinationSmartproxy VPN client option from API, persistence, manager, and web UI
|
||||||
|
|
||||||
## 2026-04-06 - 13.0.11 - fix(routing)
|
## 2026-04-06 - 13.0.11 - fix(routing)
|
||||||
serialize route updates and correct VPN-gated route application
|
serialize route updates and correct VPN-gated route application
|
||||||
|
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -35,25 +35,26 @@
|
|||||||
"@api.global/typedserver": "^8.4.6",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.61.1",
|
"@design.estate/dees-catalog": "^3.66.0",
|
||||||
"@design.estate/dees-element": "^2.2.4",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@push.rocks/lik": "^6.4.0",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/projectinfo": "^5.1.0",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^9.5.0",
|
"@push.rocks/smartacme": "^9.5.0",
|
||||||
"@push.rocks/smartdata": "^7.1.6",
|
"@push.rocks/smartdata": "^7.1.7",
|
||||||
"@push.rocks/smartdb": "^2.5.9",
|
"@push.rocks/smartdb": "^2.6.2",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@push.rocks/smartdns": "^7.9.0",
|
||||||
"@push.rocks/smartfs": "^1.5.0",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.2",
|
||||||
"@push.rocks/smartmetrics": "^3.0.3",
|
"@push.rocks/smartmetrics": "^3.0.3",
|
||||||
|
"@push.rocks/smartmigration": "1.1.1",
|
||||||
"@push.rocks/smartmta": "^5.3.1",
|
"@push.rocks/smartmta": "^5.3.1",
|
||||||
"@push.rocks/smartnetwork": "^4.5.2",
|
"@push.rocks/smartnetwork": "^4.5.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^27.4.0",
|
"@push.rocks/smartproxy": "^27.5.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
"@serve.zone/remoteingress": "^4.15.3",
|
"@serve.zone/remoteingress": "^4.15.3",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"lru-cache": "^11.2.7",
|
"lru-cache": "^11.3.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
2403
pnpm-lock.yaml
generated
2403
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.11',
|
version: '13.1.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
|
|||||||
import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
|
import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
|
||||||
// Import unified database
|
// Import unified database
|
||||||
import { DcRouterDb, type IDcRouterDbConfig, CacheCleaner, ProxyCertDoc, AcmeCertDoc } from './db/index.js';
|
import { DcRouterDb, type IDcRouterDbConfig, CacheCleaner, ProxyCertDoc, AcmeCertDoc } from './db/index.js';
|
||||||
|
// Import migration runner and app version
|
||||||
|
import { createMigrationRunner } from '../ts_migrations/index.js';
|
||||||
|
import { commitinfo } from './00_commitinfo_data.js';
|
||||||
|
|
||||||
import { OpsServer } from './opsserver/index.js';
|
import { OpsServer } from './opsserver/index.js';
|
||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
@@ -775,6 +778,19 @@ export class DcRouter {
|
|||||||
|
|
||||||
await this.dcRouterDb.start();
|
await this.dcRouterDb.start();
|
||||||
|
|
||||||
|
// Run any pending data migrations before anything else reads from the DB.
|
||||||
|
// This must complete before ConfigManagers loads profiles.
|
||||||
|
const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version);
|
||||||
|
const migrationResult = await migration.run();
|
||||||
|
if (migrationResult.stepsApplied.length > 0) {
|
||||||
|
logger.log('info',
|
||||||
|
`smartmigration: ${migrationResult.currentVersionBefore ?? 'fresh'} → ${migrationResult.currentVersionAfter} ` +
|
||||||
|
`(${migrationResult.stepsApplied.length} step(s) applied in ${migrationResult.totalDurationMs}ms)`,
|
||||||
|
);
|
||||||
|
} else if (migrationResult.wasFreshInstall) {
|
||||||
|
logger.log('info', `smartmigration: fresh install stamped to ${migrationResult.currentVersionAfter}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Start the cache cleaner for TTL-based document cleanup
|
// Start the cache cleaner for TTL-based document cleanup
|
||||||
const cleanupIntervalMs = (dbConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
|
const cleanupIntervalMs = (dbConfig.cleanupIntervalHours || 1) * 60 * 60 * 1000;
|
||||||
this.cacheCleaner = new CacheCleaner(this.dcRouterDb, {
|
this.cacheCleaner = new CacheCleaner(this.dcRouterDb, {
|
||||||
@@ -1042,15 +1058,9 @@ export class DcRouter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
// Note: smartproxy v27.5.0 emits only 'certificate-issued' and 'certificate-failed'.
|
||||||
logger.log('info', `Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
// Renewals come through 'certificate-issued' (with optional isRenewal? in the payload).
|
||||||
const routeNames = this.findRouteNamesForDomain(event.domain);
|
// The vestigial 'certificate-renewed' event from common-types.ts is never emitted.
|
||||||
this.certificateStatusMap.set(event.domain, {
|
|
||||||
status: 'valid', routeNames,
|
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
|
||||||
source: event.source,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||||
logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
|
logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingres
|
|||||||
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
||||||
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
||||||
|
|
||||||
|
/** An IP allow entry: plain IP/CIDR or domain-scoped. */
|
||||||
|
export type TIpAllowEntry = string | { ip: string; domains: string[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
|
* Simple async mutex — serializes concurrent applyRoutes() calls so the Rust engine
|
||||||
* never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
|
* never receives rapid overlapping route updates that can churn UDP/QUIC listeners.
|
||||||
@@ -52,7 +55,7 @@ export class RouteConfigManager {
|
|||||||
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
private getHardcodedRoutes: () => plugins.smartproxy.IRouteConfig[],
|
||||||
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
private getSmartProxy: () => plugins.smartproxy.SmartProxy | undefined,
|
||||||
private getHttp3Config?: () => IHttp3Config | undefined,
|
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||||
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => string[],
|
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
|
||||||
private referenceResolver?: ReferenceResolver,
|
private referenceResolver?: ReferenceResolver,
|
||||||
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
||||||
) {}
|
) {}
|
||||||
@@ -402,13 +405,13 @@ export class RouteConfigManager {
|
|||||||
if (!vpnCallback) return route;
|
if (!vpnCallback) return route;
|
||||||
const dcRoute = route as IDcRouterRouteConfig;
|
const dcRoute = route as IDcRouterRouteConfig;
|
||||||
if (!dcRoute.vpnOnly) return route;
|
if (!dcRoute.vpnOnly) return route;
|
||||||
const vpnIps = vpnCallback(dcRoute, routeId);
|
const vpnEntries = vpnCallback(dcRoute, routeId);
|
||||||
const existingIps = route.security?.ipAllowList || [];
|
const existingEntries = route.security?.ipAllowList || [];
|
||||||
return {
|
return {
|
||||||
...route,
|
...route,
|
||||||
security: {
|
security: {
|
||||||
...route.security,
|
...route.security,
|
||||||
ipAllowList: [...existingIps, ...vpnIps],
|
ipAllowList: [...existingEntries, ...vpnEntries],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class TargetProfileManager {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a set of target profile IDs, collect all explicit target host IPs.
|
* For a set of target profile IDs, collect all explicit target IPs.
|
||||||
* These IPs bypass the SmartProxy forceTarget rewrite — VPN clients can
|
* These IPs bypass the SmartProxy forceTarget rewrite — VPN clients can
|
||||||
* connect to them directly through the tunnel.
|
* connect to them directly through the tunnel.
|
||||||
*/
|
*/
|
||||||
@@ -156,7 +156,7 @@ export class TargetProfileManager {
|
|||||||
const profile = this.profiles.get(profileId);
|
const profile = this.profiles.get(profileId);
|
||||||
if (!profile?.targets?.length) continue;
|
if (!profile?.targets?.length) continue;
|
||||||
for (const t of profile.targets) {
|
for (const t of profile.targets) {
|
||||||
ips.add(t.host);
|
ips.add(t.ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...ips];
|
return [...ips];
|
||||||
@@ -168,32 +168,50 @@ export class TargetProfileManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
|
* For a vpnOnly route, find all enabled VPN clients whose assigned TargetProfile
|
||||||
* matches the route. Returns their assigned IPs for injection into ipAllowList.
|
* matches the route. Returns IP allow entries for injection into ipAllowList.
|
||||||
|
*
|
||||||
|
* Entries are domain-scoped when a profile matches via specific domains that are
|
||||||
|
* a subset of the route's wildcard. Plain IPs are returned for routeRef/target matches
|
||||||
|
* or when profile domains exactly equal the route's domains.
|
||||||
*/
|
*/
|
||||||
public getMatchingClientIps(
|
public getMatchingClientIps(
|
||||||
route: IDcRouterRouteConfig,
|
route: IDcRouterRouteConfig,
|
||||||
routeId: string | undefined,
|
routeId: string | undefined,
|
||||||
clients: VpnClientDoc[],
|
clients: VpnClientDoc[],
|
||||||
): string[] {
|
): Array<string | { ip: string; domains: string[] }> {
|
||||||
const ips: string[] = [];
|
const entries: Array<string | { ip: string; domains: string[] }> = [];
|
||||||
|
const routeDomains: string[] = (route.match as any)?.domains || [];
|
||||||
|
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (!client.enabled || !client.assignedIp) continue;
|
if (!client.enabled || !client.assignedIp) continue;
|
||||||
if (!client.targetProfileIds?.length) continue;
|
if (!client.targetProfileIds?.length) continue;
|
||||||
|
|
||||||
// Check if any of the client's profiles match this route
|
// Collect scoped domains from all matching profiles for this client
|
||||||
const matches = client.targetProfileIds.some((profileId) => {
|
let fullAccess = false;
|
||||||
const profile = this.profiles.get(profileId);
|
const scopedDomains = new Set<string>();
|
||||||
if (!profile) return false;
|
|
||||||
return this.routeMatchesProfile(route, routeId, profile);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matches) {
|
for (const profileId of client.targetProfileIds) {
|
||||||
ips.push(client.assignedIp);
|
const profile = this.profiles.get(profileId);
|
||||||
|
if (!profile) continue;
|
||||||
|
|
||||||
|
const matchResult = this.routeMatchesProfileDetailed(route, routeId, profile, routeDomains);
|
||||||
|
if (matchResult === 'full') {
|
||||||
|
fullAccess = true;
|
||||||
|
break; // No need to check more profiles
|
||||||
|
}
|
||||||
|
if (matchResult !== 'none') {
|
||||||
|
for (const d of matchResult.domains) scopedDomains.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullAccess) {
|
||||||
|
entries.push(client.assignedIp);
|
||||||
|
} else if (scopedDomains.size > 0) {
|
||||||
|
entries.push({ ip: client.assignedIp, domains: [...scopedDomains] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ips;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +241,7 @@ export class TargetProfileManager {
|
|||||||
// Direct target IP entries
|
// Direct target IP entries
|
||||||
if (profile.targets?.length) {
|
if (profile.targets?.length) {
|
||||||
for (const t of profile.targets) {
|
for (const t of profile.targets) {
|
||||||
targetIps.add(t.host);
|
targetIps.add(t.ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,34 +282,67 @@ export class TargetProfileManager {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a route matches a profile. A profile matches if ANY condition is true:
|
* Check if a route matches a profile (boolean convenience wrapper).
|
||||||
* 1. Profile's routeRefs contains the route's name or stored route id
|
|
||||||
* 2. Profile's domains overlaps with route.match.domains (wildcard matching)
|
|
||||||
* 3. Profile's targets overlaps with route.action.targets (host + port match)
|
|
||||||
*/
|
*/
|
||||||
private routeMatchesProfile(
|
private routeMatchesProfile(
|
||||||
route: IDcRouterRouteConfig,
|
route: IDcRouterRouteConfig,
|
||||||
routeId: string | undefined,
|
routeId: string | undefined,
|
||||||
profile: ITargetProfile,
|
profile: ITargetProfile,
|
||||||
): boolean {
|
): boolean {
|
||||||
// 1. Route reference match
|
const routeDomains: string[] = (route.match as any)?.domains || [];
|
||||||
|
const result = this.routeMatchesProfileDetailed(route, routeId, profile, routeDomains);
|
||||||
|
return result !== 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed match: returns 'full' (plain IP, entire route), 'scoped' (domain-limited),
|
||||||
|
* or 'none' (no match).
|
||||||
|
*
|
||||||
|
* - routeRefs / target matches → 'full' (explicit reference = full access)
|
||||||
|
* - domain match where profile domains are a subset of route wildcard → 'scoped'
|
||||||
|
* - domain match where domains are identical or profile is a wildcard → 'full'
|
||||||
|
*/
|
||||||
|
private routeMatchesProfileDetailed(
|
||||||
|
route: IDcRouterRouteConfig,
|
||||||
|
routeId: string | undefined,
|
||||||
|
profile: ITargetProfile,
|
||||||
|
routeDomains: string[],
|
||||||
|
): 'full' | { type: 'scoped'; domains: string[] } | 'none' {
|
||||||
|
// 1. Route reference match → full access
|
||||||
if (profile.routeRefs?.length) {
|
if (profile.routeRefs?.length) {
|
||||||
if (routeId && profile.routeRefs.includes(routeId)) return true;
|
if (routeId && profile.routeRefs.includes(routeId)) return 'full';
|
||||||
if (route.name && profile.routeRefs.includes(route.name)) return true;
|
if (route.name && profile.routeRefs.includes(route.name)) return 'full';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Domain match (bidirectional: profile-specific + route-wildcard, or vice versa)
|
// 2. Domain match
|
||||||
if (profile.domains?.length) {
|
if (profile.domains?.length && routeDomains.length) {
|
||||||
const routeDomains: string[] = (route.match as any)?.domains || [];
|
const matchedProfileDomains: string[] = [];
|
||||||
|
|
||||||
for (const profileDomain of profile.domains) {
|
for (const profileDomain of profile.domains) {
|
||||||
for (const routeDomain of routeDomains) {
|
for (const routeDomain of routeDomains) {
|
||||||
if (this.domainMatchesPattern(routeDomain, profileDomain) ||
|
if (this.domainMatchesPattern(routeDomain, profileDomain) ||
|
||||||
this.domainMatchesPattern(profileDomain, routeDomain)) return true;
|
this.domainMatchesPattern(profileDomain, routeDomain)) {
|
||||||
|
matchedProfileDomains.push(profileDomain);
|
||||||
|
break; // This profileDomain matched, move to the next
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matchedProfileDomains.length > 0) {
|
||||||
|
// Check if profile domains cover the route entirely (same wildcards = full access)
|
||||||
|
const isFullCoverage = routeDomains.every((rd) =>
|
||||||
|
matchedProfileDomains.some((pd) =>
|
||||||
|
rd === pd || this.domainMatchesPattern(rd, pd),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (isFullCoverage) return 'full';
|
||||||
|
|
||||||
|
// Profile domains are a subset → scoped access to those specific domains
|
||||||
|
return { type: 'scoped', domains: matchedProfileDomains };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Target match (host + port)
|
// 3. Target match (host + port) → full access (precise by nature)
|
||||||
if (profile.targets?.length) {
|
if (profile.targets?.length) {
|
||||||
const routeTargets = (route.action as any)?.targets;
|
const routeTargets = (route.action as any)?.targets;
|
||||||
if (Array.isArray(routeTargets)) {
|
if (Array.isArray(routeTargets)) {
|
||||||
@@ -299,15 +350,15 @@ export class TargetProfileManager {
|
|||||||
for (const routeTarget of routeTargets) {
|
for (const routeTarget of routeTargets) {
|
||||||
const routeHost = routeTarget.host;
|
const routeHost = routeTarget.host;
|
||||||
const routePort = routeTarget.port;
|
const routePort = routeTarget.port;
|
||||||
if (routeHost === profileTarget.host && routePort === profileTarget.port) {
|
if (routeHost === profileTarget.ip && routePort === profileTarget.port) {
|
||||||
return true;
|
return 'full';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ export class VpnClientDoc extends plugins.smartdata.SmartDataDbDoc<VpnClientDoc,
|
|||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public expiresAt?: string;
|
public expiresAt?: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
|
||||||
public forceDestinationSmartproxy: boolean = true;
|
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public destinationAllowList?: string[];
|
public destinationAllowList?: string[];
|
||||||
|
|
||||||
|
|||||||
@@ -295,7 +295,12 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy route-based reprovisioning
|
* Legacy route-based reprovisioning. Kept for backward compatibility with
|
||||||
|
* older clients that send `reprovisionCertificate` typed-requests.
|
||||||
|
*
|
||||||
|
* Like reprovisionCertificateDomain, this triggers the full route apply
|
||||||
|
* pipeline rather than smartProxy.provisionCertificate(routeName) — which
|
||||||
|
* is a no-op when certProvisionFunction is set (Rust ACME disabled).
|
||||||
*/
|
*/
|
||||||
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
@@ -305,13 +310,19 @@ export class CertificateHandler {
|
|||||||
return { success: false, message: 'SmartProxy is not running' };
|
return { success: false, message: 'SmartProxy is not running' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear event-based status for domains in this route so the
|
||||||
|
// certificate-issued event can refresh them
|
||||||
|
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
||||||
|
if (entry.routeNames.includes(routeName)) {
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await smartProxy.provisionCertificate(routeName);
|
if (dcRouter.routeConfigManager) {
|
||||||
// Clear event-based status for domains in this route
|
await dcRouter.routeConfigManager.applyRoutes();
|
||||||
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
} else {
|
||||||
if (entry.routeNames.includes(routeName)) {
|
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
||||||
dcRouter.certificateStatusMap.delete(domain);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -320,7 +331,16 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain-based reprovisioning — clears backoff first, then triggers provision
|
* Domain-based reprovisioning — clears backoff first, refreshes the smartacme
|
||||||
|
* cert (when forceRenew is set), then re-applies routes so the running Rust
|
||||||
|
* proxy actually picks up the new cert.
|
||||||
|
*
|
||||||
|
* Why applyRoutes (not smartProxy.provisionCertificate)?
|
||||||
|
* smartProxy.provisionCertificate(routeName) routes through the Rust ACME
|
||||||
|
* path, which is forcibly disabled whenever certProvisionFunction is set
|
||||||
|
* (smart-proxy.ts:168-171). The only path that re-invokes
|
||||||
|
* certProvisionFunction → bridge.loadCertificate is updateRoutes(), which
|
||||||
|
* we trigger via routeConfigManager.applyRoutes().
|
||||||
*/
|
*/
|
||||||
private async reprovisionCertificateDomain(domain: string, forceRenew?: boolean): Promise<{ success: boolean; message?: string }> {
|
private async reprovisionCertificateDomain(domain: string, forceRenew?: boolean): Promise<{ success: boolean; message?: string }> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
@@ -335,28 +355,37 @@ export class CertificateHandler {
|
|||||||
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find routes matching this domain — needed to provision through SmartProxy
|
// Find routes matching this domain — fail early if none exist
|
||||||
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
||||||
if (routeNames.length === 0) {
|
if (routeNames.length === 0) {
|
||||||
return { success: false, message: `No routes found for domain '${domain}'` };
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If forceRenew, invalidate SmartAcme's cache so the next provision gets a fresh cert
|
// If forceRenew, order a fresh cert from ACME now so it's already in
|
||||||
|
// AcmeCertDoc by the time certProvisionFunction is invoked below.
|
||||||
if (forceRenew && dcRouter.smartAcme) {
|
if (forceRenew && dcRouter.smartAcme) {
|
||||||
try {
|
try {
|
||||||
await dcRouter.smartAcme.getCertificateForDomain(domain, { forceRenew: true });
|
await dcRouter.smartAcme.getCertificateForDomain(domain, { forceRenew: true });
|
||||||
} catch {
|
} catch (err: unknown) {
|
||||||
// Cache invalidation failed — proceed with provisioning anyway
|
return { success: false, message: `Failed to renew certificate for ${domain}: ${(err as Error).message}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear status map entry so it gets refreshed by the certificate-issued event
|
// Clear status map entry so it gets refreshed by the certificate-issued event
|
||||||
dcRouter.certificateStatusMap.delete(domain);
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
|
||||||
// Provision through SmartProxy — this triggers the full pipeline:
|
// Trigger the full route apply pipeline:
|
||||||
// certProvisionFunction → bridge.loadCertificate → certificate-issued event → status map updated
|
// applyRoutes → updateRoutes → provisionCertificatesViaCallback →
|
||||||
|
// certProvisionFunction(domain) → smartAcme.getCertificateForDomain →
|
||||||
|
// bridge.loadCertificate → Rust hot-swaps `loaded_certs` →
|
||||||
|
// certificate-issued event → certificateStatusMap updated
|
||||||
try {
|
try {
|
||||||
await smartProxy.provisionCertificate(routeNames[0]);
|
if (dcRouter.routeConfigManager) {
|
||||||
|
await dcRouter.routeConfigManager.applyRoutes();
|
||||||
|
} else {
|
||||||
|
// Fallback when DB is disabled and there is no RouteConfigManager
|
||||||
|
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
||||||
|
}
|
||||||
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
|
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export class VpnHandler {
|
|||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
updatedAt: c.updatedAt,
|
updatedAt: c.updatedAt,
|
||||||
expiresAt: c.expiresAt,
|
expiresAt: c.expiresAt,
|
||||||
forceDestinationSmartproxy: c.forceDestinationSmartproxy ?? true,
|
|
||||||
destinationAllowList: c.destinationAllowList,
|
destinationAllowList: c.destinationAllowList,
|
||||||
destinationBlockList: c.destinationBlockList,
|
destinationBlockList: c.destinationBlockList,
|
||||||
useHostIp: c.useHostIp,
|
useHostIp: c.useHostIp,
|
||||||
@@ -122,7 +121,6 @@ export class VpnHandler {
|
|||||||
clientId: dataArg.clientId,
|
clientId: dataArg.clientId,
|
||||||
targetProfileIds: dataArg.targetProfileIds,
|
targetProfileIds: dataArg.targetProfileIds,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
||||||
destinationAllowList: dataArg.destinationAllowList,
|
destinationAllowList: dataArg.destinationAllowList,
|
||||||
destinationBlockList: dataArg.destinationBlockList,
|
destinationBlockList: dataArg.destinationBlockList,
|
||||||
useHostIp: dataArg.useHostIp,
|
useHostIp: dataArg.useHostIp,
|
||||||
@@ -148,7 +146,6 @@ export class VpnHandler {
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
expiresAt: bundle.entry.expiresAt,
|
expiresAt: bundle.entry.expiresAt,
|
||||||
forceDestinationSmartproxy: persistedClient?.forceDestinationSmartproxy ?? true,
|
|
||||||
destinationAllowList: persistedClient?.destinationAllowList,
|
destinationAllowList: persistedClient?.destinationAllowList,
|
||||||
destinationBlockList: persistedClient?.destinationBlockList,
|
destinationBlockList: persistedClient?.destinationBlockList,
|
||||||
useHostIp: persistedClient?.useHostIp,
|
useHostIp: persistedClient?.useHostIp,
|
||||||
@@ -180,7 +177,6 @@ export class VpnHandler {
|
|||||||
await manager.updateClient(dataArg.clientId, {
|
await manager.updateClient(dataArg.clientId, {
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
targetProfileIds: dataArg.targetProfileIds,
|
targetProfileIds: dataArg.targetProfileIds,
|
||||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
||||||
destinationAllowList: dataArg.destinationAllowList,
|
destinationAllowList: dataArg.destinationAllowList,
|
||||||
destinationBlockList: dataArg.destinationBlockList,
|
destinationBlockList: dataArg.destinationBlockList,
|
||||||
useHostIp: dataArg.useHostIp,
|
useHostIp: dataArg.useHostIp,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"order": 2
|
"order": 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ export class VpnManager {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
@@ -242,9 +241,6 @@ export class VpnManager {
|
|||||||
doc.createdAt = Date.now();
|
doc.createdAt = Date.now();
|
||||||
doc.updatedAt = Date.now();
|
doc.updatedAt = Date.now();
|
||||||
doc.expiresAt = bundle.entry.expiresAt;
|
doc.expiresAt = bundle.entry.expiresAt;
|
||||||
if (opts.forceDestinationSmartproxy !== undefined) {
|
|
||||||
doc.forceDestinationSmartproxy = opts.forceDestinationSmartproxy;
|
|
||||||
}
|
|
||||||
if (opts.destinationAllowList !== undefined) {
|
if (opts.destinationAllowList !== undefined) {
|
||||||
doc.destinationAllowList = opts.destinationAllowList;
|
doc.destinationAllowList = opts.destinationAllowList;
|
||||||
}
|
}
|
||||||
@@ -349,7 +345,6 @@ export class VpnManager {
|
|||||||
public async updateClient(clientId: string, update: {
|
public async updateClient(clientId: string, update: {
|
||||||
description?: string;
|
description?: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
@@ -362,7 +357,6 @@ export class VpnManager {
|
|||||||
if (!client) throw new Error(`Client not found: ${clientId}`);
|
if (!client) throw new Error(`Client not found: ${clientId}`);
|
||||||
if (update.description !== undefined) client.description = update.description;
|
if (update.description !== undefined) client.description = update.description;
|
||||||
if (update.targetProfileIds !== undefined) client.targetProfileIds = update.targetProfileIds;
|
if (update.targetProfileIds !== undefined) client.targetProfileIds = update.targetProfileIds;
|
||||||
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
|
|
||||||
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
|
||||||
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
|
||||||
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
if (update.useHostIp !== undefined) client.useHostIp = update.useHostIp;
|
||||||
@@ -484,12 +478,11 @@ export class VpnManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build per-client security settings for the smartvpn daemon.
|
* Build per-client security settings for the smartvpn daemon.
|
||||||
* Maps dcrouter-level fields (forceDestinationSmartproxy, allow/block lists)
|
* All VPN traffic is forced through SmartProxy (forceTarget to 127.0.0.1).
|
||||||
* to smartvpn's IClientSecurity with a destinationPolicy.
|
* TargetProfile direct IP:port targets bypass SmartProxy via allowList.
|
||||||
*/
|
*/
|
||||||
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
private buildClientSecurity(client: VpnClientDoc): plugins.smartvpn.IClientSecurity {
|
||||||
const security: plugins.smartvpn.IClientSecurity = {};
|
const security: plugins.smartvpn.IClientSecurity = {};
|
||||||
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
||||||
|
|
||||||
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
|
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
|
||||||
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
|
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
|
||||||
@@ -500,23 +493,12 @@ export class VpnManager {
|
|||||||
...profileDirectTargets,
|
...profileDirectTargets,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!forceSmartproxy) {
|
security.destinationPolicy = {
|
||||||
// Client traffic goes directly — not forced to SmartProxy
|
default: 'forceTarget' as const,
|
||||||
security.destinationPolicy = {
|
target: '127.0.0.1',
|
||||||
default: 'allow' as const,
|
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
||||||
blockList: client.destinationBlockList,
|
blockList: client.destinationBlockList,
|
||||||
};
|
};
|
||||||
} else if (mergedAllowList.length || client.destinationBlockList?.length) {
|
|
||||||
// Client is forced to SmartProxy, but with allow/block overrides
|
|
||||||
// (includes TargetProfile direct targets that bypass SmartProxy)
|
|
||||||
security.destinationPolicy = {
|
|
||||||
default: 'forceTarget' as const,
|
|
||||||
target: '127.0.0.1',
|
|
||||||
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
|
||||||
blockList: client.destinationBlockList,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// else: no per-client policy, server-wide applies
|
|
||||||
|
|
||||||
return security;
|
return security;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"order": 4
|
"order": 5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* A specific IP:port target within a TargetProfile.
|
* A specific IP:port target within a TargetProfile.
|
||||||
*/
|
*/
|
||||||
export interface ITargetProfileTarget {
|
export interface ITargetProfileTarget {
|
||||||
host: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export interface IVpnClient {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
forceDestinationSmartproxy: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export interface IReq_CreateVpnClient extends plugins.typedrequestInterfaces.imp
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
@@ -82,7 +82,7 @@ export interface IReq_UpdateVpnClient extends plugins.typedrequestInterfaces.imp
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
|
|||||||
70
ts_migrations/index.ts
Normal file
70
ts_migrations/index.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dcrouter migration runner.
|
||||||
|
*
|
||||||
|
* Uses @push.rocks/smartmigration via dynamic import so smartmigration's type
|
||||||
|
* chain (which pulls in mongodb 7.x and related types) doesn't leak into
|
||||||
|
* compile-time type checking for this folder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Matches the subset of IMigrationRunResult we actually log. */
|
||||||
|
export interface IMigrationRunResult {
|
||||||
|
stepsApplied: Array<unknown>;
|
||||||
|
wasFreshInstall: boolean;
|
||||||
|
currentVersionBefore: string | null;
|
||||||
|
currentVersionAfter: string;
|
||||||
|
totalDurationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMigrationRunner {
|
||||||
|
run(): Promise<IMigrationRunResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a configured SmartMigration runner with all dcrouter migration steps registered.
|
||||||
|
*
|
||||||
|
* Call `.run()` on the returned instance at startup (after DcRouterDb is ready,
|
||||||
|
* before any service that reads migrated collections).
|
||||||
|
*
|
||||||
|
* @param db - The initialized SmartdataDb instance from DcRouterDb.getDb()
|
||||||
|
* @param targetVersion - The current app version (from commitinfo.version)
|
||||||
|
*/
|
||||||
|
export async function createMigrationRunner(
|
||||||
|
db: unknown,
|
||||||
|
targetVersion: string,
|
||||||
|
): Promise<IMigrationRunner> {
|
||||||
|
const sm = await import('@push.rocks/smartmigration');
|
||||||
|
const migration = new sm.SmartMigration({
|
||||||
|
targetVersion,
|
||||||
|
db: db as any,
|
||||||
|
// Brand-new installs skip all migrations and stamp directly to the current version.
|
||||||
|
freshInstallVersion: targetVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register steps in execution order. Each step's .from() must match the
|
||||||
|
// previous step's .to() to form a contiguous chain.
|
||||||
|
migration
|
||||||
|
.step('rename-target-profile-host-to-ip')
|
||||||
|
.from('13.0.11').to('13.1.0')
|
||||||
|
.description('Rename ITargetProfileTarget.host → ip on all target profiles')
|
||||||
|
.up(async (ctx) => {
|
||||||
|
const collection = ctx.mongo!.collection('targetprofiledoc');
|
||||||
|
const cursor = collection.find({ 'targets.host': { $exists: true } });
|
||||||
|
let migrated = 0;
|
||||||
|
for await (const doc of cursor) {
|
||||||
|
const targets = ((doc as any).targets || []).map((t: any) => {
|
||||||
|
if (t && typeof t === 'object' && 'host' in t && !('ip' in t)) {
|
||||||
|
const { host, ...rest } = t;
|
||||||
|
return { ...rest, ip: host };
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
await collection.updateOne({ _id: (doc as any)._id }, { $set: { targets } });
|
||||||
|
migrated++;
|
||||||
|
}
|
||||||
|
ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return migration;
|
||||||
|
}
|
||||||
3
ts_migrations/tspublish.json
Normal file
3
ts_migrations/tspublish.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.11',
|
version: '13.1.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1015,7 +1015,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
@@ -1037,7 +1037,7 @@ export const createVpnClientAction = vpnStatePart.createAction<{
|
|||||||
clientId: dataArg.clientId,
|
clientId: dataArg.clientId,
|
||||||
targetProfileIds: dataArg.targetProfileIds,
|
targetProfileIds: dataArg.targetProfileIds,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
||||||
destinationAllowList: dataArg.destinationAllowList,
|
destinationAllowList: dataArg.destinationAllowList,
|
||||||
destinationBlockList: dataArg.destinationBlockList,
|
destinationBlockList: dataArg.destinationBlockList,
|
||||||
useHostIp: dataArg.useHostIp,
|
useHostIp: dataArg.useHostIp,
|
||||||
@@ -1113,7 +1113,7 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
targetProfileIds?: string[];
|
targetProfileIds?: string[];
|
||||||
forceDestinationSmartproxy?: boolean;
|
|
||||||
destinationAllowList?: string[];
|
destinationAllowList?: string[];
|
||||||
destinationBlockList?: string[];
|
destinationBlockList?: string[];
|
||||||
useHostIp?: boolean;
|
useHostIp?: boolean;
|
||||||
@@ -1135,7 +1135,7 @@ export const updateVpnClientAction = vpnStatePart.createAction<{
|
|||||||
clientId: dataArg.clientId,
|
clientId: dataArg.clientId,
|
||||||
description: dataArg.description,
|
description: dataArg.description,
|
||||||
targetProfileIds: dataArg.targetProfileIds,
|
targetProfileIds: dataArg.targetProfileIds,
|
||||||
forceDestinationSmartproxy: dataArg.forceDestinationSmartproxy,
|
|
||||||
destinationAllowList: dataArg.destinationAllowList,
|
destinationAllowList: dataArg.destinationAllowList,
|
||||||
destinationBlockList: dataArg.destinationBlockList,
|
destinationBlockList: dataArg.destinationBlockList,
|
||||||
useHostIp: dataArg.useHostIp,
|
useHostIp: dataArg.useHostIp,
|
||||||
@@ -1223,7 +1223,7 @@ export const createTargetProfileAction = targetProfilesStatePart.createAction<{
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
domains?: string[];
|
domains?: string[];
|
||||||
targets?: Array<{ host: string; port: number }>;
|
targets?: Array<{ ip: string; port: number }>;
|
||||||
routeRefs?: string[];
|
routeRefs?: string[];
|
||||||
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -1259,7 +1259,7 @@ export const updateTargetProfileAction = targetProfilesStatePart.createAction<{
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
domains?: string[];
|
domains?: string[];
|
||||||
targets?: Array<{ host: string; port: number }>;
|
targets?: Array<{ ip: string; port: number }>;
|
||||||
routeRefs?: string[];
|
routeRefs?: string[];
|
||||||
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
}>(async (statePartArg, dataArg, actionContext): Promise<ITargetProfilesState> => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|||||||
@@ -30,6 +30,20 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
accessor selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
||||||
|
|
||||||
|
private tabLabelMap: Record<string, string> = {
|
||||||
|
'overview': 'Overview',
|
||||||
|
'blocked': 'Blocked IPs',
|
||||||
|
'authentication': 'Authentication',
|
||||||
|
'email-security': 'Email Security',
|
||||||
|
};
|
||||||
|
|
||||||
|
private labelToTab: Record<string, 'overview' | 'blocked' | 'authentication' | 'email-security'> = {
|
||||||
|
'Overview': 'overview',
|
||||||
|
'Blocked IPs': 'blocked',
|
||||||
|
'Authentication': 'authentication',
|
||||||
|
'Email Security': 'email-security',
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subscription = appstate.statsStatePart
|
const subscription = appstate.statsStatePart
|
||||||
@@ -40,35 +54,23 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
this.rxSubscriptions.push(subscription);
|
this.rxSubscriptions.push(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async firstUpdated() {
|
||||||
|
const toggle = this.shadowRoot!.querySelector('dees-input-multitoggle') as any;
|
||||||
|
if (toggle) {
|
||||||
|
const sub = toggle.changeSubject.subscribe(() => {
|
||||||
|
const tab = this.labelToTab[toggle.selectedOption];
|
||||||
|
if (tab) this.selectedTab = tab;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.tabs {
|
dees-input-multitoggle {
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
border-bottom: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab:hover {
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab.active {
|
|
||||||
color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
|
||||||
border-bottom-color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -91,102 +93,10 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.securityCard.alert {
|
|
||||||
border-color: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
|
||||||
background: ${cssManager.bdTheme('#ffebee', '#4a1f1f')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.securityCard.warning {
|
|
||||||
border-color: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
|
||||||
background: ${cssManager.bdTheme('#fff3e0', '#4a3a1f')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.securityCard.success {
|
|
||||||
border-color: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
|
||||||
background: ${cssManager.bdTheme('#e8f5e9', '#1f3f1f')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardStatus {
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-critical {
|
|
||||||
background: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
|
||||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-warning {
|
|
||||||
background: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
|
||||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-good {
|
|
||||||
background: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
|
||||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.metricValue {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metricLabel {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButton {
|
.actionButton {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockedIpList {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockedIpItem {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockedIpItem:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ipAddress {
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockReason {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#666', '#999')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockTime {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#999', '#666')};
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -194,32 +104,11 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<dees-heading level="2">Security</dees-heading>
|
<dees-heading level="2">Security</dees-heading>
|
||||||
|
|
||||||
<div class="tabs">
|
<dees-input-multitoggle
|
||||||
<button
|
.type=${'single'}
|
||||||
class="tab ${this.selectedTab === 'overview' ? 'active' : ''}"
|
.options=${['Overview', 'Blocked IPs', 'Authentication', 'Email Security']}
|
||||||
@click=${() => this.selectedTab = 'overview'}
|
.selectedOption=${this.tabLabelMap[this.selectedTab]}
|
||||||
>
|
></dees-input-multitoggle>
|
||||||
Overview
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab ${this.selectedTab === 'blocked' ? 'active' : ''}"
|
|
||||||
@click=${() => this.selectedTab = 'blocked'}
|
|
||||||
>
|
|
||||||
Blocked IPs
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab ${this.selectedTab === 'authentication' ? 'active' : ''}"
|
|
||||||
@click=${() => this.selectedTab = 'authentication'}
|
|
||||||
>
|
|
||||||
Authentication
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="tab ${this.selectedTab === 'email-security' ? 'active' : ''}"
|
|
||||||
@click=${() => this.selectedTab = 'email-security'}
|
|
||||||
>
|
|
||||||
Email Security
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.renderTabContent()}
|
${this.renderTabContent()}
|
||||||
`;
|
`;
|
||||||
@@ -328,32 +217,53 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderBlockedIPs(metrics: any) {
|
private renderBlockedIPs(metrics: any) {
|
||||||
return html`
|
const blockedIPs: string[] = metrics.blockedIPs || [];
|
||||||
<div class="securityCard">
|
|
||||||
<div class="cardHeader">
|
|
||||||
<h3 class="cardTitle">Blocked IP Addresses</h3>
|
|
||||||
<dees-button @click=${() => this.clearBlockedIPs()}>
|
|
||||||
Clear All
|
|
||||||
</dees-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="blockedIpList">
|
const tiles: IStatsTile[] = [
|
||||||
${metrics.blockedIPs && metrics.blockedIPs.length > 0 ? metrics.blockedIPs.map((ipAddress, index) => html`
|
{
|
||||||
<div class="blockedIpItem">
|
id: 'totalBlocked',
|
||||||
<div>
|
title: 'Blocked IPs',
|
||||||
<div class="ipAddress">${ipAddress}</div>
|
value: blockedIPs.length,
|
||||||
<div class="blockReason">Suspicious activity</div>
|
type: 'number',
|
||||||
<div class="blockTime">Blocked</div>
|
icon: 'lucide:ShieldBan',
|
||||||
</div>
|
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
|
||||||
<dees-button @click=${() => this.unblockIP(ipAddress)}>
|
description: 'Currently blocked addresses',
|
||||||
Unblock
|
},
|
||||||
</dees-button>
|
];
|
||||||
</div>
|
|
||||||
`) : html`
|
return html`
|
||||||
<p>No blocked IPs</p>
|
<dees-statsgrid
|
||||||
`}
|
.tiles=${tiles}
|
||||||
</div>
|
.minTileWidth=${200}
|
||||||
</div>
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Blocked IP Addresses'}
|
||||||
|
.heading2=${'IPs blocked due to suspicious activity'}
|
||||||
|
.data=${blockedIPs.map((ip) => ({ ip }))}
|
||||||
|
.displayFunction=${(item) => ({
|
||||||
|
'IP Address': item.ip,
|
||||||
|
'Reason': 'Suspicious activity',
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Unblock',
|
||||||
|
iconName: 'lucide:shield-off',
|
||||||
|
type: ['contextmenu' as const],
|
||||||
|
actionFunc: async (item) => {
|
||||||
|
await this.unblockIP(item.ip);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Clear All',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
type: ['header' as const],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await this.clearBlockedIPs();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
? html`${profile.domains.map(d => html`<span class="tagBadge">${d}</span>`)}`
|
? html`${profile.domains.map(d => html`<span class="tagBadge">${d}</span>`)}`
|
||||||
: '-',
|
: '-',
|
||||||
Targets: profile.targets?.length
|
Targets: profile.targets?.length
|
||||||
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.host}:${t.port}</span>`)}`
|
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)}`
|
||||||
: '-',
|
: '-',
|
||||||
'Route Refs': profile.routeRefs?.length
|
'Route Refs': profile.routeRefs?.length
|
||||||
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)}`
|
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)}`
|
||||||
@@ -175,7 +175,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true}></dees-input-list>
|
||||||
<dees-input-list .key=${'targets'} .label=${'Targets (host:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
|
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true}></dees-input-list>
|
||||||
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list>
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true}></dees-input-list>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
@@ -197,11 +197,11 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
const lastColon = s.lastIndexOf(':');
|
const lastColon = s.lastIndexOf(':');
|
||||||
if (lastColon === -1) return null;
|
if (lastColon === -1) return null;
|
||||||
return {
|
return {
|
||||||
host: s.substring(0, lastColon),
|
ip: s.substring(0, lastColon),
|
||||||
port: parseInt(s.substring(lastColon + 1), 10),
|
port: parseInt(s.substring(lastColon + 1), 10),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port));
|
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
||||||
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
||||||
|
|
||||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.createTargetProfileAction, {
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.createTargetProfileAction, {
|
||||||
@@ -220,7 +220,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
|
|
||||||
private async showEditProfileDialog(profile: interfaces.data.ITargetProfile) {
|
private async showEditProfileDialog(profile: interfaces.data.ITargetProfile) {
|
||||||
const currentDomains = profile.domains || [];
|
const currentDomains = profile.domains || [];
|
||||||
const currentTargets = profile.targets?.map(t => `${t.host}:${t.port}`) || [];
|
const currentTargets = profile.targets?.map(t => `${t.ip}:${t.port}`) || [];
|
||||||
const currentRouteRefs = profile.routeRefs || [];
|
const currentRouteRefs = profile.routeRefs || [];
|
||||||
|
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
@@ -234,7 +234,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
|
||||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
|
||||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
|
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'e.g. *.example.com'} .allowFreeform=${true} .value=${currentDomains}></dees-input-list>
|
||||||
<dees-input-list .key=${'targets'} .label=${'Targets (host:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
|
<dees-input-list .key=${'targets'} .label=${'Targets (ip:port)'} .placeholder=${'e.g. 10.0.0.1:443'} .allowFreeform=${true} .value=${currentTargets}></dees-input-list>
|
||||||
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list>
|
<dees-input-list .key=${'routeRefs'} .label=${'Route Refs'} .placeholder=${'Type to search routes...'} .candidates=${routeCandidates} .allowFreeform=${true} .value=${currentRouteRefs}></dees-input-list>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
@@ -255,11 +255,11 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
const lastColon = s.lastIndexOf(':');
|
const lastColon = s.lastIndexOf(':');
|
||||||
if (lastColon === -1) return null;
|
if (lastColon === -1) return null;
|
||||||
return {
|
return {
|
||||||
host: s.substring(0, lastColon),
|
ip: s.substring(0, lastColon),
|
||||||
port: parseInt(s.substring(lastColon + 1), 10),
|
port: parseInt(s.substring(lastColon + 1), 10),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port));
|
.filter((t): t is { ip: string; port: number } => t !== null && !isNaN(t.port));
|
||||||
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
const routeRefs: string[] = Array.isArray(data.routeRefs) ? data.routeRefs : [];
|
||||||
|
|
||||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.updateTargetProfileAction, {
|
await appstate.targetProfilesStatePart.dispatchAction(appstate.updateTargetProfileAction, {
|
||||||
@@ -327,7 +327,7 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Targets</div>
|
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Targets</div>
|
||||||
<div style="font-size: 14px; margin-top: 4px;">
|
<div style="font-size: 14px; margin-top: 4px;">
|
||||||
${profile.targets?.length
|
${profile.targets?.length
|
||||||
? profile.targets.map(t => html`<span class="tagBadge">${t.host}:${t.port}</span>`)
|
? profile.targets.map(t => html`<span class="tagBadge">${t.ip}:${t.port}</span>`)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function setupFormVisibility(formEl: any) {
|
|||||||
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
|
||||||
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
|
||||||
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
|
||||||
if (hostIpGroup) hostIpGroup.style.display = data.forceDestinationSmartproxy ? 'none' : show;
|
if (hostIpGroup) hostIpGroup.style.display = show; // always show (forceTarget is always on)
|
||||||
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
|
||||||
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
|
||||||
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
|
||||||
@@ -317,9 +317,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
||||||
}
|
}
|
||||||
let routingHtml;
|
let routingHtml;
|
||||||
if (client.forceDestinationSmartproxy !== false) {
|
if (client.useHostIp) {
|
||||||
routingHtml = html`<span class="statusBadge enabled">SmartProxy</span>`;
|
|
||||||
} else if (client.useHostIp) {
|
|
||||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
|
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#f3e8ff', '#3b0764')}; color: ${cssManager.bdTheme('#7c3aed', '#c084fc')};">Host IP</span>`;
|
||||||
} else {
|
} else {
|
||||||
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
|
routingHtml = html`<span class="statusBadge" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">Direct</span>`;
|
||||||
@@ -355,8 +353,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
||||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||||
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false}></dees-input-list>
|
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false}></dees-input-list>
|
||||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
<div class="hostIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
|
||||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
||||||
<div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
|
<div class="hostIpDetails" style="display: none; flex-direction: column; gap: 16px;">
|
||||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
|
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${false}></dees-input-checkbox>
|
||||||
@@ -395,8 +392,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Apply conditional logic based on checkbox states
|
// Apply conditional logic based on checkbox states
|
||||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
const useHostIp = data.useHostIp ?? false;
|
||||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
|
||||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||||
@@ -414,7 +410,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
clientId: data.clientId,
|
clientId: data.clientId,
|
||||||
description: data.description || undefined,
|
description: data.description || undefined,
|
||||||
targetProfileIds,
|
targetProfileIds,
|
||||||
forceDestinationSmartproxy: forceSmartproxy,
|
|
||||||
useHostIp: useHostIp || undefined,
|
useHostIp: useHostIp || undefined,
|
||||||
useDhcp: useDhcp || undefined,
|
useDhcp: useDhcp || undefined,
|
||||||
staticIp,
|
staticIp,
|
||||||
@@ -487,7 +483,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
` : ''}
|
` : ''}
|
||||||
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
||||||
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToNames(client.targetProfileIds)?.join(', ') || '-'}</span></div>
|
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToNames(client.targetProfileIds)?.join(', ') || '-'}</span></div>
|
||||||
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</span></div>
|
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.useHostIp ? 'Host IP' : 'SmartProxy'}</span></div>
|
||||||
${client.useHostIp ? html`
|
${client.useHostIp ? html`
|
||||||
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
|
||||||
<div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
|
<div class="infoItem"><span class="infoLabel">VLAN</span><span class="infoValue">${client.forceVlan && client.vlanId != null ? `VLAN ${client.vlanId}` : 'No VLAN'}</span></div>
|
||||||
@@ -652,7 +648,6 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
const currentDescription = client.description ?? '';
|
const currentDescription = client.description ?? '';
|
||||||
const currentTargetProfileNames = this.resolveProfileIdsToNames(client.targetProfileIds) || [];
|
const currentTargetProfileNames = this.resolveProfileIdsToNames(client.targetProfileIds) || [];
|
||||||
const profileCandidates = this.getTargetProfileCandidates();
|
const profileCandidates = this.getTargetProfileCandidates();
|
||||||
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
|
||||||
const currentUseHostIp = client.useHostIp ?? false;
|
const currentUseHostIp = client.useHostIp ?? false;
|
||||||
const currentUseDhcp = client.useDhcp ?? false;
|
const currentUseDhcp = client.useDhcp ?? false;
|
||||||
const currentStaticIp = client.staticIp ?? '';
|
const currentStaticIp = client.staticIp ?? '';
|
||||||
@@ -668,8 +663,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
||||||
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false} .value=${currentTargetProfileNames}></dees-input-list>
|
<dees-input-list .key=${'targetProfileNames'} .label=${'Target Profiles'} .placeholder=${'Type to search profiles...'} .candidates=${profileCandidates} .allowFreeform=${false} .value=${currentTargetProfileNames}></dees-input-list>
|
||||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
<div class="hostIpGroup" style="display: flex; flex-direction: column; gap: 16px;">
|
||||||
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
|
||||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
||||||
<div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
<div class="hostIpDetails" style="display: ${currentUseHostIp ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||||
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
|
<dees-input-checkbox .key=${'useDhcp'} .label=${'Get IP through DHCP'} .value=${currentUseDhcp}></dees-input-checkbox>
|
||||||
@@ -703,8 +697,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Apply conditional logic based on checkbox states
|
// Apply conditional logic based on checkbox states
|
||||||
const forceSmartproxy = data.forceDestinationSmartproxy ?? true;
|
const useHostIp = data.useHostIp ?? false;
|
||||||
const useHostIp = !forceSmartproxy && (data.useHostIp ?? false);
|
|
||||||
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
const useDhcp = useHostIp && (data.useDhcp ?? false);
|
||||||
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
const staticIp = useHostIp && !useDhcp && data.staticIp ? data.staticIp : undefined;
|
||||||
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
const forceVlan = useHostIp && (data.forceVlan ?? false);
|
||||||
@@ -722,7 +715,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
clientId: client.clientId,
|
clientId: client.clientId,
|
||||||
description: data.description || undefined,
|
description: data.description || undefined,
|
||||||
targetProfileIds,
|
targetProfileIds,
|
||||||
forceDestinationSmartproxy: forceSmartproxy,
|
|
||||||
useHostIp: useHostIp || undefined,
|
useHostIp: useHostIp || undefined,
|
||||||
useDhcp: useDhcp || undefined,
|
useDhcp: useDhcp || undefined,
|
||||||
staticIp,
|
staticIp,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"order": 3
|
"order": 4
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user