650 lines
26 KiB
TypeScript
650 lines
26 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
|
|
type TAuthContext = {
|
|
userId: string;
|
|
isAdmin: boolean;
|
|
token?: interfaces.data.IStoredApiToken;
|
|
};
|
|
|
|
export class WorkHosterHandler {
|
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
|
|
constructor(private opsServerRef: OpsServer) {
|
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
this.registerHandlers();
|
|
}
|
|
|
|
private async requireAuth(
|
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
requiredScope?: interfaces.data.TApiTokenScope,
|
|
): Promise<TAuthContext> {
|
|
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
scope: requiredScope,
|
|
requireAdminIdentity: requiredScope?.endsWith(':write'),
|
|
});
|
|
return { userId: auth.userId, isAdmin: auth.isAdmin, token: auth.token };
|
|
}
|
|
|
|
private async requireAdmin(
|
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
scope: interfaces.data.TApiTokenScope = 'gateway-clients:write',
|
|
): Promise<string> {
|
|
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
scope,
|
|
requireAdminIdentity: true,
|
|
requireAdminToken: true,
|
|
});
|
|
return auth.userId;
|
|
}
|
|
|
|
private registerHandlers(): void {
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayCapabilities>(
|
|
'getGatewayCapabilities',
|
|
async (dataArg) => {
|
|
await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
return { capabilities: this.getGatewayCapabilities() };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientContext>(
|
|
'getGatewayClientContext',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
return {
|
|
context: this.getGatewayClientContext(auth),
|
|
capabilities: this.getGatewayCapabilities(),
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListGatewayClients>(
|
|
'listGatewayClients',
|
|
async (dataArg) => {
|
|
await this.requireAdmin(dataArg, 'gateway-clients:read');
|
|
return { gatewayClients: await this.listManagedGatewayClients() };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClient>(
|
|
'createGatewayClient',
|
|
async (dataArg) => {
|
|
const userId = await this.requireAdmin(dataArg);
|
|
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
try {
|
|
const gatewayClient = await manager.createClient({
|
|
id: dataArg.id,
|
|
type: dataArg.type,
|
|
name: dataArg.name,
|
|
description: dataArg.description,
|
|
hostnamePatterns: dataArg.hostnamePatterns,
|
|
allowedRouteTargets: dataArg.allowedRouteTargets,
|
|
capabilities: dataArg.capabilities,
|
|
createdBy: userId,
|
|
});
|
|
return { success: true, gatewayClient };
|
|
} catch (error) {
|
|
return { success: false, message: (error as Error).message };
|
|
}
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateGatewayClient>(
|
|
'updateGatewayClient',
|
|
async (dataArg) => {
|
|
await this.requireAdmin(dataArg);
|
|
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
const gatewayClient = await manager.updateClient(dataArg.id, {
|
|
name: dataArg.name,
|
|
description: dataArg.description,
|
|
hostnamePatterns: dataArg.hostnamePatterns,
|
|
allowedRouteTargets: dataArg.allowedRouteTargets,
|
|
capabilities: dataArg.capabilities,
|
|
enabled: dataArg.enabled,
|
|
});
|
|
return gatewayClient
|
|
? { success: true, gatewayClient }
|
|
: { success: false, message: 'Gateway client not found' };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteGatewayClient>(
|
|
'deleteGatewayClient',
|
|
async (dataArg) => {
|
|
await this.requireAdmin(dataArg);
|
|
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
if (!manager) return { success: false, message: 'Gateway client management not initialized' };
|
|
const success = await manager.deleteClient(dataArg.id);
|
|
return { success, message: success ? undefined : 'Gateway client not found' };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateGatewayClientToken>(
|
|
'createGatewayClientToken',
|
|
async (dataArg) => {
|
|
const userId = await this.requireAdmin(dataArg, 'tokens:manage');
|
|
const gatewayClient = await this.opsServerRef.dcRouterRef.gatewayClientManager?.getClient(dataArg.gatewayClientId);
|
|
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
if (!gatewayClient || !gatewayClient.enabled) {
|
|
return { success: false, message: 'Gateway client not found or disabled' };
|
|
}
|
|
if (!tokenManager) {
|
|
return { success: false, message: 'Token management not initialized' };
|
|
}
|
|
const result = await tokenManager.createToken(
|
|
dataArg.name?.trim() || `${gatewayClient.name} Token`,
|
|
['gateway-clients:read', 'gateway-clients:write'],
|
|
dataArg.expiresInDays ?? null,
|
|
userId,
|
|
{
|
|
role: 'gatewayClient',
|
|
scopes: ['gateway-clients:read', 'gateway-clients:write'],
|
|
gatewayClient: { type: gatewayClient.type, id: gatewayClient.id },
|
|
hostnamePatterns: gatewayClient.hostnamePatterns,
|
|
allowedRouteTargets: gatewayClient.allowedRouteTargets,
|
|
capabilities: gatewayClient.capabilities,
|
|
},
|
|
);
|
|
return { success: true, tokenId: result.id, tokenValue: result.rawToken };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDomains>(
|
|
'getGatewayClientDomains',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
this.assertCapability(auth, 'readDomains');
|
|
return { domains: await this.listGatewayClientDomains(auth, dataArg.gatewayClientId) };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDnsRecords>(
|
|
'getGatewayClientDnsRecords',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
this.assertCapability(auth, 'readDnsRecords');
|
|
return { records: await this.listGatewayClientDnsRecords(auth, dataArg.gatewayClientId) };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkHosterDomains>(
|
|
'getWorkHosterDomains',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'workhosters:read');
|
|
this.assertCapability(auth, 'readDomains');
|
|
return { domains: await this.listGatewayClientDomains(auth) };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncGatewayClientRoute>(
|
|
'syncGatewayClientRoute',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'gateway-clients:write');
|
|
this.assertCapability(auth, 'syncRoutes');
|
|
return await this.syncGatewayClientRoute(auth, dataArg.ownership, dataArg.route, dataArg.enabled, dataArg.delete);
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppRoute>(
|
|
'syncWorkAppRoute',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'workhosters:write');
|
|
this.assertCapability(auth, 'syncRoutes');
|
|
const ownership: interfaces.data.IGatewayClientOwnership = {
|
|
gatewayClientType: dataArg.ownership.workHosterType,
|
|
gatewayClientId: dataArg.ownership.workHosterId,
|
|
appId: dataArg.ownership.workAppId,
|
|
hostname: dataArg.ownership.hostname,
|
|
};
|
|
return await this.syncGatewayClientRoute(auth, ownership, dataArg.route, dataArg.enabled, dataArg.delete);
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkAppMailIdentities>(
|
|
'getWorkAppMailIdentities',
|
|
async (dataArg) => {
|
|
await this.requireAuth(dataArg, 'workhosters:read');
|
|
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
if (!manager) return { identities: [] };
|
|
return { identities: await manager.listMailIdentities(dataArg.ownership) };
|
|
},
|
|
),
|
|
);
|
|
|
|
this.typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppMailIdentity>(
|
|
'syncWorkAppMailIdentity',
|
|
async (dataArg) => {
|
|
const auth = await this.requireAuth(dataArg, 'workhosters:write');
|
|
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
if (!manager) {
|
|
return { success: false, message: 'WorkApp mail manager not initialized' };
|
|
}
|
|
try {
|
|
return await manager.syncMailIdentity(dataArg, auth.userId);
|
|
} catch (error) {
|
|
return { success: false, message: (error as Error).message };
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
private getGatewayCapabilities(): interfaces.data.IGatewayCapabilities {
|
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
return {
|
|
routes: {
|
|
read: Boolean(dcRouter.routeConfigManager),
|
|
write: Boolean(dcRouter.routeConfigManager),
|
|
idempotentSync: Boolean(dcRouter.routeConfigManager),
|
|
},
|
|
domains: {
|
|
read: Boolean(dcRouter.dnsManager),
|
|
write: Boolean(dcRouter.dnsManager),
|
|
},
|
|
certificates: {
|
|
read: Boolean(dcRouter.smartProxy),
|
|
export: Boolean(dcRouter.smartProxy),
|
|
forceRenew: Boolean(dcRouter.smartProxy),
|
|
},
|
|
email: {
|
|
domains: Boolean(dcRouter.emailDomainManager),
|
|
inbound: Boolean(dcRouter.emailServer),
|
|
outbound: Boolean(dcRouter.emailServer),
|
|
},
|
|
remoteIngress: {
|
|
enabled: Boolean(dcRouter.options.remoteIngressConfig?.enabled),
|
|
},
|
|
dns: {
|
|
authoritative: Boolean(dcRouter.options.dnsScopes?.length),
|
|
providerManaged: Boolean(dcRouter.dnsManager),
|
|
},
|
|
http3: {
|
|
enabled: dcRouter.options.http3?.enabled !== false,
|
|
},
|
|
};
|
|
}
|
|
|
|
private getGatewayClientContext(auth: TAuthContext): interfaces.data.IGatewayClientContext {
|
|
const policy = auth.token?.policy;
|
|
const role = auth.isAdmin ? 'admin' : policy?.role || 'operator';
|
|
return {
|
|
role,
|
|
scopes: auth.token?.scopes || ['*'],
|
|
gatewayClient: policy?.gatewayClient,
|
|
hostnamePatterns: policy?.hostnamePatterns || [],
|
|
allowedRouteTargets: policy?.allowedRouteTargets || [],
|
|
capabilities: policy?.capabilities || {},
|
|
};
|
|
}
|
|
|
|
private async listManagedGatewayClients(): Promise<interfaces.data.IGatewayClient[]> {
|
|
const manager = this.opsServerRef.dcRouterRef.gatewayClientManager;
|
|
if (!manager) return [];
|
|
const clients = await manager.listClients();
|
|
const tokens = this.opsServerRef.dcRouterRef.apiTokenManager?.listTokens() || [];
|
|
return clients.map((client) => ({
|
|
...client,
|
|
tokenCount: tokens.filter((token) => token.policy?.gatewayClient?.id === client.id).length,
|
|
}));
|
|
}
|
|
|
|
private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string {
|
|
return [
|
|
ownership.workHosterType,
|
|
ownership.workHosterId,
|
|
ownership.workAppId,
|
|
ownership.hostname,
|
|
].map((part) => part.trim()).join(':');
|
|
}
|
|
|
|
private assertCapability(
|
|
auth: TAuthContext,
|
|
capability: keyof NonNullable<interfaces.data.IApiTokenPolicy['capabilities']>,
|
|
): void {
|
|
if (auth.isAdmin) return;
|
|
const policy = auth.token?.policy;
|
|
if (!policy || policy.role !== 'gatewayClient') return;
|
|
if (policy.capabilities?.[capability] === true) return;
|
|
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${capability}`);
|
|
}
|
|
|
|
private resolveGatewayClientId(auth: TAuthContext, requestedId?: string): string | undefined {
|
|
const policyClient = auth.token?.policy?.gatewayClient;
|
|
if (!policyClient) return requestedId;
|
|
if (requestedId && requestedId !== policyClient.id) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot access another gateway client');
|
|
}
|
|
return policyClient.id;
|
|
}
|
|
|
|
private resolveGatewayClientOwnership(
|
|
auth: TAuthContext,
|
|
ownership: interfaces.data.IGatewayClientOwnership,
|
|
): Required<interfaces.data.IGatewayClientOwnership> {
|
|
const policy = auth.token?.policy;
|
|
if (policy?.role === 'gatewayClient') {
|
|
if (!policy.gatewayClient) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
|
|
}
|
|
if (ownership.gatewayClientType && ownership.gatewayClientType !== policy.gatewayClient.type) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
}
|
|
if (ownership.gatewayClientId && ownership.gatewayClientId !== policy.gatewayClient.id) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
}
|
|
return {
|
|
gatewayClientType: policy.gatewayClient.type,
|
|
gatewayClientId: policy.gatewayClient.id,
|
|
appId: ownership.appId,
|
|
hostname: ownership.hostname,
|
|
};
|
|
}
|
|
|
|
if (!ownership.gatewayClientType || !ownership.gatewayClientId) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client ownership is missing type or id');
|
|
}
|
|
return ownership as Required<interfaces.data.IGatewayClientOwnership>;
|
|
}
|
|
|
|
private assertGatewayClientOwnership(auth: TAuthContext, ownership: Required<interfaces.data.IGatewayClientOwnership>): void {
|
|
const policy = auth.token?.policy;
|
|
if (!policy || policy.role !== 'gatewayClient') return;
|
|
if (!this.matchesHostnamePatterns(ownership.hostname, policy.hostnamePatterns || [])) {
|
|
throw new plugins.typedrequest.TypedResponseError('hostname is outside token policy');
|
|
}
|
|
}
|
|
|
|
private assertRouteTargetsAllowed(auth: TAuthContext, route?: interfaces.data.IDcRouterRouteConfig): void {
|
|
const policy = auth.token?.policy;
|
|
if (!policy || policy.role !== 'gatewayClient' || !route) return;
|
|
const allowedTargets = policy.allowedRouteTargets || [];
|
|
if (allowedTargets.length === 0) {
|
|
throw new plugins.typedrequest.TypedResponseError('gateway client token has no allowed route targets');
|
|
}
|
|
const targets = ((route.action as any)?.targets || []) as Array<{ host?: string; port?: number }>;
|
|
for (const target of targets) {
|
|
const host = String(target.host || '').trim().toLowerCase();
|
|
const port = Number(target.port);
|
|
const allowed = allowedTargets.some((allowedTarget) => {
|
|
return allowedTarget.host.trim().toLowerCase() === host && allowedTarget.ports.includes(port);
|
|
});
|
|
if (!allowed) {
|
|
throw new plugins.typedrequest.TypedResponseError(`route target is outside token policy: ${host}:${port}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
private matchesHostnamePatterns(hostname: string, patterns: string[]): boolean {
|
|
const normalizedHostname = hostname.trim().toLowerCase();
|
|
if (!normalizedHostname) return false;
|
|
for (const pattern of patterns) {
|
|
const normalizedPattern = pattern.trim().toLowerCase();
|
|
if (!normalizedPattern) continue;
|
|
if (normalizedPattern === normalizedHostname) return true;
|
|
if (normalizedPattern.startsWith('*.')) {
|
|
const suffix = normalizedPattern.slice(2);
|
|
if (!normalizedHostname.endsWith(`.${suffix}`)) continue;
|
|
const prefix = normalizedHostname.slice(0, -(suffix.length + 1));
|
|
if (prefix && !prefix.includes('.')) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private getRouteHostnames(route: interfaces.data.IDcRouterRouteConfig): string[] {
|
|
const domains = (route.match as any)?.domains;
|
|
if (Array.isArray(domains)) {
|
|
return domains.map((domain) => String(domain).trim().toLowerCase()).filter(Boolean);
|
|
}
|
|
if (typeof domains === 'string') {
|
|
return domains.split(',').map((domain) => domain.trim().toLowerCase()).filter(Boolean);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private getOwnedRoutes(gatewayClientId?: string): interfaces.data.IMergedRoute[] {
|
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
if (!manager) return [];
|
|
return manager.getMergedRoutes().routes.filter((route) => {
|
|
const metadata = route.metadata;
|
|
if (!metadata) return false;
|
|
const ownerType = metadata.ownerType;
|
|
const isGatewayOwned = ownerType === 'gatewayClient' || ownerType === 'workhoster';
|
|
if (!isGatewayOwned) return false;
|
|
const routeGatewayClientId = metadata.gatewayClientId || metadata.workHosterId;
|
|
return gatewayClientId ? routeGatewayClientId === gatewayClientId : true;
|
|
});
|
|
}
|
|
|
|
private async listGatewayClientDomains(
|
|
auth: TAuthContext,
|
|
requestedGatewayClientId?: string,
|
|
): Promise<interfaces.data.IGatewayClientDomain[]> {
|
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
if (!dnsManager) return [];
|
|
const gatewayClientId = this.resolveGatewayClientId(auth, requestedGatewayClientId);
|
|
const ownedRoutes = this.getOwnedRoutes(gatewayClientId);
|
|
const routeHostnames = ownedRoutes.flatMap((route) => this.getRouteHostnames(route.route));
|
|
const docs = await dnsManager.listDomains();
|
|
|
|
return docs
|
|
.filter((domainDoc) => {
|
|
if (!auth.token?.policy || auth.token.policy.role !== 'gatewayClient') return true;
|
|
return routeHostnames.some((hostname) => this.isHostnameInDomain(hostname, domainDoc.name));
|
|
})
|
|
.map((domainDoc) => {
|
|
const domain = dnsManager.toPublicDomain(domainDoc);
|
|
const canManageDnsRecords = domain.source === 'dcrouter' || Boolean(domain.providerId);
|
|
const serviceCount = routeHostnames.filter((hostname) => this.isHostnameInDomain(hostname, domain.name)).length;
|
|
return {
|
|
...domain,
|
|
serviceCount,
|
|
managePath: `/domains/${domain.id}`,
|
|
capabilities: {
|
|
canCreateSubdomains: canManageDnsRecords,
|
|
canManageDnsRecords,
|
|
canIssueCertificates: Boolean(this.opsServerRef.dcRouterRef.smartProxy),
|
|
canHostEmail: Boolean(this.opsServerRef.dcRouterRef.emailDomainManager),
|
|
},
|
|
} satisfies interfaces.data.IGatewayClientDomain;
|
|
});
|
|
}
|
|
|
|
private async listGatewayClientDnsRecords(
|
|
auth: TAuthContext,
|
|
requestedGatewayClientId?: string,
|
|
): Promise<interfaces.data.IGatewayClientDnsRecord[]> {
|
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
if (!dnsManager) return [];
|
|
const gatewayClientId = this.resolveGatewayClientId(auth, requestedGatewayClientId);
|
|
const ownedRoutes = this.getOwnedRoutes(gatewayClientId);
|
|
const domains = await dnsManager.listDomains();
|
|
const records: interfaces.data.IGatewayClientDnsRecord[] = [];
|
|
|
|
for (const route of ownedRoutes) {
|
|
const metadata = route.metadata;
|
|
if (!metadata) continue;
|
|
const gatewayClientType = metadata.gatewayClientType || metadata.workHosterType || 'custom';
|
|
const routeGatewayClientId = metadata.gatewayClientId || metadata.workHosterId || '';
|
|
const appId = metadata.gatewayClientAppId || metadata.workAppId || '';
|
|
|
|
for (const hostname of this.getRouteHostnames(route.route)) {
|
|
if (auth.token?.policy?.role === 'gatewayClient' && !this.matchesHostnamePatterns(hostname, auth.token.policy.hostnamePatterns || [])) {
|
|
continue;
|
|
}
|
|
const domainDoc = domains.find((domain) => this.isHostnameInDomain(hostname, domain.name));
|
|
const domainRecords = domainDoc ? await dnsManager.listRecordsForDomain(domainDoc.id) : [];
|
|
const matchingRecords = domainRecords.filter((record) => record.name === hostname);
|
|
if (matchingRecords.length === 0) {
|
|
records.push({
|
|
id: `missing:${hostname}`,
|
|
domainId: domainDoc?.id || '',
|
|
domainName: domainDoc?.name,
|
|
name: hostname,
|
|
type: 'MISSING',
|
|
value: '',
|
|
ttl: 0,
|
|
source: 'local',
|
|
status: 'missing',
|
|
gatewayClientType,
|
|
gatewayClientId: routeGatewayClientId,
|
|
appId,
|
|
hostname,
|
|
routeId: route.id,
|
|
managePath: domainDoc ? `/domains/${domainDoc.id}/dns` : '/domains',
|
|
createdAt: route.createdAt || 0,
|
|
updatedAt: route.updatedAt || 0,
|
|
createdBy: '',
|
|
});
|
|
continue;
|
|
}
|
|
for (const recordDoc of matchingRecords) {
|
|
const record = dnsManager.toPublicRecord(recordDoc);
|
|
records.push({
|
|
...record,
|
|
domainName: domainDoc?.name,
|
|
status: 'active',
|
|
gatewayClientType,
|
|
gatewayClientId: routeGatewayClientId,
|
|
appId,
|
|
hostname,
|
|
routeId: route.id,
|
|
managePath: `/dns-records/${record.id}`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
private isHostnameInDomain(hostname: string, domainName: string): boolean {
|
|
const normalizedHostname = hostname.trim().toLowerCase();
|
|
const normalizedDomainName = domainName.trim().toLowerCase();
|
|
return normalizedHostname === normalizedDomainName || normalizedHostname.endsWith(`.${normalizedDomainName}`);
|
|
}
|
|
|
|
private async syncGatewayClientRoute(
|
|
auth: TAuthContext,
|
|
ownership: interfaces.data.IGatewayClientOwnership,
|
|
route?: interfaces.data.IDcRouterRouteConfig,
|
|
enabled?: boolean,
|
|
deleteRoute?: boolean,
|
|
): Promise<interfaces.data.IGatewayClientRouteSyncResult> {
|
|
const resolvedOwnership = this.resolveGatewayClientOwnership(auth, ownership);
|
|
this.assertGatewayClientOwnership(auth, resolvedOwnership);
|
|
this.assertRouteTargetsAllowed(auth, route);
|
|
|
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
if (!manager) {
|
|
return { success: false, message: 'Route management not initialized' };
|
|
}
|
|
|
|
const externalKey = this.buildGatewayClientExternalKey(resolvedOwnership);
|
|
const existingRoute = manager.findApiRouteByExternalKey(externalKey);
|
|
|
|
if (deleteRoute) {
|
|
if (!existingRoute) {
|
|
return { success: true, action: 'unchanged' };
|
|
}
|
|
const result = await manager.deleteRoute(existingRoute.id);
|
|
return result.success
|
|
? { success: true, action: 'deleted', routeId: existingRoute.id }
|
|
: { success: false, message: result.message };
|
|
}
|
|
|
|
if (!route) {
|
|
return { success: false, message: 'route is required unless delete=true' };
|
|
}
|
|
|
|
const metadata: interfaces.data.IRouteMetadata = {
|
|
ownerType: 'gatewayClient',
|
|
gatewayClientType: resolvedOwnership.gatewayClientType,
|
|
gatewayClientId: resolvedOwnership.gatewayClientId,
|
|
gatewayClientAppId: resolvedOwnership.appId,
|
|
workHosterType: resolvedOwnership.gatewayClientType,
|
|
workHosterId: resolvedOwnership.gatewayClientId,
|
|
workAppId: resolvedOwnership.appId,
|
|
externalKey,
|
|
};
|
|
const normalizedRoute = this.normalizeGatewayClientRoute(route, resolvedOwnership, externalKey);
|
|
|
|
if (existingRoute) {
|
|
const result = await manager.updateRoute(existingRoute.id, {
|
|
route: normalizedRoute,
|
|
enabled: enabled ?? true,
|
|
metadata,
|
|
});
|
|
return result.success
|
|
? { success: true, action: 'updated', routeId: existingRoute.id }
|
|
: { success: false, message: result.message };
|
|
}
|
|
|
|
const routeId = await manager.createRoute(normalizedRoute, auth.userId, enabled ?? true, metadata);
|
|
return { success: true, action: 'created', routeId };
|
|
}
|
|
|
|
private buildGatewayClientExternalKey(ownership: Required<interfaces.data.IGatewayClientOwnership>): string {
|
|
return [
|
|
ownership.gatewayClientType,
|
|
ownership.gatewayClientId,
|
|
ownership.appId,
|
|
ownership.hostname,
|
|
].map((part) => part.trim()).join(':');
|
|
}
|
|
|
|
private normalizeWorkAppRoute(
|
|
route: interfaces.data.IDcRouterRouteConfig,
|
|
ownership: interfaces.data.IWorkAppRouteOwnership,
|
|
externalKey: string,
|
|
): interfaces.data.IDcRouterRouteConfig {
|
|
const normalizedRoute = { ...route };
|
|
if (!normalizedRoute.name) {
|
|
normalizedRoute.name = `workapp-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
|
}
|
|
return normalizedRoute;
|
|
}
|
|
|
|
private normalizeGatewayClientRoute(
|
|
route: interfaces.data.IDcRouterRouteConfig,
|
|
ownership: Required<interfaces.data.IGatewayClientOwnership>,
|
|
externalKey: string,
|
|
): interfaces.data.IDcRouterRouteConfig {
|
|
const normalizedRoute = { ...route };
|
|
if (!normalizedRoute.name) {
|
|
normalizedRoute.name = `gateway-client-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
|
}
|
|
return normalizedRoute;
|
|
}
|
|
}
|