577 lines
19 KiB
TypeScript
577 lines
19 KiB
TypeScript
|
|
import * as plugins from '../plugins.js';
|
||
|
|
import { logger } from '../logger.js';
|
||
|
|
import { SecurityProfileDoc, NetworkTargetDoc, StoredRouteDoc } from '../db/index.js';
|
||
|
|
import type {
|
||
|
|
ISecurityProfile,
|
||
|
|
INetworkTarget,
|
||
|
|
IRouteMetadata,
|
||
|
|
IStoredRoute,
|
||
|
|
IRouteSecurity,
|
||
|
|
} from '../../ts_interfaces/data/route-management.js';
|
||
|
|
|
||
|
|
const MAX_INHERITANCE_DEPTH = 5;
|
||
|
|
|
||
|
|
export class ReferenceResolver {
|
||
|
|
private profiles = new Map<string, ISecurityProfile>();
|
||
|
|
private targets = new Map<string, INetworkTarget>();
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Lifecycle
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
public async initialize(): Promise<void> {
|
||
|
|
await this.loadProfiles();
|
||
|
|
await this.loadTargets();
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Profile CRUD
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
public async createProfile(data: {
|
||
|
|
name: string;
|
||
|
|
description?: string;
|
||
|
|
security: IRouteSecurity;
|
||
|
|
extendsProfiles?: string[];
|
||
|
|
createdBy: string;
|
||
|
|
}): Promise<string> {
|
||
|
|
const id = plugins.uuid.v4();
|
||
|
|
const now = Date.now();
|
||
|
|
|
||
|
|
const profile: ISecurityProfile = {
|
||
|
|
id,
|
||
|
|
name: data.name,
|
||
|
|
description: data.description,
|
||
|
|
security: data.security,
|
||
|
|
extendsProfiles: data.extendsProfiles,
|
||
|
|
createdAt: now,
|
||
|
|
updatedAt: now,
|
||
|
|
createdBy: data.createdBy,
|
||
|
|
};
|
||
|
|
|
||
|
|
this.profiles.set(id, profile);
|
||
|
|
await this.persistProfile(profile);
|
||
|
|
logger.log('info', `Created security profile '${profile.name}' (${id})`);
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async updateProfile(
|
||
|
|
id: string,
|
||
|
|
patch: Partial<Omit<ISecurityProfile, 'id' | 'createdAt' | 'createdBy'>>,
|
||
|
|
): Promise<{ affectedRouteIds: string[] }> {
|
||
|
|
const profile = this.profiles.get(id);
|
||
|
|
if (!profile) {
|
||
|
|
throw new Error(`Security profile '${id}' not found`);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (patch.name !== undefined) profile.name = patch.name;
|
||
|
|
if (patch.description !== undefined) profile.description = patch.description;
|
||
|
|
if (patch.security !== undefined) profile.security = patch.security;
|
||
|
|
if (patch.extendsProfiles !== undefined) profile.extendsProfiles = patch.extendsProfiles;
|
||
|
|
profile.updatedAt = Date.now();
|
||
|
|
|
||
|
|
await this.persistProfile(profile);
|
||
|
|
logger.log('info', `Updated security profile '${profile.name}' (${id})`);
|
||
|
|
|
||
|
|
// Find routes referencing this profile
|
||
|
|
const affectedRouteIds = await this.findRoutesByProfileRef(id);
|
||
|
|
return { affectedRouteIds };
|
||
|
|
}
|
||
|
|
|
||
|
|
public async deleteProfile(
|
||
|
|
id: string,
|
||
|
|
force: boolean,
|
||
|
|
storedRoutes?: Map<string, IStoredRoute>,
|
||
|
|
): Promise<{ success: boolean; message?: string }> {
|
||
|
|
const profile = this.profiles.get(id);
|
||
|
|
if (!profile) {
|
||
|
|
return { success: false, message: `Security profile '${id}' not found` };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check usage
|
||
|
|
const affectedIds = storedRoutes
|
||
|
|
? this.findRoutesByProfileRefSync(id, storedRoutes)
|
||
|
|
: await this.findRoutesByProfileRef(id);
|
||
|
|
|
||
|
|
if (affectedIds.length > 0 && !force) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
message: `Profile '${profile.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete from DB
|
||
|
|
const doc = await SecurityProfileDoc.findById(id);
|
||
|
|
if (doc) await doc.delete();
|
||
|
|
this.profiles.delete(id);
|
||
|
|
|
||
|
|
// If force-deleting with referencing routes, clear refs but keep resolved values
|
||
|
|
if (affectedIds.length > 0) {
|
||
|
|
await this.clearProfileRefsOnRoutes(affectedIds);
|
||
|
|
logger.log('warn', `Force-deleted profile '${profile.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
||
|
|
} else {
|
||
|
|
logger.log('info', `Deleted security profile '${profile.name}' (${id})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: true };
|
||
|
|
}
|
||
|
|
|
||
|
|
public getProfile(id: string): ISecurityProfile | undefined {
|
||
|
|
return this.profiles.get(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public getProfileByName(name: string): ISecurityProfile | undefined {
|
||
|
|
for (const profile of this.profiles.values()) {
|
||
|
|
if (profile.name === name) return profile;
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
public listProfiles(): ISecurityProfile[] {
|
||
|
|
return [...this.profiles.values()];
|
||
|
|
}
|
||
|
|
|
||
|
|
public getProfileUsage(storedRoutes: Map<string, IStoredRoute>): Map<string, Array<{ id: string; routeName: string }>> {
|
||
|
|
const usage = new Map<string, Array<{ id: string; routeName: string }>>();
|
||
|
|
for (const profile of this.profiles.values()) {
|
||
|
|
usage.set(profile.id, []);
|
||
|
|
}
|
||
|
|
for (const [routeId, stored] of storedRoutes) {
|
||
|
|
const ref = stored.metadata?.securityProfileRef;
|
||
|
|
if (ref && usage.has(ref)) {
|
||
|
|
usage.get(ref)!.push({ id: routeId, routeName: stored.route.name || routeId });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return usage;
|
||
|
|
}
|
||
|
|
|
||
|
|
public getProfileUsageForId(
|
||
|
|
profileId: string,
|
||
|
|
storedRoutes: Map<string, IStoredRoute>,
|
||
|
|
): Array<{ id: string; routeName: string }> {
|
||
|
|
const routes: Array<{ id: string; routeName: string }> = [];
|
||
|
|
for (const [routeId, stored] of storedRoutes) {
|
||
|
|
if (stored.metadata?.securityProfileRef === profileId) {
|
||
|
|
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return routes;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Target CRUD
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
public async createTarget(data: {
|
||
|
|
name: string;
|
||
|
|
description?: string;
|
||
|
|
host: string | string[];
|
||
|
|
port: number;
|
||
|
|
createdBy: string;
|
||
|
|
}): Promise<string> {
|
||
|
|
const id = plugins.uuid.v4();
|
||
|
|
const now = Date.now();
|
||
|
|
|
||
|
|
const target: INetworkTarget = {
|
||
|
|
id,
|
||
|
|
name: data.name,
|
||
|
|
description: data.description,
|
||
|
|
host: data.host,
|
||
|
|
port: data.port,
|
||
|
|
createdAt: now,
|
||
|
|
updatedAt: now,
|
||
|
|
createdBy: data.createdBy,
|
||
|
|
};
|
||
|
|
|
||
|
|
this.targets.set(id, target);
|
||
|
|
await this.persistTarget(target);
|
||
|
|
logger.log('info', `Created network target '${target.name}' (${id})`);
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async updateTarget(
|
||
|
|
id: string,
|
||
|
|
patch: Partial<Omit<INetworkTarget, 'id' | 'createdAt' | 'createdBy'>>,
|
||
|
|
): Promise<{ affectedRouteIds: string[] }> {
|
||
|
|
const target = this.targets.get(id);
|
||
|
|
if (!target) {
|
||
|
|
throw new Error(`Network target '${id}' not found`);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (patch.name !== undefined) target.name = patch.name;
|
||
|
|
if (patch.description !== undefined) target.description = patch.description;
|
||
|
|
if (patch.host !== undefined) target.host = patch.host;
|
||
|
|
if (patch.port !== undefined) target.port = patch.port;
|
||
|
|
target.updatedAt = Date.now();
|
||
|
|
|
||
|
|
await this.persistTarget(target);
|
||
|
|
logger.log('info', `Updated network target '${target.name}' (${id})`);
|
||
|
|
|
||
|
|
const affectedRouteIds = await this.findRoutesByTargetRef(id);
|
||
|
|
return { affectedRouteIds };
|
||
|
|
}
|
||
|
|
|
||
|
|
public async deleteTarget(
|
||
|
|
id: string,
|
||
|
|
force: boolean,
|
||
|
|
storedRoutes?: Map<string, IStoredRoute>,
|
||
|
|
): Promise<{ success: boolean; message?: string }> {
|
||
|
|
const target = this.targets.get(id);
|
||
|
|
if (!target) {
|
||
|
|
return { success: false, message: `Network target '${id}' not found` };
|
||
|
|
}
|
||
|
|
|
||
|
|
const affectedIds = storedRoutes
|
||
|
|
? this.findRoutesByTargetRefSync(id, storedRoutes)
|
||
|
|
: await this.findRoutesByTargetRef(id);
|
||
|
|
|
||
|
|
if (affectedIds.length > 0 && !force) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
message: `Target '${target.name}' is in use by ${affectedIds.length} route(s). Use force=true to delete.`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const doc = await NetworkTargetDoc.findById(id);
|
||
|
|
if (doc) await doc.delete();
|
||
|
|
this.targets.delete(id);
|
||
|
|
|
||
|
|
if (affectedIds.length > 0) {
|
||
|
|
await this.clearTargetRefsOnRoutes(affectedIds);
|
||
|
|
logger.log('warn', `Force-deleted target '${target.name}'; cleared refs on ${affectedIds.length} route(s)`);
|
||
|
|
} else {
|
||
|
|
logger.log('info', `Deleted network target '${target.name}' (${id})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: true };
|
||
|
|
}
|
||
|
|
|
||
|
|
public getTarget(id: string): INetworkTarget | undefined {
|
||
|
|
return this.targets.get(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public getTargetByName(name: string): INetworkTarget | undefined {
|
||
|
|
for (const target of this.targets.values()) {
|
||
|
|
if (target.name === name) return target;
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
public listTargets(): INetworkTarget[] {
|
||
|
|
return [...this.targets.values()];
|
||
|
|
}
|
||
|
|
|
||
|
|
public getTargetUsageForId(
|
||
|
|
targetId: string,
|
||
|
|
storedRoutes: Map<string, IStoredRoute>,
|
||
|
|
): Array<{ id: string; routeName: string }> {
|
||
|
|
const routes: Array<{ id: string; routeName: string }> = [];
|
||
|
|
for (const [routeId, stored] of storedRoutes) {
|
||
|
|
if (stored.metadata?.networkTargetRef === targetId) {
|
||
|
|
routes.push({ id: routeId, routeName: stored.route.name || routeId });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return routes;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Resolution
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve references for a single route.
|
||
|
|
* Materializes security profile and/or network target into the route's fields.
|
||
|
|
* Returns the resolved route and updated metadata.
|
||
|
|
*/
|
||
|
|
public resolveRoute(
|
||
|
|
route: plugins.smartproxy.IRouteConfig,
|
||
|
|
metadata?: IRouteMetadata,
|
||
|
|
): { route: plugins.smartproxy.IRouteConfig; metadata: IRouteMetadata } {
|
||
|
|
const resolvedMetadata: IRouteMetadata = { ...metadata };
|
||
|
|
|
||
|
|
if (resolvedMetadata.securityProfileRef) {
|
||
|
|
const resolvedSecurity = this.resolveSecurityProfile(resolvedMetadata.securityProfileRef);
|
||
|
|
if (resolvedSecurity) {
|
||
|
|
const profile = this.profiles.get(resolvedMetadata.securityProfileRef);
|
||
|
|
// Merge: profile provides base, route's inline values override
|
||
|
|
route = {
|
||
|
|
...route,
|
||
|
|
security: this.mergeSecurityFields(resolvedSecurity, route.security),
|
||
|
|
};
|
||
|
|
resolvedMetadata.securityProfileName = profile?.name;
|
||
|
|
resolvedMetadata.lastResolvedAt = Date.now();
|
||
|
|
} else {
|
||
|
|
logger.log('warn', `Security profile '${resolvedMetadata.securityProfileRef}' not found during resolution`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (resolvedMetadata.networkTargetRef) {
|
||
|
|
const target = this.targets.get(resolvedMetadata.networkTargetRef);
|
||
|
|
if (target) {
|
||
|
|
route = {
|
||
|
|
...route,
|
||
|
|
action: {
|
||
|
|
...route.action,
|
||
|
|
targets: [{
|
||
|
|
host: target.host as string,
|
||
|
|
port: target.port,
|
||
|
|
}],
|
||
|
|
},
|
||
|
|
};
|
||
|
|
resolvedMetadata.networkTargetName = target.name;
|
||
|
|
resolvedMetadata.lastResolvedAt = Date.now();
|
||
|
|
} else {
|
||
|
|
logger.log('warn', `Network target '${resolvedMetadata.networkTargetRef}' not found during resolution`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { route, metadata: resolvedMetadata };
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Reference lookup helpers
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
public async findRoutesByProfileRef(profileId: string): Promise<string[]> {
|
||
|
|
const docs = await StoredRouteDoc.findAll();
|
||
|
|
return docs
|
||
|
|
.filter((doc) => doc.metadata?.securityProfileRef === profileId)
|
||
|
|
.map((doc) => doc.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async findRoutesByTargetRef(targetId: string): Promise<string[]> {
|
||
|
|
const docs = await StoredRouteDoc.findAll();
|
||
|
|
return docs
|
||
|
|
.filter((doc) => doc.metadata?.networkTargetRef === targetId)
|
||
|
|
.map((doc) => doc.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public findRoutesByProfileRefSync(profileId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
|
||
|
|
const ids: string[] = [];
|
||
|
|
for (const [routeId, stored] of storedRoutes) {
|
||
|
|
if (stored.metadata?.securityProfileRef === profileId) {
|
||
|
|
ids.push(routeId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ids;
|
||
|
|
}
|
||
|
|
|
||
|
|
public findRoutesByTargetRefSync(targetId: string, storedRoutes: Map<string, IStoredRoute>): string[] {
|
||
|
|
const ids: string[] = [];
|
||
|
|
for (const [routeId, stored] of storedRoutes) {
|
||
|
|
if (stored.metadata?.networkTargetRef === targetId) {
|
||
|
|
ids.push(routeId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ids;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Private: security profile resolution with inheritance
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
private resolveSecurityProfile(
|
||
|
|
profileId: string,
|
||
|
|
visited: Set<string> = new Set(),
|
||
|
|
depth: number = 0,
|
||
|
|
): IRouteSecurity | null {
|
||
|
|
if (depth > MAX_INHERITANCE_DEPTH) {
|
||
|
|
logger.log('warn', `Max inheritance depth (${MAX_INHERITANCE_DEPTH}) exceeded resolving profile '${profileId}'`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visited.has(profileId)) {
|
||
|
|
logger.log('warn', `Circular inheritance detected for profile '${profileId}'`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const profile = this.profiles.get(profileId);
|
||
|
|
if (!profile) return null;
|
||
|
|
|
||
|
|
visited.add(profileId);
|
||
|
|
|
||
|
|
// Start with an empty base
|
||
|
|
let baseSecurity: IRouteSecurity = {};
|
||
|
|
|
||
|
|
// Resolve parent profiles first (top-down, later overrides earlier)
|
||
|
|
if (profile.extendsProfiles?.length) {
|
||
|
|
for (const parentId of profile.extendsProfiles) {
|
||
|
|
const parentSecurity = this.resolveSecurityProfile(parentId, new Set(visited), depth + 1);
|
||
|
|
if (parentSecurity) {
|
||
|
|
baseSecurity = this.mergeSecurityFields(baseSecurity, parentSecurity);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply this profile's security on top
|
||
|
|
return this.mergeSecurityFields(baseSecurity, profile.security);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Merge two IRouteSecurity objects.
|
||
|
|
* `override` values take precedence over `base` values.
|
||
|
|
* For ipAllowList/ipBlockList: union arrays and deduplicate.
|
||
|
|
* For scalar/object fields: override wins if present.
|
||
|
|
*/
|
||
|
|
private mergeSecurityFields(
|
||
|
|
base: IRouteSecurity | undefined,
|
||
|
|
override: IRouteSecurity | undefined,
|
||
|
|
): IRouteSecurity {
|
||
|
|
if (!base && !override) return {};
|
||
|
|
if (!base) return { ...override };
|
||
|
|
if (!override) return { ...base };
|
||
|
|
|
||
|
|
const merged: IRouteSecurity = { ...base };
|
||
|
|
|
||
|
|
// IP lists: union
|
||
|
|
if (override.ipAllowList || base.ipAllowList) {
|
||
|
|
merged.ipAllowList = [...new Set([
|
||
|
|
...(base.ipAllowList || []),
|
||
|
|
...(override.ipAllowList || []),
|
||
|
|
])];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (override.ipBlockList || base.ipBlockList) {
|
||
|
|
merged.ipBlockList = [...new Set([
|
||
|
|
...(base.ipBlockList || []),
|
||
|
|
...(override.ipBlockList || []),
|
||
|
|
])];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scalar/object fields: override wins
|
||
|
|
if (override.maxConnections !== undefined) merged.maxConnections = override.maxConnections;
|
||
|
|
if (override.rateLimit !== undefined) merged.rateLimit = override.rateLimit;
|
||
|
|
if (override.authentication !== undefined) merged.authentication = override.authentication;
|
||
|
|
if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
|
||
|
|
if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
|
||
|
|
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Private: persistence
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
private async loadProfiles(): Promise<void> {
|
||
|
|
const docs = await SecurityProfileDoc.findAll();
|
||
|
|
for (const doc of docs) {
|
||
|
|
if (doc.id) {
|
||
|
|
this.profiles.set(doc.id, {
|
||
|
|
id: doc.id,
|
||
|
|
name: doc.name,
|
||
|
|
description: doc.description,
|
||
|
|
security: doc.security,
|
||
|
|
extendsProfiles: doc.extendsProfiles,
|
||
|
|
createdAt: doc.createdAt,
|
||
|
|
updatedAt: doc.updatedAt,
|
||
|
|
createdBy: doc.createdBy,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (this.profiles.size > 0) {
|
||
|
|
logger.log('info', `Loaded ${this.profiles.size} security profile(s) from storage`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async loadTargets(): Promise<void> {
|
||
|
|
const docs = await NetworkTargetDoc.findAll();
|
||
|
|
for (const doc of docs) {
|
||
|
|
if (doc.id) {
|
||
|
|
this.targets.set(doc.id, {
|
||
|
|
id: doc.id,
|
||
|
|
name: doc.name,
|
||
|
|
description: doc.description,
|
||
|
|
host: doc.host,
|
||
|
|
port: doc.port,
|
||
|
|
createdAt: doc.createdAt,
|
||
|
|
updatedAt: doc.updatedAt,
|
||
|
|
createdBy: doc.createdBy,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (this.targets.size > 0) {
|
||
|
|
logger.log('info', `Loaded ${this.targets.size} network target(s) from storage`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async persistProfile(profile: ISecurityProfile): Promise<void> {
|
||
|
|
const existingDoc = await SecurityProfileDoc.findById(profile.id);
|
||
|
|
if (existingDoc) {
|
||
|
|
existingDoc.name = profile.name;
|
||
|
|
existingDoc.description = profile.description;
|
||
|
|
existingDoc.security = profile.security;
|
||
|
|
existingDoc.extendsProfiles = profile.extendsProfiles;
|
||
|
|
existingDoc.updatedAt = profile.updatedAt;
|
||
|
|
await existingDoc.save();
|
||
|
|
} else {
|
||
|
|
const doc = new SecurityProfileDoc();
|
||
|
|
doc.id = profile.id;
|
||
|
|
doc.name = profile.name;
|
||
|
|
doc.description = profile.description;
|
||
|
|
doc.security = profile.security;
|
||
|
|
doc.extendsProfiles = profile.extendsProfiles;
|
||
|
|
doc.createdAt = profile.createdAt;
|
||
|
|
doc.updatedAt = profile.updatedAt;
|
||
|
|
doc.createdBy = profile.createdBy;
|
||
|
|
await doc.save();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async persistTarget(target: INetworkTarget): Promise<void> {
|
||
|
|
const existingDoc = await NetworkTargetDoc.findById(target.id);
|
||
|
|
if (existingDoc) {
|
||
|
|
existingDoc.name = target.name;
|
||
|
|
existingDoc.description = target.description;
|
||
|
|
existingDoc.host = target.host;
|
||
|
|
existingDoc.port = target.port;
|
||
|
|
existingDoc.updatedAt = target.updatedAt;
|
||
|
|
await existingDoc.save();
|
||
|
|
} else {
|
||
|
|
const doc = new NetworkTargetDoc();
|
||
|
|
doc.id = target.id;
|
||
|
|
doc.name = target.name;
|
||
|
|
doc.description = target.description;
|
||
|
|
doc.host = target.host;
|
||
|
|
doc.port = target.port;
|
||
|
|
doc.createdAt = target.createdAt;
|
||
|
|
doc.updatedAt = target.updatedAt;
|
||
|
|
doc.createdBy = target.createdBy;
|
||
|
|
await doc.save();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// =========================================================================
|
||
|
|
// Private: ref cleanup on force-delete
|
||
|
|
// =========================================================================
|
||
|
|
|
||
|
|
private async clearProfileRefsOnRoutes(routeIds: string[]): Promise<void> {
|
||
|
|
for (const routeId of routeIds) {
|
||
|
|
const doc = await StoredRouteDoc.findById(routeId);
|
||
|
|
if (doc?.metadata) {
|
||
|
|
doc.metadata = {
|
||
|
|
...doc.metadata,
|
||
|
|
securityProfileRef: undefined,
|
||
|
|
securityProfileName: undefined,
|
||
|
|
};
|
||
|
|
doc.updatedAt = Date.now();
|
||
|
|
await doc.save();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async clearTargetRefsOnRoutes(routeIds: string[]): Promise<void> {
|
||
|
|
for (const routeId of routeIds) {
|
||
|
|
const doc = await StoredRouteDoc.findById(routeId);
|
||
|
|
if (doc?.metadata) {
|
||
|
|
doc.metadata = {
|
||
|
|
...doc.metadata,
|
||
|
|
networkTargetRef: undefined,
|
||
|
|
networkTargetName: undefined,
|
||
|
|
};
|
||
|
|
doc.updatedAt = Date.now();
|
||
|
|
await doc.save();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|