mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
looking good
This commit is contained in:
9869
web/package-lock.json
generated
9869
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -553,33 +553,8 @@ export default {
|
|||||||
580: 'Aktualisierung erforderlich',
|
580: 'Aktualisierung erforderlich',
|
||||||
581: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Versuchen Sie, die PWA mit der Schaltfläche unten neu zu laden. Wenn Sie diese Nachricht weiterhin sehen, deinstallieren und installieren Sie die PWA erneut.',
|
581: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Versuchen Sie, die PWA mit der Schaltfläche unten neu zu laden. Wenn Sie diese Nachricht weiterhin sehen, deinstallieren und installieren Sie die PWA erneut.',
|
||||||
582: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Führen Sie einen Hard-Refresh der Seite durch, um die neueste Benutzeroberfläche zu erhalten.',
|
582: 'Ihre Benutzeroberfläche ist zwischengespeichert und veraltet. Führen Sie einen Hard-Refresh der Seite durch, um die neueste Benutzeroberfläche zu erhalten.',
|
||||||
583: 'Erfordert Vertrauen in die Root-CA Ihres Servers',
|
|
||||||
584: 'Verbindungen können manchmal langsam oder unzuverlässig sein',
|
|
||||||
585: 'Öffentlich, wenn Sie die Adresse öffentlich teilen, andernfalls privat',
|
|
||||||
586: 'Erfordert ein Tor-fähiges Gerät oder einen Browser',
|
|
||||||
587: 'Sollte nur für Apps benötigt werden, die SSL erzwingen',
|
|
||||||
588: 'Ideal für anonyme, zensurresistente Bereitstellung und Fernzugriff',
|
|
||||||
589: 'Ideal für lokalen Zugriff',
|
|
||||||
590: 'Erfordert die Verbindung mit demselben lokalen Netzwerk (LAN) wie Ihr Server, entweder physisch oder über VPN',
|
|
||||||
591: 'Erfordert die Einstellung einer statischen IP-Adresse für',
|
|
||||||
592: 'Ideal für VPN-Zugriff über',
|
|
||||||
593: 'in Ihrem Gateway',
|
|
||||||
594: 'der Wireguard-Server Ihres Routers',
|
|
||||||
595: 'Erfordert Portweiterleitung im Gateway',
|
|
||||||
596: 'Erfordert einen DNS-Eintrag für',
|
|
||||||
597: 'der sich auflöst zu',
|
|
||||||
598: 'Nicht empfohlen für VPN-Zugriff. VPNs unterstützen keine „.local“-Domains ohne erweiterte Konfiguration',
|
|
||||||
599: 'Kann für Clearnet-Zugriff verwendet werden',
|
|
||||||
600: 'In den meisten Fällen nicht empfohlen. Öffentliche Domains sind üblicher und werden bevorzugt',
|
|
||||||
601: 'Lokal',
|
|
||||||
602: 'Kann für lokalen Zugriff verwendet werden',
|
|
||||||
603: 'Ideal für öffentlichen Zugriff über das Internet',
|
|
||||||
604: 'Kann für persönlichen Zugriff über das öffentliche Internet verwendet werden, aber ein VPN ist privater und sicherer',
|
|
||||||
605: 'wenn die Verwendung von IP-Adressen und Ports unerwünscht ist',
|
|
||||||
606: 'Host',
|
606: 'Host',
|
||||||
607: 'Wert',
|
607: 'Wert',
|
||||||
608: 'Zweck',
|
|
||||||
609: 'alle Subdomains von',
|
|
||||||
610: 'Dynamisches DNS',
|
610: 'Dynamisches DNS',
|
||||||
611: 'Keine Service-Schnittstellen',
|
611: 'Keine Service-Schnittstellen',
|
||||||
612: 'Grund',
|
612: 'Grund',
|
||||||
@@ -697,4 +672,6 @@ export default {
|
|||||||
732: '',
|
732: '',
|
||||||
733: '',
|
733: '',
|
||||||
734: '',
|
734: '',
|
||||||
|
735: '',
|
||||||
|
736: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -552,33 +552,8 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Refresh Needed': 580,
|
'Refresh Needed': 580,
|
||||||
'Your user interface is cached and out of date. Attempt to reload the PWA using the button below. If you continue to see this message, uninstall and reinstall the PWA.': 581,
|
'Your user interface is cached and out of date. Attempt to reload the PWA using the button below. If you continue to see this message, uninstall and reinstall the PWA.': 581,
|
||||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.': 582,
|
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.': 582,
|
||||||
"Requires trusting your server's Root CA": 583,
|
|
||||||
'Connections can be slow or unreliable at times': 584,
|
|
||||||
'Public if you share the address publicly, otherwise private': 585,
|
|
||||||
'Requires using a Tor-enabled device or browser': 586,
|
|
||||||
'Should only needed for apps that enforce SSL': 587,
|
|
||||||
'Ideal for anonymous, censorship-resistant hosting and remote access': 588,
|
|
||||||
'Ideal for local access': 589,
|
|
||||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN': 590,
|
|
||||||
'Requires setting a static IP address for': 591, // this is a partial sentence. An IP address will be added after "for" to complete the sentence.
|
|
||||||
'Ideal for VPN access via': 592, // this is a partial sentence. A connection medium will be added after "via" to complete the sentence.
|
|
||||||
'in your gateway': 593, // this is a partial sentence. It is preceded by an instruction: e.g. "do something" in your gateway. Gateway refers to a router or VPN server.
|
|
||||||
"your router's WireGuard server": 594, // this is a partial sentence. It is preceded by "ideal for access via"
|
|
||||||
'Requires port forwarding in gateway': 595,
|
|
||||||
'Requires a DNS record for': 596, // this is a partial sentence. A domain name will be added after "for" to complete the sentence.
|
|
||||||
'that resolves to': 597, // this is a partial sentence. It is preceded by "requires a DNS record for [domain] "
|
|
||||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration': 598,
|
|
||||||
'Can be used for clearnet access': 599,
|
|
||||||
'Not recommended in most cases. Using a public domain is more common and preferred': 600,
|
|
||||||
'Local': 601, // as in, not remote
|
|
||||||
'Can be used for local access': 602,
|
|
||||||
'Ideal for public access via the Internet': 603,
|
|
||||||
'Can be used for personal access via the public Internet, but a VPN is more private and secure': 604,
|
|
||||||
'when using IP addresses and ports is undesirable': 605, // this is a partial sentence. It is preceded by "Good for connections "
|
|
||||||
'Host': 606, // as in, a network host
|
'Host': 606, // as in, a network host
|
||||||
'Value': 607, // as in, the value in a column of a table
|
'Value': 607, // as in, the value in a column of a table
|
||||||
'Purpose': 608, // as in, the reason for a thing to exist
|
|
||||||
'all subdomains of': 609, // this is a partial sentence. A domain name will be added after "of" to complete the sentence.
|
|
||||||
'Dynamic DNS': 610,
|
'Dynamic DNS': 610,
|
||||||
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
|
'No service interfaces': 611, // as in, there are no available interfaces (API, UI, etc) for this software application
|
||||||
'Reason': 612, // as in, an explanation for something
|
'Reason': 612, // as in, an explanation for something
|
||||||
@@ -696,5 +671,18 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Public Domain': 731,
|
'Public Domain': 731,
|
||||||
'Private Domain': 732,
|
'Private Domain': 732,
|
||||||
'Hide': 733,
|
'Hide': 733,
|
||||||
'default outbound': 734
|
'default outbound': 734,
|
||||||
|
'Certificate': 735,
|
||||||
|
'Self signed': 736,
|
||||||
|
'Port Forwarding': 737,
|
||||||
|
'Domain Setup': 738,
|
||||||
|
'DNS': 739,
|
||||||
|
'Instructions': 740,
|
||||||
|
'In your domain registrar for': 741, // partial sentence, followed by a domain name
|
||||||
|
'create this DNS record': 742,
|
||||||
|
'In your gateway': 743, // partial sentence, followed by a gateway name
|
||||||
|
'create this port forwarding rule': 744,
|
||||||
|
'External Port': 745,
|
||||||
|
'Internal IP': 746,
|
||||||
|
'Internal Port': 747,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,33 +553,8 @@ export default {
|
|||||||
580: 'Actualización necesaria',
|
580: 'Actualización necesaria',
|
||||||
581: 'Tu interfaz de usuario está en caché y desactualizada. Intenta recargar la PWA usando el botón de abajo. Si sigues viendo este mensaje, desinstala y vuelve a instalar la PWA.',
|
581: 'Tu interfaz de usuario está en caché y desactualizada. Intenta recargar la PWA usando el botón de abajo. Si sigues viendo este mensaje, desinstala y vuelve a instalar la PWA.',
|
||||||
582: 'Tu interfaz de usuario está en caché y desactualizada. Haz un hard refresh de la página para obtener la última interfaz.',
|
582: 'Tu interfaz de usuario está en caché y desactualizada. Haz un hard refresh de la página para obtener la última interfaz.',
|
||||||
583: 'Requiere confiar en la CA raíz de tu servidor',
|
|
||||||
584: 'Las conexiones pueden ser lentas o poco confiables a veces',
|
|
||||||
585: 'Público si compartes la dirección públicamente, de lo contrario privado',
|
|
||||||
586: 'Requiere un dispositivo o navegador habilitado para Tor',
|
|
||||||
587: 'Solo debería ser necesario para aplicaciones que imponen SSL',
|
|
||||||
588: 'Ideal para alojamiento y acceso remoto anónimo y resistente a la censura',
|
|
||||||
589: 'Ideal para acceso local',
|
|
||||||
590: 'Requiere estar conectado a la misma red de área local (LAN) que tu servidor, ya sea físicamente o mediante VPN',
|
|
||||||
591: 'Requiere configurar una dirección IP estática para',
|
|
||||||
592: 'Ideal para acceso VPN a través de',
|
|
||||||
593: 'en tu gateway',
|
|
||||||
594: 'el servidor Wireguard de tu router',
|
|
||||||
595: 'Requiere reenvío de puertos en el gateway',
|
|
||||||
596: 'Requiere un registro DNS para',
|
|
||||||
597: 'que se resuelva en',
|
|
||||||
598: 'No recomendado para acceso VPN. Las VPN no admiten dominios “.local” sin configuración avanzada',
|
|
||||||
599: 'Se puede usar para acceso a clearnet',
|
|
||||||
600: 'No recomendado en la mayoría de los casos. Los dominios públicos son más comunes y preferidos',
|
|
||||||
601: 'Local',
|
|
||||||
602: 'Se puede usar para acceso local',
|
|
||||||
603: 'Ideal para acceso público a través de Internet',
|
|
||||||
604: 'Puede usarse para acceso personal a través de Internet público, pero una VPN es más privada y segura',
|
|
||||||
605: 'cuando el uso de direcciones IP y puertos no es deseable',
|
|
||||||
606: 'Host',
|
606: 'Host',
|
||||||
607: 'Valor',
|
607: 'Valor',
|
||||||
608: 'Propósito',
|
|
||||||
609: 'todos los subdominios de',
|
|
||||||
610: 'DNS dinámico',
|
610: 'DNS dinámico',
|
||||||
611: 'Sin interfaces de servicio',
|
611: 'Sin interfaces de servicio',
|
||||||
612: 'Razón',
|
612: 'Razón',
|
||||||
@@ -697,4 +672,6 @@ export default {
|
|||||||
732: '',
|
732: '',
|
||||||
733: '',
|
733: '',
|
||||||
734: '',
|
734: '',
|
||||||
|
735: '',
|
||||||
|
736: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -553,33 +553,8 @@ export default {
|
|||||||
580: 'Actualisation nécessaire',
|
580: 'Actualisation nécessaire',
|
||||||
581: 'Votre interface utilisateur est mise en cache et obsolète. Essayez de recharger le PWA à l’aide du bouton ci-dessous. Si vous continuez à voir ce message, désinstallez puis réinstallez le PWA.',
|
581: 'Votre interface utilisateur est mise en cache et obsolète. Essayez de recharger le PWA à l’aide du bouton ci-dessous. Si vous continuez à voir ce message, désinstallez puis réinstallez le PWA.',
|
||||||
582: 'Votre interface utilisateur est mise en cache et obsolète. Faites un rafraîchissement forcé de la page pour obtenir la dernière interface.',
|
582: 'Votre interface utilisateur est mise en cache et obsolète. Faites un rafraîchissement forcé de la page pour obtenir la dernière interface.',
|
||||||
583: 'Nécessite de faire confiance à l’autorité de certification racine de votre serveur',
|
|
||||||
584: 'Les connexions peuvent parfois être lentes ou peu fiables',
|
|
||||||
585: 'Public si vous partagez l’adresse publiquement, sinon privé',
|
|
||||||
586: 'Nécessite un appareil ou un navigateur compatible Tor',
|
|
||||||
587: 'Ne devrait être nécessaire que pour les applications qui imposent SSL',
|
|
||||||
588: 'Idéal pour l’hébergement et l’accès à distance anonymes et résistants à la censure',
|
|
||||||
589: 'Idéal pour un accès local',
|
|
||||||
590: 'Nécessite d’être connecté au même réseau local (LAN) que votre serveur, soit physiquement, soit via VPN',
|
|
||||||
591: 'Nécessite de définir une adresse IP statique pour',
|
|
||||||
592: 'Idéal pour un accès VPN via',
|
|
||||||
593: 'dans votre passerelle',
|
|
||||||
594: 'le serveur Wireguard de votre routeur',
|
|
||||||
595: 'Nécessite un transfert de port dans la passerelle',
|
|
||||||
596: 'Nécessite un enregistrement DNS pour',
|
|
||||||
597: 'qui se résout en',
|
|
||||||
598: 'Non recommandé pour l’accès VPN. Les VPN ne prennent pas en charge les domaines « .local » sans configuration avancée',
|
|
||||||
599: 'Peut être utilisé pour un accès clearnet',
|
|
||||||
600: 'Non recommandé dans la plupart des cas. Les domaines publics sont plus courants et préférés',
|
|
||||||
601: 'Local',
|
|
||||||
602: 'Peut être utilisé pour un accès local',
|
|
||||||
603: 'Idéal pour un accès public via Internet',
|
|
||||||
604: 'Peut être utilisé pour un accès personnel via l’Internet public, mais un VPN est plus privé et plus sécurisé',
|
|
||||||
605: 'lorsque l’utilisation des adresses IP et des ports est indésirable',
|
|
||||||
606: 'Hôte',
|
606: 'Hôte',
|
||||||
607: 'Valeur',
|
607: 'Valeur',
|
||||||
608: 'But',
|
|
||||||
609: 'tous les sous-domaines de',
|
|
||||||
610: 'DNS dynamique',
|
610: 'DNS dynamique',
|
||||||
611: 'Aucune interface de service',
|
611: 'Aucune interface de service',
|
||||||
612: 'Raison',
|
612: 'Raison',
|
||||||
@@ -697,4 +672,6 @@ export default {
|
|||||||
732: '',
|
732: '',
|
||||||
733: '',
|
733: '',
|
||||||
734: '',
|
734: '',
|
||||||
|
735: '',
|
||||||
|
736: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -553,33 +553,8 @@ export default {
|
|||||||
580: 'Wymagane odświeżenie',
|
580: 'Wymagane odświeżenie',
|
||||||
581: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Spróbuj ponownie załadować PWA za pomocą przycisku poniżej. Jeśli nadal widzisz ten komunikat, odinstaluj i ponownie zainstaluj PWA.',
|
581: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Spróbuj ponownie załadować PWA za pomocą przycisku poniżej. Jeśli nadal widzisz ten komunikat, odinstaluj i ponownie zainstaluj PWA.',
|
||||||
582: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Wykonaj twarde odświeżenie strony, aby uzyskać najnowszy interfejs.',
|
582: 'Twój interfejs użytkownika jest w pamięci podręcznej i jest nieaktualny. Wykonaj twarde odświeżenie strony, aby uzyskać najnowszy interfejs.',
|
||||||
583: 'Wymaga zaufania do głównego CA twojego serwera',
|
|
||||||
584: 'Połączenia mogą być czasami wolne lub niestabilne',
|
|
||||||
585: 'Publiczne, jeśli udostępniasz adres publicznie, w przeciwnym razie prywatne',
|
|
||||||
586: 'Wymaga urządzenia lub przeglądarki obsługującej Tor',
|
|
||||||
587: 'Powinno być wymagane tylko dla aplikacji wymuszających SSL',
|
|
||||||
588: 'Idealne do anonimowego, odpornego na cenzurę hostingu i zdalnego dostępu',
|
|
||||||
589: 'Idealne do dostępu lokalnego',
|
|
||||||
590: 'Wymaga połączenia z tą samą siecią lokalną (LAN) co serwer, fizycznie lub przez VPN',
|
|
||||||
591: 'Wymaga ustawienia statycznego adresu IP dla',
|
|
||||||
592: 'Idealne do dostępu VPN przez',
|
|
||||||
593: 'w twojej bramie',
|
|
||||||
594: 'serwer Wireguard twojego routera',
|
|
||||||
595: 'Wymaga przekierowania portów w bramie',
|
|
||||||
596: 'Wymaga rekordu DNS dla',
|
|
||||||
597: 'który rozwiązuje się na',
|
|
||||||
598: 'Niezalecane do dostępu VPN. VPN-y nie obsługują domen „.local” bez zaawansowanej konfiguracji',
|
|
||||||
599: 'Może być używane do dostępu do clearnet',
|
|
||||||
600: 'Niezalecane w większości przypadków. Domeny publiczne są bardziej powszechne i preferowane',
|
|
||||||
601: 'Lokalne',
|
|
||||||
602: 'Może być używane do dostępu lokalnego',
|
|
||||||
603: 'Idealne do publicznego dostępu przez Internet',
|
|
||||||
604: 'Może być używane do osobistego dostępu przez publiczny Internet, ale VPN jest bardziej prywatny i bezpieczny',
|
|
||||||
605: 'gdy używanie adresów IP i portów jest niepożądane',
|
|
||||||
606: 'Host',
|
606: 'Host',
|
||||||
607: 'Wartość',
|
607: 'Wartość',
|
||||||
608: 'Cel',
|
|
||||||
609: 'wszystkie subdomeny',
|
|
||||||
610: 'Dynamiczny DNS',
|
610: 'Dynamiczny DNS',
|
||||||
611: 'Brak interfejsów usług',
|
611: 'Brak interfejsów usług',
|
||||||
612: 'Powód',
|
612: 'Powód',
|
||||||
@@ -697,4 +672,6 @@ export default {
|
|||||||
732: '',
|
732: '',
|
||||||
733: '',
|
733: '',
|
||||||
734: '',
|
734: '',
|
||||||
|
735: '',
|
||||||
|
736: '',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ import { MappedDevice, PortForwardsData } from './utils'
|
|||||||
@if (show80) {
|
@if (show80) {
|
||||||
<label tuiLabel>
|
<label tuiLabel>
|
||||||
<input tuiCheckbox type="checkbox" formControlName="also80" />
|
<input tuiCheckbox type="checkbox" formControlName="also80" />
|
||||||
Also forward port 80 to port 5443? This is needed for HTTP to HTTPS
|
Also forward port 80 to port 443? This is needed for HTTP to HTTPS
|
||||||
redirects (recommended)
|
redirects (recommended)
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ export class PortForwardsAdd {
|
|||||||
|
|
||||||
protected checkShow80() {
|
protected checkShow80() {
|
||||||
const { externalport, internalport } = this.form.getRawValue()
|
const { externalport, internalport } = this.form.getRawValue()
|
||||||
this.show80 = externalport === 443 && internalport === 5443
|
this.show80 = externalport === 443 && internalport === 443
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onSave() {
|
protected async onSave() {
|
||||||
@@ -194,10 +194,10 @@ export class PortForwardsAdd {
|
|||||||
target: `${device!.ip}:${internalport}`,
|
target: `${device!.ip}:${internalport}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (externalport === 443 && internalport === 5443 && also80) {
|
if (externalport === 443 && internalport === 443 && also80) {
|
||||||
await this.api.addForward({
|
await this.api.addForward({
|
||||||
source: `${externalip}:80`,
|
source: `${externalip}:80`,
|
||||||
target: `${device!.ip}:5443`,
|
target: `${device!.ip}:443`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const mockTunnelData: TunnelData = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
portForwards: {
|
portForwards: {
|
||||||
'69.1.1.42:443': '10.59.0.2:5443',
|
'69.1.1.42:443': '10.59.0.2:443',
|
||||||
'69.1.1.42:3000': '10.59.0.2:3000',
|
'69.1.1.42:3000': '10.59.0.2:3000',
|
||||||
},
|
},
|
||||||
gateways: {
|
gateways: {
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
|||||||
selector: 'td[actions]',
|
selector: 'td[actions]',
|
||||||
template: `
|
template: `
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
|
@if (address().deletable) {
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
appearance="flat-grayscale"
|
||||||
|
iconStart="@tui.trash"
|
||||||
|
(click)="deleteDomain()"
|
||||||
|
>
|
||||||
|
{{ 'Delete' | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
tuiIconButton
|
tuiIconButton
|
||||||
appearance="flat-grayscale"
|
appearance="flat-grayscale"
|
||||||
@@ -45,16 +55,6 @@ import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
|||||||
>
|
>
|
||||||
{{ 'Copy URL' | i18n }}
|
{{ 'Copy URL' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@if (address().deletable) {
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
appearance="flat-destructive"
|
|
||||||
iconStart="@tui.trash"
|
|
||||||
(click)="deleteDomain()"
|
|
||||||
>
|
|
||||||
{{ 'Delete' | i18n }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mobile">
|
<div class="mobile">
|
||||||
<button
|
<button
|
||||||
@@ -70,17 +70,14 @@ import { GatewayAddress, MappedServiceInterface } from '../interface.service'
|
|||||||
<button
|
<button
|
||||||
tuiOption
|
tuiOption
|
||||||
new
|
new
|
||||||
[iconStart]="address().enabled ? '@tui.toggle-right' : '@tui.toggle-left'"
|
[iconStart]="
|
||||||
|
address().enabled ? '@tui.toggle-right' : '@tui.toggle-left'
|
||||||
|
"
|
||||||
(click)="toggleEnabled()"
|
(click)="toggleEnabled()"
|
||||||
>
|
>
|
||||||
{{ (address().enabled ? 'Disable' : 'Enable') | i18n }}
|
{{ (address().enabled ? 'Disable' : 'Enable') | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button tuiOption new iconStart="@tui.qr-code" (click)="showQR()">
|
||||||
tuiOption
|
|
||||||
new
|
|
||||||
iconStart="@tui.qr-code"
|
|
||||||
(click)="showQR()"
|
|
||||||
>
|
|
||||||
{{ 'Show QR' | i18n }}
|
{{ 'Show QR' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
ErrorService,
|
ErrorService,
|
||||||
i18nKey,
|
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
@@ -36,7 +35,7 @@ import {
|
|||||||
GatewayAddressGroup,
|
GatewayAddressGroup,
|
||||||
MappedServiceInterface,
|
MappedServiceInterface,
|
||||||
} from '../interface.service'
|
} from '../interface.service'
|
||||||
import { DNS, DnsGateway } from '../public-domains/dns.component'
|
import { DOMAIN_VALIDATION, DnsGateway } from '../public-domains/dns.component'
|
||||||
import { InterfaceAddressItemComponent } from './item.component'
|
import { InterfaceAddressItemComponent } from './item.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -62,7 +61,16 @@ import { InterfaceAddressItemComponent } from './item.component'
|
|||||||
</tui-data-list>
|
</tui-data-list>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<table [appTable]="['Enabled', 'Type', 'Access', 'URL', null]">
|
<table
|
||||||
|
[appTable]="[
|
||||||
|
'Enabled',
|
||||||
|
'Type',
|
||||||
|
'Access',
|
||||||
|
'Certificate Authority',
|
||||||
|
'URL',
|
||||||
|
null,
|
||||||
|
]"
|
||||||
|
>
|
||||||
@for (address of gatewayGroup().addresses; track $index) {
|
@for (address of gatewayGroup().addresses; track $index) {
|
||||||
<tr
|
<tr
|
||||||
[address]="address"
|
[address]="address"
|
||||||
@@ -72,7 +80,7 @@ import { InterfaceAddressItemComponent } from './item.component'
|
|||||||
></tr>
|
></tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="6">
|
||||||
<app-placeholder icon="@tui.list-x">
|
<app-placeholder icon="@tui.list-x">
|
||||||
{{ 'No addresses' | i18n }}
|
{{ 'No addresses' | i18n }}
|
||||||
</app-placeholder>
|
</app-placeholder>
|
||||||
@@ -86,6 +94,12 @@ import { InterfaceAddressItemComponent } from './item.component'
|
|||||||
th:first-child {
|
th:first-child {
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th:nth-child(2),
|
||||||
|
th:nth-child(3),
|
||||||
|
th:nth-child(4) {
|
||||||
|
width: 11rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
@@ -120,6 +134,7 @@ export class InterfaceAddressesComponent {
|
|||||||
async addPrivateDomain() {
|
async addPrivateDomain() {
|
||||||
this.formDialog.open<FormContext<{ fqdn: string }>>(FormComponent, {
|
this.formDialog.open<FormContext<{ fqdn: string }>>(FormComponent, {
|
||||||
label: 'New private domain',
|
label: 'New private domain',
|
||||||
|
size: 's',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(
|
spec: await configBuilderToSpec(
|
||||||
ISB.InputSpec.of({
|
ISB.InputSpec.of({
|
||||||
@@ -173,22 +188,19 @@ export class InterfaceAddressesComponent {
|
|||||||
default: null,
|
default: null,
|
||||||
patterns: [utils.Patterns.domain],
|
patterns: [utils.Patterns.domain],
|
||||||
}).map(f => f.toLocaleLowerCase()),
|
}).map(f => f.toLocaleLowerCase()),
|
||||||
...(iface.addSsl
|
authority: ISB.Value.select({
|
||||||
? {
|
name: this.i18n.transform('Certificate Authority'),
|
||||||
authority: ISB.Value.select({
|
description: this.i18n.transform(
|
||||||
name: this.i18n.transform('Certificate Authority'),
|
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
|
||||||
description: this.i18n.transform(
|
),
|
||||||
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
|
values: authorities,
|
||||||
),
|
default: Object.keys(network.acme)[0] || 'local',
|
||||||
values: authorities,
|
}),
|
||||||
default: '',
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
: ({} as { authority: ReturnType<typeof ISB.Value.select> })),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add public domain',
|
label: 'Add public domain',
|
||||||
|
size: 's',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(addSpec),
|
spec: await configBuilderToSpec(addSpec),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -251,55 +263,33 @@ export class InterfaceAddressesComponent {
|
|||||||
ip = await this.api.osUiAddPublicDomain(params)
|
ip = await this.api.osUiAddPublicDomain(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
const network = await this.patch
|
const [network, portPass] = await Promise.all([
|
||||||
.watch$('serverInfo', 'network')
|
firstValueFrom(this.patch.watch$('serverInfo', 'network')),
|
||||||
.pipe()
|
this.api
|
||||||
.toPromise()
|
.testPortForward({ gateway: gatewayId, port: 443 })
|
||||||
const gateway = network?.gateways[gatewayId]
|
.catch(() => false),
|
||||||
|
])
|
||||||
|
const gateway = network.gateways[gatewayId]
|
||||||
|
|
||||||
if (gateway?.ipInfo) {
|
if (gateway?.ipInfo) {
|
||||||
const wanIp = gateway.ipInfo.wanIp
|
|
||||||
const message = this.i18n.transform(
|
|
||||||
'Create one of the DNS records below.',
|
|
||||||
) as i18nKey
|
|
||||||
const gatewayData = {
|
const gatewayData = {
|
||||||
id: gatewayId,
|
id: gatewayId,
|
||||||
...gateway,
|
...gateway,
|
||||||
ipInfo: gateway.ipInfo,
|
ipInfo: gateway.ipInfo,
|
||||||
}
|
}
|
||||||
|
const dnsPass = ip === gateway.ipInfo.wanIp
|
||||||
|
|
||||||
if (!ip) {
|
setTimeout(
|
||||||
setTimeout(
|
() =>
|
||||||
() =>
|
this.showDomainValidation(
|
||||||
this.showDns(
|
fqdn,
|
||||||
fqdn,
|
gatewayData,
|
||||||
gatewayData,
|
443,
|
||||||
`${this.i18n.transform('No DNS record detected for')} ${fqdn}. ${message}` as i18nKey,
|
dnsPass,
|
||||||
),
|
portPass,
|
||||||
250,
|
),
|
||||||
)
|
250,
|
||||||
} else if (ip !== wanIp) {
|
)
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
this.showDns(
|
|
||||||
fqdn,
|
|
||||||
gatewayData,
|
|
||||||
`${this.i18n.transform('Invalid DNS record')}. ${fqdn} ${this.i18n.transform('resolves to')} ${ip}. ${message}` as i18nKey,
|
|
||||||
),
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
this.dialog
|
|
||||||
.openAlert(
|
|
||||||
`${fqdn} ${this.i18n.transform('resolves to')} ${wanIp}` as i18nKey,
|
|
||||||
{ label: 'DNS record detected!', appearance: 'positive' },
|
|
||||||
)
|
|
||||||
.subscribe(),
|
|
||||||
250,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -311,12 +301,18 @@ export class InterfaceAddressesComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private showDns(fqdn: string, gateway: DnsGateway, message: i18nKey) {
|
private showDomainValidation(
|
||||||
|
fqdn: string,
|
||||||
|
gateway: DnsGateway,
|
||||||
|
port: number,
|
||||||
|
dnsPass: boolean,
|
||||||
|
portPass: boolean,
|
||||||
|
) {
|
||||||
this.dialog
|
this.dialog
|
||||||
.openComponent(DNS, {
|
.openComponent(DOMAIN_VALIDATION, {
|
||||||
label: 'DNS Records',
|
label: 'Domain Setup',
|
||||||
size: 'l',
|
size: 'm',
|
||||||
data: { fqdn, gateway, message },
|
data: { fqdn, gateway, port, dnsPass, portPass },
|
||||||
})
|
})
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiObfuscatePipe } from '@taiga-ui/cdk'
|
import { TuiObfuscatePipe } from '@taiga-ui/cdk'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||||
import { TuiBadge } from '@taiga-ui/kit'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { TuiSwitch } from '@taiga-ui/kit'
|
import { TuiSwitch } from '@taiga-ui/kit'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
@@ -18,6 +17,9 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[address]',
|
selector: 'tr[address]',
|
||||||
|
host: {
|
||||||
|
'[class._disabled]': '!address().enabled',
|
||||||
|
},
|
||||||
template: `
|
template: `
|
||||||
@if (address(); as address) {
|
@if (address(); as address) {
|
||||||
<td>
|
<td>
|
||||||
@@ -26,6 +28,7 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
tuiSwitch
|
tuiSwitch
|
||||||
size="s"
|
size="s"
|
||||||
[showIcons]="false"
|
[showIcons]="false"
|
||||||
|
[disabled]="toggling()"
|
||||||
[ngModel]="address.enabled"
|
[ngModel]="address.enabled"
|
||||||
(ngModelChange)="onToggleEnabled()"
|
(ngModelChange)="onToggleEnabled()"
|
||||||
/>
|
/>
|
||||||
@@ -33,18 +36,16 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
<td>
|
<td>
|
||||||
{{ address.type }}
|
{{ address.type }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="access">
|
||||||
@if (address.access === 'public') {
|
<tui-icon
|
||||||
<tui-badge size="s" appearance="primary-success">
|
[icon]="address.access === 'public' ? '@tui.globe' : '@tui.house'"
|
||||||
{{ 'public' | i18n }}
|
/>
|
||||||
</tui-badge>
|
{{ address.access | i18n }}
|
||||||
} @else {
|
|
||||||
<tui-badge size="s" appearance="primary-destructive">
|
|
||||||
{{ 'private' | i18n }}
|
|
||||||
</tui-badge>
|
|
||||||
}
|
|
||||||
</td>
|
</td>
|
||||||
<td [style.grid-area]="'2 / 1 / 2 / 3'">
|
<td>
|
||||||
|
{{ address.certificate }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<div class="url">
|
<div class="url">
|
||||||
<span
|
<span
|
||||||
[title]="address.masked && currentlyMasked() ? '' : address.url"
|
[title]="address.masked && currentlyMasked() ? '' : address.url"
|
||||||
@@ -79,6 +80,11 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
grid-template-columns: fit-content(10rem) 1fr 2rem 2rem;
|
grid-template-columns: fit-content(10rem) 1fr 2rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.access tui-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.url {
|
.url {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -95,6 +101,23 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
|
padding-inline-start: 0.75rem !important;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0.25rem;
|
||||||
|
bottom: 0.25rem;
|
||||||
|
width: 4px;
|
||||||
|
background: var(--tui-status-positive);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&._disabled::before {
|
||||||
|
background: var(--tui-background-neutral-1-hover);
|
||||||
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@@ -110,13 +133,27 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
color: var(--tui-text-primary);
|
color: var(--tui-text-primary);
|
||||||
padding-inline-end: 0.5rem;
|
padding-inline-end: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td:nth-child(4) {
|
||||||
|
grid-area: 2 / 1 / 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(5) {
|
||||||
|
grid-area: 3 / 1 / 3 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child {
|
||||||
|
grid-area: 1 / 3 / 4 / 5;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
imports: [
|
imports: [
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
AddressActionsComponent,
|
AddressActionsComponent,
|
||||||
TuiBadge,
|
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiIcon,
|
||||||
TuiObfuscatePipe,
|
TuiObfuscatePipe,
|
||||||
TuiSwitch,
|
TuiSwitch,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -125,14 +162,15 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
})
|
})
|
||||||
export class InterfaceAddressItemComponent {
|
export class InterfaceAddressItemComponent {
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
|
||||||
readonly address = input.required<GatewayAddress>()
|
readonly address = input.required<GatewayAddress>()
|
||||||
readonly packageId = input('')
|
readonly packageId = input('')
|
||||||
readonly value = input<MappedServiceInterface | undefined>()
|
readonly value = input<MappedServiceInterface | undefined>()
|
||||||
readonly isRunning = input.required<boolean>()
|
readonly isRunning = input.required<boolean>()
|
||||||
|
|
||||||
|
readonly toggling = signal(false)
|
||||||
readonly currentlyMasked = signal(true)
|
readonly currentlyMasked = signal(true)
|
||||||
readonly recipe = computed(() =>
|
readonly recipe = computed(() =>
|
||||||
this.address()?.masked && this.currentlyMasked() ? 'mask' : 'none',
|
this.address()?.masked && this.currentlyMasked() ? 'mask' : 'none',
|
||||||
@@ -143,6 +181,7 @@ export class InterfaceAddressItemComponent {
|
|||||||
const iface = this.value()
|
const iface = this.value()
|
||||||
if (!iface) return
|
if (!iface) return
|
||||||
|
|
||||||
|
this.toggling.set(true)
|
||||||
const enabled = !addr.enabled
|
const enabled = !addr.enabled
|
||||||
const addressJson = JSON.stringify(addr.hostnameInfo)
|
const addressJson = JSON.stringify(addr.hostnameInfo)
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
@@ -167,6 +206,7 @@ export class InterfaceAddressItemComponent {
|
|||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
loader.unsubscribe()
|
loader.unsubscribe()
|
||||||
|
this.toggling.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { inject, Injectable } from '@angular/core'
|
|||||||
import { T, utils } from '@start9labs/start-sdk'
|
import { T, utils } from '@start9labs/start-sdk'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||||
|
import { toAuthorityName } from 'src/app/utils/acme'
|
||||||
|
|
||||||
function isPublicIp(h: T.HostnameInfo): boolean {
|
function isPublicIp(h: T.HostnameInfo): boolean {
|
||||||
return (
|
return h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6')
|
||||||
h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEnabled(addr: T.DerivedAddressInfo, h: T.HostnameInfo): boolean {
|
function isEnabled(addr: T.DerivedAddressInfo, h: T.HostnameInfo): boolean {
|
||||||
@@ -38,6 +37,33 @@ function getGatewayIds(h: T.HostnameInfo): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCertificate(
|
||||||
|
h: T.HostnameInfo,
|
||||||
|
host: T.Host,
|
||||||
|
addSsl: T.AddSslOptions | null,
|
||||||
|
secure: T.Security | null,
|
||||||
|
): string {
|
||||||
|
if (!h.ssl) return '-'
|
||||||
|
|
||||||
|
if (h.metadata.kind === 'public-domain') {
|
||||||
|
const config = host.publicDomains[h.host]
|
||||||
|
return config ? toAuthorityName(config.acme) : toAuthorityName(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addSsl) return toAuthorityName(null)
|
||||||
|
if (secure?.ssl) return 'Self signed'
|
||||||
|
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortDomainsFirst(a: GatewayAddress, b: GatewayAddress): number {
|
||||||
|
const isDomain = (addr: GatewayAddress) =>
|
||||||
|
addr.hostnameInfo.metadata.kind === 'public-domain' ||
|
||||||
|
(addr.hostnameInfo.metadata.kind === 'private-domain' &&
|
||||||
|
!addr.hostnameInfo.host.endsWith('.local'))
|
||||||
|
return Number(isDomain(b)) - Number(isDomain(a))
|
||||||
|
}
|
||||||
|
|
||||||
function getAddressType(h: T.HostnameInfo): string {
|
function getAddressType(h: T.HostnameInfo): string {
|
||||||
switch (h.metadata.kind) {
|
switch (h.metadata.kind) {
|
||||||
case 'ipv4':
|
case 'ipv4':
|
||||||
@@ -66,46 +92,40 @@ export class InterfaceService {
|
|||||||
host: T.Host,
|
host: T.Host,
|
||||||
gateways: GatewayPlus[],
|
gateways: GatewayPlus[],
|
||||||
): GatewayAddressGroup[] {
|
): GatewayAddressGroup[] {
|
||||||
const binding =
|
const binding = host.bindings[serviceInterface.addressInfo.internalPort]
|
||||||
host.bindings[serviceInterface.addressInfo.internalPort]
|
|
||||||
if (!binding) return []
|
if (!binding) return []
|
||||||
|
|
||||||
const addr = binding.addresses
|
const addr = binding.addresses
|
||||||
const masked = serviceInterface.masked
|
const masked = serviceInterface.masked
|
||||||
const ui = serviceInterface.type === 'ui'
|
const ui = serviceInterface.type === 'ui'
|
||||||
|
const { addSsl, secure } = binding.options
|
||||||
|
|
||||||
const groupMap = new Map<string, GatewayAddress[]>()
|
const groupMap = new Map<string, GatewayAddress[]>()
|
||||||
|
const gatewayMap = new Map<string, GatewayPlus>()
|
||||||
|
|
||||||
for (const gateway of gateways) {
|
for (const gateway of gateways) {
|
||||||
groupMap.set(gateway.id, [])
|
groupMap.set(gateway.id, [])
|
||||||
|
gatewayMap.set(gateway.id, gateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const h of addr.available) {
|
for (const h of addr.available) {
|
||||||
const enabled = isEnabled(addr, h)
|
|
||||||
const url = utils.addressHostToUrl(serviceInterface.addressInfo, h)
|
|
||||||
const type = getAddressType(h)
|
|
||||||
const isDomain =
|
|
||||||
h.metadata.kind === 'private-domain' ||
|
|
||||||
h.metadata.kind === 'public-domain'
|
|
||||||
const isMdns = h.metadata.kind === 'mdns'
|
|
||||||
|
|
||||||
const address: GatewayAddress = {
|
|
||||||
enabled,
|
|
||||||
type,
|
|
||||||
access: h.public ? 'public' : 'private',
|
|
||||||
url,
|
|
||||||
hostnameInfo: h,
|
|
||||||
masked,
|
|
||||||
ui,
|
|
||||||
deletable: isDomain && !isMdns,
|
|
||||||
}
|
|
||||||
|
|
||||||
const gatewayIds = getGatewayIds(h)
|
const gatewayIds = getGatewayIds(h)
|
||||||
for (const gid of gatewayIds) {
|
for (const gid of gatewayIds) {
|
||||||
const list = groupMap.get(gid)
|
const list = groupMap.get(gid)
|
||||||
if (list) {
|
if (!list) continue
|
||||||
list.push(address)
|
list.push({
|
||||||
}
|
enabled: isEnabled(addr, h),
|
||||||
|
type: getAddressType(h),
|
||||||
|
access: h.public ? 'public' : 'private',
|
||||||
|
url: utils.addressHostToUrl(serviceInterface.addressInfo, h),
|
||||||
|
hostnameInfo: h,
|
||||||
|
masked,
|
||||||
|
ui,
|
||||||
|
deletable:
|
||||||
|
h.metadata.kind === 'private-domain' ||
|
||||||
|
h.metadata.kind === 'public-domain',
|
||||||
|
certificate: getCertificate(h, host, addSsl, secure),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +134,7 @@ export class InterfaceService {
|
|||||||
.map(g => ({
|
.map(g => ({
|
||||||
gatewayId: g.id,
|
gatewayId: g.id,
|
||||||
gatewayName: g.name,
|
gatewayName: g.name,
|
||||||
addresses: groupMap.get(g.id)!,
|
addresses: groupMap.get(g.id)!.sort(sortDomainsFirst),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +142,7 @@ export class InterfaceService {
|
|||||||
serviceInterface: T.ServiceInterface,
|
serviceInterface: T.ServiceInterface,
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
): PluginAddressGroup[] {
|
): PluginAddressGroup[] {
|
||||||
const binding =
|
const binding = host.bindings[serviceInterface.addressInfo.internalPort]
|
||||||
host.bindings[serviceInterface.addressInfo.internalPort]
|
|
||||||
if (!binding) return []
|
if (!binding) return []
|
||||||
|
|
||||||
const addr = binding.addresses
|
const addr = binding.addresses
|
||||||
@@ -224,6 +243,7 @@ export type GatewayAddress = {
|
|||||||
masked: boolean
|
masked: boolean
|
||||||
ui: boolean
|
ui: boolean
|
||||||
deletable: boolean
|
deletable: boolean
|
||||||
|
certificate: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GatewayAddressGroup = {
|
export type GatewayAddressGroup = {
|
||||||
|
|||||||
@@ -24,47 +24,115 @@ export type DnsGateway = T.NetworkInterfaceInfo & {
|
|||||||
ipInfo: T.IpInfo
|
ipInfo: T.IpInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
export type DomainValidationData = {
|
||||||
selector: 'dns',
|
fqdn: string
|
||||||
template: `
|
gateway: DnsGateway
|
||||||
<p>{{ context.data.message }}</p>
|
port: number
|
||||||
|
dnsPass: boolean
|
||||||
|
portPass: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'domain-validation',
|
||||||
|
template: `
|
||||||
@let wanIp = context.data.gateway.ipInfo.wanIp || ('Error' | i18n);
|
@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>
|
||||||
|
<p>
|
||||||
|
{{ 'In your domain registrar for' | i18n }} {{ domain }},
|
||||||
|
{{ 'create this DNS record' | i18n }}
|
||||||
|
</p>
|
||||||
|
|
||||||
@if (context.data.gateway.ipInfo.deviceType !== 'wireguard') {
|
@if (context.data.gateway.ipInfo.deviceType !== 'wireguard') {
|
||||||
<label>
|
<label>
|
||||||
IP
|
IP
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
appearance="flat"
|
||||||
tuiSwitch
|
tuiSwitch
|
||||||
[(ngModel)]="ddns"
|
[(ngModel)]="ddns"
|
||||||
(ngModelChange)="pass.set(undefined)"
|
(ngModelChange)="dnsPass.set(undefined)"
|
||||||
/>
|
/>
|
||||||
{{ 'Dynamic DNS' | i18n }}
|
{{ 'Dynamic DNS' | i18n }}
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
|
|
||||||
<table [appTable]="['Type', 'Host', 'Value', 'Purpose']">
|
<table [appTable]="[null, 'Type', 'Host', 'Value', null]">
|
||||||
@for (row of rows(); track $index) {
|
<tr>
|
||||||
<tr>
|
<td class="status">
|
||||||
<td>
|
@if (dnsPass() === true) {
|
||||||
@if (pass() === true) {
|
<tui-icon class="g-positive" icon="@tui.check" />
|
||||||
<tui-icon class="g-positive" icon="@tui.check" />
|
} @else if (dnsPass() === false) {
|
||||||
} @else if (pass() === false) {
|
<tui-icon class="g-negative" icon="@tui.x" />
|
||||||
<tui-icon class="g-negative" icon="@tui.x" />
|
}
|
||||||
}
|
</td>
|
||||||
{{ ddns ? 'ALIAS' : 'A' }}
|
<td>{{ ddns ? 'ALIAS' : 'A' }}</td>
|
||||||
</td>
|
<td>*</td>
|
||||||
<td>{{ row.host }}</td>
|
<td>{{ ddns ? '[DDNS Address]' : wanIp }}</td>
|
||||||
<td>{{ ddns ? '[DDNS Address]' : wanIp }}</td>
|
<td>
|
||||||
<td>{{ row.purpose }}</td>
|
<button
|
||||||
</tr>
|
tuiButton
|
||||||
}
|
size="s"
|
||||||
|
[loading]="dnsLoading()"
|
||||||
|
(click)="testDns()"
|
||||||
|
>
|
||||||
|
{{ 'Test' | i18n }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>{{ 'Port Forwarding' | i18n }}</h3>
|
||||||
|
<p>
|
||||||
|
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
||||||
|
{{ 'create this port forwarding rule' | i18n }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table
|
||||||
|
[appTable]="[null, 'External Port', 'Internal IP', 'Internal Port', null]"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td class="status">
|
||||||
|
@if (portPass() === true) {
|
||||||
|
<tui-icon class="g-positive" icon="@tui.check" />
|
||||||
|
} @else if (portPass() === false) {
|
||||||
|
<tui-icon class="g-negative" icon="@tui.x" />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>{{ context.data.port }}</td>
|
||||||
|
<td>{{ internalIp }}</td>
|
||||||
|
<td>{{ context.data.port }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
size="s"
|
||||||
|
[loading]="portLoading()"
|
||||||
|
(click)="testPort()"
|
||||||
|
>
|
||||||
|
{{ 'Test' | i18n }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<footer class="g-buttons">
|
<footer class="g-buttons">
|
||||||
<button tuiButton [loading]="loading()" (click)="testDns()">
|
<button
|
||||||
{{ 'Test' | i18n }}
|
tuiButton
|
||||||
|
appearance="flat"
|
||||||
|
[disabled]="allPass()"
|
||||||
|
(click)="context.completeWith()"
|
||||||
|
>
|
||||||
|
{{ 'Later' | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
[disabled]="!allPass()"
|
||||||
|
(click)="context.completeWith()"
|
||||||
|
>
|
||||||
|
{{ 'Done' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
`,
|
`,
|
||||||
@@ -76,14 +144,57 @@ export type DnsGateway = T.NetworkInterfaceInfo & {
|
|||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 1.5rem 0 0.5rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tui-icon {
|
tui-icon {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
providers: [
|
providers: [
|
||||||
tuiSwitchOptionsProvider({
|
tuiSwitchOptionsProvider({
|
||||||
appearance: () => 'primary',
|
appearance: () => 'glass',
|
||||||
icon: () => '',
|
icon: () => '',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -98,75 +209,61 @@ export type DnsGateway = T.NetworkInterfaceInfo & {
|
|||||||
TuiIcon,
|
TuiIcon,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DnsComponent {
|
export class DomainValidationComponent {
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
|
||||||
|
|
||||||
readonly ddns = false
|
readonly ddns = false
|
||||||
|
|
||||||
readonly context =
|
readonly context =
|
||||||
injectContext<
|
injectContext<TuiDialogContext<void, DomainValidationData>>()
|
||||||
TuiDialogContext<
|
|
||||||
void,
|
|
||||||
{ fqdn: string; gateway: DnsGateway; message: string }
|
|
||||||
>
|
|
||||||
>()
|
|
||||||
|
|
||||||
readonly loading = signal(false)
|
readonly domain =
|
||||||
readonly pass = signal<boolean | undefined>(undefined)
|
parse(this.context.data.fqdn).domain || this.context.data.fqdn
|
||||||
|
|
||||||
readonly rows = computed<{ host: string; purpose: string }[]>(() => {
|
readonly dnsLoading = signal(false)
|
||||||
const { domain, subdomain } = parse(this.context.data.fqdn)
|
readonly portLoading = signal(false)
|
||||||
|
readonly dnsPass = signal<boolean | undefined>(this.context.data.dnsPass)
|
||||||
|
readonly portPass = signal<boolean | undefined>(this.context.data.portPass)
|
||||||
|
|
||||||
if (!subdomain) {
|
readonly allPass = computed(
|
||||||
return [
|
() => this.dnsPass() === true && this.portPass() === true,
|
||||||
{
|
)
|
||||||
host: '@',
|
|
||||||
purpose: domain!,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = subdomain.split('.').slice(1)
|
|
||||||
|
|
||||||
const subdomains = this.i18n.transform('all subdomains of')
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
host: subdomain,
|
|
||||||
purpose: `only ${subdomain}`,
|
|
||||||
},
|
|
||||||
...segments.map((_, i) => {
|
|
||||||
const parent = segments.slice(i).join('.')
|
|
||||||
return {
|
|
||||||
host: `*.${parent}`,
|
|
||||||
purpose: `${subdomains} ${parent}`,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
host: '*',
|
|
||||||
purpose: `${subdomains} ${domain}`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
async testDns() {
|
async testDns() {
|
||||||
this.pass.set(undefined)
|
this.dnsLoading.set(true)
|
||||||
this.loading.set(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ip = await this.api.queryDns({
|
const ip = await this.api.queryDns({
|
||||||
fqdn: this.context.data.fqdn,
|
fqdn: this.context.data.fqdn,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pass.set(ip === this.context.data.gateway.ipInfo.wanIp)
|
this.dnsPass.set(ip === this.context.data.gateway.ipInfo.wanIp)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
this.loading.set(false)
|
this.dnsLoading.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testPort() {
|
||||||
|
this.portLoading.set(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.api.testPortForward({
|
||||||
|
gateway: this.context.data.gateway.id,
|
||||||
|
port: this.context.data.port,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.portPass.set(result)
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
this.portLoading.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DNS = new PolymorpheusComponent(DnsComponent)
|
export const DOMAIN_VALIDATION = new PolymorpheusComponent(
|
||||||
|
DomainValidationComponent,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { TuiBadge } from '@taiga-ui/kit'
|
import { TuiBadge } from '@taiga-ui/kit'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -7,21 +9,26 @@ import { TuiBadge } from '@taiga-ui/kit'
|
|||||||
template: `
|
template: `
|
||||||
<td><ng-content /></td>
|
<td><ng-content /></td>
|
||||||
<td>
|
<td>
|
||||||
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
<tui-badge size="m" [appearance]="appearance">
|
||||||
|
{{ info().type }}
|
||||||
|
</tui-badge>
|
||||||
</td>
|
</td>
|
||||||
<td class="g-secondary" [style.grid-area]="'2 / 1 / 2 / 3'">
|
<td class="g-secondary" [style.grid-area]="'2 / 1 / 2 / 3'">
|
||||||
{{ info.description }}
|
{{ info().description }}
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<a
|
||||||
|
tuiIconButton
|
||||||
|
appearance="flat-grayscale"
|
||||||
|
iconStart="@tui.settings"
|
||||||
|
size="s"
|
||||||
|
[routerLink]="link()"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
:host {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--tui-background-neutral-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
td:first-child {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -35,9 +42,13 @@ import { TuiBadge } from '@taiga-ui/kit'
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
:host-context(tui-root._mobile) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: min-content;
|
grid-template-columns: 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem 0.5rem;
|
padding: 1rem 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -45,17 +56,21 @@ import { TuiBadge } from '@taiga-ui/kit'
|
|||||||
td {
|
td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
grid-area: 1 / 2 / 3 / 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiBadge],
|
imports: [TuiBadge, TuiButton, RouterLink],
|
||||||
})
|
})
|
||||||
export class ServiceInterfaceItemComponent {
|
export class ServiceInterfaceItemComponent {
|
||||||
@Input({ required: true })
|
readonly info = input.required<T.ServiceInterface>()
|
||||||
info!: T.ServiceInterface
|
readonly link = input.required<string>()
|
||||||
|
|
||||||
get appearance(): string {
|
get appearance(): string {
|
||||||
switch (this.info.type) {
|
switch (this.info().type) {
|
||||||
case 'ui':
|
case 'ui':
|
||||||
return 'positive'
|
return 'positive'
|
||||||
case 'api':
|
case 'api':
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
input,
|
input,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { RouterLink } from '@angular/router'
|
|
||||||
import { TuiTable } from '@taiga-ui/addon-table'
|
import { TuiTable } from '@taiga-ui/addon-table'
|
||||||
import { tuiDefaultSort } from '@taiga-ui/cdk'
|
import { tuiDefaultSort } from '@taiga-ui/cdk'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
@@ -22,19 +21,13 @@ import { PlaceholderComponent } from '../../../components/placeholder.component'
|
|||||||
<th tuiTh>{{ 'Name' | i18n }}</th>
|
<th tuiTh>{{ 'Name' | i18n }}</th>
|
||||||
<th tuiTh>{{ 'Type' | i18n }}</th>
|
<th tuiTh>{{ 'Type' | i18n }}</th>
|
||||||
<th tuiTh>{{ 'Description' | i18n }}</th>
|
<th tuiTh>{{ 'Description' | i18n }}</th>
|
||||||
|
<th tuiTh></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (info of interfaces(); track $index) {
|
@for (info of interfaces(); track $index) {
|
||||||
<tr
|
<tr serviceInterface [info]="info" [link]="info.routerLink">
|
||||||
tabindex="-1"
|
<strong>{{ info.name }}</strong>
|
||||||
serviceInterface
|
|
||||||
[info]="info"
|
|
||||||
[routerLink]="info.routerLink"
|
|
||||||
>
|
|
||||||
<a [routerLink]="info.routerLink">
|
|
||||||
<strong>{{ info.name }}</strong>
|
|
||||||
</a>
|
|
||||||
</tr>
|
</tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<app-placeholder icon="@tui.monitor-x">
|
<app-placeholder icon="@tui.monitor-x">
|
||||||
@@ -56,7 +49,6 @@ import { PlaceholderComponent } from '../../../components/placeholder.component'
|
|||||||
TuiTable,
|
TuiTable,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
PlaceholderComponent,
|
PlaceholderComponent,
|
||||||
RouterLink,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ServiceInterfacesComponent {
|
export class ServiceInterfacesComponent {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AuthorityService } from './authority.service'
|
|||||||
selector: 'authorities-table',
|
selector: 'authorities-table',
|
||||||
template: `
|
template: `
|
||||||
<table [appTable]="['Provider', 'URL', 'Contact', null]">
|
<table [appTable]="['Provider', 'URL', 'Contact', null]">
|
||||||
<tr [authority]="{ name: 'Local Root CA' }"></tr>
|
<tr [authority]="{ name: 'Root CA' }"></tr>
|
||||||
@for (authority of authorityService.authorities(); track $index) {
|
@for (authority of authorityService.authorities(); track $index) {
|
||||||
<tr [authority]="authority"></tr>
|
<tr [authority]="authority"></tr>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ export namespace RR {
|
|||||||
} // net.dns.query
|
} // net.dns.query
|
||||||
export type QueryDnsRes = string | null
|
export type QueryDnsRes = string | null
|
||||||
|
|
||||||
|
export type TestPortForwardReq = {
|
||||||
|
gateway: string
|
||||||
|
port: number
|
||||||
|
} // net.port-forward.test
|
||||||
|
export type TestPortForwardRes = boolean
|
||||||
|
|
||||||
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
||||||
export type SetKeyboardRes = null
|
export type SetKeyboardRes = null
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
|
||||||
import { T } from '@start9labs/start-sdk'
|
|
||||||
import { RR } from './api.types'
|
import { RR } from './api.types'
|
||||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||||
|
|
||||||
@@ -119,6 +117,10 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
||||||
|
|
||||||
|
abstract testPortForward(
|
||||||
|
params: RR.TestPortForwardReq,
|
||||||
|
): Promise<RR.TestPortForwardRes>
|
||||||
|
|
||||||
// smtp
|
// smtp
|
||||||
|
|
||||||
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DOCUMENT, Inject, Injectable } from '@angular/core'
|
import { DOCUMENT, Inject, Injectable } from '@angular/core'
|
||||||
import { blake3 } from '@noble/hashes/blake3'
|
import { blake3 } from '@noble/hashes/blake3'
|
||||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
|
||||||
import {
|
import {
|
||||||
HttpOptions,
|
HttpOptions,
|
||||||
HttpService,
|
HttpService,
|
||||||
@@ -8,7 +7,6 @@ import {
|
|||||||
RpcError,
|
RpcError,
|
||||||
RPCOptions,
|
RPCOptions,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { T } from '@start9labs/start-sdk'
|
|
||||||
import { Dump, pathFromArray } from 'patch-db-client'
|
import { Dump, pathFromArray } from 'patch-db-client'
|
||||||
import { filter, firstValueFrom, Observable } from 'rxjs'
|
import { filter, firstValueFrom, Observable } from 'rxjs'
|
||||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
|
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
|
||||||
@@ -268,6 +266,15 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testPortForward(
|
||||||
|
params: RR.TestPortForwardReq,
|
||||||
|
): Promise<RR.TestPortForwardRes> {
|
||||||
|
return this.rpcRequest({
|
||||||
|
method: 'net.port-forward.test',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
|
|
||||||
async checkOSUpdate(
|
async checkOSUpdate(
|
||||||
@@ -358,7 +365,10 @@ export class LiveApiService extends ApiService {
|
|||||||
async setDefaultOutbound(
|
async setDefaultOutbound(
|
||||||
params: RR.SetDefaultOutboundReq,
|
params: RR.SetDefaultOutboundReq,
|
||||||
): Promise<RR.SetDefaultOutboundRes> {
|
): Promise<RR.SetDefaultOutboundRes> {
|
||||||
return this.rpcRequest({ method: 'net.gateway.set-default-outbound', params })
|
return this.rpcRequest({
|
||||||
|
method: 'net.gateway.set-default-outbound',
|
||||||
|
params,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async setServiceOutbound(
|
async setServiceOutbound(
|
||||||
|
|||||||
@@ -483,6 +483,14 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async testPortForward(
|
||||||
|
params: RR.TestPortForwardReq,
|
||||||
|
): Promise<RR.TestPortForwardRes> {
|
||||||
|
await pauseFor(2000)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// marketplace URLs
|
// marketplace URLs
|
||||||
|
|
||||||
async checkOSUpdate(
|
async checkOSUpdate(
|
||||||
@@ -589,7 +597,7 @@ export class MockApiService extends ApiService {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (params.setAsDefaultOutbound) {
|
if (params.setAsDefaultOutbound) {
|
||||||
;(patch as any[]).push({
|
(patch as any[]).push({
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: '/serverInfo/network/defaultOutbound',
|
path: '/serverInfo/network/defaultOutbound',
|
||||||
value: id,
|
value: id,
|
||||||
@@ -1394,7 +1402,9 @@ export class MockApiService extends ApiService {
|
|||||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
const basePath = `/serverInfo/network/host/bindings/${params.internalPort}/addresses`
|
||||||
|
this.mockSetAddressEnabled(basePath, params.address, params.enabled)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1493,7 +1503,9 @@ export class MockApiService extends ApiService {
|
|||||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
const basePath = `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/addresses`
|
||||||
|
this.mockSetAddressEnabled(basePath, params.address, params.enabled)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1813,6 +1825,63 @@ export class MockApiService extends ApiService {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mockSetAddressEnabled(
|
||||||
|
basePath: string,
|
||||||
|
addressJson: string,
|
||||||
|
enabled: boolean | null,
|
||||||
|
): void {
|
||||||
|
const h: T.HostnameInfo = JSON.parse(addressJson)
|
||||||
|
const isPublicIp =
|
||||||
|
h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6')
|
||||||
|
|
||||||
|
const current = this.mockData(basePath) as T.DerivedAddressInfo
|
||||||
|
|
||||||
|
if (isPublicIp) {
|
||||||
|
if (h.port === null) return
|
||||||
|
const sa =
|
||||||
|
h.metadata.kind === 'ipv6'
|
||||||
|
? `[${h.host}]:${h.port}`
|
||||||
|
: `${h.host}:${h.port}`
|
||||||
|
|
||||||
|
const arr = [...current.enabled]
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
if (!arr.includes(sa)) arr.push(sa)
|
||||||
|
} else {
|
||||||
|
const idx = arr.indexOf(sa)
|
||||||
|
if (idx >= 0) arr.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
current.enabled = arr
|
||||||
|
this.mockRevision([
|
||||||
|
{ op: PatchOp.REPLACE, path: `${basePath}/enabled`, value: arr },
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
const port = h.port ?? 0
|
||||||
|
const arr = current.disabled.filter(
|
||||||
|
([dHost, dPort]) => !(dHost === h.host && dPort === port),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
arr.push([h.host, port])
|
||||||
|
}
|
||||||
|
|
||||||
|
current.disabled = arr
|
||||||
|
this.mockRevision([
|
||||||
|
{ op: PatchOp.REPLACE, path: `${basePath}/disabled`, value: arr },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mockData(path: string): any {
|
||||||
|
const parts = path.split('/').filter(Boolean)
|
||||||
|
let obj: any = mockPatchData
|
||||||
|
for (const part of parts) {
|
||||||
|
obj = obj[part]
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
private async mockRevision<T>(patch: Operation<T>[]): Promise<void> {
|
private async mockRevision<T>(patch: Operation<T>[]): Promise<void> {
|
||||||
const revision = {
|
const revision = {
|
||||||
id: ++this.sequence,
|
id: ++this.sequence,
|
||||||
|
|||||||
@@ -588,9 +588,13 @@ export const mockPatchData: DataModel = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
addSsl: null,
|
|
||||||
preferredExternalPort: 443,
|
preferredExternalPort: 443,
|
||||||
secure: { ssl: true },
|
addSsl: {
|
||||||
|
preferredExternalPort: 443,
|
||||||
|
alpn: { specified: ['http/1.1', 'h2'] },
|
||||||
|
addXForwardedHeaders: false,
|
||||||
|
},
|
||||||
|
secure: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export function toAuthorityName(
|
export function toAuthorityName(
|
||||||
url: string | null,
|
url: string | null,
|
||||||
addSsl = true,
|
addSsl = true,
|
||||||
): string | 'Local Root CA' | '-' {
|
): string | 'Root CA' | '-' {
|
||||||
if (url) {
|
if (url) {
|
||||||
return knownAuthorities.find(ca => ca.url === url)?.name || url
|
return knownAuthorities.find(ca => ca.url === url)?.name || url
|
||||||
} else {
|
} else {
|
||||||
return addSsl ? 'Local Root CA' : '-'
|
return addSsl ? 'Root CA' : '-'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user