-
+
{{ 'No addresses' | i18n }}
@@ -94,12 +83,6 @@ import { InterfaceAddressItemComponent } from './item.component'
th:first-child {
width: 5rem;
}
-
- th:nth-child(2),
- th:nth-child(3),
- th:nth-child(4) {
- width: 11rem;
- }
}
`,
host: { class: 'g-card' },
@@ -118,11 +101,11 @@ import { InterfaceAddressItemComponent } from './item.component'
export class InterfaceAddressesComponent {
private readonly patch = inject>(PatchDB)
private readonly formDialog = inject(FormDialogService)
- private readonly dialog = inject(DialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly i18n = inject(i18nPipe)
+ private readonly domainHealth = inject(DomainHealthService)
readonly gatewayGroup = input.required()
readonly packageId = input('')
@@ -230,6 +213,9 @@ export class InterfaceAddressesComponent {
} else {
await this.api.osUiAddPrivateDomain({ fqdn, gateway: gatewayId })
}
+
+ await this.domainHealth.checkPrivateDomain(gatewayId)
+
return true
} catch (e: any) {
this.errorService.handleError(e)
@@ -254,46 +240,17 @@ export class InterfaceAddressesComponent {
}
try {
- let ip: string | null
if (this.packageId()) {
- ip = await this.api.pkgAddPublicDomain({
+ await this.api.pkgAddPublicDomain({
...params,
package: this.packageId(),
host: iface?.addressInfo.hostId || '',
})
} else {
- ip = await this.api.osUiAddPublicDomain(params)
+ await this.api.osUiAddPublicDomain(params)
}
- const [network, portPass] = await Promise.all([
- firstValueFrom(this.patch.watch$('serverInfo', 'network')),
- this.api
- .checkPort({ gateway: gatewayId, port: 443 })
- .then(r => r.reachable)
- .catch(() => false),
- ])
- const gateway = network.gateways[gatewayId]
-
- if (gateway?.ipInfo) {
- const gatewayData = {
- id: gatewayId,
- ...gateway,
- ipInfo: gateway.ipInfo,
- }
- const dnsPass = ip === gateway.ipInfo.wanIp
-
- setTimeout(
- () =>
- this.showDomainValidation(
- fqdn,
- gatewayData,
- 443,
- dnsPass,
- portPass,
- ),
- 250,
- )
- }
+ await this.domainHealth.checkPublicDomain(fqdn, gatewayId)
return true
} catch (e: any) {
@@ -303,20 +260,4 @@ export class InterfaceAddressesComponent {
loader.unsubscribe()
}
}
-
- private showDomainValidation(
- fqdn: string,
- gateway: DnsGateway,
- port: number,
- dnsPass: boolean,
- portPass: boolean,
- ) {
- this.dialog
- .openComponent(DOMAIN_VALIDATION, {
- label: 'Domain Setup',
- size: 'm',
- data: { fqdn, gateway, port, dnsPass, portPass },
- })
- .subscribe()
- }
}
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/public-domains/dns.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/dns.component.ts
similarity index 75%
rename from web/projects/ui/src/app/routes/portal/components/interfaces/public-domains/dns.component.ts
rename to web/projects/ui/src/app/routes/portal/components/interfaces/addresses/dns.component.ts
index 3b3921a68..77d639a9f 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/public-domains/dns.component.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/dns.component.ts
@@ -7,7 +7,7 @@ import {
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ErrorService, i18nPipe } from '@start9labs/shared'
-import { TuiButton, TuiDialogContext, TuiIcon } from '@taiga-ui/core'
+import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
import {
TuiButtonLoading,
TuiSwitch,
@@ -28,8 +28,7 @@ export type DomainValidationData = {
fqdn: string
gateway: DnsGateway
port: number
- dnsPass: boolean
- portPass: boolean
+ initialResults?: { dnsPass: boolean; portPass: boolean }
}
@Component({
@@ -38,9 +37,8 @@ export type DomainValidationData = {
@let wanIp = context.data.gateway.ipInfo.wanIp || ('Error' | i18n);
@let gatewayName =
context.data.gateway.name || context.data.gateway.ipInfo.name;
- @let internalIp = context.data.gateway.ipInfo.lanIp[0] || ('Error' | i18n);
- {{ 'DNS' | i18n }}
+ {{ 'DNS' | i18n }}
{{ 'In your domain registrar for' | i18n }} {{ domain }},
{{ 'create this DNS record' | i18n }}
@@ -63,10 +61,14 @@ export type DomainValidationData = {
- @if (dnsPass() === true) {
+ @if (dnsLoading()) {
+
+ } @else if (dnsPass() === true) {
} @else if (dnsPass() === false) {
+ } @else {
+
}
{{ ddns ? 'ALIAS' : 'A' }}
@@ -85,25 +87,26 @@ export type DomainValidationData = {
- {{ 'Port Forwarding' | i18n }}
+ {{ 'Port Forwarding' | i18n }}
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
{{ 'create this port forwarding rule' | i18n }}
-
+
- @if (portPass() === true) {
+ @if (portLoading()) {
+
+ } @else if (portPass() === true) {
} @else if (portPass() === false) {
+ } @else {
+
}
{{ context.data.port }}
- {{ internalIp }}
{{ context.data.port }}
-
+ @if (!isManualMode) {
+
+ }
`,
styles: `
label {
@@ -144,21 +149,25 @@ export type DomainValidationData = {
margin: 1rem 0;
}
- h3 {
- margin: 1.5rem 0 0.5rem;
+ h2 {
+ margin: 2rem 0 0 0;
+ }
- &:first-child {
- margin-top: 0;
- }
+ p {
+ margin-top: 0.5rem;
}
tui-icon {
- font-size: 1rem;
+ font-size: 1.3rem;
vertical-align: text-bottom;
}
.status {
- width: 1.5rem;
+ width: 3.2rem;
+ }
+
+ .padding-top {
+ padding-top: 2rem;
}
td:last-child {
@@ -207,6 +216,7 @@ export type DomainValidationData = {
FormsModule,
TuiButtonLoading,
TuiIcon,
+ TuiLoader,
],
})
export class DomainValidationComponent {
@@ -223,13 +233,23 @@ export class DomainValidationComponent {
readonly dnsLoading = signal(false)
readonly portLoading = signal(false)
- readonly dnsPass = signal(this.context.data.dnsPass)
- readonly portPass = signal(this.context.data.portPass)
+ readonly dnsPass = signal(undefined)
+ readonly portPass = signal(undefined)
readonly allPass = computed(
() => this.dnsPass() === true && this.portPass() === true,
)
+ readonly isManualMode = !this.context.data.initialResults
+
+ constructor() {
+ const initial = this.context.data.initialResults
+ if (initial) {
+ this.dnsPass.set(initial.dnsPass)
+ this.portPass.set(initial.portPass)
+ }
+ }
+
async testDns() {
this.dnsLoading.set(true)
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/domain-health.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/domain-health.service.ts
new file mode 100644
index 000000000..afa0baa79
--- /dev/null
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/domain-health.service.ts
@@ -0,0 +1,173 @@
+import { inject, Injectable } from '@angular/core'
+import { DialogService, ErrorService } from '@start9labs/shared'
+import { PatchDB } from 'patch-db-client'
+import { firstValueFrom } from 'rxjs'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { DataModel } from 'src/app/services/patch-db/data-model'
+import { DOMAIN_VALIDATION, DnsGateway } from './dns.component'
+import { PORT_FORWARD_VALIDATION } from './port-forward.component'
+import { PRIVATE_DNS_VALIDATION } from './private-dns.component'
+
+@Injectable({ providedIn: 'root' })
+export class DomainHealthService {
+ private readonly patch = inject>(PatchDB)
+ private readonly dialog = inject(DialogService)
+ private readonly api = inject(ApiService)
+ private readonly errorService = inject(ErrorService)
+
+ async checkPublicDomain(fqdn: string, gatewayId: string): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ const [dnsPass, portPass] = await Promise.all([
+ this.api
+ .queryDns({ fqdn })
+ .then(ip => ip === gateway.ipInfo.wanIp)
+ .catch(() => false),
+ this.api
+ .checkPort({ gateway: gatewayId, port: 443 })
+ .then(r => r.reachable)
+ .catch(() => false),
+ ])
+
+ if (!dnsPass || !portPass) {
+ setTimeout(
+ () =>
+ this.openPublicDomainModal(fqdn, gateway, 443, {
+ dnsPass,
+ portPass,
+ }),
+ 250,
+ )
+ }
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ async checkPrivateDomain(gatewayId: string): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ const configured = await this.api
+ .checkDns({ gateway: gatewayId })
+ .catch(() => false)
+
+ if (!configured) {
+ setTimeout(
+ () => this.openPrivateDomainModal(gateway, { configured }),
+ 250,
+ )
+ }
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ async showPublicDomainSetup(fqdn: string, gatewayId: string): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ this.openPublicDomainModal(fqdn, gateway, 443)
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ async checkPortForward(gatewayId: string, port: number): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ const portPass = await this.api
+ .checkPort({ gateway: gatewayId, port })
+ .then(r => r.reachable)
+ .catch(() => false)
+
+ if (!portPass) {
+ setTimeout(
+ () => this.openPortForwardModal(gateway, port, { portPass }),
+ 250,
+ )
+ }
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ async showPortForwardSetup(gatewayId: string, port: number): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ this.openPortForwardModal(gateway, port)
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ async showPrivateDomainSetup(gatewayId: string): Promise {
+ try {
+ const gateway = await this.getGatewayData(gatewayId)
+ if (!gateway) return
+
+ this.openPrivateDomainModal(gateway)
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ }
+ }
+
+ private async getGatewayData(gatewayId: string): Promise {
+ const network = await firstValueFrom(
+ this.patch.watch$('serverInfo', 'network'),
+ )
+ const gateway = network.gateways[gatewayId]
+ if (!gateway?.ipInfo) return null
+ return { id: gatewayId, ...gateway, ipInfo: gateway.ipInfo }
+ }
+
+ private openPublicDomainModal(
+ fqdn: string,
+ gateway: DnsGateway,
+ port: number,
+ initialResults?: { dnsPass: boolean; portPass: boolean },
+ ) {
+ this.dialog
+ .openComponent(DOMAIN_VALIDATION, {
+ label: 'Domain Setup',
+ size: 'm',
+ data: { fqdn, gateway, port, initialResults },
+ })
+ .subscribe()
+ }
+
+ private openPortForwardModal(
+ gateway: DnsGateway,
+ port: number,
+ initialResults?: { portPass: boolean },
+ ) {
+ this.dialog
+ .openComponent(PORT_FORWARD_VALIDATION, {
+ label: 'Port Forwarding',
+ size: 'm',
+ data: { gateway, port, initialResults },
+ })
+ .subscribe()
+ }
+
+ private openPrivateDomainModal(
+ gateway: DnsGateway,
+ initialResults?: { configured: boolean },
+ ) {
+ this.dialog
+ .openComponent(PRIVATE_DNS_VALIDATION, {
+ label: 'Domain Setup',
+ size: 'm',
+ data: { gateway, initialResults },
+ })
+ .subscribe()
+ }
+}
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/item.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/item.component.ts
index 6740da5ba..30bcd54d5 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/item.component.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/item.component.ts
@@ -14,6 +14,7 @@ import { TuiSwitch } from '@taiga-ui/kit'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { GatewayAddress, MappedServiceInterface } from '../interface.service'
import { AddressActionsComponent } from './actions.component'
+import { DomainHealthService } from './domain-health.service'
@Component({
selector: 'tr[address]',
@@ -33,14 +34,11 @@ import { AddressActionsComponent } from './actions.component'
(ngModelChange)="onToggleEnabled()"
/>
-
- {{ address.type }}
-
-
+
- {{ address.access | i18n }}
+ {{ address.type }}
{{ address.certificate }}
@@ -71,6 +69,7 @@ import { AddressActionsComponent } from './actions.component'
[packageId]="packageId()"
[value]="value()"
[disabled]="!isRunning()"
+ [gatewayId]="gatewayId()"
[style.width.rem]="5"
>
}
@@ -80,8 +79,9 @@ import { AddressActionsComponent } from './actions.component'
grid-template-columns: fit-content(10rem) 1fr 2rem 2rem;
}
- .access tui-icon {
- font-size: 1rem;
+ .type tui-icon {
+ font-size: 1.3rem;
+ margin-right: 0.7rem;
vertical-align: middle;
}
@@ -134,11 +134,11 @@ import { AddressActionsComponent } from './actions.component'
padding-inline-end: 0.5rem;
}
- td:nth-child(4) {
+ td:nth-child(3) {
grid-area: 2 / 1 / 2 / 3;
}
- td:nth-child(5) {
+ td:nth-child(4) {
grid-area: 3 / 1 / 3 / 3;
}
@@ -164,11 +164,13 @@ export class InterfaceAddressItemComponent {
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
+ private readonly domainHealth = inject(DomainHealthService)
readonly address = input.required()
readonly packageId = input('')
readonly value = input()
readonly isRunning = input.required()
+ readonly gatewayId = input('')
readonly toggling = signal(false)
readonly currentlyMasked = signal(true)
@@ -202,6 +204,27 @@ export class InterfaceAddressItemComponent {
enabled,
})
}
+
+ if (enabled) {
+ const kind = addr.hostnameInfo.metadata.kind
+ if (kind === 'public-domain') {
+ await this.domainHealth.checkPublicDomain(
+ addr.hostnameInfo.host,
+ this.gatewayId(),
+ )
+ } else if (kind === 'private-domain') {
+ await this.domainHealth.checkPrivateDomain(this.gatewayId())
+ } else if (
+ kind === 'ipv4' &&
+ addr.access === 'public' &&
+ addr.hostnameInfo.port !== null
+ ) {
+ await this.domainHealth.checkPortForward(
+ this.gatewayId(),
+ addr.hostnameInfo.port,
+ )
+ }
+ }
} catch (e: any) {
this.errorService.handleError(e)
} finally {
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/port-forward.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/port-forward.component.ts
new file mode 100644
index 000000000..ce08b8c2c
--- /dev/null
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/port-forward.component.ts
@@ -0,0 +1,178 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ signal,
+} from '@angular/core'
+import { ErrorService, i18nPipe } from '@start9labs/shared'
+import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
+import { TuiButtonLoading } from '@taiga-ui/kit'
+import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
+import { TableComponent } from 'src/app/routes/portal/components/table.component'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { DnsGateway } from './dns.component'
+
+export type PortForwardValidationData = {
+ gateway: DnsGateway
+ port: number
+ initialResults?: { portPass: boolean }
+}
+
+@Component({
+ selector: 'port-forward-validation',
+ template: `
+ @let gatewayName =
+ context.data.gateway.name || context.data.gateway.ipInfo.name;
+
+ {{ 'Port Forwarding' | i18n }}
+
+ {{ 'In your gateway' | i18n }} "{{ gatewayName }}",
+ {{ 'create this port forwarding rule' | i18n }}
+
+
+
+
+
+ @if (loading()) {
+
+ } @else if (pass() === true) {
+
+ } @else if (pass() === false) {
+
+ } @else {
+
+ }
+
+ {{ context.data.port }}
+ {{ context.data.port }}
+
+
+ {{ 'Test' | i18n }}
+
+
+
+
+
+ @if (!isManualMode) {
+
+ }
+ `,
+ styles: `
+ h2 {
+ margin: 2rem 0 0 0;
+ }
+
+ p {
+ margin-top: 0.5rem;
+ }
+
+ tui-icon {
+ font-size: 1.3rem;
+ vertical-align: text-bottom;
+ }
+
+ .status {
+ width: 3.2rem;
+ }
+
+ .padding-top {
+ padding-top: 2rem;
+ }
+
+ td:last-child {
+ text-align: end;
+ }
+
+ footer {
+ margin-top: 1.5rem;
+ }
+
+ :host-context(tui-root._mobile) table {
+ thead {
+ display: table-header-group !important;
+ }
+
+ tr {
+ display: table-row !important;
+ box-shadow: none !important;
+ }
+
+ td,
+ th {
+ padding: 0.5rem 0.5rem !important;
+ font: var(--tui-font-text-s) !important;
+ color: var(--tui-text-primary) !important;
+ font-weight: normal !important;
+ }
+
+ th {
+ font-weight: bold !important;
+ }
+ }
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ TuiButton,
+ i18nPipe,
+ TableComponent,
+ TuiButtonLoading,
+ TuiIcon,
+ TuiLoader,
+ ],
+})
+export class PortForwardValidationComponent {
+ private readonly errorService = inject(ErrorService)
+ private readonly api = inject(ApiService)
+
+ readonly context =
+ injectContext>()
+
+ readonly loading = signal(false)
+ readonly pass = signal(undefined)
+
+ readonly isManualMode = !this.context.data.initialResults
+
+ constructor() {
+ const initial = this.context.data.initialResults
+ if (initial) {
+ this.pass.set(initial.portPass)
+ }
+ }
+
+ async testPort() {
+ this.loading.set(true)
+
+ try {
+ const result = await this.api.checkPort({
+ gateway: this.context.data.gateway.id,
+ port: this.context.data.port,
+ })
+
+ this.pass.set(result.reachable)
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ } finally {
+ this.loading.set(false)
+ }
+ }
+}
+
+export const PORT_FORWARD_VALIDATION = new PolymorpheusComponent(
+ PortForwardValidationComponent,
+)
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/private-dns.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/private-dns.component.ts
new file mode 100644
index 000000000..dd246d442
--- /dev/null
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/private-dns.component.ts
@@ -0,0 +1,180 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ signal,
+} from '@angular/core'
+import { ErrorService, i18nPipe } from '@start9labs/shared'
+import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
+import { TuiButtonLoading } from '@taiga-ui/kit'
+import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
+import { TableComponent } from 'src/app/routes/portal/components/table.component'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { DnsGateway } from './dns.component'
+
+export type PrivateDnsValidationData = {
+ gateway: DnsGateway
+ initialResults?: { configured: boolean }
+}
+
+@Component({
+ selector: 'private-dns-validation',
+ template: `
+ @let gatewayName =
+ context.data.gateway.name || context.data.gateway.ipInfo.name;
+ @let internalIp = context.data.gateway.ipInfo.lanIp[0] || ('Error' | i18n);
+
+ {{ 'DNS Server Config' | i18n }}
+
+ {{ 'Gateway' | i18n }} "{{ gatewayName }}"
+ {{ 'must be configured to use' | i18n }}
+ {{ internalIp }}
+ ({{ 'the LAN IP address of this server' | i18n }})
+ {{ 'as its DNS server' | i18n }}.
+
+
+
+
+
+ @if (loading()) {
+
+ } @else if (pass() === true) {
+
+ } @else if (pass() === false) {
+
+ } @else {
+
+ }
+
+ {{ gatewayName }}
+ {{ internalIp }}
+
+
+ {{ 'Test' | i18n }}
+
+
+
+
+
+ @if (!isManualMode) {
+
+ }
+ `,
+ styles: `
+ h2 {
+ margin: 2rem 0 0 0;
+ }
+
+ p {
+ margin-top: 0.5rem;
+ }
+
+ tui-icon {
+ font-size: 1rem;
+ vertical-align: text-bottom;
+ }
+
+ .status {
+ width: 3.2rem;
+ }
+
+ .padding-top {
+ padding-top: 2rem;
+ }
+
+ td:last-child {
+ text-align: end;
+ }
+
+ footer {
+ margin-top: 1.5rem;
+ }
+
+ :host-context(tui-root._mobile) table {
+ thead {
+ display: table-header-group !important;
+ }
+
+ tr {
+ display: table-row !important;
+ box-shadow: none !important;
+ }
+
+ td,
+ th {
+ padding: 0.5rem 0.5rem !important;
+ font: var(--tui-font-text-s) !important;
+ color: var(--tui-text-primary) !important;
+ font-weight: normal !important;
+ }
+
+ th {
+ font-weight: bold !important;
+ }
+ }
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ TuiButton,
+ i18nPipe,
+ TableComponent,
+ TuiButtonLoading,
+ TuiIcon,
+ TuiLoader,
+ ],
+})
+export class PrivateDnsValidationComponent {
+ private readonly errorService = inject(ErrorService)
+ private readonly api = inject(ApiService)
+
+ readonly context =
+ injectContext>()
+
+ readonly loading = signal(false)
+ readonly pass = signal(undefined)
+
+ readonly isManualMode = !this.context.data.initialResults
+
+ constructor() {
+ const initial = this.context.data.initialResults
+ if (initial) {
+ this.pass.set(initial.configured)
+ }
+ }
+
+ async testDns() {
+ this.loading.set(true)
+
+ try {
+ const result = await this.api.checkDns({
+ gateway: this.context.data.gateway.id,
+ })
+
+ this.pass.set(result)
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ } finally {
+ this.loading.set(false)
+ }
+ }
+}
+
+export const PRIVATE_DNS_VALIDATION = new PolymorpheusComponent(
+ PrivateDnsValidationComponent,
+)
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts
index 857fbd669..aa33492b9 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts
@@ -71,11 +71,10 @@ function getAddressType(h: T.HostnameInfo): string {
case 'ipv6':
return 'IPv6'
case 'public-domain':
- return 'Public Domain'
+ case 'private-domain':
+ return h.host
case 'mdns':
return 'mDNS'
- case 'private-domain':
- return 'Private Domain'
case 'plugin':
return 'Plugin'
}
diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts
index 72c67e8d9..970745044 100644
--- a/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/backups/modals/jobs.component.ts
@@ -26,7 +26,9 @@ import { DocsLinkDirective } from 'projects/shared/src/public-api'
Scheduling automatic backups is an excellent way to ensure your StartOS
data is safely backed up. StartOS will issue a notification whenever one
of your scheduled backups succeeds or fails.
- View instructions
+
+ View instructions
+
Saved Jobs
diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts
index a0fd73bfa..6bde58daa 100644
--- a/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/backups/modals/targets.component.ts
@@ -31,7 +31,9 @@ import { DocsLinkDirective } from 'projects/shared/src/public-api'
backups. They can be physical drives plugged into your server, shared
folders on your Local Area Network (LAN), or third party clouds such as
Dropbox or Google Drive.
- View instructions
+
+ View instructions
+
Unknown Physical Drives
diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts
index 73cae2664..b6f1247e7 100644
--- a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts
@@ -49,7 +49,7 @@ import { TimeService } from 'src/app/services/time.service'
docsLink
iconEnd="@tui.external-link"
appearance=""
- path="/help/common-issues.html"
+ path="/start-os/faq/index.html"
fragment="#clock-sync-failure"
[pseudo]="true"
[textContent]="'the docs' | i18n"
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts
index 0b56e05de..95e37e4f3 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/authorities/authorities.component.ts
@@ -21,7 +21,7 @@ import { AuthoritiesTableComponent } from './table.component'
tuiIconButton
size="xs"
docsLink
- path="/user-manual/authorities.html"
+ path="/start-os/user-manual/trust-ca.html"
appearance="icon"
iconStart="@tui.external-link"
>
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts
index 0f33d4f26..604cf9918 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts
@@ -66,7 +66,7 @@ import { BACKUP_RESTORE } from './restore.component'
@@ -184,7 +184,9 @@ export default class SystemDnsComponent {
if (
Object.values(pkgs).some(p =>
- Object.values(p.hosts).some(h => Object.keys(h?.privateDomains || {}).length),
+ Object.values(p.hosts).some(
+ h => Object.keys(h?.privateDomains || {}).length,
+ ),
)
) {
Object.values(gateways)
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts
index 114f487d6..8b7d413c4 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts
@@ -40,7 +40,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
tuiIconButton
size="xs"
docsLink
- path="/user-manual/smtp.html"
+ path="/start-os/user-manual/smtp.html"
appearance="icon"
iconStart="@tui.external-link"
>
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts
index 6c5484d6f..3bd243957 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts
@@ -32,7 +32,7 @@ import { ISB } from '@start9labs/start-sdk'
tuiIconButton
size="xs"
docsLink
- path="/user-manual/gateways.html"
+ path="/start-os/user-manual/gateways.html"
appearance="icon"
iconStart="@tui.external-link"
>
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
index e2e7706f4..75c806d90 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/item.component.ts
@@ -26,12 +26,24 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { GatewayPlus } from 'src/app/services/gateway.service'
import { TuiBadge } from '@taiga-ui/kit'
+import { PORT_FORWARDS_MODAL } from './port-forwards.component'
@Component({
selector: 'tr[gateway]',
template: `
@if (gateway(); as gateway) {
+ @switch (gateway.ipInfo.deviceType) {
+ @case ('ethernet') {
+
+ }
+ @case ('wireless') {
+
+ }
+ @case ('wireguard') {
+
+ }
+ }
{{ gateway.name }}
@if (gateway.isDefaultOutbound) {
@@ -39,31 +51,10 @@ import { TuiBadge } from '@taiga-ui/kit'
}
-
- @switch (gateway.ipInfo.deviceType) {
- @case ('ethernet') {
-
- {{ 'Ethernet' | i18n }}
- }
- @case ('wireless') {
-
- {{ 'WiFi' | i18n }}
- }
- @case ('wireguard') {
-
- WireGuard
- }
- @default {
- {{ gateway.ipInfo.deviceType }}
- }
- }
-
@if (gateway.type === 'outbound-only') {
-
{{ 'Outbound Only' | i18n }}
} @else {
-
{{ 'Inbound/Outbound' | i18n }}
}
@@ -93,25 +84,23 @@ import { TuiBadge } from '@taiga-ui/kit'
{{ 'Rename' | i18n }}
+ @if (gateway.type !== 'outbound-only') {
+
+
+ {{ 'View port forwards' | i18n }}
+
+
+ }
@if (!gateway.isDefaultOutbound) {
-
+
{{ 'Set as default outbound' | i18n }}
}
@if (gateway.ipInfo.deviceType === 'wireguard') {
-
+
{{ 'Delete' | i18n }}
@@ -122,41 +111,55 @@ import { TuiBadge } from '@taiga-ui/kit'
}
`,
styles: `
+ tui-icon {
+ font-size: 1.3rem;
+ margin-right: 0.7rem;
+ }
+
+ tui-badge {
+ margin-left: 1rem;
+ }
+
td:last-child {
- grid-area: 1 / 3 / 7;
- align-self: center;
text-align: right;
}
:host-context(tui-root._mobile) {
- grid-template-columns: min-content 1fr min-content;
-
- .name {
- grid-column: span 2;
+ td {
+ width: auto !important;
+ align-content: center;
}
- .connection {
- grid-column: span 2;
- order: -1;
+ td:first-child {
+ font: var(--tui-font-text-m);
+ font-weight: bold;
+ color: var(--tui-text-primary);
}
- .type {
- grid-column: span 2;
+ td:nth-child(2) {
+ grid-area: 2 / 1 / 2 / 3;
}
- .lan,
- .wan {
- grid-column: span 2;
+ td:nth-child(3),
+ td:nth-child(4) {
+ grid-area: auto / 1 / auto / 3;
&::before {
- content: 'LAN IP: ';
color: var(--tui-text-primary);
}
}
- .wan::before {
+ td:nth-child(3)::before {
+ content: 'LAN IP: ';
+ }
+
+ td:nth-child(4)::before {
content: 'WAN IP: ';
}
+
+ td:last-child {
+ grid-area: 1 / 3 / 6;
+ }
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -183,6 +186,17 @@ export class GatewaysItemComponent {
open = false
+ viewPortForwards() {
+ const { id, name } = this.gateway()
+ this.dialog
+ .openComponent(PORT_FORWARDS_MODAL, {
+ label: 'Port Forwards',
+ size: 'l',
+ data: { gatewayId: id, gatewayName: name },
+ })
+ .subscribe()
+ }
+
remove() {
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/port-forwards.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/port-forwards.component.ts
new file mode 100644
index 000000000..857f40d65
--- /dev/null
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/port-forwards.component.ts
@@ -0,0 +1,268 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ signal,
+} from '@angular/core'
+import { toSignal } from '@angular/core/rxjs-interop'
+import { ErrorService, i18nPipe } from '@start9labs/shared'
+import { TuiButton, TuiDialogContext, TuiIcon, TuiLoader } from '@taiga-ui/core'
+import { TuiButtonLoading } from '@taiga-ui/kit'
+import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
+import { PatchDB } from 'patch-db-client'
+import { combineLatest, map } from 'rxjs'
+import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
+import { TableComponent } from 'src/app/routes/portal/components/table.component'
+import { ApiService } from 'src/app/services/api/embassy-api.service'
+import { DataModel } from 'src/app/services/patch-db/data-model'
+
+export type PortForwardsModalData = {
+ gatewayId: string
+ gatewayName: string
+}
+
+type PortForwardRow = {
+ interfaces: string[]
+ externalPort: number
+ internalPort: number
+}
+
+function parseSocketAddr(s: string): { ip: string; port: number } {
+ const lastColon = s.lastIndexOf(':')
+ return {
+ ip: s.substring(0, lastColon),
+ port: Number(s.substring(lastColon + 1)),
+ }
+}
+
+@Component({
+ selector: 'port-forwards-modal',
+ template: `
+
+ {{ 'Port forwarding rules required on gateway' | i18n }}
+ "{{ context.data.gatewayName }}"
+
+
+
+ @for (row of rows(); track row.externalPort; let i = $index) {
+
+
+ @for (iface of row.interfaces; track iface) {
+ {{ iface }}
+ }
+
+
+ @if (loading()[i]) {
+
+ } @else if (results()[i] === true) {
+
+ } @else if (results()[i] === false) {
+
+ } @else {
+
+ }
+
+ {{ row.externalPort }}
+ {{ row.internalPort }}
+
+
+ {{ 'Test' | i18n }}
+
+
+
+ } @empty {
+
+
+
+ {{ 'No port forwarding rules' | i18n }}
+
+
+
+ }
+
+ `,
+ styles: `
+ p {
+ margin: 0 0 1rem 0;
+ }
+
+ .interfaces {
+ white-space: nowrap;
+ }
+
+ tui-icon {
+ font-size: 1.3rem;
+ vertical-align: text-bottom;
+ }
+
+ .status {
+ width: 3.2rem;
+ }
+
+ td:last-child {
+ text-align: end;
+ }
+
+ :host-context(tui-root._mobile) table {
+ thead {
+ display: table-header-group !important;
+ }
+
+ tr {
+ display: table-row !important;
+ box-shadow: none !important;
+ }
+
+ td,
+ th {
+ padding: 0.5rem 0.5rem !important;
+ font: var(--tui-font-text-s) !important;
+ color: var(--tui-text-primary) !important;
+ font-weight: normal !important;
+ }
+
+ th {
+ font-weight: bold !important;
+ }
+ }
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ TuiButton,
+ i18nPipe,
+ TableComponent,
+ PlaceholderComponent,
+ TuiIcon,
+ TuiLoader,
+ TuiButtonLoading,
+ ],
+})
+export class PortForwardsModalComponent {
+ private readonly patch = inject>(PatchDB)
+ private readonly api = inject(ApiService)
+ private readonly errorService = inject(ErrorService)
+
+ readonly context =
+ injectContext>()
+
+ readonly loading = signal>({})
+ readonly results = signal>({})
+
+ private readonly portForwards$ = combineLatest([
+ this.patch.watch$('serverInfo', 'network', 'host', 'portForwards').pipe(
+ map(pfs =>
+ pfs.map(pf => ({
+ ...pf,
+ interfaces: ['StartOS - UI'],
+ })),
+ ),
+ ),
+ this.patch.watch$('packageData').pipe(
+ map(pkgData => {
+ const rows: Array<{
+ src: string
+ dst: string
+ gateway: string
+ interfaces: string[]
+ }> = []
+
+ for (const [pkgId, pkg] of Object.entries(pkgData)) {
+ const title =
+ pkg.stateInfo.manifest?.title ??
+ pkg.stateInfo.installingInfo?.newManifest?.title ??
+ pkgId
+
+ for (const [hostId, host] of Object.entries(pkg.hosts)) {
+ // Find interface names pointing to this host
+ const ifaceNames: string[] = []
+ for (const iface of Object.values(pkg.serviceInterfaces)) {
+ if (iface.addressInfo.hostId === hostId) {
+ ifaceNames.push(`${title} - ${iface.name}`)
+ }
+ }
+
+ const label =
+ ifaceNames.length > 0 ? ifaceNames : [`${title} - ${hostId}`]
+
+ for (const pf of host.portForwards) {
+ rows.push({ ...pf, interfaces: label })
+ }
+ }
+ }
+
+ return rows
+ }),
+ ),
+ ]).pipe(
+ map(([osForwards, pkgForwards]) => {
+ const gatewayId = this.context.data.gatewayId
+ const all = [...osForwards, ...pkgForwards].filter(
+ pf => pf.gateway === gatewayId,
+ )
+
+ // Group by (externalPort, internalPort)
+ const grouped = new Map()
+
+ for (const pf of all) {
+ const src = parseSocketAddr(pf.src)
+ const dst = parseSocketAddr(pf.dst)
+ const key = `${src.port}:${dst.port}`
+
+ const existing = grouped.get(key)
+ if (existing) {
+ for (const iface of pf.interfaces) {
+ if (!existing.interfaces.includes(iface)) {
+ existing.interfaces.push(iface)
+ }
+ }
+ } else {
+ grouped.set(key, {
+ interfaces: [...pf.interfaces],
+ externalPort: src.port,
+ internalPort: dst.port,
+ })
+ }
+ }
+
+ return [...grouped.values()].sort(
+ (a, b) => a.externalPort - b.externalPort,
+ )
+ }),
+ )
+
+ readonly rows = toSignal(this.portForwards$, { initialValue: [] })
+
+ async testPort(index: number, port: number) {
+ this.loading.update(l => ({ ...l, [index]: true }))
+
+ try {
+ const result = await this.api.checkPort({
+ gateway: this.context.data.gatewayId,
+ port,
+ })
+
+ this.results.update(r => ({ ...r, [index]: result.reachable }))
+ } catch (e: any) {
+ this.errorService.handleError(e)
+ } finally {
+ this.loading.update(l => ({ ...l, [index]: false }))
+ }
+ }
+}
+
+export const PORT_FORWARDS_MODAL = new PolymorpheusComponent(
+ PortForwardsModalComponent,
+)
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
index a904aeab7..d7e5d7d03 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/table.component.ts
@@ -8,21 +8,12 @@ import { GatewayService } from 'src/app/services/gateway.service'
@Component({
selector: 'gateways-table',
template: `
-
+
@for (gateway of gatewayService.gateways(); track $index) {
} @empty {
-
+
{{ 'Loading' | i18n }}
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts
index 565916acc..e76e0e92f 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/ssh.component.ts
@@ -39,7 +39,7 @@ import { SSHTableComponent } from './table.component'
tuiIconButton
size="xs"
docsLink
- path="/user-manual/ssh.html"
+ path="/start-os/user-manual/ssh.html"
appearance="icon"
iconStart="@tui.external-link"
>
diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts
index a2c0dffe8..d9409d96b 100644
--- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts
+++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts
@@ -54,7 +54,7 @@ import { wifiSpec } from './wifi.const'
tuiIconButton
size="xs"
docsLink
- path="/user-manual/wifi.html"
+ path="/start-os/user-manual/wifi.html"
appearance="icon"
iconStart="@tui.external-link"
>
diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts
index 815364b93..1f179b20a 100644
--- a/web/projects/ui/src/app/services/api/api.types.ts
+++ b/web/projects/ui/src/app/services/api/api.types.ts
@@ -89,6 +89,10 @@ export type GetRegistryPackageReq = GetPackageReq & { registry: string }
export type GetRegistryPackagesReq = GetPackagesReq & { registry: string }
+// dns
+// TODO: Replace with T.CheckDnsRes when SDK types are generated
+export type CheckDnsRes = boolean
+
// backup
export type DiskBackupTarget = Extract
diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts
index d44667ca9..e69d6c243 100644
--- a/web/projects/ui/src/app/services/api/embassy-api.service.ts
+++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts
@@ -6,6 +6,7 @@ import { WebSocketSubject } from 'rxjs/webSocket'
import { DataModel } from '../patch-db/data-model'
import {
ActionRes,
+ CheckDnsRes,
CifsBackupTarget,
DiagnosticErrorRes,
FollowPackageLogsReq,
@@ -128,9 +129,9 @@ export abstract class ApiService {
abstract queryDns(params: T.QueryDnsParams): Promise
- abstract checkPort(
- params: T.CheckPortParams,
- ): Promise
+ abstract checkPort(params: T.CheckPortParams): Promise
+
+ abstract checkDns(params: T.CheckDnsParams): Promise
// smtp
@@ -191,9 +192,7 @@ export abstract class ApiService {
abstract setDefaultOutbound(params: { gateway: string | null }): Promise
- abstract setServiceOutbound(
- params: T.SetOutboundGatewayParams,
- ): Promise
+ abstract setServiceOutbound(params: T.SetOutboundGatewayParams): Promise
// ** domains **
diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts
index 0d12469d8..3441ba69c 100644
--- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts
+++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts
@@ -19,6 +19,7 @@ import { AuthService } from '../auth.service'
import { DataModel } from '../patch-db/data-model'
import {
ActionRes,
+ CheckDnsRes,
CifsBackupTarget,
DiagnosticErrorRes,
FollowPackageLogsReq,
@@ -283,6 +284,13 @@ export class LiveApiService extends ApiService {
})
}
+ async checkDns(params: T.CheckDnsParams): Promise {
+ return this.rpcRequest({
+ method: 'net.gateway.check-dns',
+ params,
+ })
+ }
+
// marketplace URLs
async checkOSUpdate(params: {
diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts
index ab6399eb8..8905c468d 100644
--- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts
+++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts
@@ -26,6 +26,7 @@ import {
import { GetPackageRes, GetPackagesRes } from '@start9labs/marketplace'
import {
ActionRes,
+ CheckDnsRes,
CifsBackupTarget,
DiagnosticErrorRes,
FollowPackageLogsReq,
@@ -497,14 +498,18 @@ export class MockApiService extends ApiService {
return null
}
- async checkPort(
- params: T.CheckPortParams,
- ): Promise {
+ async checkPort(params: T.CheckPortParams): Promise {
await pauseFor(2000)
return { ip: '0.0.0.0', port: params.port, reachable: false }
}
+ async checkDns(params: T.CheckDnsParams): Promise {
+ await pauseFor(2000)
+
+ return false
+ }
+
// marketplace URLs
async checkOSUpdate(params: {
@@ -662,9 +667,7 @@ export class MockApiService extends ApiService {
return null
}
- async setServiceOutbound(
- params: T.SetOutboundGatewayParams,
- ): Promise {
+ async setServiceOutbound(params: T.SetOutboundGatewayParams): Promise {
await pauseFor(2000)
const patch = [
{
diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts
index 1f0e7cf59..f81a5afdb 100644
--- a/web/projects/ui/src/app/services/api/mock-patch.ts
+++ b/web/projects/ui/src/app/services/api/mock-patch.ts
@@ -54,32 +54,42 @@ export const mockPatchData: DataModel = {
},
},
{
- ssl: true,
+ ssl: false,
public: false,
host: '10.0.0.1',
- port: 443,
+ port: 80,
metadata: { kind: 'ipv4', gateway: 'eth0' },
},
{
- ssl: true,
+ ssl: false,
public: false,
host: '10.0.0.2',
- port: 443,
+ port: 80,
metadata: { kind: 'ipv4', gateway: 'wlan0' },
},
{
- ssl: true,
+ ssl: false,
public: false,
host: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
- port: 443,
+ port: 80,
metadata: { kind: 'ipv6', gateway: 'eth0', scopeId: 2 },
},
+ {
+ ssl: false,
+ public: false,
+ host: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
+ port: 80,
+ metadata: { kind: 'ipv6', gateway: 'wlan0', scopeId: 3 },
+ },
{
ssl: true,
public: false,
- host: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
+ host: 'my-server.home',
port: 443,
- metadata: { kind: 'ipv6', gateway: 'wlan0', scopeId: 3 },
+ metadata: {
+ kind: 'private-domain',
+ gateways: ['eth0'],
+ },
},
{
ssl: false,
@@ -109,8 +119,16 @@ export const mockPatchData: DataModel = {
},
},
publicDomains: {},
- privateDomains: {},
- portForwards: [],
+ privateDomains: {
+ 'my-server.home': ['eth0'],
+ },
+ portForwards: [
+ {
+ src: '203.0.113.45:443',
+ dst: '10.0.0.1:443',
+ gateway: 'eth0',
+ },
+ ],
},
gateways: {
eth0: {
@@ -504,70 +522,70 @@ export const mockPatchData: DataModel = {
80: {
enabled: true,
net: {
- assignedPort: 80,
- assignedSslPort: 443,
+ assignedPort: 42080,
+ assignedSslPort: 42443,
},
addresses: {
- enabled: ['203.0.113.45:443'],
+ enabled: ['203.0.113.45:42443'],
disabled: [],
available: [
{
ssl: true,
public: false,
host: 'adjective-noun.local',
- port: 443,
+ port: 42443,
metadata: {
kind: 'mdns',
gateways: ['eth0'],
},
},
{
- ssl: true,
+ ssl: false,
public: false,
host: '10.0.0.1',
- port: 443,
+ port: 42080,
metadata: { kind: 'ipv4', gateway: 'eth0' },
},
{
- ssl: true,
+ ssl: false,
public: false,
host: 'fe80::cd00:0cde:1257:211e:72cd',
- port: 443,
+ port: 42080,
metadata: { kind: 'ipv6', gateway: 'eth0', scopeId: 2 },
},
{
ssl: true,
public: true,
host: '203.0.113.45',
- port: 443,
+ port: 42443,
metadata: { kind: 'ipv4', gateway: 'eth0' },
},
{
ssl: true,
public: true,
host: 'bitcoin.example.com',
- port: 443,
+ port: 42443,
metadata: { kind: 'public-domain', gateway: 'eth0' },
},
{
- ssl: true,
+ ssl: false,
public: false,
host: '192.168.10.11',
- port: 443,
+ port: 42080,
metadata: { kind: 'ipv4', gateway: 'wlan0' },
},
{
- ssl: true,
+ ssl: false,
public: false,
host: 'fe80::cd00:0cde:1257:211e:1234',
- port: 443,
+ port: 42080,
metadata: { kind: 'ipv6', gateway: 'wlan0', scopeId: 3 },
},
{
ssl: true,
public: false,
host: 'my-bitcoin.home',
- port: 443,
+ port: 42443,
metadata: {
kind: 'private-domain',
gateways: ['wlan0'],
@@ -577,22 +595,22 @@ export const mockPatchData: DataModel = {
ssl: false,
public: false,
host: 'xyz789abc123def456ghi789jkl012mno345pqr678stu901vwx234.onion',
- port: 80,
+ port: 42080,
metadata: { kind: 'plugin', package: 'tor' },
},
{
ssl: true,
public: false,
host: 'xyz789abc123def456ghi789jkl012mno345pqr678stu901vwx234.onion',
- port: 443,
+ port: 42443,
metadata: { kind: 'plugin', package: 'tor' },
},
],
},
options: {
- preferredExternalPort: 443,
+ preferredExternalPort: 42443,
addSsl: {
- preferredExternalPort: 443,
+ preferredExternalPort: 42443,
alpn: { specified: ['http/1.1', 'h2'] },
addXForwardedHeaders: false,
},
@@ -609,14 +627,25 @@ export const mockPatchData: DataModel = {
privateDomains: {
'my-bitcoin.home': ['wlan0'],
},
- portForwards: [],
+ portForwards: [
+ {
+ src: '203.0.113.45:443',
+ dst: '10.0.0.1:443',
+ gateway: 'eth0',
+ },
+ {
+ src: '203.0.113.45:42443',
+ dst: '10.0.0.1:42443',
+ gateway: 'eth0',
+ },
+ ],
},
bcdefgh: {
bindings: {
8332: {
enabled: true,
net: {
- assignedPort: 8332,
+ assignedPort: 48332,
assignedSslPort: null,
},
addresses: {
@@ -627,7 +656,7 @@ export const mockPatchData: DataModel = {
ssl: false,
public: false,
host: 'adjective-noun.local',
- port: 8332,
+ port: 48332,
metadata: {
kind: 'mdns',
gateways: ['eth0'],
@@ -637,14 +666,14 @@ export const mockPatchData: DataModel = {
ssl: false,
public: false,
host: '10.0.0.1',
- port: 8332,
+ port: 48332,
metadata: { kind: 'ipv4', gateway: 'eth0' },
},
],
},
options: {
addSsl: null,
- preferredExternalPort: 8332,
+ preferredExternalPort: 48332,
secure: { ssl: false },
},
},
@@ -658,7 +687,7 @@ export const mockPatchData: DataModel = {
8333: {
enabled: true,
net: {
- assignedPort: 8333,
+ assignedPort: 48333,
assignedSslPort: null,
},
addresses: {
@@ -668,7 +697,7 @@ export const mockPatchData: DataModel = {
},
options: {
addSsl: null,
- preferredExternalPort: 8333,
+ preferredExternalPort: 48333,
secure: { ssl: false },
},
},