feat(network-routes): add route source policy editor
This commit is contained in:
@@ -15,6 +15,13 @@
|
||||
- Hydrate DB-backed generated email routes to the same runtime handlers when their email system keys match.
|
||||
- Add bidirectional socket proxy cleanup and tests for route hydration and SMTP banner relay.
|
||||
|
||||
### Features
|
||||
|
||||
- add route source policy editor (network-routes)
|
||||
- Replace fixed source binding dropdown rows with the catalog route source policy input in route create and edit dialogs.
|
||||
- Add source profile normalization, path class options, Gitea source policy presets, and validation for route source policies.
|
||||
- Bump catalog UI dependencies and update pnpm built dependency configuration.
|
||||
|
||||
## 2026-06-04 - 13.44.1
|
||||
|
||||
### Fixes
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@
|
||||
"@api.global/typedserver": "^8.4.7",
|
||||
"@api.global/typedsocket": "^4.1.4",
|
||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||
"@design.estate/dees-catalog": "^3.83.0",
|
||||
"@design.estate/dees-catalog": "^3.84.0",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@idp.global/sdk": "^1.4.0",
|
||||
"@push.rocks/lik": "^6.4.1",
|
||||
@@ -69,7 +69,7 @@
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/smartvpn": "1.20.0",
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.12.8",
|
||||
"@serve.zone/catalog": "^2.13.0",
|
||||
"@serve.zone/interfaces": "^6.2.1",
|
||||
"@serve.zone/remoteingress": "^4.23.0",
|
||||
"@tsclass/tsclass": "^9.5.1",
|
||||
|
||||
Generated
+12
-12
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^3.83.0
|
||||
version: 3.83.0(@tiptap/pm@2.27.2)
|
||||
specifier: ^3.84.0
|
||||
version: 3.84.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
@@ -108,8 +108,8 @@ importers:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
'@serve.zone/catalog':
|
||||
specifier: ^2.12.8
|
||||
version: 2.12.8(@tiptap/pm@2.27.2)
|
||||
specifier: ^2.13.0
|
||||
version: 2.13.0(@tiptap/pm@2.27.2)
|
||||
'@serve.zone/interfaces':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
@@ -365,8 +365,8 @@ packages:
|
||||
'@configvault.io/interfaces@1.0.17':
|
||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||
|
||||
'@design.estate/dees-catalog@3.83.0':
|
||||
resolution: {integrity: sha512-Ia4fwZ5ndziJkSE000nCro83rD8Rujki7ASHBQhL6ZDflZRJRlfuc13azVnQC2sazKlo/bWSgiiLcpc3V2IYrw==}
|
||||
'@design.estate/dees-catalog@3.84.0':
|
||||
resolution: {integrity: sha512-CYNsKwOcu3FvkA+G3fli4P9fVfDcMK3my5AbhN6jLNM0JPMlKyKV8s3q6bAqQPc9QGAtm+XhY2zLI4Cgurs2HA==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.30':
|
||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||
@@ -1689,8 +1689,8 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@serve.zone/catalog@2.12.8':
|
||||
resolution: {integrity: sha512-TBclzYbDH3OJlbLkWpLrBij2MU4eFpBs5MIJU7njBMZZaQ37IVYftG+vn6N4W2E2WfuTxaPVshN7MV3A/oR81g==}
|
||||
'@serve.zone/catalog@2.13.0':
|
||||
resolution: {integrity: sha512-w2zfbcbJLR1jbwJQkeLNCOW/WB71FyMBfVb+uiIO5XTVK+7zTD0cFozySjBDOrueCFDcL7GcoO8Ohgs9jCfuhQ==}
|
||||
|
||||
'@serve.zone/interfaces@6.2.1':
|
||||
resolution: {integrity: sha512-t2wrpBmd8zDdnyeeY/LG2hfjCXdm/uTHB6oovJ/xHgOws1E2VimYJPFiN7zqs1aEJAmFukfgOq79+eZeq3hfWw==}
|
||||
@@ -4276,7 +4276,7 @@ snapshots:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 4.1.4(@push.rocks/smartserve@2.0.4)
|
||||
'@cloudflare/workers-types': 4.20260602.1
|
||||
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.84.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-comms': 1.0.30
|
||||
'@push.rocks/lik': 6.4.1
|
||||
'@push.rocks/smartdelay': 3.1.0
|
||||
@@ -4809,7 +4809,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
|
||||
'@design.estate/dees-catalog@3.83.0(@tiptap/pm@2.27.2)':
|
||||
'@design.estate/dees-catalog@3.84.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.6
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
@@ -6915,9 +6915,9 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@serve.zone/catalog@2.12.8(@tiptap/pm@2.27.2)':
|
||||
'@serve.zone/catalog@2.13.0(@tiptap/pm@2.27.2)':
|
||||
dependencies:
|
||||
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-catalog': 3.84.0(@tiptap/pm@2.27.2)
|
||||
'@design.estate/dees-domtools': 2.5.6
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.9.0
|
||||
|
||||
+5
-4
@@ -1,4 +1,5 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
mongodb-memory-server: true
|
||||
puppeteer: true
|
||||
onlyBuiltDependencies:
|
||||
- '@design.estate/dees-catalog'
|
||||
- esbuild
|
||||
- mongodb-memory-server
|
||||
- puppeteer
|
||||
|
||||
@@ -2,6 +2,12 @@ import * as appstate from '../../appstate.js';
|
||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from '../shared/css.js';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
import type {
|
||||
IRoutePathClassOption as ISzRoutePathClassOption,
|
||||
IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
|
||||
ISourceProfileOption as ISzSourceProfileOption,
|
||||
SzInputRouteSourcePolicy,
|
||||
} from '@serve.zone/catalog';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -24,8 +30,8 @@ const tlsCertOptions = [
|
||||
{ key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
|
||||
{ key: 'custom', option: 'Custom certificate' },
|
||||
];
|
||||
const maxSourceBindingRows = 16;
|
||||
const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
|
||||
type TSzRouteSecurity = NonNullable<ISzSourceProfileOption['security']>;
|
||||
|
||||
function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
|
||||
return { enabled: true, maxRequests, window: 60, keyBy: 'ip' };
|
||||
@@ -35,36 +41,6 @@ function getDropdownKey(value: any): string {
|
||||
return typeof value === 'string' ? value : value?.key || '';
|
||||
}
|
||||
|
||||
function getSourceBindingRefsFromFormData(formData: Record<string, any>): string[] {
|
||||
const refs: string[] = [];
|
||||
for (let index = 0; index < maxSourceBindingRows; index++) {
|
||||
const ref = getDropdownKey(formData[`sourceBindingProfileRef${index}`]);
|
||||
if (ref && !refs.includes(ref)) {
|
||||
refs.push(ref);
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function buildSourceBindingsMetadata(
|
||||
profileRefs: string[],
|
||||
existingSourceBindings?: interfaces.data.IRouteSourceBinding[],
|
||||
): interfaces.data.IRouteSourceBinding[] {
|
||||
return profileRefs.map((sourceProfileRef) => {
|
||||
const existingBinding = existingSourceBindings?.find((binding) => binding.sourceProfileRef === sourceProfileRef);
|
||||
return existingBinding
|
||||
? {
|
||||
...existingBinding,
|
||||
sourceProfileRef,
|
||||
onExceeded: existingBinding.onExceeded || { type: '429' as const },
|
||||
}
|
||||
: {
|
||||
sourceProfileRef,
|
||||
onExceeded: { type: '429' as const },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]): {
|
||||
refs: string[];
|
||||
missingNames: string[];
|
||||
@@ -116,70 +92,111 @@ function buildGiteaSourceBindingsMetadata(profileRefs: string[]): interfaces.dat
|
||||
];
|
||||
}
|
||||
|
||||
function getGiteaPresetSourceBindings(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourceBinding[] | null {
|
||||
function getGiteaSourcePolicyPresets(profiles: interfaces.data.ISourceProfile[]): ISzRouteSourcePolicyPreset[] {
|
||||
const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
|
||||
if (missingNames.length > 0) {
|
||||
alert(`Gitea source-policy preset needs these seeded profiles: ${missingNames.join(', ')}`);
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
if (!validateSourceBindingSelection(refs, profiles)) {
|
||||
return null;
|
||||
}
|
||||
return buildGiteaSourceBindingsMetadata(refs);
|
||||
return [
|
||||
{
|
||||
key: 'gitea-bot-protection',
|
||||
label: 'Gitea bot protection',
|
||||
description: 'TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC with path-class rate limits.',
|
||||
bindings: buildGiteaSourceBindingsMetadata(refs),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function metadataUsesPathPolicies(metadata?: interfaces.data.IRouteMetadata): boolean {
|
||||
return Boolean(metadata?.sourceBindings?.some((binding) => binding.pathPolicies?.length));
|
||||
function normalizeSecurityListEntries(entries: unknown): string[] {
|
||||
if (!Array.isArray(entries)) {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
.map((entry) => {
|
||||
if (typeof entry === 'string') return entry.trim();
|
||||
if (entry && typeof entry === 'object' && 'ip' in entry) {
|
||||
const ip = (entry as Record<string, unknown>).ip;
|
||||
return typeof ip === 'string' ? ip.trim() : '';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function sourceProfileMatchesAll(profile: interfaces.data.ISourceProfile): boolean {
|
||||
return (profile.security?.ipAllowList || []).some((entry) => {
|
||||
const source = typeof entry === 'string' ? entry : entry.ip;
|
||||
return normalizeSecurityListEntries(profile.security?.ipAllowList).some((source) => {
|
||||
return ['*', '0.0.0.0/0', '::/0'].includes(source.trim());
|
||||
});
|
||||
}
|
||||
|
||||
function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile): boolean {
|
||||
return (profile.security?.ipAllowList || []).some((entry) => {
|
||||
const source = typeof entry === 'string' ? entry : entry.ip;
|
||||
return source.trim().length > 0;
|
||||
return normalizeSecurityListEntries(profile.security?.ipAllowList).length > 0;
|
||||
}
|
||||
|
||||
function normalizeCatalogRateLimit(
|
||||
rateLimitValue: interfaces.data.IRouteSecurity['rateLimit'] | undefined,
|
||||
): TSzRouteSecurity['rateLimit'] | undefined {
|
||||
if (!rateLimitValue) return undefined;
|
||||
return {
|
||||
enabled: Boolean(rateLimitValue.enabled),
|
||||
maxRequests: Number(rateLimitValue.maxRequests) || 0,
|
||||
window: Number(rateLimitValue.window) || 0,
|
||||
...(rateLimitValue.keyBy ? { keyBy: String(rateLimitValue.keyBy) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function getSourceProfileOptions(profiles: interfaces.data.ISourceProfile[]): ISzSourceProfileOption[] {
|
||||
return profiles.map((profile) => {
|
||||
const ipAllowList = normalizeSecurityListEntries(profile.security?.ipAllowList);
|
||||
const ipBlockList = normalizeSecurityListEntries(profile.security?.ipBlockList);
|
||||
const rateLimitValue = normalizeCatalogRateLimit(profile.security?.rateLimit);
|
||||
const security: TSzRouteSecurity = {
|
||||
...(ipAllowList.length ? { ipAllowList } : {}),
|
||||
...(ipBlockList.length ? { ipBlockList } : {}),
|
||||
...(typeof profile.security?.maxConnections === 'number' ? { maxConnections: profile.security.maxConnections } : {}),
|
||||
...(rateLimitValue ? { rateLimit: rateLimitValue } : {}),
|
||||
};
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
description: profile.description,
|
||||
security,
|
||||
hasSourceMatches: sourceProfileHasSourceMatches(profile),
|
||||
matchesAllSources: sourceProfileMatchesAll(profile),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function validateSourceBindingSelection(
|
||||
profileRefs: string[],
|
||||
profiles: interfaces.data.ISourceProfile[],
|
||||
): boolean {
|
||||
if (profileRefs.length === 0) {
|
||||
function getRoutePathClassOptions(): ISzRoutePathClassOption[] {
|
||||
return interfaces.data.routePathClasses.map((pathClass) => ({
|
||||
key: pathClass,
|
||||
label: interfaces.data.giteaRoutePathClassLabels[pathClass],
|
||||
defaultPatterns: interfaces.data.giteaRoutePathClassPatterns[pathClass],
|
||||
}));
|
||||
}
|
||||
|
||||
function getSourcePolicyInfoText(profiles: interfaces.data.ISourceProfile[]): string {
|
||||
const { missingNames } = getGiteaPresetProfileRefs(profiles);
|
||||
const presetText = missingNames.length > 0
|
||||
? `Gitea preset hidden until these source profiles exist: ${missingNames.join(', ')}.`
|
||||
: 'Use the Gitea preset as a starting point, then edit the generated bindings before saving.';
|
||||
return `First matching source profile wins. Leave empty for no route-level source access control. ${presetText}`;
|
||||
}
|
||||
|
||||
function validateSourcePolicyInput(form: Element): boolean {
|
||||
const sourcePolicyInput = form.querySelector('sz-input-route-source-policy') as SzInputRouteSourcePolicy | null;
|
||||
if (!sourcePolicyInput || sourcePolicyInput.isValid()) {
|
||||
return true;
|
||||
}
|
||||
alert(sourcePolicyInput.getValidationMessages().join('\n'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedProfiles = profileRefs
|
||||
.map((profileRef) => profiles.find((profile) => profile.id === profileRef))
|
||||
.filter(Boolean) as interfaces.data.ISourceProfile[];
|
||||
|
||||
if (selectedProfiles.length !== profileRefs.length) {
|
||||
alert('One or more selected source profiles could not be found. Refresh profiles and try again.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const profilesWithoutMatches = selectedProfiles.filter((profile) => !sourceProfileHasSourceMatches(profile));
|
||||
if (profilesWithoutMatches.length > 0) {
|
||||
alert(`Source profiles need IP/CIDR match entries before use: ${profilesWithoutMatches.map((profile) => profile.name).join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedProfiles.slice(0, -1).some((profile) => sourceProfileMatchesAll(profile))) {
|
||||
alert('Wildcard source profiles must be last. Earlier wildcard profiles would shadow all following profiles.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const fallbackProfile = selectedProfiles[selectedProfiles.length - 1];
|
||||
if (sourceProfileMatchesAll(fallbackProfile) && fallbackProfile.security?.rateLimit?.enabled !== true) {
|
||||
return confirm(`The wildcard profile "${fallbackProfile.name}" has no enabled rate limit. Save anyway?`);
|
||||
}
|
||||
|
||||
return true;
|
||||
function getSourceBindingsFromFormData(formData: Record<string, unknown>): interfaces.data.IRouteSourceBinding[] {
|
||||
const sourceBindings = formData.sourceBindings;
|
||||
return Array.isArray(sourceBindings)
|
||||
? sourceBindings as interfaces.data.IRouteSourceBinding[]
|
||||
: [];
|
||||
}
|
||||
|
||||
function parseTargetPort(value: any): number | undefined {
|
||||
@@ -620,13 +637,6 @@ export class OpsViewRoutes extends DeesElement {
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
const targets = this.profilesTargetsState.targets;
|
||||
|
||||
const profileOptions = [
|
||||
{ key: '', option: '(none — inline security)' },
|
||||
...profiles.map((p) => ({
|
||||
key: p.id,
|
||||
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
|
||||
})),
|
||||
];
|
||||
const targetOptions = [
|
||||
{ key: '', option: '(none — inline target)' },
|
||||
...targets.map((t) => ({
|
||||
@@ -651,7 +661,10 @@ export class OpsViewRoutes extends DeesElement {
|
||||
const currentVpnOnly = route.vpnOnly === true;
|
||||
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
|
||||
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
|
||||
const currentSourceBindingRefs = this.getSourceBindingRefs(merged.metadata);
|
||||
const sourceProfileOptions = getSourceProfileOptions(profiles);
|
||||
const pathClassOptions = getRoutePathClassOptions();
|
||||
const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
|
||||
const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
|
||||
|
||||
// Compute current TLS state for pre-population
|
||||
const currentTls = (route.action as any).tls;
|
||||
@@ -672,24 +685,15 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .value=${currentPorts} .required=${true}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
|
||||
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
|
||||
<strong>Source Bindings</strong>
|
||||
<small>First matching source profile wins. Leave all rows empty to remove route-level source access control.</small>
|
||||
<dees-input-checkbox
|
||||
.key=${'useGiteaTemplate'}
|
||||
.label=${'Apply Gitea bot protection template on save'}
|
||||
.description=${'Replaces these rows with TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
|
||||
.value=${false}
|
||||
></dees-input-checkbox>
|
||||
${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
|
||||
<dees-input-dropdown
|
||||
.key=${`sourceBindingProfileRef${index}`}
|
||||
.label=${`Binding ${index + 1}`}
|
||||
.options=${profileOptions}
|
||||
.selectedOption=${profileOptions.find((o) => o.key === (currentSourceBindingRefs[index] || '')) || profileOptions[0]}
|
||||
></dees-input-dropdown>
|
||||
`)}
|
||||
</div>
|
||||
<sz-input-route-source-policy
|
||||
.key=${'sourceBindings'}
|
||||
.label=${'Source Policy'}
|
||||
.infoText=${sourcePolicyInfoText}
|
||||
.sourceProfiles=${sourceProfileOptions}
|
||||
.pathClassOptions=${pathClassOptions}
|
||||
.presets=${sourcePolicyPresets}
|
||||
.value=${merged.metadata?.sourceBindings || []}
|
||||
></sz-input-route-source-policy>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
|
||||
@@ -723,6 +727,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
if (!formData.name || !formData.ports) return;
|
||||
if (!validateSourcePolicyInput(form)) return;
|
||||
|
||||
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||
const domains: string[] = Array.isArray(formData.domains)
|
||||
@@ -730,11 +735,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
|
||||
const sourceBindingRefs = useGiteaTemplate
|
||||
? []
|
||||
: getSourceBindingRefsFromFormData(formData);
|
||||
if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
|
||||
const sourceBindings = getSourceBindingsFromFormData(formData);
|
||||
const targetKey = getDropdownKey(formData.networkTargetRef);
|
||||
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
||||
const targetPort = preserveMatchPort
|
||||
@@ -798,12 +799,8 @@ export class OpsViewRoutes extends DeesElement {
|
||||
}
|
||||
|
||||
const metadata: any = {};
|
||||
if (useGiteaTemplate) {
|
||||
const sourceBindings = getGiteaPresetSourceBindings(profiles);
|
||||
if (!sourceBindings) return;
|
||||
if (sourceBindings.length > 0) {
|
||||
metadata.sourceBindings = sourceBindings;
|
||||
} else if (sourceBindingRefs.length > 0) {
|
||||
metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs, merged.metadata?.sourceBindings);
|
||||
} else if (merged.metadata?.sourceBindings) {
|
||||
metadata.sourceBindings = [];
|
||||
}
|
||||
@@ -841,14 +838,11 @@ export class OpsViewRoutes extends DeesElement {
|
||||
const profiles = this.profilesTargetsState.profiles;
|
||||
const targets = this.profilesTargetsState.targets;
|
||||
|
||||
// Build dropdown options for profiles and targets
|
||||
const profileOptions = [
|
||||
{ key: '', option: '(none — inline security)' },
|
||||
...profiles.map((p) => ({
|
||||
key: p.id,
|
||||
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
|
||||
})),
|
||||
];
|
||||
// Build dropdown options for targets and source policy metadata
|
||||
const sourceProfileOptions = getSourceProfileOptions(profiles);
|
||||
const pathClassOptions = getRoutePathClassOptions();
|
||||
const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
|
||||
const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
|
||||
const targetOptions = [
|
||||
{ key: '', option: '(none — inline target)' },
|
||||
...targets.map((t) => ({
|
||||
@@ -865,24 +859,15 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .required=${true}></dees-input-text>
|
||||
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
|
||||
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
|
||||
<strong>Source Bindings</strong>
|
||||
<small>First matching source profile wins. Leave all rows empty for no route-level source access control.</small>
|
||||
<dees-input-checkbox
|
||||
.key=${'useGiteaTemplate'}
|
||||
.label=${'Apply Gitea bot protection template on save'}
|
||||
.description=${'Writes TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
|
||||
.value=${false}
|
||||
></dees-input-checkbox>
|
||||
${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
|
||||
<dees-input-dropdown
|
||||
.key=${`sourceBindingProfileRef${index}`}
|
||||
.label=${`Binding ${index + 1}`}
|
||||
.options=${profileOptions}
|
||||
.selectedOption=${profileOptions[0]}
|
||||
></dees-input-dropdown>
|
||||
`)}
|
||||
</div>
|
||||
<sz-input-route-source-policy
|
||||
.key=${'sourceBindings'}
|
||||
.label=${'Source Policy'}
|
||||
.infoText=${sourcePolicyInfoText}
|
||||
.sourceProfiles=${sourceProfileOptions}
|
||||
.pathClassOptions=${pathClassOptions}
|
||||
.presets=${sourcePolicyPresets}
|
||||
.value=${[]}
|
||||
></sz-input-route-source-policy>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
|
||||
@@ -916,6 +901,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
if (!formData.name || !formData.ports) return;
|
||||
if (!validateSourcePolicyInput(form)) return;
|
||||
|
||||
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||
const domains: string[] = Array.isArray(formData.domains)
|
||||
@@ -923,11 +909,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
|
||||
const sourceBindingRefs = useGiteaTemplate
|
||||
? []
|
||||
: getSourceBindingRefsFromFormData(formData);
|
||||
if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
|
||||
const sourceBindings = getSourceBindingsFromFormData(formData);
|
||||
const targetKey = getDropdownKey(formData.networkTargetRef);
|
||||
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
||||
const targetPort = preserveMatchPort
|
||||
@@ -992,12 +974,8 @@ export class OpsViewRoutes extends DeesElement {
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
const metadata: any = {};
|
||||
if (useGiteaTemplate) {
|
||||
const sourceBindings = getGiteaPresetSourceBindings(profiles);
|
||||
if (!sourceBindings) return;
|
||||
if (sourceBindings.length > 0) {
|
||||
metadata.sourceBindings = sourceBindings;
|
||||
} else if (sourceBindingRefs.length > 0) {
|
||||
metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs);
|
||||
}
|
||||
if (targetKey) {
|
||||
metadata.networkTargetRef = targetKey;
|
||||
|
||||
Reference in New Issue
Block a user