mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
certificate authorities
This commit is contained in:
@@ -179,7 +179,6 @@ export default {
|
|||||||
177: 'Kernelspeicher',
|
177: 'Kernelspeicher',
|
||||||
178: 'Leerlauf',
|
178: 'Leerlauf',
|
||||||
179: 'I/O-Wartezeit',
|
179: 'I/O-Wartezeit',
|
||||||
180: 'ACME',
|
|
||||||
181: 'Gesamt',
|
181: 'Gesamt',
|
||||||
182: 'Verwendet',
|
182: 'Verwendet',
|
||||||
183: 'Verfügbar',
|
183: 'Verfügbar',
|
||||||
@@ -294,12 +293,12 @@ export default {
|
|||||||
296: 'Hochladen',
|
296: 'Hochladen',
|
||||||
297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.',
|
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',
|
298: 'Ungültige Paketdatei',
|
||||||
299: 'Fügen Sie ACME-Anbieter hinzu, um SSL-(https)-Zertifikate für den Clearnet-Zugriff zu generieren.',
|
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',
|
300: 'Anleitung anzeigen',
|
||||||
303: 'Kontakt',
|
303: 'Kontakt',
|
||||||
304: 'Bearbeiten',
|
304: 'Bearbeiten',
|
||||||
305: 'ACME-Anbieter hinzufügen',
|
305: 'Zertifizierungsstelle hinzufügen',
|
||||||
306: 'ACME-Anbieter bearbeiten',
|
306: 'Kontaktinformationen bearbeiten',
|
||||||
307: 'Kontakt-E-Mails',
|
307: 'Kontakt-E-Mails',
|
||||||
308: 'Erforderlich, um ein Zertifikat von einer Zertifizierungsstelle zu erhalten',
|
308: 'Erforderlich, um ein Zertifikat von einer Zertifizierungsstelle zu erhalten',
|
||||||
309: 'Alle umschalten',
|
309: 'Alle umschalten',
|
||||||
@@ -532,12 +531,12 @@ export default {
|
|||||||
536: 'Umbenennen',
|
536: 'Umbenennen',
|
||||||
537: 'Zugriff',
|
537: 'Zugriff',
|
||||||
538: 'Domains',
|
538: 'Domains',
|
||||||
539: 'ACME-Anbieter',
|
539: 'Zertifizierungsstellen',
|
||||||
540: 'Domain',
|
540: 'Domain',
|
||||||
541: 'Gateway',
|
541: 'Gateway',
|
||||||
542: 'Standard-ACME',
|
542: 'Standard-Zertifizierungsstelle',
|
||||||
543: 'Gateway ändern',
|
543: 'Zertifizierungsstelle',
|
||||||
544: 'Standard-ACME ändern',
|
544: 'Domain bearbeiten',
|
||||||
545: 'Keine Domains',
|
545: 'Keine Domains',
|
||||||
546: 'Anbieter',
|
546: 'Anbieter',
|
||||||
547: 'DNS anzeigen',
|
547: 'DNS anzeigen',
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ export const ENGLISH = {
|
|||||||
'Kernel space': 177,
|
'Kernel space': 177,
|
||||||
'Idle': 178, // a CPU metric
|
'Idle': 178, // a CPU metric
|
||||||
'I/O wait': 179,
|
'I/O wait': 179,
|
||||||
'ACME': 180,
|
|
||||||
'Total': 181,
|
'Total': 181,
|
||||||
'Used': 182,
|
'Used': 182,
|
||||||
'Available': 183,
|
'Available': 183,
|
||||||
@@ -293,12 +292,12 @@ export const ENGLISH = {
|
|||||||
'Upload': 296,
|
'Upload': 296,
|
||||||
'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297,
|
'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,
|
'Invalid package file': 298,
|
||||||
'Add ACME providers in order to generate SSL (https) certificates for clearnet access.': 299,
|
'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,
|
'View instructions': 300,
|
||||||
'Contact': 303, // as in, "contact us"
|
'Contact': 303, // as in, "contact us"
|
||||||
'Edit': 304,
|
'Edit': 304,
|
||||||
'Add ACME Provider': 305,
|
'Add Certificate Authority': 305,
|
||||||
'Edit ACME Provider': 306,
|
'Edit Contact Info': 306,
|
||||||
'Contact Emails': 307,
|
'Contact Emails': 307,
|
||||||
'Needed to obtain a certificate from a Certificate Authority': 308,
|
'Needed to obtain a certificate from a Certificate Authority': 308,
|
||||||
'Toggle all': 309,
|
'Toggle all': 309,
|
||||||
@@ -531,12 +530,12 @@ export const ENGLISH = {
|
|||||||
'Rename': 536,
|
'Rename': 536,
|
||||||
'Access': 537, // as in, public or private access, almost "permission"
|
'Access': 537, // as in, public or private access, almost "permission"
|
||||||
'Domains': 538, // as in, internet domains
|
'Domains': 538, // as in, internet domains
|
||||||
'ACME Providers': 539,
|
'Certificate Authorities': 539,
|
||||||
'Domain': 540, // as in, an internat domain name
|
'Domain': 540, // as in, an internat domain name
|
||||||
'Gateway': 541, // as in, a device or software that connects two different networks
|
'Gateway': 541, // as in, a device or software that connects two different networks
|
||||||
'Default ACME': 542, // as in, the default ACME provider for signing certificates
|
'Default Certificate Authority': 542,
|
||||||
'Change gateway': 543, // as in, change the network gateway for a computer
|
'Certificate Authority': 543,
|
||||||
'Change default ACME': 544, // as in, change the default ACME provider for a domain
|
'Edit Domain': 544,
|
||||||
'No domains': 545,
|
'No domains': 545,
|
||||||
'Provider': 546,
|
'Provider': 546,
|
||||||
'Show DNS': 547,
|
'Show DNS': 547,
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export default {
|
|||||||
177: 'Espacio del kernel',
|
177: 'Espacio del kernel',
|
||||||
178: 'Inactivo',
|
178: 'Inactivo',
|
||||||
179: 'Espera de E/S',
|
179: 'Espera de E/S',
|
||||||
180: 'ACME',
|
|
||||||
181: 'Total',
|
181: 'Total',
|
||||||
182: 'Usado',
|
182: 'Usado',
|
||||||
183: 'Disponible',
|
183: 'Disponible',
|
||||||
@@ -294,12 +293,13 @@ export default {
|
|||||||
296: 'Subir',
|
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.',
|
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',
|
298: 'Archivo de paquete inválido',
|
||||||
299: 'Agrega proveedores ACME para generar certificados SSL (https) para el acceso desde clearnet.',
|
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',
|
300: 'Ver instrucciones',
|
||||||
303: 'Contacto',
|
303: 'Contacto',
|
||||||
304: 'Editar',
|
304: 'Editar',
|
||||||
305: 'Agregar proveedor ACME',
|
305: 'Agregar autoridad certificadora',
|
||||||
306: 'Editar proveedor ACME',
|
306: 'Editar información de contacto',
|
||||||
307: 'Correos de contacto',
|
307: 'Correos de contacto',
|
||||||
308: 'Necesarios para obtener un certificado de una Autoridad Certificadora',
|
308: 'Necesarios para obtener un certificado de una Autoridad Certificadora',
|
||||||
309: 'Alternar todo',
|
309: 'Alternar todo',
|
||||||
@@ -532,12 +532,12 @@ export default {
|
|||||||
536: 'Renombrar',
|
536: 'Renombrar',
|
||||||
537: 'Acceso',
|
537: 'Acceso',
|
||||||
538: 'Dominios',
|
538: 'Dominios',
|
||||||
539: 'Proveedores ACME',
|
539: 'Autoridades certificadoras',
|
||||||
540: 'Dominio',
|
540: 'Dominio',
|
||||||
541: 'Puerta de enlace',
|
541: 'Puerta de enlace',
|
||||||
542: 'ACME predeterminado',
|
542: 'Autoridad certificadora predeterminada',
|
||||||
543: 'Cambiar puerta de enlace',
|
543: 'Autoridad certificadora',
|
||||||
544: 'Cambiar ACME predeterminado',
|
544: 'Editar dominio',
|
||||||
545: 'Sin dominios',
|
545: 'Sin dominios',
|
||||||
546: 'Proveedor',
|
546: 'Proveedor',
|
||||||
547: 'Mostrar DNS',
|
547: 'Mostrar DNS',
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export default {
|
|||||||
177: 'Espace noyau',
|
177: 'Espace noyau',
|
||||||
178: 'Inactif',
|
178: 'Inactif',
|
||||||
179: 'Attente E/S',
|
179: 'Attente E/S',
|
||||||
180: 'ACME',
|
|
||||||
181: 'Total',
|
181: 'Total',
|
||||||
182: 'Utilisé',
|
182: 'Utilisé',
|
||||||
183: 'Disponible',
|
183: 'Disponible',
|
||||||
@@ -294,12 +293,12 @@ export default {
|
|||||||
296: 'Téléverser',
|
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.',
|
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',
|
298: 'Fichier paquet invalide',
|
||||||
299: 'Ajoutez des fournisseurs ACME pour générer des certificats SSL (https) pour l’accès clearnet.',
|
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',
|
300: 'Voir les instructions',
|
||||||
303: 'Contact',
|
303: 'Contact',
|
||||||
304: 'Modifier',
|
304: 'Modifier',
|
||||||
305: 'Ajouter un fournisseur ACME',
|
305: 'Ajouter une autorité de certification',
|
||||||
306: 'Modifier le fournisseur ACME',
|
306: 'Modifier les informations de contact',
|
||||||
307: 'Emails de contact',
|
307: 'Emails de contact',
|
||||||
308: 'Nécessaire pour obtenir un certificat d’une autorité de certification',
|
308: 'Nécessaire pour obtenir un certificat d’une autorité de certification',
|
||||||
309: 'Tout cocher',
|
309: 'Tout cocher',
|
||||||
@@ -532,12 +531,12 @@ export default {
|
|||||||
536: 'Renommer',
|
536: 'Renommer',
|
||||||
537: 'Accès',
|
537: 'Accès',
|
||||||
538: 'Domaines',
|
538: 'Domaines',
|
||||||
539: 'Fournisseurs ACME',
|
539: 'Autorités de certification',
|
||||||
540: 'Domaine',
|
540: 'Domaine',
|
||||||
541: 'Passerelle',
|
541: 'Passerelle',
|
||||||
542: 'ACME par défaut',
|
542: 'Autorité de certification par défaut',
|
||||||
543: 'Changer de passerelle',
|
543: 'Autorité de certification',
|
||||||
544: 'Changer l’ACME par défaut',
|
544: 'Modifier le domaine',
|
||||||
545: 'Aucun domaine',
|
545: 'Aucun domaine',
|
||||||
546: 'Fournisseur',
|
546: 'Fournisseur',
|
||||||
547: 'Afficher le DNS',
|
547: 'Afficher le DNS',
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export default {
|
|||||||
177: 'Przestrzeń jądra',
|
177: 'Przestrzeń jądra',
|
||||||
178: 'Bezczynność',
|
178: 'Bezczynność',
|
||||||
179: 'Oczekiwanie na I/O',
|
179: 'Oczekiwanie na I/O',
|
||||||
180: 'ACME',
|
|
||||||
181: 'Łącznie',
|
181: 'Łącznie',
|
||||||
182: 'Wykorzystane',
|
182: 'Wykorzystane',
|
||||||
183: 'Dostępne',
|
183: 'Dostępne',
|
||||||
@@ -294,12 +293,12 @@ export default {
|
|||||||
296: 'Prześlij',
|
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.',
|
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',
|
298: 'Nieprawidłowy plik pakietu',
|
||||||
299: 'Dodaj dostawców ACME, aby wygenerować certyfikaty SSL (https) dla dostępu przez clearnet.',
|
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',
|
300: 'Zobacz instrukcje',
|
||||||
303: 'Kontakt',
|
303: 'Kontakt',
|
||||||
304: 'Edytuj',
|
304: 'Edytuj',
|
||||||
305: 'Dodaj dostawcę ACME',
|
305: 'Dodaj urząd certyfikacji',
|
||||||
306: 'Edytuj dostawcę ACME',
|
306: 'Edytuj dane kontaktowe',
|
||||||
307: 'Adresy e-mail kontaktowe',
|
307: 'Adresy e-mail kontaktowe',
|
||||||
308: 'Wymagane do uzyskania certyfikatu od urzędu certyfikacji',
|
308: 'Wymagane do uzyskania certyfikatu od urzędu certyfikacji',
|
||||||
309: 'Zaznacz wszystkie',
|
309: 'Zaznacz wszystkie',
|
||||||
@@ -532,12 +531,12 @@ export default {
|
|||||||
536: 'Zmień nazwę',
|
536: 'Zmień nazwę',
|
||||||
537: 'Dostęp',
|
537: 'Dostęp',
|
||||||
538: 'Domeny',
|
538: 'Domeny',
|
||||||
539: 'Dostawcy ACME',
|
539: 'Urzędy certyfikacji',
|
||||||
540: 'Domena',
|
540: 'Domena',
|
||||||
541: 'Brama',
|
541: 'Brama',
|
||||||
542: 'Domyślny ACME',
|
542: 'Domyślny urząd certyfikacji',
|
||||||
543: 'Zmień bramę',
|
543: 'Urząd certyfikacji',
|
||||||
544: 'Zmień domyślny ACME',
|
544: 'Edytuj domenę',
|
||||||
545: 'Brak domen',
|
545: 'Brak domen',
|
||||||
546: 'Dostawca',
|
546: 'Dostawca',
|
||||||
547: 'Pokaż DNS',
|
547: 'Pokaż DNS',
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import { toAcmeName } from 'src/app/utils/acme'
|
import { toAuthorityName } from 'src/app/utils/acme'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'acme',
|
name: 'authorityName',
|
||||||
})
|
})
|
||||||
export class AcmePipe implements PipeTransform {
|
export class AuthorityNamePipe implements PipeTransform {
|
||||||
transform(value: string | null = null): string {
|
transform(value: string | null = null): string {
|
||||||
return toAcmeName(value)
|
return toAuthorityName(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ import {
|
|||||||
FormComponent,
|
FormComponent,
|
||||||
FormContext,
|
FormContext,
|
||||||
} from 'src/app/routes/portal/components/form.component'
|
} from 'src/app/routes/portal/components/form.component'
|
||||||
import { AcmePipe } from 'src/app/routes/portal/components/interfaces/acme.pipe'
|
import { AuthorityNamePipe } from 'src/app/routes/portal/components/interfaces/acme.pipe'
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { toAcmeName } from 'src/app/utils/acme'
|
import { toAuthorityName } from 'src/app/utils/acme'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { InterfaceActionsComponent } from './actions.component'
|
import { InterfaceActionsComponent } from './actions.component'
|
||||||
import { ClearnetAddress } from './interface.utils'
|
import { ClearnetAddress } from './interface.utils'
|
||||||
@@ -43,7 +43,7 @@ import { MaskPipe } from './mask.pipe'
|
|||||||
|
|
||||||
type ClearnetForm = {
|
type ClearnetForm = {
|
||||||
domain: string
|
domain: string
|
||||||
acme: string
|
authority: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -85,11 +85,15 @@ type ClearnetForm = {
|
|||||||
}}
|
}}
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
}
|
}
|
||||||
<table [appTable]="['ACME', 'URL', null]">
|
<table [appTable]="['Certificate Authority', 'URL', null]">
|
||||||
@for (address of clearnet(); track $index) {
|
@for (address of clearnet(); track $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td [style.width.rem]="12">
|
<td [style.width.rem]="12">
|
||||||
{{ interface.value().addSsl ? (address.acme | acme) : '-' }}
|
{{
|
||||||
|
interface.value().addSsl
|
||||||
|
? (address.authority | authorityName)
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td [style.order]="-1">{{ address.url | mask }}</td>
|
<td [style.order]="-1">{{ address.url | mask }}</td>
|
||||||
<td
|
<td
|
||||||
@@ -154,7 +158,7 @@ type ClearnetForm = {
|
|||||||
PlaceholderComponent,
|
PlaceholderComponent,
|
||||||
TableComponent,
|
TableComponent,
|
||||||
MaskPipe,
|
MaskPipe,
|
||||||
AcmePipe,
|
AuthorityNamePipe,
|
||||||
InterfaceActionsComponent,
|
InterfaceActionsComponent,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
DocsLinkDirective,
|
DocsLinkDirective,
|
||||||
@@ -175,7 +179,7 @@ export class InterfaceClearnetComponent {
|
|||||||
readonly isRunning = input.required<boolean>()
|
readonly isRunning = input.required<boolean>()
|
||||||
readonly isPublic = input.required<boolean>()
|
readonly isPublic = input.required<boolean>()
|
||||||
|
|
||||||
readonly acme = toSignal(
|
readonly authorityUrls = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
inject<PatchDB<DataModel>>(PatchDB)
|
||||||
.watch$('serverInfo', 'network', 'acme')
|
.watch$('serverInfo', 'network', 'acme')
|
||||||
.pipe(map(acme => Object.keys(acme))),
|
.pipe(map(acme => Object.keys(acme))),
|
||||||
@@ -237,16 +241,16 @@ export class InterfaceClearnetComponent {
|
|||||||
default: null,
|
default: null,
|
||||||
patterns: [utils.Patterns.domain],
|
patterns: [utils.Patterns.domain],
|
||||||
})
|
})
|
||||||
const acme = ISB.Value.select({
|
const authority = ISB.Value.select({
|
||||||
name: 'ACME Provider',
|
name: 'Certificate Authority',
|
||||||
description:
|
description:
|
||||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
'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.acme().reduce<Record<string, string>>(
|
values: this.authorityUrls().reduce<Record<string, string>>(
|
||||||
(obj, url) => ({
|
(obj, url) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[url]: toAcmeName(url),
|
[url]: toAuthorityName(url),
|
||||||
}),
|
}),
|
||||||
{ none: 'None (use system Root CA)' },
|
{ local: toAuthorityName(null) },
|
||||||
),
|
),
|
||||||
default: '',
|
default: '',
|
||||||
})
|
})
|
||||||
@@ -256,7 +260,7 @@ export class InterfaceClearnetComponent {
|
|||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(
|
spec: await configBuilderToSpec(
|
||||||
ISB.InputSpec.of(
|
ISB.InputSpec.of(
|
||||||
this.interface.value().addSsl ? { domain, acme } : { domain },
|
this.interface.value().addSsl ? { domain, authority } : { domain },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -272,11 +276,11 @@ export class InterfaceClearnetComponent {
|
|||||||
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
const { domain, acme } = domainInfo
|
const { domain, authority } = domainInfo
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
domain,
|
domain,
|
||||||
acme: acme === 'none' ? null : acme,
|
acme: authority === 'local' ? null : authority,
|
||||||
private: false,
|
private: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function getAddresses(
|
|||||||
url,
|
url,
|
||||||
disabled: !h.public,
|
disabled: !h.public,
|
||||||
isDomain: hostnameKind == 'domain',
|
isDomain: hostnameKind == 'domain',
|
||||||
acme:
|
authority:
|
||||||
hostnameKind == 'domain'
|
hostnameKind == 'domain'
|
||||||
? host.domains[h.hostname.domain]?.acme || null
|
? host.domains[h.hostname.domain]?.acme || null
|
||||||
: null,
|
: null,
|
||||||
@@ -118,7 +118,7 @@ export type MappedServiceInterface = T.ServiceInterface & {
|
|||||||
|
|
||||||
export type ClearnetAddress = {
|
export type ClearnetAddress = {
|
||||||
url: string
|
url: string
|
||||||
acme: string | null
|
authority: string | null
|
||||||
isDomain: boolean
|
isDomain: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
|
||||||
import {
|
|
||||||
TuiButton,
|
|
||||||
TuiDataList,
|
|
||||||
TuiDropdown,
|
|
||||||
TuiTextfield,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { ACMEInfo, AcmeService } from './acme.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'tr[acme]',
|
|
||||||
template: `
|
|
||||||
<td>{{ acme().name }}</td>
|
|
||||||
<td>{{ acme().contact.join(', ') }}</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)="service.edit(acme())"
|
|
||||||
>
|
|
||||||
{{ 'Edit' | i18n }}
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
<tui-opt-group>
|
|
||||||
<button
|
|
||||||
tuiOption
|
|
||||||
new
|
|
||||||
iconStart="@tui.trash"
|
|
||||||
class="g-negative"
|
|
||||||
(click)="service.remove(acme())"
|
|
||||||
>
|
|
||||||
{{ 'Delete' | i18n }}
|
|
||||||
</button>
|
|
||||||
</tui-opt-group>
|
|
||||||
</tui-data-list>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
td:last-child {
|
|
||||||
grid-area: 1 / 2 / 3;
|
|
||||||
align-self: center;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
grid-template-columns: 1fr min-content;
|
|
||||||
|
|
||||||
td:first-child {
|
|
||||||
font: var(--tui-font-text-m);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
|
||||||
})
|
|
||||||
export class AcmeItemComponent {
|
|
||||||
protected readonly service = inject(AcmeService)
|
|
||||||
|
|
||||||
readonly acme = input.required<ACMEInfo>()
|
|
||||||
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
|
||||||
import { AcmeItemComponent } from './item.component'
|
|
||||||
import { ACMEInfo } from './acme.service'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'acme-table',
|
|
||||||
template: `
|
|
||||||
<table [appTable]="['Provider', 'Contact', null]">
|
|
||||||
@for (acme of acmes(); track $index) {
|
|
||||||
<tr [acme]="acme"></tr>
|
|
||||||
} @empty {
|
|
||||||
<tr>
|
|
||||||
<td [attr.colspan]="3">
|
|
||||||
@if (acmes()) {
|
|
||||||
<app-placeholder icon="@tui.shield-question">
|
|
||||||
{{ 'No saved providers' | i18n }}
|
|
||||||
</app-placeholder>
|
|
||||||
} @else {
|
|
||||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
|
||||||
TuiSkeleton,
|
|
||||||
i18nPipe,
|
|
||||||
TableComponent,
|
|
||||||
PlaceholderComponent,
|
|
||||||
AcmeItemComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AcmeTableComponent {
|
|
||||||
readonly acmes = input<ACMEInfo[]>()
|
|
||||||
}
|
|
||||||
@@ -13,18 +13,19 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { knownACME } from 'src/app/utils/acme'
|
import { knownAuthorities, toAuthorityName } from 'src/app/utils/acme'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { toAcmeName } from 'src/app/utils/acme'
|
|
||||||
|
|
||||||
export type ACMEInfo = {
|
export type Authority = {
|
||||||
|
url: string | null
|
||||||
name: string
|
name: string
|
||||||
url: string
|
contact: readonly string[] | null
|
||||||
contact: readonly string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RemoteAuthority = Authority & { url: string }
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AcmeService {
|
export class AuthorityService {
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -33,31 +34,36 @@ export class AcmeService {
|
|||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
private readonly dialog = inject(DialogService)
|
private readonly dialog = inject(DialogService)
|
||||||
|
|
||||||
readonly acmes = toSignal<ACMEInfo[]>(
|
readonly authorities = toSignal<Authority[]>(
|
||||||
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||||
map(acme =>
|
map(acme => [
|
||||||
Object.keys(acme).map(url => ({
|
{
|
||||||
|
url: null,
|
||||||
|
name: toAuthorityName(null),
|
||||||
|
contact: null,
|
||||||
|
},
|
||||||
|
...Object.keys(acme).map(url => ({
|
||||||
url,
|
url,
|
||||||
name: toAcmeName(url),
|
name: toAuthorityName(url),
|
||||||
contact:
|
contact:
|
||||||
acme[url]?.contact.map(mailto => mailto.replace('mailto:', '')) ||
|
acme[url]?.contact.map(mailto => mailto.replace('mailto:', '')) ||
|
||||||
[],
|
null,
|
||||||
})),
|
})),
|
||||||
),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async add(acmes: ACMEInfo[]) {
|
async add(authorities: Authority[]) {
|
||||||
const availableAcme = knownACME.filter(
|
const availableAuthorities = knownAuthorities.filter(
|
||||||
acme => !acmes.map(a => a.url).includes(acme.url),
|
ca => !authorities.map(a => a.url).includes(ca.url),
|
||||||
)
|
)
|
||||||
|
|
||||||
const addSpec = ISB.InputSpec.of({
|
const addSpec = ISB.InputSpec.of({
|
||||||
provider: ISB.Value.union({
|
provider: ISB.Value.union({
|
||||||
name: 'Provider',
|
name: 'Provider',
|
||||||
default: (availableAcme[0]?.url as any) || 'other',
|
default: (availableAuthorities[0]?.url as any) || 'other',
|
||||||
variants: ISB.Variants.of({
|
variants: ISB.Variants.of({
|
||||||
...availableAcme.reduce(
|
...availableAuthorities.reduce(
|
||||||
(obj, curr) => ({
|
(obj, curr) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[curr.url]: {
|
[curr.url]: {
|
||||||
@@ -85,7 +91,7 @@ export class AcmeService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add ACME Provider',
|
label: 'Add Certificate Authority',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(addSpec),
|
spec: await configBuilderToSpec(addSpec),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -105,13 +111,13 @@ export class AcmeService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit({ url, contact }: ACMEInfo) {
|
async edit({ url, contact }: RemoteAuthority) {
|
||||||
const editSpec = ISB.InputSpec.of({
|
const editSpec = ISB.InputSpec.of({
|
||||||
contact: this.emailListSpec(),
|
contact: this.emailListSpec(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Edit ACME Provider',
|
label: 'Edit Contact Info',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(editSpec),
|
spec: await configBuilderToSpec(editSpec),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -126,7 +132,7 @@ export class AcmeService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
remove({ url }: ACMEInfo) {
|
remove({ url }: RemoteAuthority) {
|
||||||
this.dialog
|
this.dialog
|
||||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||||
.pipe(filter(Boolean))
|
.pipe(filter(Boolean))
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
|
import {
|
||||||
|
TuiButton,
|
||||||
|
TuiDataList,
|
||||||
|
TuiDropdown,
|
||||||
|
TuiTextfield,
|
||||||
|
} from '@taiga-ui/core'
|
||||||
|
import { Authority, AuthorityService } from './authority.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tr[authority]',
|
||||||
|
template: `
|
||||||
|
@if (authority(); as authority) {
|
||||||
|
<td>{{ authority.name }}</td>
|
||||||
|
<td>{{ authority.url || '-' }}</td>
|
||||||
|
<td>{{ authority.contact ? authority.contact.join(', ') : '-' }}</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>
|
||||||
|
@if (authority.url) {
|
||||||
|
<tui-opt-group>
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
new
|
||||||
|
iconStart="@tui.pencil"
|
||||||
|
(click)="service.edit($any(authority))"
|
||||||
|
>
|
||||||
|
{{ 'Edit' | i18n }}
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
<tui-opt-group>
|
||||||
|
<button
|
||||||
|
tuiOption
|
||||||
|
new
|
||||||
|
iconStart="@tui.trash"
|
||||||
|
class="g-negative"
|
||||||
|
(click)="service.remove($any(authority))"
|
||||||
|
>
|
||||||
|
{{ 'Delete' | i18n }}
|
||||||
|
</button>
|
||||||
|
</tui-opt-group>
|
||||||
|
} @else {
|
||||||
|
<tui-opt-group>
|
||||||
|
<a
|
||||||
|
tuiOption
|
||||||
|
new
|
||||||
|
iconStart="@tui.download"
|
||||||
|
href="/static/local-root-ca.crt"
|
||||||
|
>
|
||||||
|
{{ 'Download your Root CA' | i18n }}
|
||||||
|
</a>
|
||||||
|
</tui-opt-group>
|
||||||
|
}
|
||||||
|
</tui-data-list>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
td:last-child {
|
||||||
|
grid-area: 1 / 2 / 3;
|
||||||
|
align-self: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
grid-template-columns: 1fr min-content;
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
font: var(--tui-font-text-m);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
||||||
|
})
|
||||||
|
export class AuthorityItemComponent {
|
||||||
|
protected readonly service = inject(AuthorityService)
|
||||||
|
|
||||||
|
readonly authority = input.required<Authority>()
|
||||||
|
|
||||||
|
open = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
|
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||||
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
|
import { AuthorityItemComponent } from './item.component'
|
||||||
|
import { AuthorityService } from './authority.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'authorities-table',
|
||||||
|
template: `
|
||||||
|
<table [appTable]="['Provider', 'URL', 'Contact', null]">
|
||||||
|
@for (authority of authorityService.authorities(); track $index) {
|
||||||
|
<tr [authority]="authority"></tr>
|
||||||
|
} @empty {
|
||||||
|
<td [attr.colspan]="4">
|
||||||
|
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [TuiSkeleton, i18nPipe, TableComponent, AuthorityItemComponent],
|
||||||
|
})
|
||||||
|
export class AuthoritiesTableComponent {
|
||||||
|
protected readonly authorityService = inject(AuthorityService)
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
|||||||
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
|
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiHeader } from '@taiga-ui/layout'
|
import { TuiHeader } from '@taiga-ui/layout'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { AcmeService } from './acme/acme.service'
|
import { AuthorityService } from './authorities/authority.service'
|
||||||
import { DomainsService } from './domains/domains.service'
|
import { DomainService } from './domains/domain.service'
|
||||||
import { DomainsTableComponent } from './domains/table.component'
|
import { DomainsTableComponent } from './domains/table.component'
|
||||||
import { AcmeTableComponent } from './acme/table.component'
|
import { AuthoritiesTableComponent } from './authorities/table.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -21,9 +21,9 @@ import { AcmeTableComponent } from './acme/table.component'
|
|||||||
<hgroup tuiTitle>
|
<hgroup tuiTitle>
|
||||||
<h3>{{ 'Domains' | i18n }}</h3>
|
<h3>{{ 'Domains' | i18n }}</h3>
|
||||||
<p tuiSubtitle>
|
<p tuiSubtitle>
|
||||||
<!-- @TODO translation -->
|
|
||||||
{{
|
{{
|
||||||
'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.'
|
'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.'
|
||||||
|
| i18n
|
||||||
}}
|
}}
|
||||||
<a
|
<a
|
||||||
tuiLink
|
tuiLink
|
||||||
@@ -40,38 +40,38 @@ import { AcmeTableComponent } from './acme/table.component'
|
|||||||
|
|
||||||
<section class="g-card">
|
<section class="g-card">
|
||||||
<header>
|
<header>
|
||||||
{{ 'ACME Providers' | i18n }}
|
{{ 'Certificate Authorities' | i18n }}
|
||||||
@if (acmeService.acmes(); as acmes) {
|
@if (authorityService.authorities(); as authorities) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
size="xs"
|
size="xs"
|
||||||
iconStart="@tui.plus"
|
iconStart="@tui.plus"
|
||||||
[style.margin-inline-start]="'auto'"
|
[style.margin-inline-start]="'auto'"
|
||||||
(click)="acmeService.add(acmes)"
|
(click)="authorityService.add(authorities)"
|
||||||
>
|
>
|
||||||
{{ 'Add' | i18n }}
|
{{ 'Add' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
<acme-table [acmes]="acmeService.acmes()" />
|
<authorities-table />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="g-card">
|
<section class="g-card">
|
||||||
<header>
|
<header>
|
||||||
{{ 'Domains' | i18n }}
|
{{ 'Domains' | i18n }}
|
||||||
@if (domainsService.data(); as value) {
|
@if (domainService.data(); as value) {
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
size="xs"
|
size="xs"
|
||||||
iconStart="@tui.plus"
|
iconStart="@tui.plus"
|
||||||
[style.margin-inline-start]="'auto'"
|
[style.margin-inline-start]="'auto'"
|
||||||
(click)="domainsService.add()"
|
(click)="domainService.add()"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
<domains-table [domains]="domainsService.data()?.domains" />
|
<domains-table />
|
||||||
</section>
|
</section>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -85,11 +85,11 @@ import { AcmeTableComponent } from './acme/table.component'
|
|||||||
i18nPipe,
|
i18nPipe,
|
||||||
DocsLinkDirective,
|
DocsLinkDirective,
|
||||||
DomainsTableComponent,
|
DomainsTableComponent,
|
||||||
AcmeTableComponent,
|
AuthoritiesTableComponent,
|
||||||
],
|
],
|
||||||
providers: [AcmeService, DomainsService],
|
providers: [AuthorityService, DomainService],
|
||||||
})
|
})
|
||||||
export default class SystemDomainsComponent {
|
export default class SystemDomainsComponent {
|
||||||
protected readonly acmeService = inject(AcmeService)
|
protected readonly authorityService = inject(AuthorityService)
|
||||||
protected readonly domainsService = inject(DomainsService)
|
protected readonly domainService = inject(DomainService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { toAcmeName } from 'src/app/utils/acme'
|
import { toAuthorityName } from 'src/app/utils/acme'
|
||||||
|
|
||||||
// @TODO translations
|
// @TODO translations
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DomainsService {
|
export class DomainService {
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
@@ -46,32 +46,32 @@ export class DomainsService {
|
|||||||
{
|
{
|
||||||
domain: 'blog.mydomain.com',
|
domain: 'blog.mydomain.com',
|
||||||
gateway: {
|
gateway: {
|
||||||
id: '',
|
id: 'wireguard1',
|
||||||
name: 'StartTunnel',
|
name: 'StartTunnel',
|
||||||
},
|
},
|
||||||
acme: {
|
authority: {
|
||||||
url: '',
|
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||||
name: `Lert's Encrypt`,
|
name: `Let's Encrypt`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
domain: 'store.mydomain.com',
|
domain: 'store.mydomain.com',
|
||||||
gateway: {
|
gateway: {
|
||||||
id: '',
|
id: 'eth0',
|
||||||
name: 'Ethernet',
|
name: 'Ethernet',
|
||||||
},
|
},
|
||||||
acme: {
|
authority: {
|
||||||
url: null,
|
url: 'local',
|
||||||
name: 'System',
|
name: toAuthorityName(null),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
acme: Object.keys(network.acme).reduce<Record<string, string>>(
|
authorities: Object.keys(network.acme).reduce<Record<string, string>>(
|
||||||
(obj, url) => ({
|
(obj, url) => ({
|
||||||
...obj,
|
...obj,
|
||||||
[url]: toAcmeName(url),
|
[url]: toAuthorityName(url),
|
||||||
}),
|
}),
|
||||||
{ none: 'None (use system Root CA)' },
|
{ local: toAuthorityName(null) },
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -88,7 +88,7 @@ export class DomainsService {
|
|||||||
default: null,
|
default: null,
|
||||||
patterns: [utils.Patterns.domain],
|
patterns: [utils.Patterns.domain],
|
||||||
}),
|
}),
|
||||||
...this.gatewaysAndAcme(),
|
...this.gatewaysAndAuthorities(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
@@ -107,11 +107,11 @@ export class DomainsService {
|
|||||||
|
|
||||||
async edit(domain: any) {
|
async edit(domain: any) {
|
||||||
const editSpec = ISB.InputSpec.of({
|
const editSpec = ISB.InputSpec.of({
|
||||||
...this.gatewaysAndAcme(),
|
...this.gatewaysAndAuthorities(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Edit Domain' as any, // @TODO translation
|
label: 'Edit Domain',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(editSpec),
|
spec: await configBuilderToSpec(editSpec),
|
||||||
buttons: [
|
buttons: [
|
||||||
@@ -126,7 +126,7 @@ export class DomainsService {
|
|||||||
],
|
],
|
||||||
value: {
|
value: {
|
||||||
gateway: domain.gateway.id,
|
gateway: domain.gateway.id,
|
||||||
acme: domain.acme.url,
|
authority: domain.authority.url,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -172,7 +172,7 @@ export class DomainsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private gatewaysAndAcme() {
|
private gatewaysAndAuthorities() {
|
||||||
return {
|
return {
|
||||||
gateway: ISB.Value.select({
|
gateway: ISB.Value.select({
|
||||||
name: 'Gateway',
|
name: 'Gateway',
|
||||||
@@ -181,11 +181,11 @@ export class DomainsService {
|
|||||||
values: this.data()!.gateways,
|
values: this.data()!.gateways,
|
||||||
default: '',
|
default: '',
|
||||||
}),
|
}),
|
||||||
acme: ISB.Value.select({
|
authority: ISB.Value.select({
|
||||||
name: 'Default ACME',
|
name: 'Default Certificate Authority',
|
||||||
description:
|
description:
|
||||||
'Select the default ACME provider for this domain. This can be overridden on a case-by-case basis.',
|
'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()!.acme,
|
values: this.data()!.authorities,
|
||||||
default: '',
|
default: '',
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -11,66 +11,68 @@ import {
|
|||||||
TuiDropdown,
|
TuiDropdown,
|
||||||
TuiTextfield,
|
TuiTextfield,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { DomainsService } from './domains.service'
|
import { DomainService } from './domain.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[domain]',
|
selector: 'tr[domain]',
|
||||||
template: `
|
template: `
|
||||||
<td>{{ domain().domain }}</td>
|
@if (domain(); as domain) {
|
||||||
<td [style.order]="-1">{{ domain().gateway.name }}</td>
|
<td>{{ domain.domain }}</td>
|
||||||
<td>{{ domain().acme.name }}</td>
|
<td [style.order]="-1">{{ domain.gateway.name }}</td>
|
||||||
<td>
|
<td>{{ domain.authority.name }}</td>
|
||||||
<button
|
<td>
|
||||||
tuiIconButton
|
<button
|
||||||
tuiDropdown
|
tuiIconButton
|
||||||
size="s"
|
tuiDropdown
|
||||||
appearance="flat-grayscale"
|
size="s"
|
||||||
iconStart="@tui.ellipsis-vertical"
|
appearance="flat-grayscale"
|
||||||
[tuiAppearanceState]="open ? 'hover' : null"
|
iconStart="@tui.ellipsis-vertical"
|
||||||
[(tuiDropdownOpen)]="open"
|
[tuiAppearanceState]="open ? 'hover' : null"
|
||||||
>
|
[(tuiDropdownOpen)]="open"
|
||||||
{{ 'More' | i18n }}
|
>
|
||||||
<tui-data-list size="s" *tuiTextfieldDropdown>
|
{{ 'More' | i18n }}
|
||||||
<tui-opt-group>
|
<tui-data-list size="s" *tuiTextfieldDropdown>
|
||||||
<button
|
<tui-opt-group>
|
||||||
tuiOption
|
<button
|
||||||
new
|
tuiOption
|
||||||
iconStart="@tui.pencil"
|
new
|
||||||
(click)="domainsService.edit(domain())"
|
iconStart="@tui.pencil"
|
||||||
>
|
(click)="domainService.edit(domain)"
|
||||||
{{ 'Edit' | i18n }}
|
>
|
||||||
</button>
|
{{ 'Edit' | i18n }}
|
||||||
<button
|
</button>
|
||||||
tuiOption
|
<button
|
||||||
new
|
tuiOption
|
||||||
iconStart="@tui.shield"
|
new
|
||||||
(click)="domainsService.showDns(domain())"
|
iconStart="@tui.eye"
|
||||||
>
|
(click)="domainService.showDns(domain)"
|
||||||
{{ 'Show DNS' | i18n }}
|
>
|
||||||
</button>
|
{{ 'Show DNS' | i18n }}
|
||||||
<button
|
</button>
|
||||||
tuiOption
|
<button
|
||||||
new
|
tuiOption
|
||||||
iconStart="@tui.shield"
|
new
|
||||||
(click)="domainsService.testDns(domain())"
|
iconStart="@tui.arrow-up-down"
|
||||||
>
|
(click)="domainService.testDns(domain)"
|
||||||
{{ 'Test DNS' | i18n }}
|
>
|
||||||
</button>
|
{{ 'Test DNS' | i18n }}
|
||||||
</tui-opt-group>
|
</button>
|
||||||
<tui-opt-group>
|
</tui-opt-group>
|
||||||
<button
|
<tui-opt-group>
|
||||||
tuiOption
|
<button
|
||||||
new
|
tuiOption
|
||||||
iconStart="@tui.trash"
|
new
|
||||||
class="g-negative"
|
iconStart="@tui.trash"
|
||||||
(click)="domainsService.remove(domain())"
|
class="g-negative"
|
||||||
>
|
(click)="domainService.remove(domain)"
|
||||||
{{ 'Delete' | i18n }}
|
>
|
||||||
</button>
|
{{ 'Delete' | i18n }}
|
||||||
</tui-opt-group>
|
</button>
|
||||||
</tui-data-list>
|
</tui-opt-group>
|
||||||
</button>
|
</tui-data-list>
|
||||||
</td>
|
</button>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
td:last-child {
|
td:last-child {
|
||||||
@@ -91,8 +93,8 @@ import { DomainsService } from './domains.service'
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
||||||
})
|
})
|
||||||
export class DomainsItemComponent {
|
export class DomainItemComponent {
|
||||||
protected readonly domainsService = inject(DomainsService)
|
protected readonly domainService = inject(DomainService)
|
||||||
|
|
||||||
readonly domain = input.required<any>()
|
readonly domain = input.required<any>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { DomainsItemComponent } from './item.component'
|
import { DomainItemComponent } from './item.component'
|
||||||
|
import { DomainService } from './domain.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'domains-table',
|
selector: 'domains-table',
|
||||||
template: `
|
template: `
|
||||||
<table [appTable]="['Domain', 'Gateway', 'Default ACME', null]">
|
<table
|
||||||
@for (domain of domains(); track $index) {
|
[appTable]="['Domain', 'Gateway', 'Default Certificate Authority', null]"
|
||||||
|
>
|
||||||
|
@for (domain of domainService.data()?.domains; track $index) {
|
||||||
<tr [domain]="domain"></tr>
|
<tr [domain]="domain"></tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td [attr.colspan]="4">
|
<td [attr.colspan]="4">
|
||||||
@if (domains()) {
|
@if (domainService.data()?.domains) {
|
||||||
<app-placeholder icon="@tui.globe">
|
<app-placeholder icon="@tui.globe">
|
||||||
{{ 'No domains' | i18n }}
|
{{ 'No domains' | i18n }}
|
||||||
</app-placeholder>
|
</app-placeholder>
|
||||||
@@ -32,10 +35,9 @@ import { DomainsItemComponent } from './item.component'
|
|||||||
i18nPipe,
|
i18nPipe,
|
||||||
TableComponent,
|
TableComponent,
|
||||||
PlaceholderComponent,
|
PlaceholderComponent,
|
||||||
DomainsItemComponent,
|
DomainItemComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DomainsTableComponent {
|
export class DomainsTableComponent {
|
||||||
// @TODO Alex proper types
|
protected readonly domainService = inject(DomainService)
|
||||||
readonly domains = input<readonly any[] | null>()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { TuiIcon } from '@taiga-ui/core'
|
|||||||
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { Session } from 'src/app/services/api/api.types'
|
import { Session } from 'src/app/services/api/api.types'
|
||||||
import { toAcmeName } from 'src/app/utils/acme'
|
|
||||||
import { PlatformInfoPipe } from './platform-info.pipe'
|
import { PlatformInfoPipe } from './platform-info.pipe'
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
|
|
||||||
@@ -182,6 +181,4 @@ export class SessionsTableComponent<T extends Session> implements OnChanges {
|
|||||||
this.selected.update(selected => [...selected, session])
|
this.selected.update(selected => [...selected, session])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly toAcmeName = toAcmeName
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ export const SYSTEM_MENU = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
|
||||||
icon: '@tui.globe',
|
|
||||||
item: 'Domains',
|
|
||||||
link: 'domains',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: '@tui.door-open',
|
icon: '@tui.door-open',
|
||||||
item: 'Gateways',
|
item: 'Gateways',
|
||||||
link: 'gateways',
|
link: 'gateways',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: '@tui.globe',
|
||||||
|
item: 'Domains',
|
||||||
|
link: 'domains',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { AuthService } from '../auth.service'
|
|||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||||
import { toAcmeUrl } from 'src/app/utils/acme'
|
import { toAuthorityUrl } from 'src/app/utils/acme'
|
||||||
|
|
||||||
import markdown from './md-sample.md'
|
import markdown from './md-sample.md'
|
||||||
|
|
||||||
@@ -1396,7 +1396,7 @@ export class MockApiService extends ApiService {
|
|||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/serverInfo/acme`,
|
path: `/serverInfo/acme`,
|
||||||
value: {
|
value: {
|
||||||
[toAcmeUrl(params.provider)]: { contact: params.contact },
|
[toAuthorityUrl(params.provider)]: { contact: params.contact },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import { knownACME } from 'src/app/utils/acme'
|
import { knownAuthorities } from 'src/app/utils/acme'
|
||||||
const version = require('../../../../../../package.json').version
|
const version = require('../../../../../../package.json').version
|
||||||
|
|
||||||
export const mockPatchData: DataModel = {
|
export const mockPatchData: DataModel = {
|
||||||
@@ -28,7 +28,7 @@ export const mockPatchData: DataModel = {
|
|||||||
lastRegion: null,
|
lastRegion: null,
|
||||||
},
|
},
|
||||||
acme: {
|
acme: {
|
||||||
[knownACME[0].url]: {
|
[knownAuthorities[0].url]: {
|
||||||
contact: ['mailto:support@start9.com'],
|
contact: ['mailto:support@start9.com'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -160,6 +160,20 @@ export const mockPatchData: DataModel = {
|
|||||||
ntpServers: [],
|
ntpServers: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
wireguard1: {
|
||||||
|
public: false,
|
||||||
|
ipInfo: {
|
||||||
|
name: 'StartTunnel',
|
||||||
|
scopeId: 2,
|
||||||
|
deviceType: 'wireguard',
|
||||||
|
subnets: [
|
||||||
|
'10.0.90.12/24',
|
||||||
|
'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64',
|
||||||
|
],
|
||||||
|
wanIp: '203.0.113.45',
|
||||||
|
ntpServers: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unreadNotificationCount: 4,
|
unreadNotificationCount: 4,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
export function toAcmeName(url: string | null): string | 'System CA' {
|
export function toAuthorityName(url: string | null): string | 'Local Root CA' {
|
||||||
return knownACME.find(acme => acme.url === url)?.name || url || 'System CA'
|
return (
|
||||||
|
knownAuthorities.find(ca => ca.url === url)?.name || url || 'Local Root CA'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toAcmeUrl(name: string): string {
|
export function toAuthorityUrl(name: string): string {
|
||||||
return knownACME.find(acme => acme.name === name)?.url || name
|
return knownAuthorities.find(ca => ca.name === name)?.url || name
|
||||||
}
|
}
|
||||||
|
|
||||||
export const knownACME = [
|
export const knownAuthorities = [
|
||||||
{
|
{
|
||||||
name: `Let's Encrypt`,
|
name: `Let's Encrypt`,
|
||||||
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||||
|
|||||||
Reference in New Issue
Block a user