start service interface page, WIP

This commit is contained in:
Matt Hill
2025-08-06 17:55:21 -06:00
parent d6dfaf8feb
commit 177232ab28
43 changed files with 816 additions and 1178 deletions

View File

@@ -15,7 +15,6 @@ export default {
12: 'Aktive Sitzungen',
13: 'Passwort ändern',
14: 'Allgemeine Einstellungen',
15: 'Verwalten Sie Ihre Gesamteinrichtung und Einstellungen',
16: 'Browser-Tab Titel',
17: 'Sprache',
18: 'Festplattenreparatur',
@@ -91,8 +90,6 @@ export default {
88: 'Aktionen',
89: 'nicht empfohlen',
90: 'Root-CA ist vertrauenswürdig!',
91: 'Fügen Sie eine Clearnet-Adresse hinzu, um diese Oberfläche im Internet verfügbar zu machen. Clearnet-Adressen sind vollständig öffentlich und nicht anonym.',
92: 'Mehr erfahren',
93: 'Öffentlich machen',
94: 'Privat machen',
95: 'Keine öffentlichen Adressen',
@@ -105,16 +102,12 @@ export default {
102: 'Verlassen',
103: 'Sind Sie sicher?',
104: 'Domain auswählen',
105: 'Lokal',
106: 'Lokale Adressen sind nur von Geräten erreichbar, die direkt oder über VPN mit demselben LAN wie Ihr Server verbunden sind.',
107: 'Mehr erfahren',
108: 'Öffentlich',
109: 'Privat',
110: 'Fügen Sie eine Onion-Adresse hinzu, um dieses Interface anonym im Darknet verfügbar zu machen. Onion-Adressen sind nur über das Tor-Netzwerk erreichbar.',
111: 'Keine Onion-Adressen',
112: 'Neue onion-adresse',
111: 'Keine Onion-Domains',
112: 'Neue Onion-Domain',
113: 'Privater Schlüssel (optional)',
114: 'Optional können Sie einen base64-codierten ed25519-Schlüssel angeben, um die Tor V3 (.onion)-Adresse zu generieren. Wenn nicht angegeben, wird ein zufälliger Schlüssel erstellt.',
114: '',
115: 'Verarbeite 10.000 Logs',
116: 'Ältere Logs werden geladen',
117: 'Warten auf Netzwerkverbindung',
@@ -241,7 +234,7 @@ export default {
240: 'Name',
241: 'Status',
242: 'Öffnen',
243: 'Schnittstellen',
243: '',
244: 'Hosting',
245: 'Installation läuft',
246: 'Siehe unten',
@@ -293,7 +286,6 @@ export default {
296: 'Hochladen',
297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.',
298: 'Ungültige Paketdatei',
299: 'Das Hinzufügen einer Domain zu StartOS bedeutet, dass du sie und ihre Subdomains verwenden kannst, um Service-Oberflächen im öffentlichen Internet zu hosten.',
300: 'Anleitung anzeigen',
303: 'Kontakt',
304: 'Bearbeiten',
@@ -324,8 +316,6 @@ export default {
329: 'Hostname',
330: 'Pfad',
331: 'URL',
332: 'Netzwerkschnittstelle',
333: 'Protokoll',
334: 'Modell',
335: 'User-Agent',
336: 'Plattform',
@@ -372,7 +362,6 @@ export default {
377: 'StartOS-Sicherungen erkannt',
378: 'Keine StartOS-Sicherungen erkannt',
379: 'StartOS-Version',
380: 'Die Verbindung zu einem externen SMTP-Server ermöglicht es StartOS und seinen Diensten, E-Mails zu senden.',
381: 'SMTP-Zugangsdaten',
382: 'Test-E-Mail senden',
383: 'Senden',
@@ -380,7 +369,6 @@ export default {
385: 'Eine Test-E-Mail wurde gesendet an',
386: 'Prüfen Sie Ihren Spam-Ordner und markieren Sie die Nachricht als „kein Spam“.',
387: 'Die Web-Benutzeroberfläche Ihres StartOS-Servers, zugänglich über jeden Browser.',
388: 'Ändern Sie Ihr Master-Passwort für StartOS.',
389: 'Sie benötigen weiterhin Ihr aktuelles Passwort, um bestehende Sicherungen zu entschlüsseln!',
390: 'Neue Passwörter stimmen nicht überein',
391: 'Neues Passwort muss mindestens 12 Zeichen lang sein',
@@ -390,7 +378,6 @@ export default {
395: 'Aktuelles Passwort',
396: 'Neues Passwort',
397: 'Neues Passwort erneut eingeben',
398: 'Eine Sitzung ist ein Gerät, das aktuell bei StartOS angemeldet ist. Beenden Sie Sitzungen, die Sie nicht kennen oder nicht mehr verwenden.',
399: 'Aktuelle Sitzung',
400: 'Weitere Sitzungen',
401: 'Ausgewählte beenden',
@@ -497,7 +484,6 @@ export default {
502: 'souveränes computing',
503: 'Passen Sie den Namen an, der in Ihrem Browser-Tab erscheint',
504: 'Verwalten',
505: 'Möchten Sie diese Adresse wirklich löschen?',
506: '"Weiches Deinstallieren" entfernt den Dienst aus StartOS, behält jedoch die Daten bei.',
507: 'Keine gespeicherten Anbieter',
508: 'Kiosk-Modus',
@@ -511,22 +497,20 @@ export default {
516: 'Empfohlen',
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
518: 'Verwerfen',
519: 'Um Clearnet-Domains zu veröffentlichen, musst du oben auf „Öffentlich machen“ klicken.',
520: 'Update verfügbar',
521: 'Um das Problem zu beheben, siehe',
522: 'SDK Version',
523: 'Sicherungsbericht',
524: 'Ausgewählte löschen',
525: 'Keine schlüssel',
526: 'Öffentlichen SSH-Schlüssel hinzufügen',
527: 'Standardmäßig kannst du dich per SSH von jedem Gerät aus mit deinem Server verbinden, indem du dein Master-Passwort verwendest. Optional kannst du SSH-öffentliche Schlüssel hinzufügen, um bestimmten Geräten den Zugriff ohne Passworteingabe zu ermöglichen.',
525: '',
526: '',
527: '',
528: 'Quellcode',
529: 'Upstream-Dienst',
530: 'StartOS-Paket',
531: 'Fehler beim Initialisieren des Servers',
532: 'Abgeschlossen',
533: 'Gateways',
534: 'Gateways verbinden Ihren Server mit dem Internet. Sie verarbeiten ausgehenden Datenverkehr und erlauben unter bestimmten Bedingungen auch eingehenden Verkehr.',
535: 'Gateway hinzufügen',
536: 'Umbenennen',
537: 'Zugriff',
@@ -534,10 +518,15 @@ export default {
539: 'Zertifizierungsstellen',
540: 'Domain',
541: 'Gateway',
542: 'Standard-Zertifizierungsstelle',
543: 'Zertifizierungsstelle',
544: 'Domain bearbeiten',
545: 'Keine Domains',
546: 'Anbieter',
547: 'DNS verwalten',
548: '',
549: '',
550: '',
551: '',
552: '',
553: '',
} satisfies i18n

View File

@@ -14,7 +14,6 @@ export const ENGLISH = {
'Active Sessions': 12,
'Change Password': 13,
'General Settings': 14,
'Manage your overall setup and preferences': 15,
'Browser tab title': 16,
'Language': 17,
'Disk Repair': 18,
@@ -90,8 +89,6 @@ export const ENGLISH = {
'Actions': 88, // as in, actions available to the user
'not recommended': 89,
'Root CA Trusted!': 90,
'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.': 91,
'Learn more': 92,
'Make public': 93,
'Make private': 94,
'No public addresses': 95,
@@ -104,16 +101,12 @@ export const ENGLISH = {
'Leave': 102,
'Are you sure?': 103,
'Select domain': 104,
'Local': 105,
'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.': 106,
'Learn More': 107,
'Public': 108,
'Private': 109,
'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.': 110,
'No onion addresses': 111,
'New onion address': 112,
'No Tor domains': 111,
'New Tor domain': 112,
'Private Key (optional)': 113,
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.': 114,
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.': 114,
'Processing 10,000 logs': 115,
'Loading older logs': 116,
'Waiting for network connectivity': 117,
@@ -240,7 +233,7 @@ export const ENGLISH = {
'Name': 240,
'Status': 241,
'Open': 242, // verb
'Interfaces': 243, // as in user interface or application program interface
'Service Interfaces': 243, // as in, a UI or API for an application
'Hosting': 244,
'Installing': 245,
'See below': 246,
@@ -292,7 +285,6 @@ export const ENGLISH = {
'Upload': 296,
'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297,
'Invalid package file': 298,
'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.': 299,
'View instructions': 300,
'Contact': 303, // as in, "contact us"
'Edit': 304,
@@ -323,8 +315,6 @@ export const ENGLISH = {
'Hostname': 329,
'Path': 330, // as in, a URL path
'URL': 331,
'Network Interface': 332,
'Protocol': 333, // as in, http protocol
'Model': 334, // as in, a product model
'User Agent': 335,
'Platform': 336, // as in, OS platform, such as iOS, Android, Linux, etc
@@ -371,7 +361,6 @@ export const ENGLISH = {
'StartOS backups detected': 377,
'No StartOS backups detected': 378,
'StartOS Version': 379,
'Connecting an external SMTP server allows StartOS and your installed services to send you emails.': 380,
'SMTP Credentials': 381,
'Send test email': 382,
'Send': 383,
@@ -379,7 +368,6 @@ export const ENGLISH = {
'A test email has been sent to': 385,
'Check your spam folder and mark as not spam.': 386,
'The web user interface for your StartOS server, accessible from any browser.': 387,
'Change your StartOS master password.': 388,
'You will still need your current password to decrypt existing backups!': 389,
'New passwords do not match': 390,
'New password must be 12 characters or greater': 391,
@@ -389,7 +377,6 @@ export const ENGLISH = {
'Current Password': 395,
'New Password': 396,
'Retype New Password': 397,
'A session is a device that is currently logged into StartOS. For best security, terminate sessions you do not recognize or no longer use.': 398,
'Current session': 399,
'Other sessions': 400,
'Terminate selected': 401,
@@ -496,7 +483,6 @@ export const ENGLISH = {
'sovereign computing': 502,
'Customize the name appearing in your browser tab': 503,
'Manage': 504, // as in, administer
'Are you sure you want to delete this address?': 505, // this address referes to a domain or URL
'"Soft uninstall" will remove the service from StartOS but preserve its data.': 506,
'No saved providers': 507,
'Kiosk Mode': 508, // an OS mode that permits attaching a monitor to the computer
@@ -510,22 +496,20 @@ export const ENGLISH = {
'Recommended': 516, // as in, we recommend this
'Are you sure you want to dismiss this task?': 517,
'Dismiss': 518, // as in, dismiss or delete a task
'To publish clearnet domains, you must click "Make Public", above.': 519,
'Update available': 520,
'To resolve the issue, refer to': 521,
'SDK Version': 522,
'Backup Report': 523,
'Delete selected': 524,
'No keys': 525,
'Add SSH Public Key': 526,
'By default, you can SSH into your server from any device using your master password. Optionally add SSH public keys to grant specific devices access without needing to enter a password.': 527,
'No SSH keys': 525,
'Add SSH key': 526,
'SSH Keys': 527,
'Source Code': 528,
'Upstream service': 529, // as in, the URL of the source code for the original software
'StartOS package': 530, // as in, the URL of the source code for the StartOS package
'Error initializing server': 531,
'Finished': 532, // an in, complete
'Gateways': 533, // as in, a device or software that connects two different networks
'Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic.': 534,
'Add gateway': 535, // as in, add a new network gateway to StartOS
'Rename': 536,
'Access': 537, // as in, public or private access, almost "permission"
@@ -533,10 +517,15 @@ export const ENGLISH = {
'Certificate Authorities': 539,
'Domain': 540, // as in, an internat domain name
'Gateway': 541, // as in, a device or software that connects two different networks
'Default Certificate Authority': 542,
'Certificate Authority': 543,
'Edit domain': 544,
'No domains': 545,
'Provider': 546,
'Manage DNS': 547,
'Clearnet Domains': 548,
'No clearnet domains': 549,
'Addresses': 550,
'Common': 551,
'Uncommon': 552,
'No addresses': 553,
} as const

View File

@@ -15,7 +15,6 @@ export default {
12: 'Sesiones activas',
13: 'Cambiar contraseña',
14: 'Configuración general',
15: 'Administra tu configuración y preferencias generales',
16: 'Título de la pestaña del navegador',
17: 'Idioma',
18: 'Reparación de disco',
@@ -91,8 +90,6 @@ export default {
88: 'Acciones',
89: 'no recomendado',
90: '¡CA raíz confiable!',
91: 'Agrega una dirección clearnet para exponer esta interfaz en Internet. Las direcciones clearnet son totalmente públicas y no anónimas.',
92: 'Saber más',
93: 'Hacer público',
94: 'Hacer privado',
95: 'Sin direcciones públicas',
@@ -105,16 +102,12 @@ export default {
102: 'Salir',
103: '¿Estás seguro?',
104: 'Seleccionar dominio',
105: 'Local',
106: 'Las direcciones locales solo pueden ser accedidas por dispositivos conectados a la misma red local que tu servidor, ya sea directamente o mediante una VPN.',
107: 'Más información',
108: 'Público',
109: 'Privado',
110: 'Agrega una dirección onion para exponer esta interfaz de forma anónima en la darknet. Las direcciones onion solo se pueden acceder a través de la red Tor.',
111: 'Sin direcciones onion',
112: 'Nueva dirección onion',
111: 'Sin dominios onion',
112: 'Nueva dominio onion',
113: 'Clave privada (opcional)',
114: 'Opcionalmente proporciona una clave privada ed25519 codificada en base64 para generar la dirección Tor V3 (.onion). Si no se proporciona, se generará una clave aleatoria.',
114: '',
115: 'Procesando 10,000 registros',
116: 'Cargando registros anteriores',
117: 'Esperando conectividad de red',
@@ -241,7 +234,7 @@ export default {
240: 'Nombre',
241: 'Estado',
242: 'Abrir',
243: 'Interfaces',
243: '',
244: 'Alojamiento',
245: 'Instalando',
246: 'Ver abajo',
@@ -293,7 +286,6 @@ export default {
296: 'Subir',
297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.',
298: 'Archivo de paquete inválido',
299: 'Agregar un dominio a StartOS significa que puedes usarlo y sus subdominios para alojar interfaces de servicios en Internet público.',
300: 'Ver instrucciones',
303: 'Contacto',
304: 'Editar',
@@ -324,8 +316,6 @@ export default {
329: 'Nombre del host',
330: 'Ruta',
331: 'URL',
332: 'Interfaz de red',
333: 'Protocolo',
334: 'Modelo',
335: 'Agente de usuario',
336: 'Plataforma',
@@ -372,7 +362,6 @@ export default {
377: 'Copias de seguridad de StartOS detectadas',
378: 'No se detectaron copias de seguridad de StartOS',
379: 'Versión de StartOS',
380: 'Conectar un servidor SMTP externo permite que StartOS y tus servicios instalados te envíen correos electrónicos.',
381: 'Credenciales SMTP',
382: 'Enviar correo de prueba',
383: 'Enviar',
@@ -380,7 +369,6 @@ export default {
385: 'Se ha enviado un correo de prueba a',
386: 'Revisa tu carpeta de spam y márcalo como no spam.',
387: 'La interfaz web de tu servidor StartOS, accesible desde cualquier navegador.',
388: 'Cambia tu contraseña maestra de StartOS.',
389: '¡Aún necesitarás tu contraseña actual para descifrar copias de seguridad existentes!',
390: 'Las nuevas contraseñas no coinciden',
391: 'La nueva contraseña debe tener al menos 12 caracteres',
@@ -390,7 +378,6 @@ export default {
395: 'Contraseña actual',
396: 'Nueva contraseña',
397: 'Reingresa nueva contraseña',
398: 'Una sesión es un dispositivo que actualmente ha iniciado sesión en StartOS. Para mayor seguridad, cierra las sesiones que no reconozcas o que ya no uses.',
399: 'Sesión actual',
400: 'Otras sesiones',
401: 'Terminar seleccionados',
@@ -497,7 +484,6 @@ export default {
502: 'computación soberana',
503: 'Personaliza el nombre que aparece en la pestaña de tu navegador',
504: 'Administrar',
505: '¿Estás seguro de que deseas eliminar esta dirección?',
506: '"Desinstalación suave" eliminará el servicio de StartOS pero conservará sus datos.',
507: 'No hay proveedores guardados',
508: 'Modo quiosco',
@@ -511,22 +497,20 @@ export default {
516: 'Recomendado',
517: '¿Estás seguro de que deseas descartar esta tarea?',
518: 'Descartar',
519: 'Para publicar dominios en clearnet, debes hacer clic en "Hacer público" arriba.',
520: 'Actualización disponible',
521: 'Para resolver el problema, consulta',
522: 'Versión de SDK',
523: 'Informe de respaldo',
524: 'Eliminar seleccionado',
525: 'Sin llaves',
526: 'Agregar clave pública SSH',
527: 'De forma predeterminada, puedes conectarte por SSH a tu servidor desde cualquier dispositivo usando tu contraseña maestra. Opcionalmente, añade claves públicas SSH para otorgar acceso a dispositivos específicos sin necesidad de ingresar una contraseña.',
525: '',
526: '',
527: '',
528: 'Código fuente',
529: 'Servicio original',
530: 'Paquete StartOS',
531: 'Error al inicializar el servidor',
532: 'Finalizado',
533: 'Puertas de enlace',
534: 'Las puertas de enlace conectan su servidor a Internet. Procesan el tráfico saliente y, en ciertas condiciones, también permiten tráfico entrante.',
535: 'Agregar puerta de enlace',
536: 'Renombrar',
537: 'Acceso',
@@ -534,10 +518,15 @@ export default {
539: 'Autoridades certificadoras',
540: 'Dominio',
541: 'Puerta de enlace',
542: 'Autoridad certificadora predeterminada',
543: 'Autoridad certificadora',
544: 'Editar dominio',
545: 'Sin dominios',
546: 'Proveedor',
547: 'Administrar DNS',
548: '',
549: '',
550: '',
551: '',
552: '',
553: '',
} satisfies i18n

View File

@@ -15,7 +15,6 @@ export default {
12: 'Sessions actives',
13: 'Changer le mot de passe',
14: 'Paramètres généraux',
15: 'Gérez votre configuration et vos préférences globales',
16: 'Titre de longlet du navigateur',
17: 'Langue',
18: 'Réparation du disque',
@@ -91,8 +90,6 @@ export default {
88: 'Actions',
89: 'non recommandé',
90: 'Certificat racine approuvé !',
91: 'Ajoutez une addresse clearnet pour exposer cette interface sur Internet. Les adresses clearnet sont entièrement publiques et non anonymes.',
92: 'En savoir plus',
93: 'Rendre public',
94: 'Rendre privé',
95: 'Aucune adresse publique',
@@ -105,16 +102,12 @@ export default {
102: 'Quitter',
103: 'Êtes-vous sûr ?',
104: 'Sélectionner un domaine',
105: 'Local',
106: 'Les adresses locales ne sont accessibles quaux appareils connectés au même réseau local (LAN) que votre serveur, directement ou via un VPN.',
107: 'En savoir plus',
108: 'Public',
109: 'Privé',
110: 'Ajoutez une adresse onion (tor) pour exposer cette interface anonymement sur le darknet. Les adresses onion sont accessibles uniquement via le réseau Tor.',
111: 'Aucune adresse onion',
112: 'Nouvelle adresse onion',
111: 'Aucune domaine onion',
112: 'Nouvelle domaine onion',
113: 'Clé privée (optionnel)',
114: 'Vous pouvez fournir une clé privée ed25519 encodée en base64 pour générer ladresse Tor V3 (.onion). Sinon, une clé aléatoire sera générée et utilisée.',
114: '',
115: 'Traitement de 10 000 journaux',
116: 'Chargement des journaux plus anciens',
117: 'En attente dune connexion réseau',
@@ -241,7 +234,7 @@ export default {
240: 'Nom',
241: 'Statut',
242: 'Ouvrir',
243: 'Interfaces',
243: '',
244: 'Hébergement',
245: 'Installation',
246: 'Voir ci-dessous',
@@ -293,7 +286,6 @@ export default {
296: 'Téléverser',
297: 'Version 1 de s9pk détectée. Ce format de paquet est obsolète. Vous pouvez installer manuellement un s9pk V1 via start-cli si nécessaire.',
298: 'Fichier paquet invalide',
299: 'Ajouter un domaine à StartOS signifie que vous pouvez lutiliser, ainsi que ses sous-domaines, pour héberger des interfaces de services sur Internet public.',
300: 'Voir les instructions',
303: 'Contact',
304: 'Modifier',
@@ -324,8 +316,6 @@ export default {
329: 'Nom dhôte',
330: 'Chemin',
331: 'URL',
332: 'Interface réseau',
333: 'Protocole',
334: 'Modèle',
335: 'Agent utilisateur',
336: 'Plateforme',
@@ -372,7 +362,6 @@ export default {
377: 'Sauvegardes StartOS détectées',
378: 'Aucune sauvegarde StartOS détectée',
379: 'Version de StartOS',
380: 'Connecter un serveur SMTP externe permet à StartOS et à vos services installés de vous envoyer des emails.',
381: 'Identifiants SMTP',
382: 'Envoyer un email de test',
383: 'Envoyer',
@@ -380,7 +369,6 @@ export default {
385: 'Un email de test a été envoyé à',
386: 'Vérifiez votre dossier spam et marquez-le comme non spam.',
387: 'Linterface web de votre serveur StartOS, accessible depuis nimporte quel navigateur.',
388: 'Changez le mot de passe maître de StartOS.',
389: 'Vous aurez toujours besoin de votre mot de passe actuel pour déchiffrer les sauvegardes existantes !',
390: 'Les nouveaux mots de passe ne correspondent pas',
391: 'Le nouveau mot de passe doit comporter au moins 12 caractères',
@@ -390,7 +378,6 @@ export default {
395: 'Mot de passe actuel',
396: 'Nouveau mot de passe',
397: 'Retapez le nouveau mot de passe',
398: 'Une session correspond à un appareil actuellement connecté à StartOS. Pour plus de sécurité, terminez les sessions que vous ne reconnaissez pas ou que vous nutilisez plus.',
399: 'Session en cours',
400: 'Autres sessions',
401: 'Terminer les sessions séléctionnées',
@@ -497,7 +484,6 @@ export default {
502: 'informatique souveraine',
503: 'Personnalisez le nom qui apparaît dans longlet de votre navigateur',
504: 'Gérer',
505: 'Êtes-vous sûr de vouloir supprimer cette adresse ?',
506:  Désinstallation douce » supprimera le service de StartOS tout en conservant ses données.',
507: 'Aucun fournisseur enregistré',
508: 'Mode kiosque',
@@ -511,22 +497,20 @@ export default {
516: 'Recommandé',
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
518: 'Ignorer',
519: 'Pour publier des domaines clearnet, vous devez cliquer sur « Rendre public » ci-dessus.',
520: 'Mise à jour disponible',
521: 'Pour résoudre le problème, consultez',
522: 'Version de SDK',
523: 'Rapport de sauvegarde',
524: 'Supprimer la sélection',
525: 'Pas de clés',
526: 'Ajouter une clé publique SSH',
527: 'Par défaut, vous pouvez accéder à votre serveur en SSH depuis nimporte quel appareil en utilisant votre mot de passe maître. Vous pouvez également ajouter des clés publiques SSH pour accorder laccès à certains appareils sans avoir à saisir de mot de passe.',
525: '',
526: '',
527: '',
528: 'Code source',
529: 'Service en amont',
530: 'Paquet StartOS',
531: "Erreur lors de l'initialisation du serveur",
532: 'Terminé',
533: 'Passerelles',
534: 'Les passerelles connectent votre serveur à Internet. Elles traitent le trafic sortant et, dans certaines conditions, autorisent également le trafic entrant.',
535: 'Ajouter une passerelle',
536: 'Renommer',
537: 'Accès',
@@ -534,10 +518,15 @@ export default {
539: 'Autorités de certification',
540: 'Domaine',
541: 'Passerelle',
542: 'Autorité de certification par défaut',
543: 'Autorité de certification',
544: 'Modifier le domaine',
545: 'Aucun domaine',
546: 'Fournisseur',
547: 'Gérer le DNS',
548: '',
549: '',
550: '',
551: '',
552: '',
553: '',
} satisfies i18n

View File

@@ -15,7 +15,6 @@ export default {
12: 'Aktywne sesje',
13: 'Zmień hasło',
14: 'Ustawienia ogólne',
15: 'Zarządzaj ustawieniami i preferencjami systemu',
16: 'Tytuł karty przeglądarki',
17: 'Język',
18: 'Naprawa dysku',
@@ -91,8 +90,6 @@ export default {
88: 'Akcje',
89: 'niezalecane',
90: 'Główny certyfikat CA zaufany!',
91: 'Dodaj adres clearnet, aby udostępnić ten interfejs w Internecie. Adresy clearnet są w pełni publiczne i nie zapewniają anonimowości.',
92: 'Dowiedz się więcej',
93: 'Upublicznij',
94: 'Ukryj',
95: 'Brak publicznych adresów',
@@ -105,16 +102,12 @@ export default {
102: 'Opuść',
103: 'Czy jesteś pewien?',
104: 'Wybierz domenę',
105: 'Lokalne',
106: 'Adresy lokalne są dostępne tylko dla urządzeń podłączonych do tej samej sieci LAN co twój serwer, bezpośrednio lub przez VPN.',
107: 'Dowiedz się więcej',
108: 'Publiczny',
109: 'Prywatny',
110: 'Dodaj adres onion, aby anonimowo udostępnić ten interfejs w sieci Tor. Adresy onion są dostępne tylko przez sieć Tor.',
111: 'Brak adresów onion',
112: 'Nowy adres onion',
111: 'Brak domeny onion',
112: 'Nowy domenę onion',
113: 'Klucz prywatny (opcjonalnie)',
114: 'Opcjonalnie podaj klucz prywatny ed25519 zakodowany w base64, aby wygenerować adres Tor V3 (.onion). Jeśli nie zostanie podany, zostanie wygenerowany i użyty losowy klucz.',
114: '',
115: 'Przetwarzanie 10 000 logów',
116: 'Ładowanie starszych logów',
117: 'Oczekiwanie na połączenie sieciowe',
@@ -241,7 +234,7 @@ export default {
240: 'Nazwa',
241: 'Stan',
242: 'Otwórz',
243: 'Przyłącza',
243: '',
244: 'Hosting',
245: 'Instalowanie',
246: 'Zobacz poniżej',
@@ -293,7 +286,6 @@ export default {
296: 'Prześlij',
297: 'Wykryto pakiet s9pk w wersji 1. Ten format pakietu jest przestarzały. Możesz zainstalować pakiet s9pk V1 przez start-cli, jeśli to konieczne.',
298: 'Nieprawidłowy plik pakietu',
299: 'Dodanie domeny do StartOS oznacza, że możesz używać jej i jej subdomen do hostowania interfejsów usług w publicznym Internecie.',
300: 'Zobacz instrukcje',
303: 'Kontakt',
304: 'Edytuj',
@@ -324,8 +316,6 @@ export default {
329: 'Nazwa hosta',
330: 'Ścieżka',
331: 'URL',
332: 'Interfejs sieciowy',
333: 'Protokół',
334: 'Model',
335: 'Agent użytkownika',
336: 'Platforma',
@@ -372,7 +362,6 @@ export default {
377: 'Wykryto kopie zapasowe StartOS',
378: 'Nie wykryto kopii zapasowych StartOS',
379: 'Wersja StartOS',
380: 'Podłączenie zewnętrznego serwera SMTP umożliwia StartOS i zainstalowanym serwisom wysyłanie wiadomości e-mail.',
381: 'Dane logowania SMTP',
382: 'Wyślij e-mail testowy',
383: 'Wyślij',
@@ -380,7 +369,6 @@ export default {
385: 'Wiadomość testowa została wysłana na adres',
386: 'Sprawdź folder spam i oznacz wiadomość jako "nie spam".',
387: 'Przyłącze użytkownika twojego serwera StartOS, dostępne z dowolnej przeglądarki.',
388: 'Zmień swoje hasło główne StartOS.',
389: 'Nadal będziesz potrzebować aktualnego hasła, aby odszyfrować istniejące kopie zapasowe!',
390: 'Nowe hasła nie są zgodne',
391: 'Nowe hasło musi mieć co najmniej 12 znaków',
@@ -390,7 +378,6 @@ export default {
395: 'Bieżące hasło',
396: 'Nowe hasło',
397: 'Powtórz nowe hasło',
398: 'Sesja to urządzenie, które jest obecnie zalogowane do StartOS. Dla najlepszego bezpieczeństwa zakończ sesje, których nie rozpoznajesz lub już nie używasz.',
399: 'Bieżąca sesja',
400: 'Inne sesje',
401: 'Zakończ wybrane',
@@ -497,7 +484,6 @@ export default {
502: 'suwerenne przetwarzanie',
503: 'Dostosuj nazwę wyświetlaną na karcie przeglądarki',
504: 'Zarządzać',
505: 'Czy na pewno chcesz usunąć ten adres?',
506: '„Miękkie odinstalowanie” usunie usługę z StartOS, ale zachowa jej dane.',
507: 'Brak zapisanych dostawców',
508: 'Tryb kiosku',
@@ -511,22 +497,20 @@ export default {
516: 'Zalecane',
517: 'Czy na pewno chcesz odrzucić to zadanie?',
518: 'Odrzuć',
519: 'Aby opublikować domeny w clearnet, kliknij „Upublicznij” powyżej.',
520: 'Aktualizacja dostępna',
521: 'Aby rozwiązać problem, zapoznaj się z',
522: 'Wersja SDK',
523: 'Raport kopii zapasowej',
524: 'Usuń wybrane',
525: 'Brak kluczy',
526: 'Dodaj klucz publiczny SSH',
527: 'Domyślnie możesz połączyć się z serwerem przez SSH z dowolnego urządzenia, używając hasła głównego. Opcjonalnie dodaj klucze publiczne SSH, aby przyznać dostęp określonym urządzeniom bez potrzeby wpisywania hasła.',
525: '',
526: '',
527: '',
528: 'Kod źródłowy',
529: 'Usługa źródłowa',
530: 'Pakiet StartOS',
531: 'Błąd inicjalizacji serwera',
532: 'Zakończono',
533: 'Bramy sieciowe',
534: 'Bramy łączą twój serwer z Internetem. Przetwarzają ruch wychodzący, a w pewnych warunkach również dopuszczają ruch przychodzący.',
535: 'Dodaj bramę',
536: 'Zmień nazwę',
537: 'Dostęp',
@@ -534,10 +518,15 @@ export default {
539: 'Urzędy certyfikacji',
540: 'Domena',
541: 'Brama',
542: 'Domyślny urząd certyfikacji',
543: 'Urząd certyfikacji',
544: 'Edytuj domenę',
545: 'Brak domen',
546: 'Dostawca',
547: 'Zarządzaj DNS',
548: '',
549: '',
550: '',
551: '',
552: '',
553: '',
} satisfies i18n

View File

@@ -15,7 +15,7 @@ import {
} from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
import { InterfaceComponent } from './interface.component'
import { InterfaceComponent } from '../interface.component'
@Component({
selector: 'td[actions]',
@@ -114,7 +114,7 @@ import { InterfaceComponent } from './interface.component'
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceActionsComponent {
export class AddressActionsComponent {
private readonly document = inject(DOCUMENT)
readonly isMobile = inject(TUI_IS_MOBILE)

View File

@@ -0,0 +1,85 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { MappedServiceInterface } from '../interface.utils'
import { AddressActionsComponent } from './actions.component'
@Component({
selector: 'section[addresses]',
template: `
<header>{{ 'Addresses' | i18n }}</header>
@if (addresses().common.length) {
<section class="g-card">
<header>{{ 'Common' | i18n }}</header>
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
@for (address of addresses().common; track $index) {
<tr>
<td>
<button
tuiIconButton
appearance="flat-grayscale"
iconStart="@tui.eye"
(click)="instructions()"
>
{{ 'View instructions' | i18n }}
</button>
</td>
<td>{{ address.type }}</td>
<td>{{ address.gateway }}</td>
<td>{{ address.url }}</td>
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
</tr>
}
</table>
</section>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No addresses' | i18n }}
</app-placeholder>
}
@if (addresses().uncommon.length) {
<section class="g-card">
<header>{{ 'Uncommon' | i18n }}</header>
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
@for (address of addresses().uncommon; track $index) {
<tr>
<td>
<button
tuiIconButton
appearance="flat-grayscale"
iconStart="@tui.eye"
(click)="instructions()"
>
{{ 'View instructions' | i18n }}
</button>
</td>
<td>{{ address.type }}</td>
<td>{{ address.gateway }}</td>
<td>{{ address.url }}</td>
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
</tr>
}
</table>
</section>
}
`,
imports: [
TableComponent,
PlaceholderComponent,
i18nPipe,
TuiDropdown,
TuiDataList,
AddressActionsComponent,
TuiButton,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceAddressesComponent {
readonly addresses = input.required<MappedServiceInterface['addresses']>()
readonly isRunning = input.required<boolean>()
instructions() {}
}

View File

@@ -0,0 +1,166 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import {
DialogService,
DocsLinkDirective,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import {
TuiAppearance,
TuiButton,
TuiDataList,
TuiDropdown,
TuiLink,
} from '@taiga-ui/core'
import { filter } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/routes/portal/components/form.component'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { ClearnetDomain } from './interface.utils'
@Component({
selector: 'section[clearnetDomains]',
template: `
<header>
{{ 'Clearnet Domains' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/connecting-remotely/clearnet.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'Add' | i18n }}
</button>
</header>
@if (clearnetDomains().length) {
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
@for (domain of clearnetDomains(); track $index) {
<tr>
<td>{{ domain.fqdn }}</td>
<td>{{ domain.authority }}</td>
<td>{{ domain.public ? 'public' : 'private' }}</td>
<td>
<button
tuiIconButton
tuiDropdown
size="s"
appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical"
[tuiAppearanceState]="open ? 'hover' : null"
[(tuiDropdownOpen)]="open"
>
{{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown>
<tui-opt-group>
<button
tuiOption
new
iconStart="@tui.pencil"
(click)="edit(domain)"
>
{{ 'Edit' | i18n }}
</button>
</tui-opt-group>
<tui-opt-group>
<button
tuiOption
new
iconStart="@tui.trash"
class="g-negative"
(click)="remove(domain.fqdn)"
>
{{ 'Delete' | i18n }}
</button>
</tui-opt-group>
</tui-data-list>
</button>
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No clearnet domains' | i18n }}
</app-placeholder>
}
`,
imports: [
TuiButton,
TuiLink,
TuiAppearance,
TableComponent,
PlaceholderComponent,
i18nPipe,
DocsLinkDirective,
TuiDropdown,
TuiDataList,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceClearnetDomainsComponent {
private readonly dialog = inject(DialogService)
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe)
readonly clearnetDomains = input.required<readonly ClearnetDomain[]>()
open = false
async add() {}
async edit(domain: ClearnetDomain) {}
async remove(fqdn: string) {
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Removing').subscribe()
const params = { fqdn }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveDomain({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.osUiRemoveDomain(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
})
}
}

View File

@@ -1,305 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
DialogService,
DocsLinkDirective,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import {
TuiAppearance,
TuiButton,
TuiDataList,
TuiIcon,
TuiLink,
TuiNotification,
} from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client'
import { defaultIfEmpty, firstValueFrom, map } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/routes/portal/components/form.component'
import { AuthorityNamePipe } from 'src/app/routes/portal/components/interfaces/acme.pipe'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { toAuthorityName } from 'src/app/utils/acme'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceActionsComponent } from './actions.component'
import { ClearnetAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe'
type ClearnetForm = {
domain: string
authority: string
}
@Component({
selector: 'section[clearnet]',
template: `
<header>
Clearnet
<tui-icon [tuiTooltip]="tooltip" />
<ng-template #tooltip>
{{
'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.'
| i18n
}}
<a
tuiLink
docsLink
path="/user-manual/connecting-remotely/clearnet.html"
>
{{ 'Learn more' | i18n }}
</a>
</ng-template>
@if (clearnet().length) {
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'Add' | i18n }}
</button>
}
</header>
@if (clearnet().length) {
@if (!isPublic()) {
<tui-notification appearance="negative" [style.margin-bottom]="'1rem'">
{{
'To publish clearnet domains, you must click "Make Public", above.'
| i18n
}}
</tui-notification>
}
<table [appTable]="['Certificate Authority', 'URL', null]">
@for (address of clearnet(); track $index) {
<tr>
<td [style.width.rem]="12">
{{
interface.value().addSsl
? (address.authority | authorityName)
: '-'
}}
</td>
<td [style.order]="-1">{{ address.url | mask }}</td>
<td
actions
[href]="address.url"
[disabled]="!isRunning() || !isPublic()"
>
@if (address.isDomain) {
<button
tuiIconButton
iconStart="@tui.trash"
appearance="flat-grayscale"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
}
@if (address.isDomain) {
<button
tuiOption
tuiAppearance="action-destructive"
iconStart="@tui.trash"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
}
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No public addresses' | i18n }}
<button tuiButton iconStart="@tui.plus" (click)="add()">
{{ 'Add domain' | i18n }}
</button>
</app-placeholder>
}
`,
styles: `
:host-context(tui-root._mobile) {
td {
font-weight: bold;
color: var(--tui-text-primary);
&:first-child {
font-weight: normal;
color: var(--tui-text-secondary);
}
}
}
`,
host: { class: 'g-card' },
imports: [
TuiButton,
TuiIcon,
TuiTooltip,
TuiLink,
TuiDataList,
TuiAppearance,
PlaceholderComponent,
TableComponent,
MaskPipe,
AuthorityNamePipe,
InterfaceActionsComponent,
i18nPipe,
DocsLinkDirective,
TuiNotification,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceClearnetComponent {
private readonly dialog = inject(DialogService)
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
readonly interface = inject(InterfaceComponent)
readonly clearnet = input.required<readonly ClearnetAddress[]>()
readonly isRunning = input.required<boolean>()
readonly isPublic = input.required<boolean>()
readonly authorityUrls = toSignal(
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'network', 'acme')
.pipe(map(acme => Object.keys(acme))),
{ initialValue: [] },
)
async remove({ url }: ClearnetAddress) {
const confirm = await firstValueFrom(
this.dialog
.openConfirm({
label: 'Confirm',
size: 's',
data: {
yes: 'Delete',
no: 'Cancel',
content: 'Are you sure you want to delete this address?',
},
})
.pipe(defaultIfEmpty(false)),
)
if (!confirm) {
return
}
const loader = this.loader.open('Removing').subscribe()
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)) {
url = 'http://' + url
}
const params = { domain: new URL(url).hostname }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveDomain({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.osUiRemoveDomain(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
async add() {
const domain = ISB.Value.text({
name: 'Domain',
description: 'The domain or subdomain you want to use',
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
required: true,
default: null,
patterns: [utils.Patterns.domain],
})
const authority = ISB.Value.select({
name: 'Certificate Authority',
description:
'Select which Certificate authority to use for obtaining your SSL certificate. Add new authority in the System tab. Optionally use your local= Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
values: this.authorityUrls().reduce<Record<string, string>>(
(obj, url) => ({
...obj,
[url]: toAuthorityName(url),
}),
{ local: toAuthorityName(null) },
),
default: '',
})
this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, {
label: 'Select domain',
data: {
spec: await configBuilderToSpec(
ISB.InputSpec.of(
this.interface.value().addSsl ? { domain, authority } : { domain },
),
),
buttons: [
{
text: 'Save',
handler: async value => this.save(value),
},
],
},
})
}
private async save(domainInfo: ClearnetForm): Promise<boolean> {
const loader = this.loader.open('Saving').subscribe()
const { domain, authority } = domainInfo
const params = {
domain,
acme: authority === 'local' ? null : authority,
private: false,
}
try {
if (this.interface.packageId()) {
await this.api.pkgAddDomain({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.osUiAddDomain(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -0,0 +1,35 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { TuiSwitch } from '@taiga-ui/kit'
import { FormsModule } from '@angular/forms'
import { i18nPipe } from '@start9labs/shared'
@Component({
selector: 'section[gateways]',
template: `
<header>{{ 'Gateways' | i18n }}</header>
<ul>
@for (gateway of gateways(); track $index) {
<li>
{{ gateway.name }}
<input
type="checkbox"
tuiSwitch
[style.margin-inline-start]="'auto'"
[showIcons]="false"
[ngModel]="gateway.enabled"
(ngModelChange)="onToggle(gateway)"
/>
</li>
}
<ul></ul>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule, TuiSwitch, i18nPipe],
})
export class InterfaceGatewaysComponent {
readonly gateways = input.required<any>()
async onToggle(event: any) {}
}

View File

@@ -1,43 +1,28 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
import { InterfaceClearnetComponent } from 'src/app/routes/portal/components/interfaces/clearnet.component'
import { InterfaceLocalComponent } from 'src/app/routes/portal/components/interfaces/local.component'
import { InterfaceTorComponent } from 'src/app/routes/portal/components/interfaces/tor.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
import { MappedServiceInterface } from './interface.utils'
import { InterfaceGatewaysComponent } from './gateways.component'
import { InterfaceTorDomainsComponent } from './tor-domains.component'
import { InterfaceClearnetDomainsComponent } from './clearnet-domains.component'
import { InterfaceAddressesComponent } from './addresses/addresses.component'
@Component({
selector: 'app-interface',
selector: 'service-interface',
template: `
<button
tuiButton
size="s"
[appearance]="value().public ? 'primary-destructive' : 'primary-success'"
[iconStart]="value().public ? '@tui.globe-lock' : '@tui.globe'"
(click)="toggle()"
>
{{ value().public ? ('Make private' | i18n) : ('Make public' | i18n) }}
</button>
<section class="g-card" [gateways]="value().gateways"></section>
<section class="g-card" [torDomains]="value().torDomains"></section>
<section
[clearnet]="value().addresses.clearnet"
[isPublic]="value().public"
[isRunning]="isRunning()"
class="g-card"
[clearnetDomains]="value().clearnetDomains"
></section>
<section [tor]="value().addresses.tor" [isRunning]="isRunning()"></section>
<section
[local]="value().addresses.local"
[isRunning]="isRunning()"
class="g-card"
[addresses]="value().addresses"
[isRunning]="true"
></section>
`,
styles: `
:host {
max-width: 56rem;
display: flex;
flex-direction: column;
gap: 1rem;
@@ -48,54 +33,18 @@ import { MappedServiceInterface } from './interface.utils'
overflow-wrap: anywhere;
}
}
button {
margin: -0.5rem auto 0 0;
}
`,
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
imports: [
InterfaceClearnetComponent,
InterfaceTorComponent,
InterfaceLocalComponent,
TuiButton,
i18nPipe,
InterfaceGatewaysComponent,
InterfaceTorDomainsComponent,
InterfaceClearnetDomainsComponent,
InterfaceAddressesComponent,
],
})
export class InterfaceComponent {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
readonly packageId = input('')
readonly value = input.required<MappedServiceInterface>()
readonly isRunning = input.required<boolean>()
async toggle() {
const loader = this.loader
.open(`Making ${this.value().public ? 'private' : 'public'}`)
.subscribe()
const params = {
internalPort: this.value().addressInfo.internalPort,
public: !this.value().public,
}
try {
if (this.packageId()) {
await this.api.pkgBindingSetPubic({
...params,
host: this.value().addressInfo.hostId,
package: this.packageId(),
})
} else {
await this.api.serverBindingSetPubic(params)
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -1,21 +1,11 @@
import { T, utils } from '@start9labs/start-sdk'
import { T } from '@start9labs/start-sdk'
import { ConfigService } from 'src/app/services/config.service'
export abstract class AddressesService {
abstract static: boolean
abstract add(): Promise<void>
abstract remove(): Promise<void>
}
export function getAddresses(
serviceInterface: T.ServiceInterface,
host: T.Host,
config: ConfigService,
): {
clearnet: ClearnetAddress[]
local: LocalAddress[]
tor: TorAddress[]
} {
): MappedServiceInterface['addresses'] {
const addressInfo = serviceInterface.addressInfo
const hostnames =
host.hostnameInfo[addressInfo.internalPort]?.filter(
@@ -46,60 +36,75 @@ export function getAddresses(
}
}
const clearnet: ClearnetAddress[] = []
const local: LocalAddress[] = []
const tor: TorAddress[] = []
const common: Address[] = [
{
type: 'Local',
description: '',
gateway: 'Wire Conenction 1',
url: 'https://test.local:1234',
},
{
type: 'IPv4 (LAN)',
description: '',
gateway: 'Wire Connction 1',
url: 'https://192.168.1.10.local:1234',
},
]
const uncommon: Address[] = [
{
type: 'IPv4 (WAN)',
description: '',
gateway: 'Wire Conenction 1',
url: 'https://72.72.72.72',
},
]
hostnames.forEach(h => {
const addresses = utils.addressHostToUrl(addressInfo, h)
// hostnames.forEach(h => {
// const addresses = utils.addressHostToUrl(addressInfo, h)
addresses.forEach(url => {
if (h.kind === 'onion') {
tor.push({
protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)
? new URL(url).protocol.replace(':', '').toUpperCase()
: null,
url,
})
} else {
const hostnameKind = h.hostname.kind
// addresses.forEach(url => {
// if (h.kind === 'onion') {
// tor.push({
// protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)
// ? new URL(url).protocol.replace(':', '').toUpperCase()
// : null,
// url,
// })
// } else {
// const hostnameKind = h.hostname.kind
if (
h.public ||
(hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public)
) {
clearnet.push({
url,
disabled: !h.public,
isDomain: hostnameKind == 'domain',
authority:
hostnameKind == 'domain'
? host.domains[h.hostname.domain]?.acme || null
: null,
})
} else {
local.push({
nid:
hostnameKind === 'local'
? 'Local'
: `${h.gatewayId} (${hostnameKind})`,
url,
})
}
}
})
})
// if (
// h.public ||
// (hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public)
// ) {
// clearnet.push({
// url,
// disabled: !h.public,
// isDomain: hostnameKind == 'domain',
// authority:
// hostnameKind == 'domain'
// ? host.domains[h.hostname.domain]?.acme || null
// : null,
// })
// } else {
// local.push({
// nid:
// hostnameKind === 'local'
// ? 'Local'
// : `${h.gatewayId} (${hostnameKind})`,
// url,
// })
// }
// }
// })
// })
return {
clearnet: clearnet.filter(
common: common.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
local: local.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
tor: tor.filter(
uncommon: uncommon.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
@@ -107,28 +112,28 @@ export function getAddresses(
}
export type MappedServiceInterface = T.ServiceInterface & {
addSsl?: T.AddSslOptions | null
public: boolean
gateways: {
id: string
name: string
enabled: boolean
}[]
torDomains: string[]
clearnetDomains: ClearnetDomain[]
addresses: {
clearnet: ClearnetAddress[]
local: LocalAddress[]
tor: TorAddress[]
common: Address[]
uncommon: Address[]
}
}
export type ClearnetAddress = {
url: string
export type ClearnetDomain = {
fqdn: string
authority: string | null
isDomain: boolean
disabled: boolean
public: boolean
}
export type LocalAddress = {
export type Address = {
type: string
gateway: string
url: string
nid: string
}
export type TorAddress = {
url: string
protocol: string | null
description: string
}

View File

@@ -1,52 +0,0 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { TuiIcon, TuiLink } from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { InterfaceActionsComponent } from './actions.component'
import { LocalAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
@Component({
selector: 'section[local]',
template: `
<header>
{{ 'Local' | i18n }}
<tui-icon [tuiTooltip]="tooltip" />
<ng-template #tooltip>
{{
'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.'
| i18n
}}
<a tuiLink docsLink path="/user-manual/connecting-locally.html">
{{ 'Learn More' | i18n }}
</a>
</ng-template>
</header>
<table [appTable]="['Network Interface', 'URL', null]">
@for (address of local(); track $index) {
<tr>
<td [style.width.rem]="12">{{ address.nid }}</td>
<td>{{ address.url | mask }}</td>
<td actions [href]="address.url" [disabled]="!isRunning()"></td>
</tr>
}
</table>
`,
host: { class: 'g-card' },
imports: [
TuiIcon,
TuiTooltip,
TuiLink,
TableComponent,
InterfaceActionsComponent,
MaskPipe,
i18nPipe,
DocsLinkDirective,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceLocalComponent {
readonly local = input.required<readonly LocalAddress[]>()
readonly isRunning = input.required<boolean>()
}

View File

@@ -1,26 +0,0 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiBadge } from '@taiga-ui/kit'
@Component({
selector: 'interface-status',
template: `
<tui-badge
size="l"
[iconStart]="public() ? '@tui.globe' : '@tui.lock'"
[appearance]="public() ? 'positive' : 'negative'"
>
{{ public() ? ('Public' | i18n) : ('Private' | i18n) }}
</tui-badge>
`,
styles: `
:host {
display: inline-flex;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiBadge, i18nPipe],
})
export class InterfaceStatusComponent {
readonly public = input(false)
}

View File

@@ -0,0 +1,182 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import {
DialogService,
DocsLinkDirective,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import { TuiAppearance, TuiButton, TuiLink } from '@taiga-ui/core'
import { filter } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/routes/portal/components/form.component'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
type OnionForm = {
key: string
}
@Component({
selector: 'section[torDomains]',
template: `
<header>
<!-- @TODO translation -->
Tor Domains
<a
tuiLink
docsLink
path="/user-manual/connecting-remotely/tor.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'Add' | i18n }}
</button>
</header>
@if (torDomains().length) {
<table [appTable]="['Domain', null]">
@for (domain of torDomains(); track $index) {
<tr>
<td>{{ domain }}</td>
<td>
<button
tuiIconButton
iconStart="@tui.trash"
appearance="action-destructive"
(click)="remove(domain)"
>
{{ 'Delete' | i18n }}
</button>
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No Tor domains' | i18n }}
</app-placeholder>
}
`,
imports: [
TuiButton,
TuiLink,
TuiAppearance,
TableComponent,
PlaceholderComponent,
i18nPipe,
DocsLinkDirective,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceTorDomainsComponent {
private readonly dialog = inject(DialogService)
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe)
readonly torDomains = input.required<readonly string[]>()
async remove(domain: string) {
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean))
.subscribe(async () => {
const loader = this.loader.open('Removing').subscribe()
const params = { onion: domain }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveOnion({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.serverRemoveOnion(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
})
}
async add() {
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
label: 'New Tor domain',
data: {
spec: await configBuilderToSpec(
ISB.InputSpec.of({
key: ISB.Value.text({
name: this.i18n.transform('Private Key (optional)')!,
description: this.i18n.transform(
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.',
),
required: false,
default: null,
patterns: [utils.Patterns.base64],
}),
}),
),
buttons: [
{
text: this.i18n.transform('Save')!,
handler: async value => this.save(value),
},
],
},
})
}
private async save(form: OnionForm): Promise<boolean> {
const loader = this.loader.open('Saving').subscribe()
try {
let onion = form.key
? await this.api.addTorKey({ key: form.key })
: await this.api.generateTorKey({})
onion = `${onion}.onion`
if (this.interface.packageId) {
await this.api.pkgAddOnion({
onion,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.serverAddOnion({ onion })
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -1,233 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
} from '@angular/core'
import {
DialogService,
DocsLinkDirective,
ErrorService,
i18nPipe,
LoadingService,
} from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import {
TuiAppearance,
TuiButton,
TuiIcon,
TuiLink,
TuiOption,
} from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
import {
FormComponent,
FormContext,
} from 'src/app/routes/portal/components/form.component'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceActionsComponent } from './actions.component'
import { TorAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe'
type OnionForm = {
key: string
}
@Component({
selector: 'section[tor]',
template: `
<header>
Tor
<tui-icon [tuiTooltip]="tooltip" />
<ng-template #tooltip>
{{
'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.'
| i18n
}}
<a tuiLink docsLink path="/user-manual/connecting-remotely/tor.html">
{{ 'Learn More' | i18n }}
</a>
</ng-template>
@if (tor().length) {
<button
tuiButton
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="add()"
>
{{ 'Add' | i18n }}
</button>
}
</header>
@if (tor().length) {
<table [appTable]="['Protocol', 'URL', null]">
@for (address of tor(); track $index) {
<tr>
<td [style.width.rem]="12">{{ address.protocol || '-' }}</td>
<td>{{ address.url | mask }}</td>
<td actions [href]="address.url" [disabled]="!isRunning()">
<button
tuiIconButton
iconStart="@tui.trash"
appearance="flat-grayscale"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
<button
tuiOption
tuiAppearance="action-destructive"
iconStart="@tui.trash"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
</td>
</tr>
}
</table>
} @else {
<app-placeholder icon="@tui.app-window">
{{ 'No onion addresses' | i18n }}
<button tuiButton iconStart="@tui.plus" (click)="add()">
{{ 'Add' | i18n }}
</button>
</app-placeholder>
}
`,
styles: `
[tuiFade] {
white-space: nowrap;
max-width: 30rem;
}
`,
host: { class: 'g-card' },
imports: [
TuiButton,
TuiIcon,
TuiTooltip,
TuiLink,
TuiAppearance,
TuiOption,
TableComponent,
PlaceholderComponent,
MaskPipe,
InterfaceActionsComponent,
i18nPipe,
DocsLinkDirective,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceTorComponent {
private readonly dialog = inject(DialogService)
private readonly formDialog = inject(FormDialogService)
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe)
readonly tor = input.required<readonly TorAddress[]>()
readonly isRunning = input.required<boolean>()
async remove({ url }: TorAddress) {
const confirm = await firstValueFrom(
this.dialog
.openConfirm({
label: 'Confirm',
size: 's',
data: {
yes: 'Delete',
no: 'Cancel',
content: 'Are you sure you want to delete this address?',
},
})
.pipe(defaultIfEmpty(false)),
)
if (!confirm) {
return
}
const loader = this.loader.open('Removing').subscribe()
const params = { onion: new URL(url).hostname }
try {
if (this.interface.packageId()) {
await this.api.pkgRemoveOnion({
...params,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.serverRemoveOnion(params)
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
async add() {
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
label: 'New onion address',
data: {
spec: await configBuilderToSpec(
ISB.InputSpec.of({
key: ISB.Value.text({
name: this.i18n.transform('Private Key (optional)')!,
description: this.i18n.transform(
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
),
required: false,
default: null,
patterns: [utils.Patterns.base64],
}),
}),
),
buttons: [
{
text: this.i18n.transform('Save')!,
handler: async value => this.save(value),
},
],
},
})
}
private async save(form: OnionForm): Promise<boolean> {
const loader = this.loader.open('Saving').subscribe()
try {
let onion = form.key
? await this.api.addTorKey({ key: form.key })
: await this.api.generateTorKey({})
onion = `${onion}.onion`
if (this.interface.packageId) {
await this.api.pkgAddOnion({
onion,
package: this.interface.packageId(),
host: this.interface.value().addressInfo.hostId,
})
} else {
await this.api.serverAddOnion({ onion })
}
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
}

View File

@@ -7,7 +7,7 @@ import {
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiIcon } from '@taiga-ui/core'
import { TuiButton } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit'
import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -21,13 +21,6 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
<td>
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
</td>
<td [style.text-align]="'center'">
@if (info.public) {
<tui-icon class="g-positive" icon="@tui.globe" />
} @else {
<tui-icon class="g-negative" icon="@tui.lock" />
}
</td>
<td class="g-secondary" [style.grid-area]="'2 / span 4'">
{{ info.description }}
</td>
@@ -86,7 +79,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiBadge, TuiIcon, RouterLink],
imports: [TuiButton, TuiBadge, RouterLink],
})
export class ServiceInterfaceItemComponent {
private readonly config = inject(ConfigService)
@@ -94,7 +87,6 @@ export class ServiceInterfaceItemComponent {
@Input({ required: true })
info!: T.ServiceInterface & {
public: boolean
routerLink: string
}

View File

@@ -2,27 +2,23 @@ import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
} from '@angular/core'
import { TuiTable } from '@taiga-ui/addon-table'
import { tuiDefaultSort } from '@taiga-ui/cdk'
import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { getAddresses } from '../../../components/interfaces/interface.utils'
import { ServiceInterfaceItemComponent } from './interface-item.component'
import { i18nPipe } from '@start9labs/shared'
@Component({
selector: 'service-interfaces',
template: `
<header>{{ 'Interfaces' | i18n }}</header>
<header>{{ 'Service Interfaces' | i18n }}</header>
<table tuiTable class="g-table">
<thead>
<tr>
<th tuiTh>{{ 'Name' | i18n }}</th>
<th tuiTh>{{ 'Type' | i18n }}</th>
<th tuiTh [style.text-align]="'center'">{{ 'Hosting' | i18n }}</th>
<th tuiTh>{{ 'Description' | i18n }}</th>
<th tuiTh></th>
</tr>
@@ -49,8 +45,6 @@ import { i18nPipe } from '@start9labs/shared'
imports: [ServiceInterfaceItemComponent, TuiTable, i18nPipe],
})
export class ServiceInterfacesComponent {
private readonly config = inject(ConfigService)
readonly pkg = input.required<PackageDataEntry>()
readonly disabled = input(false)
@@ -58,14 +52,8 @@ export class ServiceInterfacesComponent {
Object.entries(serviceInterfaces)
.sort((a, b) => tuiDefaultSort(a[1], b[1]))
.map(([id, value]) => {
const host = hosts[value.addressInfo.hostId]
const port = value.addressInfo.internalPort
return {
...value,
addSsl: host?.bindings[port]?.options.addSsl,
public: !!host?.bindings[port]?.net.public,
addresses: host ? getAddresses(value, host, this.config) : {},
routerLink: `./interface/${id}`,
}
}),

View File

@@ -16,7 +16,6 @@ import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
@@ -28,10 +27,6 @@ import { TitleDirective } from 'src/app/services/title.service'
{{ 'Back' | i18n }}
</a>
{{ interface()?.name }}
<interface-status
[style.margin-left.rem]="0.5"
[public]="!!interface()?.public"
/>
</ng-container>
<tui-breadcrumbs size="l">
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
@@ -47,12 +42,11 @@ import { TitleDirective } from 'src/app/services/title.service'
<tui-badge size="l" [appearance]="getAppearance(value.type)">
{{ value.type }}
</tui-badge>
<interface-status [public]="value.public" />
</h3>
<p tuiSubtitle>{{ value.description }}</p>
</hgroup>
</header>
<app-interface
<service-interface
[packageId]="pkgId"
[value]="value"
[isRunning]="isRunning()"
@@ -86,7 +80,6 @@ import { TitleDirective } from 'src/app/services/title.service'
TuiBreadcrumbs,
TuiItem,
TuiLink,
InterfaceStatusComponent,
i18nPipe,
TuiBadge,
TuiHeader,
@@ -127,9 +120,10 @@ export default class ServiceInterfaceRoute {
return {
...item,
addSsl: host?.bindings[port]?.options.addSsl,
public: !!host?.bindings[port]?.net.public,
addresses: getAddresses(item, host, this.config),
gateways: [],
torDomains: [],
clearnetDomains: [],
}
})

View File

@@ -0,0 +1,57 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TitleDirective } from 'src/app/services/title.service'
import { AuthorityService } from './authority.service'
import { AuthoritiesTableComponent } from './table.component'
@Component({
template: `
<ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
{{ 'Back' | i18n }}
</a>
{{ 'Certificate Authorities' | i18n }}
</ng-container>
<section class="g-card">
<header>
{{ 'Certificate Authorities' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/authorities.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
@if (authorityService.authorities(); as authorities) {
<button
tuiButton
size="xs"
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="authorityService.add(authorities)"
>
{{ 'Add' | i18n }}
</button>
}
</header>
<authorities-table />
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiButton,
TuiLink,
RouterLink,
TitleDirective,
i18nPipe,
DocsLinkDirective,
AuthoritiesTableComponent,
],
providers: [AuthorityService],
})
export default class SystemAuthoritiesComponent {
protected readonly authorityService = inject(AuthorityService)
}

View File

@@ -14,7 +14,6 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { PatchDB } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { toAuthorityName } from 'src/app/utils/acme'
import { parse } from 'tldts'
import { RR } from 'src/app/services/api/api.types'
import { DNS } from './dns.component'
@@ -29,10 +28,6 @@ export type MappedDomain = {
name: string | null
ipInfo: T.IpInfo | null
}
authority: {
url: string | null
name: string | null
}
}
@Injectable()
@@ -64,19 +59,8 @@ export class DomainService {
id: gateway,
ipInfo: gateways[gateway]?.ipInfo || null,
},
authority: {
url: acme,
name: toAuthorityName(acme),
},
}) as MappedDomain,
),
authorities: Object.keys(acme).reduce<Record<string, string>>(
(obj, url) => ({
...obj,
[url]: toAuthorityName(url),
}),
{ local: toAuthorityName(null) },
),
})),
),
)
@@ -91,7 +75,7 @@ export class DomainService {
default: null,
patterns: [utils.Patterns.domain],
}),
...this.gatewaysAndAuthorities(),
...this.gatewaysSpec(),
})
this.formDialog.open(FormComponent, {
@@ -105,7 +89,6 @@ export class DomainService {
this.save({
fqdn: input.fqdn,
gateway: input.gateway,
acme: input.authority === 'local' ? null : input.authority,
}),
},
],
@@ -115,7 +98,7 @@ export class DomainService {
async edit(domain: MappedDomain) {
const editSpec = ISB.InputSpec.of({
...this.gatewaysAndAuthorities(),
...this.gatewaysSpec(),
})
this.formDialog.open(FormComponent, {
@@ -129,13 +112,11 @@ export class DomainService {
this.save({
fqdn: domain.fqdn,
gateway: input.gateway,
acme: input.authority === 'local' ? null : input.authority,
}),
},
],
value: {
gateway: domain.gateway.id,
authority: domain.authority.url || 'local',
},
},
})
@@ -178,22 +159,14 @@ export class DomainService {
}
}
private gatewaysAndAuthorities() {
private gatewaysSpec() {
return {
gateway: ISB.Value.select({
name: 'Gateway',
description:
'Select the public gateway for this domain. Whichever gateway you select is the IP address that will be exposed to the Internet.',
description: 'Select which gateway to use for this domain.',
values: this.data()!.gateways,
default: '',
}),
authority: ISB.Value.select({
name: 'Default Certificate Authority',
description:
'Select the default certificate authority that will sign certificates for this domain. You can override this on a case-by-case basis.',
values: this.data()!.authorities,
default: '',
}),
}
}
}

View File

@@ -1,13 +1,10 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { RouterLink } from '@angular/router'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { TuiButton, TuiLink } from '@taiga-ui/core'
import { TitleDirective } from 'src/app/services/title.service'
import { AuthorityService } from './authorities/authority.service'
import { DomainService } from './domains/domain.service'
import { DomainsTableComponent } from './domains/table.component'
import { AuthoritiesTableComponent } from './authorities/table.component'
import { DomainService } from './domain.service'
import { DomainsTableComponent } from './table.component'
@Component({
template: `
@@ -17,48 +14,18 @@ import { AuthoritiesTableComponent } from './authorities/table.component'
</a>
{{ 'Domains' | i18n }}
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ 'Domains' | i18n }}</h3>
<p tuiSubtitle>
{{
'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.'
| i18n
}}
<a
tuiLink
docsLink
path="/user-manual/domains.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions' | i18n"
></a>
</p>
</hgroup>
</header>
<section class="g-card">
<header>
{{ 'Certificate Authorities' | i18n }}
@if (authorityService.authorities(); as authorities) {
<button
tuiButton
size="xs"
iconStart="@tui.plus"
[style.margin-inline-start]="'auto'"
(click)="authorityService.add(authorities)"
>
{{ 'Add' | i18n }}
</button>
}
</header>
<authorities-table />
</section>
<section class="g-card">
<header>
{{ 'Domains' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/domains.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
@if (domainService.data(); as value) {
<button
tuiButton
@@ -77,19 +44,15 @@ import { AuthoritiesTableComponent } from './authorities/table.component'
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiButton,
TuiTitle,
TuiHeader,
TuiLink,
RouterLink,
TitleDirective,
i18nPipe,
DocsLinkDirective,
DomainsTableComponent,
AuthoritiesTableComponent,
],
providers: [AuthorityService, DomainService],
providers: [DomainService],
})
export default class SystemDomainsComponent {
protected readonly authorityService = inject(AuthorityService)
protected readonly domainService = inject(DomainService)
}

View File

@@ -19,7 +19,6 @@ import { DomainService, MappedDomain } from './domain.service'
@if (domain(); as domain) {
<td>{{ domain.fqdn }}</td>
<td [style.order]="-1">{{ domain.gateway.ipInfo?.name || '-' }}</td>
<td>{{ domain.authority.name }}</td>
<td>
<button
tuiIconButton

View File

@@ -9,9 +9,7 @@ import { DomainService } from './domain.service'
@Component({
selector: 'domains-table',
template: `
<table
[appTable]="['Domain', 'Gateway', 'Default Certificate Authority', null]"
>
<table [appTable]="['Domain', 'Gateway', null]">
@for (domain of domainService.data()?.domains; track $index) {
<tr [domain]="domain"></tr>
} @empty {

View File

@@ -31,31 +31,21 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
</a>
{{ 'Email' | i18n }}
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ 'Email' | i18n }}</h3>
<p tuiSubtitle>
{{
'Connecting an external SMTP server allows StartOS and your installed services to send you emails.'
| i18n
}}
<a
tuiLink
docsLink
path="/user-manual/smtp.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions' | i18n"
></a>
</p>
</hgroup>
</header>
@if (form$ | async; as form) {
<form [formGroup]="form">
<header tuiHeader="body-l">
<h3 tuiTitle>
<b>{{ 'SMTP Credentials' | i18n }}</b>
<b>
{{ 'SMTP Credentials' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/smtp.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
</b>
</h3>
</header>
@if (spec | async; as resolved) {

View File

@@ -15,7 +15,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { GatewaysTableComponent } from './table.component'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { TitleDirective } from 'src/app/services/title.service'
import { TuiHeader } from '@taiga-ui/layout'
import { map } from 'rxjs'
import { ISB } from '@start9labs/start-sdk'
import { GatewayPlus } from './item.component'
@@ -28,30 +27,18 @@ import { GatewayPlus } from './item.component'
</a>
{{ 'Gateways' | i18n }}
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ 'Gateways' | i18n }}</h3>
<p tuiSubtitle>
{{
'Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic.'
| i18n
}}
<a
tuiLink
docsLink
path="/user-manual/gateways.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions' | i18n"
></a>
</p>
</hgroup>
</header>
<section class="g-card">
<header>
{{ 'Gateways' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/gateways.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
<button
tuiButton
size="xs"
@@ -70,7 +57,6 @@ import { GatewayPlus } from './item.component'
CommonModule,
TuiButton,
GatewaysTableComponent,
TuiHeader,
TitleDirective,
i18nPipe,
TuiLink,

View File

@@ -38,7 +38,7 @@ import {
TuiButtonSelect,
TuiDataListWrapper,
} from '@taiga-ui/kit'
import { TuiCell, tuiCellOptionsProvider, TuiHeader } from '@taiga-ui/layout'
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PatchDB } from 'patch-db-client'
import { filter } from 'rxjs'
@@ -59,14 +59,6 @@ import { SystemWipeComponent } from './wipe.component'
</a>
{{ 'General Settings' | i18n }}
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ 'General Settings' | i18n }}</h3>
<p tuiSubtitle>
{{ 'Manage your overall setup and preferences' | i18n }}
</p>
</hgroup>
</header>
@if (server(); as server) {
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.zap" />
@@ -138,16 +130,6 @@ import { SystemWipeComponent } from './wipe.component'
/>
</button>
</div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.award" />
<span tuiTitle>
<strong>{{ 'Root Certificate Authority' | i18n }}</strong>
<span tuiSubtitle>{{ 'Download your Root CA' | i18n }}</span>
</span>
<button tuiButton iconStart="@tui.download" (click)="downloadCA()">
{{ 'Download' | i18n }}
</button>
</div>
<div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.monitor" />
<span tuiTitle>
@@ -205,8 +187,6 @@ import { SystemWipeComponent } from './wipe.component'
src="assets/img/icons/snek.png"
/>
}
<!-- hidden element for downloading cert -->
<a id="download-ca" href="/static/local-root-ca.crt"></a>
`,
styles: `
:host {
@@ -239,7 +219,6 @@ import { SystemWipeComponent } from './wipe.component'
RouterLink,
i18nPipe,
TuiTitle,
TuiHeader,
TuiCell,
TuiAppearance,
TuiButton,
@@ -347,10 +326,6 @@ export default class SystemGeneralComponent {
.subscribe(() => this.resetTor(this.wipe))
}
downloadCA() {
this.document.getElementById('download-ca')?.click()
}
async tryToggleKiosk() {
if (
this.server()?.kiosk &&

View File

@@ -31,13 +31,10 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
<hgroup tuiTitle>
<h3>{{ 'Change Password' | i18n }}</h3>
<p tuiSubtitle>
{{ 'Change your StartOS master password.' | i18n }}
<strong>
{{
'You will still need your current password to decrypt existing backups!'
| i18n
}}
</strong>
{{
'You will still need your current password to decrypt existing backups!'
| i18n
}}
</p>
</hgroup>
</header>

View File

@@ -7,8 +7,7 @@ import {
} from '@angular/core'
import { RouterLink } from '@angular/router'
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { TuiButton } from '@taiga-ui/core'
import { from, map, merge, Observable, Subject } from 'rxjs'
import { Session } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -21,17 +20,7 @@ import { SessionsTableComponent } from './table.component'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
{{ 'Active Sessions' | i18n }}
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ 'Active Sessions' | i18n }}</h3>
<p tuiSubtitle>
{{
'A session is a device that is currently logged into StartOS. For best security, terminate sessions you do not recognize or no longer use.'
| i18n
}}
</p>
</hgroup>
</header>
<section class="g-card">
<header>{{ 'Current session' | i18n }}</header>
<div [single]="true" [sessions]="current$ | async"></div>
@@ -62,8 +51,6 @@ import { SessionsTableComponent } from './table.component'
SessionsTableComponent,
RouterLink,
TitleDirective,
TuiHeader,
TuiTitle,
i18nPipe,
],
})

View File

@@ -14,8 +14,7 @@ import {
LoadingService,
} from '@start9labs/shared'
import { ISB } from '@start9labs/start-sdk'
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { TuiButton, TuiLink } from '@taiga-ui/core'
import { filter, from, merge, Subject } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { SSHKey } from 'src/app/services/api/api.types'
@@ -33,30 +32,18 @@ import { SSHTableComponent } from './table.component'
</a>
SSH
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>SSH</h3>
<p tuiSubtitle>
{{
'By default, you can SSH into your server from any device using your master password. Optionally add SSH public keys to grant specific devices access without needing to enter a password.'
| i18n
}}
<a
tuiLink
docsLink
path="/user-manual/ssh.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions' | i18n"
></a>
</p>
</hgroup>
</header>
@let keys = keys$ | async;
<section class="g-card">
<header>
Saved Keys
{{ 'SSH Keys' | i18n }}
<a
tuiLink
docsLink
path="/user-manual/ssh.html"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
></a>
<button
tuiButton
size="xs"
@@ -95,8 +82,6 @@ import { SSHTableComponent } from './table.component'
SSHTableComponent,
RouterLink,
TitleDirective,
TuiHeader,
TuiTitle,
TuiLink,
i18nPipe,
DocsLinkDirective,
@@ -118,7 +103,7 @@ export default class SystemSSHComponent {
async add(all: readonly SSHKey[]) {
this.formDialog.open(FormComponent, {
label: 'Add SSH Public Key',
label: 'Add SSH key',
data: {
spec: await configBuilderToSpec(SSHSpec),
buttons: [

View File

@@ -47,7 +47,7 @@ import { SSHKey } from 'src/app/services/api/api.types'
} @empty {
@if (keys()) {
<tr>
<td colspan="5">{{ 'No keys' | i18n }}</td>
<td colspan="5">{{ 'No SSH keys' | i18n }}</td>
</tr>
} @else {
@for (i of ['', '']; track $index) {

View File

@@ -14,7 +14,6 @@ import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
@@ -26,19 +25,17 @@ import { TitleDirective } from 'src/app/services/title.service'
{{ 'Back' | i18n }}
</a>
{{ iface.name }}
<interface-status [style.margin-left.rem]="0.5" [public]="public()" />
</ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>
{{ iface.name }}
<interface-status [public]="public()" />
</h3>
<p tuiSubtitle>{{ iface.description }}</p>
</hgroup>
</header>
@if (ui(); as ui) {
<app-interface [value]="ui" [isRunning]="true" />
<service-interface [value]="ui" [isRunning]="true" />
}
`,
host: { class: 'g-subpage' },
@@ -50,7 +47,6 @@ import { TitleDirective } from 'src/app/services/title.service'
TitleDirective,
TuiHeader,
TuiTitle,
InterfaceStatusComponent,
i18nPipe,
],
})
@@ -81,17 +77,14 @@ export default class StartOsUiComponent {
.watch$('serverInfo', 'network', 'host')
.pipe(
map(host => {
const port = this.iface.addressInfo.internalPort
return {
...this.iface,
addSsl: host.bindings[port]?.options.addSsl,
public: !!host.bindings[port]?.net.public,
addresses: getAddresses(this.iface, host, this.config),
gateways: [],
torDomains: [],
clearnetDomains: [],
}
}),
),
)
readonly public = computed((ui = this.ui()) => !!ui?.public)
}

View File

@@ -34,7 +34,7 @@ import { map } from 'rxjs'
<span tuiTitle>
<span>
{{ page.item | i18n }}
@if (page.item === 'General' && badge()) {
@if (page.item === 'General Settings' && badge()) {
<tui-badge-notification>{{ badge() }}</tui-badge-notification>
}
</span>

View File

@@ -1,10 +1,8 @@
import { i18nKey } from '@start9labs/shared'
export const SYSTEM_MENU = [
[
{
icon: '@tui.settings',
item: 'General',
icon: '@tui.wrench',
item: 'General Settings',
link: 'general',
},
],
@@ -43,6 +41,11 @@ export const SYSTEM_MENU = [
item: 'Gateways',
link: 'gateways',
},
{
icon: '@tui.award',
item: 'Certificate Authorities',
link: 'authorities',
},
{
icon: '@tui.globe',
item: 'Domains',
@@ -57,7 +60,7 @@ export const SYSTEM_MENU = [
},
{
icon: '@tui.terminal',
item: 'SSH' as i18nKey,
item: 'SSH Keys',
link: 'ssh',
},
{

View File

@@ -71,6 +71,12 @@ export default [
path: 'gateways',
loadComponent: () => import('./routes/gateways/gateways.component'),
},
{
path: 'authorities',
title: titleResolver,
loadComponent: () =>
import('./routes/authorities/authorities.component'),
},
{
path: 'domains',
title: titleResolver,

View File

@@ -239,7 +239,6 @@ export namespace RR {
export type AddDomainReq = {
fqdn: string
gateway: string
acme: string | null
} // net.domain.add
export type AddDomainRes = null
@@ -251,7 +250,7 @@ export namespace RR {
export type TestDomainReq = {
fqdn: string
gateway: string
} // net.domain.test
} // net.domain.test-dns
export type TestDomainRes = {
root: boolean
wildcard: boolean
@@ -293,12 +292,13 @@ export namespace RR {
export type GenerateTorKeyReq = {} // net.tor.key.generate
export type AddTorKeyRes = string // onion address without .onion suffix
export type ServerBindingSetPublicReq = {
// server.host.binding.set-public
internalPort: number
public: boolean | null // default true
export type ServerBindingToggleGatewayReq = {
// server.host.binding.set-gateway-enabled
gateway: T.GatewayId
internalPort: 80
enabled: boolean
}
export type BindingSetPublicRes = null
export type ServerBindingToggleGatewayRes = null
export type ServerAddOnionReq = {
// server.host.address.onion.add
@@ -311,23 +311,25 @@ export namespace RR {
export type OsUiAddDomainReq = {
// server.host.address.domain.add
domain: string // FQDN
fqdn: string // FQDN
private: boolean
acme: string | null // Url | null
acme: string | null // URL. null means local Root CA
}
export type OsUiAddDomainRes = null
export type OsUiRemoveDomainReq = {
// server.host.address.domain.remove
domain: string // FQDN
fqdn: string // FQDN
}
export type OsUiRemoveDomainRes = null
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
// package.host.binding.set-public
export type PkgBindingToggleGatewayReq = ServerBindingToggleGatewayReq & {
// package.host.binding.set-gateway-enabled
internalPort: number
package: T.PackageId // string
host: T.HostId // string
}
export type PkgBindingToggleGatewayRes = null
export type PkgAddOnionReq = ServerAddOnionReq & {
// package.host.address.onion.add

View File

@@ -359,9 +359,9 @@ export abstract class ApiService {
params: RR.GenerateTorKeyReq,
): Promise<RR.AddTorKeyRes>
abstract serverBindingSetPubic(
params: RR.ServerBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes>
abstract serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes>
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
@@ -377,9 +377,9 @@ export abstract class ApiService {
params: RR.OsUiRemoveDomainReq,
): Promise<RR.OsUiRemoveDomainRes>
abstract pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes>
abstract pkgBindingToggleGateway(
params: RR.PkgBindingToggleGatewayReq,
): Promise<RR.PkgBindingToggleGatewayRes>
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>

View File

@@ -369,7 +369,7 @@ export class LiveApiService extends ApiService {
}
async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
return this.rpcRequest({ method: 'net.domain.test', params })
return this.rpcRequest({ method: 'net.domain.test-dns', params })
}
// wifi
@@ -638,11 +638,11 @@ export class LiveApiService extends ApiService {
})
}
async serverBindingSetPubic(
params: RR.ServerBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
async serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes> {
return this.rpcRequest({
method: 'server.host.binding.set-public',
method: 'server.host.binding.set-gateway-enabled',
params,
})
}
@@ -681,11 +681,11 @@ export class LiveApiService extends ApiService {
})
}
async pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
async pkgBindingToggleGateway(
params: RR.PkgBindingToggleGatewayReq,
): Promise<RR.PkgBindingToggleGatewayRes> {
return this.rpcRequest({
method: 'package.host.binding.set-public',
method: 'package.host.binding.set-gateway-enabled',
params,
})
}

View File

@@ -613,7 +613,6 @@ export class MockApiService extends ApiService {
value: {
[params.fqdn]: {
gateway: params.gateway,
acme: params.acme,
},
},
},
@@ -1369,16 +1368,16 @@ export class MockApiService extends ApiService {
return 'abcdefghijklmnopqrstuv'
}
async serverBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
async serverBindingToggleGateway(
params: RR.ServerBindingToggleGatewayReq,
): Promise<RR.ServerBindingToggleGatewayRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: `/serverInfo/host/bindings/${params.internalPort}/net/public`,
value: params.public,
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
value: params.enabled ? [params.gateway] : [],
},
]
this.mockRevision(patch)
@@ -1443,7 +1442,7 @@ export class MockApiService extends ApiService {
op: PatchOp.ADD,
path: `/serverInfo/host/domains`,
value: {
[params.domain]: { public: !params.private, acme: params.acme },
[params.fqdn]: { public: !params.private, acme: params.acme },
},
},
{
@@ -1455,7 +1454,7 @@ export class MockApiService extends ApiService {
public: false,
hostname: {
kind: 'domain',
domain: params.domain,
domain: params.fqdn,
subdomain: null,
port: null,
sslPort: 443,
@@ -1476,7 +1475,7 @@ export class MockApiService extends ApiService {
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/serverInfo/host/domains/${params.domain}`,
path: `/serverInfo/host/domains/${params.fqdn}`,
},
{
op: PatchOp.REMOVE,
@@ -1488,16 +1487,16 @@ export class MockApiService extends ApiService {
return null
}
async pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
async pkgBindingToggleGateway(
params: RR.PkgBindingToggleGatewayReq,
): Promise<RR.PkgBindingToggleGatewayRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`,
value: params.public,
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
value: params.enabled ? [] : [params.gateway],
},
]
this.mockRevision(patch)
@@ -1560,7 +1559,7 @@ export class MockApiService extends ApiService {
op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
value: {
[params.domain]: { public: !params.private, acme: params.acme },
[params.fqdn]: { public: !params.private, acme: params.acme },
},
},
{
@@ -1572,7 +1571,7 @@ export class MockApiService extends ApiService {
public: false,
hostname: {
kind: 'domain',
domain: params.domain,
domain: params.fqdn,
subdomain: null,
port: null,
sslPort: 443,
@@ -1593,7 +1592,7 @@ export class MockApiService extends ApiService {
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`,
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.fqdn}`,
},
{
op: PatchOp.REMOVE,