fix: scope public domain to single binding and return single port check

Accept internalPort in AddPublicDomainParams to target a specific
binding. Disable the domain on all other bindings. Return a single
CheckPortRes instead of Vec. Revert multi-port UI to singular port
display from 0f8a66b35.
This commit is contained in:
Aiden McClelland
2026-03-04 21:43:34 -07:00
parent d982ffa722
commit e077b5425b
13 changed files with 131 additions and 146 deletions

View File

@@ -678,7 +678,7 @@ export default {
741: 'In Ihrem Domain-Registrar für',
742: 'diesen DNS-Eintrag erstellen',
743: 'In Ihrem Gateway',
744: 'diese Portweiterleitungsregeln erstellen',
744: 'diese Portweiterleitungsregel erstellen',
745: 'Externer Port',
747: 'Interner Port',
749: 'DNS-Server-Konfiguration',

View File

@@ -678,7 +678,7 @@ export const ENGLISH: Record<string, number> = {
'In your domain registrar for': 741, // partial sentence, followed by a domain name
'create this DNS record': 742,
'In your gateway': 743, // partial sentence, followed by a gateway name
'create these port forwarding rules': 744,
'create this port forwarding rule': 744,
'External Port': 745,
'Internal Port': 747,
'DNS Server Config': 749,

View File

@@ -678,7 +678,7 @@ export default {
741: 'En su registrador de dominios para',
742: 'cree este registro DNS',
743: 'En su puerta de enlace',
744: 'cree estas reglas de reenvío de puertos',
744: 'cree esta regla de reenvío de puertos',
745: 'Puerto externo',
747: 'Puerto interno',
749: 'Configuración del servidor DNS',

View File

@@ -678,7 +678,7 @@ export default {
741: 'Dans votre registraire de domaine pour',
742: 'créez cet enregistrement DNS',
743: 'Dans votre passerelle',
744: 'créez ces règles de redirection de port',
744: 'créez cette règle de redirection de port',
745: 'Port externe',
747: 'Port interne',
749: 'Configuration du serveur DNS',

View File

@@ -678,7 +678,7 @@ export default {
741: 'W rejestratorze domeny dla',
742: 'utwórz ten rekord DNS',
743: 'W bramie',
744: 'utwórz te reguły przekierowania portów',
744: 'utwórz tę regułę przekierowania portów',
745: 'Port zewnętrzny',
747: 'Port wewnętrzny',
749: 'Konfiguracja serwera DNS',

View File

@@ -269,6 +269,7 @@ export class InterfaceAddressesComponent {
fqdn,
gateway: gatewayId,
acme: !authority || authority === 'local' ? null : authority,
internalPort: iface?.addressInfo.internalPort || 80,
}
try {

View File

@@ -15,6 +15,7 @@ import {
} from '@taiga-ui/kit'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { T } from '@start9labs/start-sdk'
@@ -28,11 +29,8 @@ export type DnsGateway = T.NetworkInterfaceInfo & {
export type DomainValidationData = {
fqdn: string
gateway: DnsGateway
ports: number[]
initialResults?: {
dnsPass: boolean
portResults: (T.CheckPortRes | null)[]
}
port: number
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null }
}
@Component({
@@ -94,50 +92,32 @@ export type DomainValidationData = {
<h2>{{ 'Port Forwarding' | i18n }}</h2>
<p>
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
{{ 'create these port forwarding rules' | i18n }}
{{ 'create this port forwarding rule' | i18n }}
</p>
@let portRes = portResult();
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
@for (port of context.data.ports; track port; let i = $index) {
<tr>
<td class="status">
<port-check-icon
[result]="portResults()[i]"
[loading]="!!portLoadings()[i]"
/>
</td>
<td>{{ port }}</td>
<td>{{ port }}</td>
<td>
<button
tuiButton
size="s"
[loading]="!!portLoadings()[i]"
(click)="testPort(i)"
>
{{ 'Test' | i18n }}
</button>
</td>
</tr>
}
<tr>
<td class="status">
<port-check-icon [result]="portRes" [loading]="portLoading()" />
</td>
<td>{{ context.data.port }}</td>
<td>{{ context.data.port }}</td>
<td>
<button
tuiButton
size="s"
[loading]="portLoading()"
(click)="testPort()"
>
{{ 'Test' | i18n }}
</button>
</td>
</tr>
</table>
@if (anyNotRunning()) {
<p class="g-warning">
{{
'Port status cannot be determined while service is not running'
| i18n
}}
</p>
}
@if (anyNoHairpinning()) {
<p class="g-warning">
{{
'This address will not work from your local network due to a router hairpinning limitation'
| i18n
}}
</p>
}
<port-check-warnings [result]="portRes" />
@if (!isManualMode) {
<footer class="g-buttons padding-top">
@@ -236,6 +216,7 @@ export type DomainValidationData = {
TuiIcon,
TuiLoader,
PortCheckIconComponent,
PortCheckWarningsComponent,
],
})
export class DomainValidationComponent {
@@ -251,28 +232,16 @@ export class DomainValidationComponent {
parse(this.context.data.fqdn).domain || this.context.data.fqdn
readonly dnsLoading = signal(false)
readonly portLoadings = signal<boolean[]>(
this.context.data.ports.map(() => false),
)
readonly portLoading = signal(false)
readonly dnsPass = signal<boolean | undefined>(undefined)
readonly portResults = signal<(T.CheckPortRes | undefined)[]>(
this.context.data.ports.map(() => undefined),
)
readonly anyNotRunning = computed(() =>
this.portResults().some(r => r && !r.openInternally),
)
readonly anyNoHairpinning = computed(() =>
this.portResults().some(r => r && r.openExternally && !r.hairpinning),
)
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
readonly allPass = computed(() => {
const results = this.portResults()
const result = this.portResult()
return (
this.dnsPass() === true &&
results.length > 0 &&
results.every(r => !!r?.openInternally && !!r?.openExternally)
!!result?.openInternally &&
!!result?.openExternally
)
})
@@ -282,9 +251,7 @@ export class DomainValidationComponent {
const initial = this.context.data.initialResults
if (initial) {
this.dnsPass.set(initial.dnsPass)
this.portResults.set(
initial.portResults.map(r => r ?? undefined),
)
if (initial.portResult) this.portResult.set(initial.portResult)
}
}
@@ -304,32 +271,20 @@ export class DomainValidationComponent {
}
}
async testPort(index: number) {
this.portLoadings.update(l => {
const copy = [...l]
copy[index] = true
return copy
})
async testPort() {
this.portLoading.set(true)
try {
const result = await this.api.checkPort({
gateway: this.context.data.gateway.id,
port: this.context.data.ports[index]!,
port: this.context.data.port,
})
this.portResults.update(r => {
const copy = [...r]
copy[index] = result
return copy
})
this.portResult.set(result)
} catch (e: any) {
this.errorService.handleError(e)
} finally {
this.portLoadings.update(l => {
const copy = [...l]
copy[index] = false
return copy
})
this.portLoading.set(false)
}
}
}

View File

@@ -26,12 +26,12 @@ export class DomainHealthService {
if (!gateway) return
let dnsPass: boolean
let ports: number[]
let portResults: (T.CheckPortRes | null)[]
let port: number
let portResult: T.CheckPortRes | null
if (typeof portOrRes === 'number') {
ports = [portOrRes]
const [dns, portResult] = await Promise.all([
port = portOrRes
const [dns, portRes] = await Promise.all([
this.api
.queryDns({ fqdn })
.then(ip => ip === gateway.ipInfo.wanIp)
@@ -41,23 +41,24 @@ export class DomainHealthService {
.catch((): null => null),
])
dnsPass = dns
portResults = [portResult]
portResult = portRes
} else {
dnsPass = portOrRes.dns === gateway.ipInfo.wanIp
ports = portOrRes.port.map(r => r.port)
portResults = portOrRes.port
port = portOrRes.port.port
portResult = portOrRes.port
}
const allPortsOk = portResults.every(
r => !!r?.openInternally && !!r?.openExternally && !!r?.hairpinning,
)
const portOk =
!!portResult?.openInternally &&
!!portResult?.openExternally &&
!!portResult?.hairpinning
if (!dnsPass || !allPortsOk) {
if (!dnsPass || !portOk) {
setTimeout(
() =>
this.openPublicDomainModal(fqdn, gateway, ports, {
this.openPublicDomainModal(fqdn, gateway, port, {
dnsPass,
portResults,
portResult,
}),
250,
)
@@ -99,7 +100,7 @@ export class DomainHealthService {
const gateway = await this.getGatewayData(gatewayId)
if (!gateway) return
this.openPublicDomainModal(fqdn, gateway, [port])
this.openPublicDomainModal(fqdn, gateway, port)
} catch (e: any) {
this.errorService.handleError(e)
}
@@ -164,17 +165,17 @@ export class DomainHealthService {
private openPublicDomainModal(
fqdn: string,
gateway: DnsGateway,
ports: number[],
port: number,
initialResults?: {
dnsPass: boolean
portResults: (T.CheckPortRes | null)[]
portResult: T.CheckPortRes | null
},
) {
this.dialog
.openComponent(DOMAIN_VALIDATION, {
label: 'Address Requirements',
size: 'm',
data: { fqdn, gateway, ports, initialResults },
data: { fqdn, gateway, port, initialResults },
})
.subscribe()
}

View File

@@ -31,7 +31,7 @@ export type PortForwardValidationData = {
<h2>{{ 'Port Forwarding' | i18n }}</h2>
<p>
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
{{ 'create these port forwarding rules' | i18n }}
{{ 'create this port forwarding rule' | i18n }}
</p>
@let portRes = portResult();

View File

@@ -1467,15 +1467,13 @@ export class MockApiService extends ApiService {
return {
dns: null,
port: [
{
ip: '0.0.0.0',
port: 443,
openExternally: false,
openInternally: false,
hairpinning: false,
},
],
port: {
ip: '0.0.0.0',
port: 443,
openExternally: false,
openInternally: false,
hairpinning: false,
},
}
}
@@ -1575,15 +1573,13 @@ export class MockApiService extends ApiService {
return {
dns: null,
port: [
{
ip: '0.0.0.0',
port: 443,
openExternally: false,
openInternally: false,
hairpinning: false,
},
],
port: {
ip: '0.0.0.0',
port: 443,
openExternally: false,
openInternally: false,
hairpinning: false,
},
}
}