mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
round out dns check, dns server check, port forward check, and gateway port forwards
This commit is contained in:
@@ -78,6 +78,15 @@ Form controls live in `ui/src/app/routes/portal/components/form/controls/` — e
|
||||
- **Dictionaries** live in `shared/src/i18n/dictionaries/` (en, es, de, fr, pl).
|
||||
- Usage in templates: `{{ 'Some English Text' | i18n }}`
|
||||
|
||||
### How dictionaries work
|
||||
|
||||
- **`en.ts`** is the source of truth. Keys are English strings; values are numeric IDs (e.g. `'Domain Health': 748`).
|
||||
- **Other language files** (`de.ts`, `es.ts`, `fr.ts`, `pl.ts`) use those same numeric IDs as keys, mapping to translated strings (e.g. `748: 'Santé du domaine'`).
|
||||
- When adding a new i18n key:
|
||||
1. Add the English string and next available numeric ID to `en.ts`.
|
||||
2. Add the same numeric ID with a proper translation to every other language file.
|
||||
3. Always provide real translations, not empty strings.
|
||||
|
||||
## Services & State
|
||||
|
||||
Services often extend `Observable` and expose reactive streams via DI:
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div>
|
||||
<!-- link to store for brochure -->
|
||||
<ng-content select="[slot=store-mobile]" />
|
||||
<a docsLink path="/packaging-guide">
|
||||
<a docsLink path="/packaging/quick-start.html">
|
||||
<span>{{ 'Package a service' | i18n }}</span>
|
||||
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
||||
</a>
|
||||
@@ -86,7 +86,7 @@
|
||||
<div>
|
||||
<!-- link to store for brochure -->
|
||||
<ng-content select="[slot=store]" />
|
||||
<a docsLink path="/packaging-guide">
|
||||
<a docsLink path="/packaging/quick-start.html">
|
||||
<span>{{ 'Package a service' | i18n }}</span>
|
||||
<tui-icon tuiAppearance="icon" icon="@tui.external-link" />
|
||||
</a>
|
||||
|
||||
@@ -46,7 +46,7 @@ import { DocsLinkDirective } from '@start9labs/shared'
|
||||
Download your server's Root CA and
|
||||
<a
|
||||
docsLink
|
||||
path="/user-manual/trust-ca.html"
|
||||
path="/start-os/user-manual/trust-ca.html"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
>
|
||||
follow instructions
|
||||
|
||||
@@ -683,6 +683,14 @@ export default {
|
||||
743: 'In Ihrem Gateway',
|
||||
744: 'diese Portweiterleitungsregel erstellen',
|
||||
745: 'Externer Port',
|
||||
746: 'Interne IP',
|
||||
747: 'Interner Port',
|
||||
749: 'DNS-Server-Konfiguration',
|
||||
750: 'muss konfiguriert werden, um',
|
||||
751: 'die LAN-IP-Adresse dieses Servers',
|
||||
752: 'als DNS-Server zu verwenden',
|
||||
753: 'DNS-Server',
|
||||
754: 'Portweiterleitungen anzeigen',
|
||||
755: 'Schnittstelle(n)',
|
||||
756: 'Keine Portweiterleitungsregeln',
|
||||
757: 'Portweiterleitungsregeln am Gateway erforderlich',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -683,6 +683,14 @@ export const ENGLISH: Record<string, number> = {
|
||||
'In your gateway': 743, // partial sentence, followed by a gateway name
|
||||
'create this port forwarding rule': 744,
|
||||
'External Port': 745,
|
||||
'Internal IP': 746,
|
||||
'Internal Port': 747,
|
||||
'DNS Server Config': 749,
|
||||
'must be configured to use': 750,
|
||||
'the LAN IP address of this server': 751,
|
||||
'as its DNS server': 752,
|
||||
'DNS Server': 753,
|
||||
'View port forwards': 754,
|
||||
'Interface(s)': 755,
|
||||
'No port forwarding rules': 756,
|
||||
'Port forwarding rules required on gateway': 757,
|
||||
}
|
||||
|
||||
@@ -683,6 +683,14 @@ export default {
|
||||
743: 'En su puerta de enlace',
|
||||
744: 'cree esta regla de reenvío de puertos',
|
||||
745: 'Puerto externo',
|
||||
746: 'IP interna',
|
||||
747: 'Puerto interno',
|
||||
749: 'Configuración del servidor DNS',
|
||||
750: 'debe estar configurada para usar',
|
||||
751: 'la dirección IP LAN de este servidor',
|
||||
752: 'como su servidor DNS',
|
||||
753: 'Servidor DNS',
|
||||
754: 'Ver redirecciones de puertos',
|
||||
755: 'Interfaz/Interfaces',
|
||||
756: 'Sin reglas de redirección de puertos',
|
||||
757: 'Reglas de redirección de puertos requeridas en la puerta de enlace',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -683,6 +683,14 @@ export default {
|
||||
743: 'Dans votre passerelle',
|
||||
744: 'créez cette règle de redirection de port',
|
||||
745: 'Port externe',
|
||||
746: 'IP interne',
|
||||
747: 'Port interne',
|
||||
749: 'Configuration du serveur DNS',
|
||||
750: 'doit être configurée pour utiliser',
|
||||
751: "l'adresse IP LAN de ce serveur",
|
||||
752: 'comme serveur DNS',
|
||||
753: 'Serveur DNS',
|
||||
754: 'Voir les redirections de ports',
|
||||
755: 'Interface(s)',
|
||||
756: 'Aucune règle de redirection de port',
|
||||
757: 'Règles de redirection de ports requises sur la passerelle',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -683,6 +683,14 @@ export default {
|
||||
743: 'W bramie',
|
||||
744: 'utwórz tę regułę przekierowania portów',
|
||||
745: 'Port zewnętrzny',
|
||||
746: 'Wewnętrzny IP',
|
||||
747: 'Port wewnętrzny',
|
||||
749: 'Konfiguracja serwera DNS',
|
||||
750: 'musi być skonfigurowana do używania',
|
||||
751: 'adresu IP LAN tego serwera',
|
||||
752: 'jako serwera DNS',
|
||||
753: 'Serwer DNS',
|
||||
754: 'Wyświetl przekierowania portów',
|
||||
755: 'Interfejs(y)',
|
||||
756: 'Brak reguł przekierowania portów',
|
||||
757: 'Reguły przekierowania portów wymagane na bramce',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -30,7 +30,6 @@ export function getErrorMessage(e: HttpError | string, link?: string): string {
|
||||
'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again'
|
||||
} else if (!e.message) {
|
||||
message = 'Unknown Error'
|
||||
link = 'https://docs.start9.com/help/common-issues.html'
|
||||
} else {
|
||||
message = e.message
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
tuiButton
|
||||
docsLink
|
||||
size="s"
|
||||
path="/user-manual/trust-ca.html"
|
||||
path="/start-os/user-manual/trust-ca.html"
|
||||
iconEnd="@tui.external-link"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
|
||||
@@ -50,7 +50,12 @@ import { ABOUT } from './about.component'
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group label="" safeLinks>
|
||||
<a tuiOption docsLink iconStart="@tui.book-open" path="/user-manual">
|
||||
<a
|
||||
tuiOption
|
||||
docsLink
|
||||
iconStart="@tui.book-open"
|
||||
path="/start-os/user-manual/index.html"
|
||||
>
|
||||
{{ 'User manual' | i18n }}
|
||||
</a>
|
||||
<a
|
||||
|
||||
@@ -24,6 +24,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
||||
import { DomainHealthService } from './domain-health.service'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
@@ -39,6 +40,40 @@ import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address().hostnameInfo.metadata.kind === 'public-domain') {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.globe"
|
||||
(click)="showDnsValidation()"
|
||||
>
|
||||
{{ 'Domain Setup' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address().hostnameInfo.metadata.kind === 'private-domain') {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.globe"
|
||||
(click)="showPrivateDnsValidation()"
|
||||
>
|
||||
{{ 'Domain Setup' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (
|
||||
address().hostnameInfo.metadata.kind === 'ipv4' &&
|
||||
address().access === 'public' &&
|
||||
address().hostnameInfo.port !== null
|
||||
) {
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.globe"
|
||||
(click)="showPortForwardValidation()"
|
||||
>
|
||||
{{ 'Port Forwarding' | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
@@ -88,6 +123,39 @@ import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
||||
>
|
||||
{{ 'Copy URL' | i18n }}
|
||||
</button>
|
||||
@if (address().hostnameInfo.metadata.kind === 'public-domain') {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.heart-pulse"
|
||||
(click)="showDnsValidation()"
|
||||
>
|
||||
{{ 'Domain Setup' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address().hostnameInfo.metadata.kind === 'private-domain') {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.heart-pulse"
|
||||
(click)="showPrivateDnsValidation()"
|
||||
>
|
||||
{{ 'Domain Setup' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (
|
||||
address().hostnameInfo.metadata.kind === 'ipv4' &&
|
||||
address().hostnameInfo.port !== null
|
||||
) {
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.heart-pulse"
|
||||
(click)="showPortForwardValidation()"
|
||||
>
|
||||
{{ 'Port Forwarding' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address().deletable) {
|
||||
<button
|
||||
tuiOption
|
||||
@@ -135,6 +203,7 @@ export class AddressActionsComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly domainHealth = inject(DomainHealthService)
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly open = signal(false)
|
||||
|
||||
@@ -142,6 +211,7 @@ export class AddressActionsComponent {
|
||||
readonly packageId = input('')
|
||||
readonly value = input<MappedServiceInterface | undefined>()
|
||||
readonly disabled = input.required<boolean>()
|
||||
readonly gatewayId = input('')
|
||||
|
||||
showQR() {
|
||||
this.dialog
|
||||
@@ -185,6 +255,23 @@ export class AddressActionsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
showDnsValidation() {
|
||||
this.domainHealth.showPublicDomainSetup(
|
||||
this.address().hostnameInfo.host,
|
||||
this.gatewayId(),
|
||||
)
|
||||
}
|
||||
|
||||
showPrivateDnsValidation() {
|
||||
this.domainHealth.showPrivateDomainSetup(this.gatewayId())
|
||||
}
|
||||
|
||||
showPortForwardValidation() {
|
||||
const port = this.address().hostnameInfo.port
|
||||
if (port === null) return
|
||||
this.domainHealth.showPortForwardSetup(this.gatewayId(), port)
|
||||
}
|
||||
|
||||
async deleteDomain() {
|
||||
const addr = this.address()
|
||||
const iface = this.value()
|
||||
|
||||
@@ -5,12 +5,7 @@ import {
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiButton,
|
||||
@@ -35,7 +30,7 @@ import {
|
||||
GatewayAddressGroup,
|
||||
MappedServiceInterface,
|
||||
} from '../interface.service'
|
||||
import { DOMAIN_VALIDATION, DnsGateway } from '../public-domains/dns.component'
|
||||
import { DomainHealthService } from './domain-health.service'
|
||||
import { InterfaceAddressItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
@@ -62,14 +57,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
</button>
|
||||
</header>
|
||||
<table
|
||||
[appTable]="[
|
||||
'Enabled',
|
||||
'Type',
|
||||
'Access',
|
||||
'Certificate Authority',
|
||||
'URL',
|
||||
null,
|
||||
]"
|
||||
[appTable]="['Enabled', 'Type', 'Certificate Authority', 'URL', null]"
|
||||
>
|
||||
@for (address of gatewayGroup().addresses; track $index) {
|
||||
<tr
|
||||
@@ -77,10 +65,11 @@ import { InterfaceAddressItemComponent } from './item.component'
|
||||
[packageId]="packageId()"
|
||||
[value]="value()"
|
||||
[isRunning]="isRunning()"
|
||||
[gatewayId]="gatewayGroup().gatewayId"
|
||||
></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<td colspan="5">
|
||||
<app-placeholder icon="@tui.list-x">
|
||||
{{ 'No addresses' | i18n }}
|
||||
</app-placeholder>
|
||||
@@ -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<DataModel>>(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<GatewayAddressGroup>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
<h3>{{ 'DNS' | i18n }}</h3>
|
||||
<h2>{{ 'DNS' | i18n }}</h2>
|
||||
<p>
|
||||
{{ 'In your domain registrar for' | i18n }} {{ domain }},
|
||||
{{ 'create this DNS record' | i18n }}
|
||||
@@ -63,10 +61,14 @@ export type DomainValidationData = {
|
||||
<table [appTable]="[null, 'Type', 'Host', 'Value', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (dnsPass() === true) {
|
||||
@if (dnsLoading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (dnsPass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (dnsPass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
</td>
|
||||
<td>{{ ddns ? 'ALIAS' : 'A' }}</td>
|
||||
@@ -85,25 +87,26 @@ export type DomainValidationData = {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>{{ 'Port Forwarding' | i18n }}</h3>
|
||||
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
||||
<p>
|
||||
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
||||
{{ 'create this port forwarding rule' | i18n }}
|
||||
</p>
|
||||
|
||||
<table
|
||||
[appTable]="[null, 'External Port', 'Internal IP', 'Internal Port', null]"
|
||||
>
|
||||
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (portPass() === true) {
|
||||
@if (portLoading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (portPass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (portPass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>{{ internalIp }}</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>
|
||||
<button
|
||||
@@ -118,23 +121,25 @@ export type DomainValidationData = {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<footer class="g-buttons">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[disabled]="allPass()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Later' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!allPass()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Done' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
@if (!isManualMode) {
|
||||
<footer class="g-buttons padding-top">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[disabled]="allPass()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Later' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="!allPass()"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Done' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
`,
|
||||
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<boolean | undefined>(this.context.data.dnsPass)
|
||||
readonly portPass = signal<boolean | undefined>(this.context.data.portPass)
|
||||
readonly dnsPass = signal<boolean | undefined>(undefined)
|
||||
readonly portPass = signal<boolean | undefined>(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)
|
||||
|
||||
@@ -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<DataModel>>(PatchDB)
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
async checkPublicDomain(fqdn: string, gatewayId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<DnsGateway | null> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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()"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{ address.type }}
|
||||
</td>
|
||||
<td class="access">
|
||||
<td class="type">
|
||||
<tui-icon
|
||||
[icon]="address.access === 'public' ? '@tui.globe' : '@tui.house'"
|
||||
/>
|
||||
{{ address.access | i18n }}
|
||||
{{ address.type }}
|
||||
</td>
|
||||
<td>
|
||||
{{ address.certificate }}
|
||||
@@ -71,6 +69,7 @@ import { AddressActionsComponent } from './actions.component'
|
||||
[packageId]="packageId()"
|
||||
[value]="value()"
|
||||
[disabled]="!isRunning()"
|
||||
[gatewayId]="gatewayId()"
|
||||
[style.width.rem]="5"
|
||||
></td>
|
||||
}
|
||||
@@ -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<GatewayAddress>()
|
||||
readonly packageId = input('')
|
||||
readonly value = input<MappedServiceInterface | undefined>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
||||
<p>
|
||||
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
||||
{{ 'create this port forwarding rule' | i18n }}
|
||||
</p>
|
||||
|
||||
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (loading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (pass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (pass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>{{ context.data.port }}</td>
|
||||
<td>
|
||||
<button tuiButton size="s" [loading]="loading()" (click)="testPort()">
|
||||
{{ 'Test' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@if (!isManualMode) {
|
||||
<footer class="g-buttons padding-top">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[disabled]="pass() === true"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Later' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="pass() !== true"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Done' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
`,
|
||||
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<TuiDialogContext<void, PortForwardValidationData>>()
|
||||
|
||||
readonly loading = signal(false)
|
||||
readonly pass = signal<boolean | undefined>(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,
|
||||
)
|
||||
@@ -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);
|
||||
|
||||
<h2>{{ 'DNS Server Config' | i18n }}</h2>
|
||||
<p>
|
||||
{{ 'Gateway' | i18n }} "{{ gatewayName }}"
|
||||
{{ 'must be configured to use' | i18n }}
|
||||
{{ internalIp }}
|
||||
({{ 'the LAN IP address of this server' | i18n }})
|
||||
{{ 'as its DNS server' | i18n }}.
|
||||
</p>
|
||||
|
||||
<table [appTable]="[null, 'Gateway', 'DNS Server', null]">
|
||||
<tr>
|
||||
<td class="status">
|
||||
@if (loading()) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (pass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (pass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
</td>
|
||||
<td>{{ gatewayName }}</td>
|
||||
<td>{{ internalIp }}</td>
|
||||
<td>
|
||||
<button tuiButton size="s" [loading]="loading()" (click)="testDns()">
|
||||
{{ 'Test' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@if (!isManualMode) {
|
||||
<footer class="g-buttons padding-top">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[disabled]="pass() === true"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Later' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
[disabled]="pass() !== true"
|
||||
(click)="context.completeWith()"
|
||||
>
|
||||
{{ 'Done' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
`,
|
||||
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<TuiDialogContext<void, PrivateDnsValidationData>>()
|
||||
|
||||
readonly loading = signal(false)
|
||||
readonly pass = signal<boolean | undefined>(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,
|
||||
)
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
<a tuiLink docsLink path="/@TODO">View instructions</a>
|
||||
<a tuiLink docsLink path="/start-os/user-manual/backup-create.html">
|
||||
View instructions
|
||||
</a>
|
||||
</tui-notification>
|
||||
<h3 class="g-title">
|
||||
Saved Jobs
|
||||
|
||||
@@ -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.
|
||||
<a tuiLink docsLink path="/@TODO">View instructions</a>
|
||||
<a tuiLink docsLink path="/start-os/user-manual/backup-create.html">
|
||||
View instructions
|
||||
</a>
|
||||
</tui-notification>
|
||||
<h3 class="g-title">
|
||||
Unknown Physical Drives
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -66,7 +66,7 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/backup-create.html"
|
||||
path="/start-os/user-manual/backup-create.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
@@ -79,7 +79,7 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/backup-restore.html"
|
||||
path="/start-os/user-manual/backup-restore.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
@@ -121,7 +121,7 @@ import { BACKUP_RESTORE } from './restore.component'
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/backup-create.html"
|
||||
path="/start-os/user-manual/backup-create.html"
|
||||
fragment="#network-folder"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
|
||||
@@ -47,7 +47,7 @@ const ipv6 =
|
||||
tuiIconButton
|
||||
size="xs"
|
||||
docsLink
|
||||
path="/user-manual/dns.html"
|
||||
path="/start-os/user-manual/dns.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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) {
|
||||
<td>
|
||||
@switch (gateway.ipInfo.deviceType) {
|
||||
@case ('ethernet') {
|
||||
<tui-icon icon="@tui.ethernet-port" />
|
||||
}
|
||||
@case ('wireless') {
|
||||
<tui-icon icon="@tui.wifi" />
|
||||
}
|
||||
@case ('wireguard') {
|
||||
<tui-icon icon="@tui.shield" />
|
||||
}
|
||||
}
|
||||
{{ gateway.name }}
|
||||
@if (gateway.isDefaultOutbound) {
|
||||
<tui-badge appearance="primary-success">
|
||||
@@ -39,31 +51,10 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
</tui-badge>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@switch (gateway.ipInfo.deviceType) {
|
||||
@case ('ethernet') {
|
||||
<tui-icon icon="@tui.cable" />
|
||||
{{ 'Ethernet' | i18n }}
|
||||
}
|
||||
@case ('wireless') {
|
||||
<tui-icon icon="@tui.wifi" />
|
||||
{{ 'WiFi' | i18n }}
|
||||
}
|
||||
@case ('wireguard') {
|
||||
<tui-icon icon="@tui.shield" />
|
||||
WireGuard
|
||||
}
|
||||
@default {
|
||||
{{ gateway.ipInfo.deviceType }}
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (gateway.type === 'outbound-only') {
|
||||
<tui-icon icon="@tui.arrow-up" />
|
||||
{{ 'Outbound Only' | i18n }}
|
||||
} @else {
|
||||
<tui-icon icon="@tui.arrow-up-down" />
|
||||
{{ 'Inbound/Outbound' | i18n }}
|
||||
}
|
||||
</td>
|
||||
@@ -93,25 +84,23 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
{{ 'Rename' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
@if (gateway.type !== 'outbound-only') {
|
||||
<tui-opt-group>
|
||||
<button tuiOption new (click)="viewPortForwards()">
|
||||
{{ 'View port forwards' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
}
|
||||
@if (!gateway.isDefaultOutbound) {
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
(click)="setDefaultOutbound()"
|
||||
>
|
||||
<button tuiOption new (click)="setDefaultOutbound()">
|
||||
{{ 'Set as default outbound' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
}
|
||||
@if (gateway.ipInfo.deviceType === 'wireguard') {
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
class="g-negative"
|
||||
(click)="remove()"
|
||||
>
|
||||
<button tuiOption new class="g-negative" (click)="remove()">
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
@@ -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' })
|
||||
|
||||
@@ -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: `
|
||||
<p>
|
||||
{{ 'Port forwarding rules required on gateway' | i18n }}
|
||||
"{{ context.data.gatewayName }}"
|
||||
</p>
|
||||
|
||||
<table
|
||||
[appTable]="[
|
||||
'Interface(s)',
|
||||
null,
|
||||
'External Port',
|
||||
'Internal Port',
|
||||
null,
|
||||
]"
|
||||
>
|
||||
@for (row of rows(); track row.externalPort; let i = $index) {
|
||||
<tr>
|
||||
<td class="interfaces">
|
||||
@for (iface of row.interfaces; track iface) {
|
||||
<div>{{ iface }}</div>
|
||||
}
|
||||
</td>
|
||||
<td class="status">
|
||||
@if (loading()[i]) {
|
||||
<tui-loader size="s" />
|
||||
} @else if (results()[i] === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (results()[i] === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
} @else {
|
||||
<tui-icon class="g-secondary" icon="@tui.minus" />
|
||||
}
|
||||
</td>
|
||||
<td>{{ row.externalPort }}</td>
|
||||
<td>{{ row.internalPort }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
[loading]="!!loading()[i]"
|
||||
(click)="testPort(i, row.externalPort)"
|
||||
>
|
||||
{{ 'Test' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<app-placeholder icon="@tui.list-x">
|
||||
{{ 'No port forwarding rules' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
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<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
readonly context =
|
||||
injectContext<TuiDialogContext<void, PortForwardsModalData>>()
|
||||
|
||||
readonly loading = signal<Record<number, boolean>>({})
|
||||
readonly results = signal<Record<number, boolean>>({})
|
||||
|
||||
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<string, PortForwardRow>()
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -8,21 +8,12 @@ import { GatewayService } from 'src/app/services/gateway.service'
|
||||
@Component({
|
||||
selector: 'gateways-table',
|
||||
template: `
|
||||
<table
|
||||
[appTable]="[
|
||||
'Name',
|
||||
'Connection',
|
||||
'Type',
|
||||
$any('LAN IP'),
|
||||
$any('WAN IP'),
|
||||
null,
|
||||
]"
|
||||
>
|
||||
<table [appTable]="['Name', 'Type', $any('LAN IP'), $any('WAN IP'), null]">
|
||||
@for (gateway of gatewayService.gateways(); track $index) {
|
||||
<tr [gateway]="gateway"></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="6">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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<T.BackupTarget, { type: 'disk' }>
|
||||
|
||||
@@ -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<string | null>
|
||||
|
||||
abstract checkPort(
|
||||
params: T.CheckPortParams,
|
||||
): Promise<T.CheckPortRes>
|
||||
abstract checkPort(params: T.CheckPortParams): Promise<T.CheckPortRes>
|
||||
|
||||
abstract checkDns(params: T.CheckDnsParams): Promise<CheckDnsRes>
|
||||
|
||||
// smtp
|
||||
|
||||
@@ -191,9 +192,7 @@ export abstract class ApiService {
|
||||
|
||||
abstract setDefaultOutbound(params: { gateway: string | null }): Promise<null>
|
||||
|
||||
abstract setServiceOutbound(
|
||||
params: T.SetOutboundGatewayParams,
|
||||
): Promise<null>
|
||||
abstract setServiceOutbound(params: T.SetOutboundGatewayParams): Promise<null>
|
||||
|
||||
// ** domains **
|
||||
|
||||
|
||||
@@ -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<CheckDnsRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.gateway.check-dns',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async checkOSUpdate(params: {
|
||||
|
||||
@@ -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<T.CheckPortRes> {
|
||||
async checkPort(params: T.CheckPortParams): Promise<T.CheckPortRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
return { ip: '0.0.0.0', port: params.port, reachable: false }
|
||||
}
|
||||
|
||||
async checkDns(params: T.CheckDnsParams): Promise<CheckDnsRes> {
|
||||
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<null> {
|
||||
async setServiceOutbound(params: T.SetOutboundGatewayParams): Promise<null> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user