mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
start service interface page, WIP
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 l’onglet 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 qu’aux 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 l’adresse 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 d’une 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 l’utiliser, 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 d’hô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: 'L’interface web de votre serveur StartOS, accessible depuis n’importe 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 n’utilisez 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 l’onglet 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 n’importe quel appareil en utilisant votre mot de passe maître. Vous pouvez également ajouter des clés publiques SSH pour accorder l’accè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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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: '',
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user