mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
more translations
This commit is contained in:
@@ -524,4 +524,55 @@ export default {
|
||||
557: 'Keine privaten Domains',
|
||||
558: 'Neue private Domain',
|
||||
559: 'DNS-Server',
|
||||
560: 'Geben Sie einen vollständig qualifizierten Domainnamen ein. Da die Domain für private Zwecke verwendet wird, kann es jede gewünschte Domain sein, auch eine, die Sie nicht kontrollieren.',
|
||||
561: 'Geben Sie einen vollständig qualifizierten Domainnamen ein. Wenn Sie beispielsweise domain.com kontrollieren, könnten Sie domain.com oder subdomain.domain.com oder another.subdomain.domain.com eingeben.',
|
||||
562: 'DNS-Einträge',
|
||||
563: 'Erstellen Sie einen der unten aufgeführten DNS-Einträge.',
|
||||
564: 'Kein DNS-Eintrag erkannt für',
|
||||
565: 'Ungültiger DNS-Eintrag',
|
||||
566: 'löst auf in',
|
||||
567: 'DNS-Eintrag erkannt!',
|
||||
568: 'Wählen Sie ein Gateway für diese Domain aus.',
|
||||
569: 'Wählen Sie eine Zertifizierungsstelle aus, um SSL/TLS-Zertifikate für diese Domain auszustellen.',
|
||||
570: 'Andere',
|
||||
571: 'Ein Name zur einfachen Identifizierung des Gateways',
|
||||
572: 'Wählen Sie diese Option, wenn das Gateway für den privaten Zugriff nur für autorisierte Clients konfiguriert ist. StartTunnel ist ein privates Gateway.',
|
||||
573: 'Wählen Sie diese Option, wenn das Gateway für uneingeschränkten öffentlichen Zugriff konfiguriert ist.',
|
||||
574: 'Datei',
|
||||
575: 'Wireguard-Konfigurationsdatei',
|
||||
576: 'Kopieren/Einfügen',
|
||||
577: 'Dateiinhalt',
|
||||
578: 'Öffentlicher Schlüssel',
|
||||
579: 'muss ein gültiger SSH-Öffentlicher Schlüssel sein',
|
||||
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.',
|
||||
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: 'Nur nützlich für Clients, die HTTPS 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. Clearnet-Domains 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. VPN ist privater und sicherer',
|
||||
605: 'wenn die Verwendung von IP-Adressen und Ports unerwünscht ist',
|
||||
606: 'Host',
|
||||
607: 'Wert',
|
||||
608: 'Zweck',
|
||||
609: 'Subdomains von',
|
||||
610: 'Dynamisches DNS',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -523,4 +523,55 @@ export const ENGLISH = {
|
||||
'No private domains': 557,
|
||||
'New private domain': 558,
|
||||
'DNS Servers': 559,
|
||||
'Enter a fully qualified domain name. Since the domain is for private use, it can be any domain you want, even one you do not control.': 560,
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.': 561,
|
||||
'DNS Records': 562,
|
||||
'Create one of the DNS records below.': 563,
|
||||
'No DNS record detected for': 564, // this is a partial sentence. A domain name will be added after "for" to complete the sentence.
|
||||
'Invalid DNS record': 565,
|
||||
'resolves to': 566, // as in "domain.com 'resolves to' [IP address]"
|
||||
'DNS record detected!': 567,
|
||||
'Select a gateway to use for this domain.': 568,
|
||||
'Select a Certificate Authority to issue SSL/TLS certificates for this domain': 569,
|
||||
'Other': 570, // as in, a list option to indicate none of the options listed
|
||||
'A name to easily identify the gateway': 571,
|
||||
'select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.': 572,
|
||||
'select this option if the gateway is configured for unfettered public access.': 573,
|
||||
'File': 574, // as in, a computer file
|
||||
'Wireguard Config File': 575,
|
||||
'Copy/Paste': 576,
|
||||
'File Contents': 577,
|
||||
'Public Key': 578, // as in, a cryptographic public key
|
||||
'must be a valid SSH public key': 579,
|
||||
'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. 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,
|
||||
'Only useful for clients that enforce HTTPS': 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. Clearnet domains are 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. 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
|
||||
'Value': 607, // as in, the value in a column of a table
|
||||
'Purpose': 608, // as in, the reason for a thing to exist
|
||||
'subdomains of': 609, // this is a partial sentence. A domain name will be added after "of" to complete the sentence.
|
||||
'Dynamic DNS': 610,
|
||||
} as const
|
||||
|
||||
@@ -524,4 +524,55 @@ export default {
|
||||
557: 'Sin dominios privados',
|
||||
558: 'Nuevo dominio privado',
|
||||
559: 'Servidores DNS',
|
||||
560: 'Introduce un nombre de dominio completo. Dado que el dominio es para uso privado, puede ser cualquier dominio que desees, incluso uno que no controles.',
|
||||
561: 'Introduce un nombre de dominio completo. Por ejemplo, si controlas domain.com, podrías introducir domain.com o subdomain.domain.com o another.subdomain.domain.com.',
|
||||
562: 'Registros DNS',
|
||||
563: 'Crea uno de los registros DNS a continuación.',
|
||||
564: 'No se detectó ningún registro DNS para',
|
||||
565: 'Registro DNS inválido',
|
||||
566: 'se resuelve en',
|
||||
567: '¡Registro DNS detectado!',
|
||||
568: 'Selecciona una puerta de enlace para usar con este dominio.',
|
||||
569: 'Selecciona una Autoridad Certificadora para emitir certificados SSL/TLS para este dominio.',
|
||||
570: 'Otro',
|
||||
571: 'Un nombre para identificar fácilmente la puerta de enlace',
|
||||
572: 'Selecciona esta opción si la puerta de enlace está configurada para acceso privado solo a clientes autorizados. StartTunnel es una puerta de enlace privada.',
|
||||
573: 'Selecciona esta opción si la puerta de enlace está configurada para acceso público sin restricciones.',
|
||||
574: 'Archivo',
|
||||
575: 'Archivo de configuración de Wireguard',
|
||||
576: 'Copiar/Pegar',
|
||||
577: 'Contenido del archivo',
|
||||
578: 'Clave pública',
|
||||
579: 'debe ser una clave pública SSH válida',
|
||||
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.',
|
||||
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 útil para clientes que imponen HTTPS',
|
||||
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. Se prefieren los dominios de clearnet',
|
||||
601: 'Local',
|
||||
602: 'Se puede usar para acceso local',
|
||||
603: 'Ideal para acceso público a través de Internet',
|
||||
604: 'Se puede usar para acceso personal a través de Internet público. VPN es más privado y seguro',
|
||||
605: 'cuando el uso de direcciones IP y puertos no es deseable',
|
||||
606: 'Host',
|
||||
607: 'Valor',
|
||||
608: 'Propósito',
|
||||
609: 'Subdominios de',
|
||||
610: 'DNS dinámico',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -524,4 +524,55 @@ export default {
|
||||
557: 'Aucun domaine privé',
|
||||
558: 'Nouveau domaine privé',
|
||||
559: 'Serveurs DNS',
|
||||
560: 'Entrez un nom de domaine complet. Comme le domaine est destiné à un usage privé, il peut s’agir de n’importe quel domaine, même d’un domaine que vous ne contrôlez pas.',
|
||||
561: 'Entrez un nom de domaine complet. Par exemple, si vous contrôlez domain.com, vous pourriez entrer domain.com, subdomain.domain.com ou another.subdomain.domain.com.',
|
||||
562: 'Enregistrements DNS',
|
||||
563: 'Créez l’un des enregistrements DNS ci-dessous.',
|
||||
564: 'Aucun enregistrement DNS détecté pour',
|
||||
565: 'Enregistrement DNS invalide',
|
||||
566: 'se résout en',
|
||||
567: 'Enregistrement DNS détecté !',
|
||||
568: 'Sélectionnez une passerelle à utiliser pour ce domaine.',
|
||||
569: 'Sélectionnez une Autorité de Certification pour émettre des certificats SSL/TLS pour ce domaine.',
|
||||
570: 'Autre',
|
||||
571: 'Un nom pour identifier facilement la passerelle',
|
||||
572: 'Sélectionnez cette option si la passerelle est configurée pour un accès privé uniquement aux clients autorisés. StartTunnel est une passerelle privée.',
|
||||
573: 'Sélectionnez cette option si la passerelle est configurée pour un accès public illimité.',
|
||||
574: 'Fichier',
|
||||
575: 'Fichier de configuration Wireguard',
|
||||
576: 'Copier/Coller',
|
||||
577: 'Contenu du fichier',
|
||||
578: 'Clé publique',
|
||||
579: 'doit être une clé publique SSH valide',
|
||||
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.',
|
||||
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: 'Utile uniquement pour les clients qui imposent HTTPS',
|
||||
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 clearnet sont 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 Internet public. Le VPN est plus privé et sécurisé',
|
||||
605: 'lorsque l’utilisation des adresses IP et des ports est indésirable',
|
||||
606: 'Hôte',
|
||||
607: 'Valeur',
|
||||
608: 'But',
|
||||
609: 'Sous-domaines de',
|
||||
610: 'DNS dynamique',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -524,4 +524,55 @@ export default {
|
||||
557: 'Brak domen prywatnych',
|
||||
558: 'Nowa domena prywatna',
|
||||
559: 'Serwery DNS',
|
||||
560: 'Wprowadź w pełni kwalifikowaną nazwę domeny. Ponieważ domena jest przeznaczona do użytku prywatnego, może to być dowolna domena, nawet taka, której nie kontrolujesz.',
|
||||
561: 'Wprowadź w pełni kwalifikowaną nazwę domeny. Na przykład, jeśli kontrolujesz domain.com, możesz wprowadzić domain.com, subdomain.domain.com lub another.subdomain.domain.com.',
|
||||
562: 'Rekordy DNS',
|
||||
563: 'Utwórz jeden z poniższych rekordów DNS.',
|
||||
564: 'Nie wykryto rekordu DNS dla',
|
||||
565: 'Nieprawidłowy rekord DNS',
|
||||
566: 'rozwiązuje się na',
|
||||
567: 'Wykryto rekord DNS!',
|
||||
568: 'Wybierz bramę do użycia dla tej domeny.',
|
||||
569: 'Wybierz Urząd Certyfikacji, aby wystawić certyfikaty SSL/TLS dla tej domeny.',
|
||||
570: 'Inne',
|
||||
571: 'Nazwa ułatwiająca identyfikację bramy',
|
||||
572: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do prywatnego dostępu tylko dla autoryzowanych klientów. StartTunnel to prywatna brama.',
|
||||
573: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do nieograniczonego publicznego dostępu.',
|
||||
574: 'Plik',
|
||||
575: 'Plik konfiguracyjny Wireguard',
|
||||
576: 'Kopiuj/Wklej',
|
||||
577: 'Zawartość pliku',
|
||||
578: 'Klucz publiczny',
|
||||
579: 'musi być prawidłowym kluczem publicznym SSH',
|
||||
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.',
|
||||
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: 'Przydatne tylko dla klientów wymuszających HTTPS',
|
||||
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. Preferowane są domeny clearnet',
|
||||
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. VPN jest bardziej prywatny i bezpieczny',
|
||||
605: 'gdy używanie adresów IP i portów jest niepożądane',
|
||||
606: 'Host',
|
||||
607: 'Wartość',
|
||||
608: 'Cel',
|
||||
609: 'Subdomeny',
|
||||
610: 'Dynamiczny DNS',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -10,9 +10,9 @@ import { I18N, i18nKey } from './i18n.providers'
|
||||
export class i18nPipe implements PipeTransform {
|
||||
private readonly i18n = inject(I18N)
|
||||
|
||||
transform(englishKey: i18nKey | null | undefined): string | undefined {
|
||||
return englishKey
|
||||
? this.i18n()?.[ENGLISH[englishKey as i18nKey]] || englishKey
|
||||
: undefined
|
||||
transform(englishKey: i18nKey | null | undefined): string {
|
||||
englishKey = englishKey || ('' as i18nKey)
|
||||
|
||||
return this.i18n()?.[ENGLISH[englishKey]] || englishKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { SwUpdate } from '@angular/service-worker'
|
||||
import { WA_WINDOW } from '@ng-web-apis/common'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
import { TuiResponsiveDialog } from '@taiga-ui/addon-mobile'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
@@ -12,21 +12,23 @@ import { distinctUntilChanged, map, merge, Subject } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'refresh-alert',
|
||||
template: `
|
||||
<ng-template
|
||||
[tuiResponsiveDialog]="show()"
|
||||
[tuiResponsiveDialogOptions]="{ label: 'Refresh Needed', size: 's' }"
|
||||
[tuiResponsiveDialogOptions]="{
|
||||
label: i18n.transform('Refresh Needed'),
|
||||
size: 's',
|
||||
}"
|
||||
(tuiResponsiveDialogChange)="dismiss$.next()"
|
||||
>
|
||||
@if (isPwa) {
|
||||
<p>
|
||||
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.
|
||||
{{
|
||||
'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.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
tuiButton
|
||||
@@ -36,11 +38,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="pwaReload()"
|
||||
>
|
||||
Reload
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
Your user interface is cached and out of date. Hard refresh the page to
|
||||
get the latest UI.
|
||||
{{
|
||||
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.'
|
||||
| i18n
|
||||
}}
|
||||
<ul>
|
||||
<li>
|
||||
<b>On Mac</b>
|
||||
@@ -59,13 +63,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="dismiss$.next()"
|
||||
>
|
||||
Ok
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus],
|
||||
imports: [TuiResponsiveDialog, TuiButton, TuiAutoFocus, i18nPipe],
|
||||
})
|
||||
export class RefreshAlertComponent {
|
||||
private readonly win = inject(WA_WINDOW)
|
||||
@@ -73,6 +77,8 @@ export class RefreshAlertComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly version = Version.parse(inject(ConfigService).version)
|
||||
|
||||
readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly dismiss$ = new Subject<void>()
|
||||
readonly isPwa = this.win.matchMedia('(display-mode: standalone)').matches
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'service-interface',
|
||||
template: `
|
||||
|
||||
@@ -3,7 +3,7 @@ import { T, utils } from '@start9labs/start-sdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { PublicDomain } from './public-domains/pd.service'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
|
||||
type AddressWithInfo = {
|
||||
url: URL
|
||||
@@ -98,155 +98,6 @@ function cmpClearnet(
|
||||
])
|
||||
}
|
||||
|
||||
// @TODO translations
|
||||
function toDisplayAddress(
|
||||
{ info, url }: AddressWithInfo,
|
||||
gateways: GatewayPlus[],
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
let gatewayName: DisplayAddress['gatewayName']
|
||||
let type: DisplayAddress['type']
|
||||
let bullets: any[]
|
||||
// let bullets: DisplayAddress['bullets']
|
||||
|
||||
const rootCaRequired = `Requires trusting your server's Root CA`
|
||||
|
||||
// ** Tor **
|
||||
if (info.kind === 'onion') {
|
||||
access = null
|
||||
gatewayName = null
|
||||
type = 'Tor'
|
||||
bullets = [
|
||||
'Connections can be slow or unreliable at times',
|
||||
'Public if you share the address publicly, otherwise private',
|
||||
'Requires using a Tor-enabled device or browser',
|
||||
]
|
||||
// Tor (HTTPS)
|
||||
if (url.protocol.startsWith('https')) {
|
||||
type = `${type} (HTTPS)`
|
||||
bullets = [
|
||||
'Only useful for clients that enforce HTTPS',
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
// Tor (HTTP)
|
||||
} else {
|
||||
bullets.unshift(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
)
|
||||
type = `${type} (HTTP)`
|
||||
}
|
||||
// ** Not Tor **
|
||||
} else {
|
||||
const port = info.hostname.sslPort || info.hostname.port
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayLanIpv4 = gateway.lanIpv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = 'Ideal for local access'
|
||||
const lanRequired =
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN'
|
||||
const staticRequired = `Requires setting a static IP address for ${gatewayLanIpv4} in your gateway`
|
||||
const vpnAccess = 'Ideal for VPN access via your'
|
||||
|
||||
// * Local *
|
||||
if (info.hostname.kind === 'local') {
|
||||
type = 'Local'
|
||||
access = 'private'
|
||||
bullets = [
|
||||
localIdeal,
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration',
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * IPv4 *
|
||||
} else if (info.hostname.kind === 'ipv4') {
|
||||
type = 'IPv4'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
'Can be used for clearnet access',
|
||||
'Not recommended in most cases. Clearnet domains are preferred',
|
||||
rootCaRequired,
|
||||
]
|
||||
if (!gateway.public) {
|
||||
bullets.push(
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
if (isWireguard) {
|
||||
bullets = [`${vpnAccess} StartTunnel (or similar)`, rootCaRequired]
|
||||
} else {
|
||||
bullets = [
|
||||
localIdeal,
|
||||
`${vpnAccess} router's Wireguard server`,
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
// * IPv6 *
|
||||
} else if (info.hostname.kind === 'ipv6') {
|
||||
type = 'IPv6'
|
||||
access = 'private'
|
||||
bullets = ['Can be used for local access', lanRequired, rootCaRequired]
|
||||
// * Domain *
|
||||
} else {
|
||||
type = 'Domain'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`Requires a DNS record for ${info.hostname.value} that resolves to ${gateway.ipInfo.wanIp}`,
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (publicDomains[info.hostname.value]?.acme) {
|
||||
bullets.unshift('Ideal for public access via the Internet')
|
||||
} else {
|
||||
bullets = [
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure',
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = 'when using IP addresses and ports is undesirable'
|
||||
const customDnsRequired = `Requires a DNS record for ${info.hostname.value} that resolves to ${gatewayLanIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel (or similar) ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
} else {
|
||||
bullets = [
|
||||
`${localIdeal} ${ipPortBad}`,
|
||||
`${vpnAccess} router's Wireguard server ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
lanRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.href,
|
||||
access,
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPublicDomains(
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
gateways: GatewayPlus[],
|
||||
@@ -263,6 +114,7 @@ export function getPublicDomains(
|
||||
})
|
||||
export class InterfaceService {
|
||||
private readonly config = inject(ConfigService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
@@ -304,11 +156,11 @@ export class InterfaceService {
|
||||
|
||||
return {
|
||||
common: bestAddrs.map(a =>
|
||||
toDisplayAddress(a, gateways, host.publicDomains),
|
||||
this.toDisplayAddress(a, gateways, host.publicDomains),
|
||||
),
|
||||
uncommon: allAddressesWithInfo
|
||||
.filter(a => !bestAddrs.includes(a))
|
||||
.map(a => toDisplayAddress(a, gateways, host.publicDomains)),
|
||||
.map(a => this.toDisplayAddress(a, gateways, host.publicDomains)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +300,183 @@ export class InterfaceService {
|
||||
) || []
|
||||
)
|
||||
}
|
||||
|
||||
private toDisplayAddress(
|
||||
{ info, url }: AddressWithInfo,
|
||||
gateways: GatewayPlus[],
|
||||
publicDomains: Record<string, T.PublicDomainConfig>,
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
let gatewayName: DisplayAddress['gatewayName']
|
||||
let type: DisplayAddress['type']
|
||||
let bullets: any[]
|
||||
// let bullets: DisplayAddress['bullets']
|
||||
|
||||
const rootCaRequired = this.i18n.transform(
|
||||
"Requires trusting your server's Root CA",
|
||||
)
|
||||
|
||||
// ** Tor **
|
||||
if (info.kind === 'onion') {
|
||||
access = null
|
||||
gatewayName = null
|
||||
type = 'Tor'
|
||||
bullets = [
|
||||
this.i18n.transform('Connections can be slow or unreliable at times'),
|
||||
this.i18n.transform(
|
||||
'Public if you share the address publicly, otherwise private',
|
||||
),
|
||||
this.i18n.transform('Requires using a Tor-enabled device or browser'),
|
||||
]
|
||||
// Tor (HTTPS)
|
||||
if (url.protocol.startsWith('https')) {
|
||||
type = `${type} (HTTPS)`
|
||||
bullets = [
|
||||
this.i18n.transform('Only useful for clients that enforce HTTPS'),
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
// Tor (HTTP)
|
||||
} else {
|
||||
bullets.unshift(
|
||||
this.i18n.transform(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
),
|
||||
)
|
||||
type = `${type} (HTTP)`
|
||||
}
|
||||
// ** Not Tor **
|
||||
} else {
|
||||
const port = info.hostname.sslPort || info.hostname.port
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayLanIpv4 = gateway.lanIpv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = this.i18n.transform('Ideal for local access')
|
||||
const lanRequired = this.i18n.transform(
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN',
|
||||
)
|
||||
const staticRequired = `${this.i18n.transform('Requires setting a static IP address for')} ${gatewayLanIpv4} ${this.i18n.transform('in your gateway')}`
|
||||
const vpnAccess = this.i18n.transform('Ideal for VPN access via')
|
||||
const routerWireguard = this.i18n.transform(
|
||||
"your router's Wireguard server",
|
||||
)
|
||||
const portForwarding = this.i18n.transform(
|
||||
'Requires port forwarding in gateway',
|
||||
)
|
||||
const dnsFor = this.i18n.transform('Requires a DNS record for')
|
||||
const resolvesTo = this.i18n.transform('that resolves to')
|
||||
|
||||
// * Local *
|
||||
if (info.hostname.kind === 'local') {
|
||||
type = this.i18n.transform('Local')
|
||||
access = 'private'
|
||||
bullets = [
|
||||
localIdeal,
|
||||
this.i18n.transform(
|
||||
'Not recommended for VPN access. VPNs do not support ".local" domains without advanced configuration',
|
||||
),
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * IPv4 *
|
||||
} else if (info.hostname.kind === 'ipv4') {
|
||||
type = 'IPv4'
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
this.i18n.transform('Can be used for clearnet access'),
|
||||
this.i18n.transform(
|
||||
'Not recommended in most cases. Clearnet domains are preferred',
|
||||
),
|
||||
rootCaRequired,
|
||||
]
|
||||
if (!gateway.public) {
|
||||
bullets.push(
|
||||
`${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
if (isWireguard) {
|
||||
bullets = [`${vpnAccess} StartTunnel`, rootCaRequired]
|
||||
} else {
|
||||
bullets = [
|
||||
localIdeal,
|
||||
`${vpnAccess} ${routerWireguard}`,
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
// * IPv6 *
|
||||
} else if (info.hostname.kind === 'ipv6') {
|
||||
type = 'IPv6'
|
||||
access = 'private'
|
||||
bullets = [
|
||||
this.i18n.transform('Can be used for local access'),
|
||||
lanRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
// * Domain *
|
||||
} else {
|
||||
type = this.i18n.transform('Domain')
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway.ipInfo.wanIp}`,
|
||||
`${portForwarding} "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (publicDomains[info.hostname.value]?.acme) {
|
||||
bullets.unshift(
|
||||
this.i18n.transform('Ideal for public access via the Internet'),
|
||||
)
|
||||
} else {
|
||||
bullets = [
|
||||
this.i18n.transform(
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure',
|
||||
),
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
}
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = this.i18n.transform(
|
||||
'when using IP addresses and ports is undesirable',
|
||||
)
|
||||
const customDnsRequired = `${dnsFor} ${info.hostname.value} ${resolvesTo} ${gatewayLanIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
} else {
|
||||
bullets = [
|
||||
`${localIdeal} ${ipPortBad}`,
|
||||
`${vpnAccess} ${routerWireguard} ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
lanRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: url.href,
|
||||
access,
|
||||
gatewayName,
|
||||
type,
|
||||
bullets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
|
||||
@@ -26,8 +26,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'section[privateDomains]',
|
||||
template: `
|
||||
@@ -113,9 +111,10 @@ export class InterfacePrivateDomainsComponent {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
name: this.i18n.transform('Domain'),
|
||||
description: this.i18n.transform(
|
||||
'Enter a fully qualified domain name. Since the domain is for private use, it can be any domain you want, even one you do not control.',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
|
||||
@@ -19,8 +19,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { parse } from 'tldts'
|
||||
import { GatewayWithId } from './pd.service'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'dns',
|
||||
template: `
|
||||
@@ -37,11 +35,11 @@ import { GatewayWithId } from './pd.service'
|
||||
[(ngModel)]="ddns"
|
||||
(ngModelChange)="pass.set(undefined)"
|
||||
/>
|
||||
Dynamic DNS
|
||||
{{ 'Dynamic DNS' | i18n }}
|
||||
</label>
|
||||
}
|
||||
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
<table [appTable]="['Type', 'Host', 'Value', 'Purpose']">
|
||||
@for (row of rows(); track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
@@ -98,6 +96,7 @@ import { GatewayWithId } from './pd.service'
|
||||
export class DnsComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly ddns = false
|
||||
|
||||
@@ -126,6 +125,8 @@ export class DnsComponent {
|
||||
|
||||
const segments = subdomain.split('.')
|
||||
|
||||
const subdomains = this.i18n.transform('subdomains of')
|
||||
|
||||
return [
|
||||
{
|
||||
host: subdomain,
|
||||
@@ -135,12 +136,12 @@ export class DnsComponent {
|
||||
const parent = segments.slice(i + 1).join('.')
|
||||
return {
|
||||
host: `*.${parent}`,
|
||||
purpose: `subdomains of ${parent}`,
|
||||
purpose: `${subdomains} ${parent}`,
|
||||
}
|
||||
}),
|
||||
{
|
||||
host: '*',
|
||||
purpose: `subdomains of ${domain}`,
|
||||
purpose: `${subdomains} ${domain}`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
LoadingService,
|
||||
i18nPipe,
|
||||
} from '@start9labs/shared'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
||||
@@ -18,8 +19,6 @@ import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { DNS } from './dns.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
export type PublicDomain = {
|
||||
fqdn: string
|
||||
gateway: GatewayWithId | null
|
||||
@@ -40,6 +39,7 @@ export class PublicDomainService {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly data = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network').pipe(
|
||||
@@ -61,9 +61,10 @@ export class PublicDomainService {
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
name: this.i18n.transform('Domain'),
|
||||
description: this.i18n.transform(
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
@@ -140,7 +141,7 @@ export class PublicDomainService {
|
||||
showDns(fqdn: string, gateway: GatewayWithId, message: i18nKey) {
|
||||
this.dialog
|
||||
.openComponent(DNS, {
|
||||
label: 'DNS Records' as i18nKey,
|
||||
label: 'DNS Records',
|
||||
size: 'l',
|
||||
data: {
|
||||
fqdn,
|
||||
@@ -177,7 +178,9 @@ export class PublicDomainService {
|
||||
}
|
||||
|
||||
const wanIp = gateway.ipInfo.wanIp
|
||||
let message = `Create one of the DNS records below to cause ${fqdn} to resolve to ${wanIp}`
|
||||
let message = this.i18n.transform(
|
||||
'Create one of the DNS records below.',
|
||||
) as i18nKey
|
||||
|
||||
if (!ip) {
|
||||
setTimeout(
|
||||
@@ -185,7 +188,7 @@ export class PublicDomainService {
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`No DNS detected for ${fqdn}. ${message}` as i18nKey,
|
||||
`${this.i18n.transform('No DNS record detected for')} ${fqdn}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -195,7 +198,7 @@ export class PublicDomainService {
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`Invalid DNS. ${fqdn} is currently resolving to ${ip}. ${message}` as i18nKey,
|
||||
`${this.i18n.transform('Invalid DNS record')}. ${fqdn} ${this.i18n.transform('resolves to')} ${ip}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -203,8 +206,8 @@ export class PublicDomainService {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.dialog.openAlert(
|
||||
`${fqdn} is successfully resolving to ${wanIp}` as i18nKey,
|
||||
{ label: 'DNS detected!' as i18nKey, appearance: 'positive' },
|
||||
`${fqdn} ${this.i18n.transform('resolves to')} ${wanIp}` as i18nKey,
|
||||
{ label: 'DNS record detected!', appearance: 'positive' },
|
||||
),
|
||||
250,
|
||||
)
|
||||
@@ -224,8 +227,10 @@ export class PublicDomainService {
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Gateway',
|
||||
description: 'Select a gateway to use for this domain.',
|
||||
name: this.i18n.transform('Gateway'),
|
||||
description: this.i18n.transform(
|
||||
'Select a gateway to use for this domain.',
|
||||
),
|
||||
values: data.gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
@@ -241,9 +246,10 @@ export class PublicDomainService {
|
||||
.map(g => g.id),
|
||||
})),
|
||||
authority: ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select the Certificate Authority that will issue the SSL/TLS certificate for this domain',
|
||||
name: this.i18n.transform('Certificate Authority'),
|
||||
description: this.i18n.transform(
|
||||
'Select a Certificate Authority to issue SSL/TLS certificates for this domain',
|
||||
),
|
||||
values: data.authorities,
|
||||
default: '',
|
||||
}),
|
||||
|
||||
@@ -55,7 +55,7 @@ export class AuthorityService {
|
||||
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
provider: ISB.Value.union({
|
||||
name: 'Provider',
|
||||
name: this.i18n.transform('Provider'),
|
||||
default: (availableAuthorities[0]?.url as any) || 'other',
|
||||
variants: ISB.Variants.of({
|
||||
...availableAuthorities.reduce(
|
||||
@@ -69,7 +69,7 @@ export class AuthorityService {
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
name: this.i18n.transform('Other'),
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
|
||||
@@ -72,82 +72,82 @@ export default class GatewaysComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
async add() {
|
||||
const spec = ISB.InputSpec.of({
|
||||
name: ISB.Value.text({
|
||||
name: this.i18n.transform('Name'),
|
||||
description: this.i18n.transform(
|
||||
'A name to easily identify the gateway',
|
||||
),
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
type: ISB.Value.select({
|
||||
name: this.i18n.transform('Type'),
|
||||
description: `-**${this.i18n.transform('private')}**: ${this.i18n.transform('select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.')}\n-**${this.i18n.transform('public')}**: ${this.i18n.transform('select this option if the gateway is configured for unfettered public access.')}`,
|
||||
default: 'private',
|
||||
values: {
|
||||
private: this.i18n.transform('private'),
|
||||
public: this.i18n.transform('public'),
|
||||
},
|
||||
}),
|
||||
config: ISB.Value.union({
|
||||
name: this.i18n.transform('Wireguard Config File'),
|
||||
default: 'paste',
|
||||
variants: ISB.Variants.of({
|
||||
paste: {
|
||||
name: this.i18n.transform('Copy/Paste'),
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.textarea({
|
||||
name: this.i18n.transform('File Contents'),
|
||||
default: null,
|
||||
required: true,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
upload: {
|
||||
name: this.i18n.transform('Upload'),
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.file({
|
||||
name: this.i18n.transform('File'),
|
||||
required: true,
|
||||
extensions: ['.conf'],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add gateway',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(gatewaySpec),
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (input: typeof gatewaySpec._TYPE) => this.save(input),
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (input: typeof spec._TYPE) => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addTunnel({
|
||||
name: input.name,
|
||||
config: '' as string, // @TODO alex/matt when types arrive
|
||||
public: input.type === 'public',
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(input: typeof gatewaySpec._TYPE): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addTunnel({
|
||||
name: input.name,
|
||||
config: '' as string, // @TODO alex/matt when types arrive
|
||||
public: input.type === 'public',
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gatewaySpec = ISB.InputSpec.of({
|
||||
name: ISB.Value.text({
|
||||
name: 'Name',
|
||||
description: 'A name to easily identify the gateway',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
type: ISB.Value.select({
|
||||
name: 'Type',
|
||||
description:
|
||||
'-**Private**: select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.\n-**Public**: select this option if the gateway is configured for unfettered public access.',
|
||||
default: 'private',
|
||||
values: {
|
||||
private: 'Private',
|
||||
public: 'Public',
|
||||
},
|
||||
}),
|
||||
config: ISB.Value.union({
|
||||
name: 'Wireguard Config',
|
||||
default: 'paste',
|
||||
variants: ISB.Variants.of({
|
||||
paste: {
|
||||
name: 'Paste File Contents',
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.textarea({
|
||||
name: 'Paste File Contents',
|
||||
default: null,
|
||||
required: true,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
upload: {
|
||||
name: 'Upload File',
|
||||
spec: ISB.InputSpec.of({
|
||||
file: ISB.Value.file({
|
||||
name: 'File',
|
||||
required: true,
|
||||
extensions: ['.conf'],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -143,6 +143,7 @@ export class GatewaysItemComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly gateway = input.required<GatewayPlus>()
|
||||
|
||||
@@ -169,7 +170,7 @@ export class GatewaysItemComponent {
|
||||
const { ipInfo, id } = this.gateway()
|
||||
const renameSpec = ISB.InputSpec.of({
|
||||
label: ISB.Value.text({
|
||||
name: 'Label',
|
||||
name: this.i18n.transform('Name'),
|
||||
required: true,
|
||||
default: ipInfo?.name || null,
|
||||
}),
|
||||
|
||||
@@ -107,14 +107,29 @@ export default class SystemSSHComponent {
|
||||
protected tableKeys = viewChild<SSHTableComponent<SSHKey>>('table')
|
||||
|
||||
async add(all: readonly SSHKey[]) {
|
||||
const spec = ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: this.i18n.transform('Public Key'),
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex:
|
||||
'^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-nistp(256|384|521))\\s+[A-Za-z0-9+/=]+(\\s[^\\s]+)?$',
|
||||
description: this.i18n.transform('must be a valid SSH public key'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add SSH key',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(SSHSpec),
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async ({ key }: typeof SSHSpec._TYPE) => {
|
||||
handler: async ({ key }: typeof spec._TYPE) => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
@@ -157,18 +172,3 @@ export default class SystemSSHComponent {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const SSHSpec = ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: 'Public Key',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex:
|
||||
'^(ssh-(rsa|ed25519|dss|ecdsa)|ecdsa-sha2-nistp(256|384|521))\\s+[A-Za-z0-9+/=]+(\\s[^\\s]+)?$',
|
||||
description: 'must be a valid SSH public key',
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user