diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 6f0969a99..b6f638dc0 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -179,7 +179,6 @@ export default { 177: 'Kernelspeicher', 178: 'Leerlauf', 179: 'I/O-Wartezeit', - 180: 'ACME', 181: 'Gesamt', 182: 'Verwendet', 183: 'Verfügbar', @@ -294,12 +293,12 @@ 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: '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', 303: 'Kontakt', 304: 'Bearbeiten', - 305: 'ACME-Anbieter hinzufügen', - 306: 'ACME-Anbieter bearbeiten', + 305: 'Zertifizierungsstelle hinzufügen', + 306: 'Kontaktinformationen bearbeiten', 307: 'Kontakt-E-Mails', 308: 'Erforderlich, um ein Zertifikat von einer Zertifizierungsstelle zu erhalten', 309: 'Alle umschalten', @@ -532,12 +531,12 @@ export default { 536: 'Umbenennen', 537: 'Zugriff', 538: 'Domains', - 539: 'ACME-Anbieter', + 539: 'Zertifizierungsstellen', 540: 'Domain', 541: 'Gateway', - 542: 'Standard-ACME', - 543: 'Gateway ändern', - 544: 'Standard-ACME ändern', + 542: 'Standard-Zertifizierungsstelle', + 543: 'Zertifizierungsstelle', + 544: 'Domain bearbeiten', 545: 'Keine Domains', 546: 'Anbieter', 547: 'DNS anzeigen', diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index 3e41596cc..e621ae040 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -178,7 +178,6 @@ export const ENGLISH = { 'Kernel space': 177, 'Idle': 178, // a CPU metric 'I/O wait': 179, - 'ACME': 180, 'Total': 181, 'Used': 182, 'Available': 183, @@ -293,12 +292,12 @@ 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, - '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, 'Contact': 303, // as in, "contact us" 'Edit': 304, - 'Add ACME Provider': 305, - 'Edit ACME Provider': 306, + 'Add Certificate Authority': 305, + 'Edit Contact Info': 306, 'Contact Emails': 307, 'Needed to obtain a certificate from a Certificate Authority': 308, 'Toggle all': 309, @@ -531,12 +530,12 @@ export const ENGLISH = { 'Rename': 536, 'Access': 537, // as in, public or private access, almost "permission" 'Domains': 538, // as in, internet domains - 'ACME Providers': 539, + '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 ACME': 542, // as in, the default ACME provider for signing certificates - 'Change gateway': 543, // as in, change the network gateway for a computer - 'Change default ACME': 544, // as in, change the default ACME provider for a domain + 'Default Certificate Authority': 542, + 'Certificate Authority': 543, + 'Edit Domain': 544, 'No domains': 545, 'Provider': 546, 'Show DNS': 547, diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index ab18c0415..346b656ce 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -179,7 +179,6 @@ export default { 177: 'Espacio del kernel', 178: 'Inactivo', 179: 'Espera de E/S', - 180: 'ACME', 181: 'Total', 182: 'Usado', 183: 'Disponible', @@ -294,12 +293,13 @@ 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: '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', 303: 'Contacto', 304: 'Editar', - 305: 'Agregar proveedor ACME', - 306: 'Editar proveedor ACME', + 305: 'Agregar autoridad certificadora', + 306: 'Editar información de contacto', 307: 'Correos de contacto', 308: 'Necesarios para obtener un certificado de una Autoridad Certificadora', 309: 'Alternar todo', @@ -532,12 +532,12 @@ export default { 536: 'Renombrar', 537: 'Acceso', 538: 'Dominios', - 539: 'Proveedores ACME', + 539: 'Autoridades certificadoras', 540: 'Dominio', 541: 'Puerta de enlace', - 542: 'ACME predeterminado', - 543: 'Cambiar puerta de enlace', - 544: 'Cambiar ACME predeterminado', + 542: 'Autoridad certificadora predeterminada', + 543: 'Autoridad certificadora', + 544: 'Editar dominio', 545: 'Sin dominios', 546: 'Proveedor', 547: 'Mostrar DNS', diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index c780e1362..e9b498a14 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -179,7 +179,6 @@ export default { 177: 'Espace noyau', 178: 'Inactif', 179: 'Attente E/S', - 180: 'ACME', 181: 'Total', 182: 'Utilisé', 183: 'Disponible', @@ -294,12 +293,12 @@ 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: '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', 303: 'Contact', 304: 'Modifier', - 305: 'Ajouter un fournisseur ACME', - 306: 'Modifier le fournisseur ACME', + 305: 'Ajouter une autorité de certification', + 306: 'Modifier les informations de contact', 307: 'Emails de contact', 308: 'Nécessaire pour obtenir un certificat d’une autorité de certification', 309: 'Tout cocher', @@ -532,12 +531,12 @@ export default { 536: 'Renommer', 537: 'Accès', 538: 'Domaines', - 539: 'Fournisseurs ACME', + 539: 'Autorités de certification', 540: 'Domaine', 541: 'Passerelle', - 542: 'ACME par défaut', - 543: 'Changer de passerelle', - 544: 'Changer l’ACME par défaut', + 542: 'Autorité de certification par défaut', + 543: 'Autorité de certification', + 544: 'Modifier le domaine', 545: 'Aucun domaine', 546: 'Fournisseur', 547: 'Afficher le DNS', diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 0feb88469..121856c07 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -179,7 +179,6 @@ export default { 177: 'Przestrzeń jądra', 178: 'Bezczynność', 179: 'Oczekiwanie na I/O', - 180: 'ACME', 181: 'Łącznie', 182: 'Wykorzystane', 183: 'Dostępne', @@ -294,12 +293,12 @@ 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: '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', 303: 'Kontakt', 304: 'Edytuj', - 305: 'Dodaj dostawcę ACME', - 306: 'Edytuj dostawcę ACME', + 305: 'Dodaj urząd certyfikacji', + 306: 'Edytuj dane kontaktowe', 307: 'Adresy e-mail kontaktowe', 308: 'Wymagane do uzyskania certyfikatu od urzędu certyfikacji', 309: 'Zaznacz wszystkie', @@ -532,12 +531,12 @@ export default { 536: 'Zmień nazwę', 537: 'Dostęp', 538: 'Domeny', - 539: 'Dostawcy ACME', + 539: 'Urzędy certyfikacji', 540: 'Domena', 541: 'Brama', - 542: 'Domyślny ACME', - 543: 'Zmień bramę', - 544: 'Zmień domyślny ACME', + 542: 'Domyślny urząd certyfikacji', + 543: 'Urząd certyfikacji', + 544: 'Edytuj domenę', 545: 'Brak domen', 546: 'Dostawca', 547: 'Pokaż DNS', diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/acme.pipe.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/acme.pipe.ts index 08c385d2d..420ef1da8 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/acme.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/acme.pipe.ts @@ -1,11 +1,11 @@ import { Pipe, PipeTransform } from '@angular/core' -import { toAcmeName } from 'src/app/utils/acme' +import { toAuthorityName } from 'src/app/utils/acme' @Pipe({ - name: 'acme', + name: 'authorityName', }) -export class AcmePipe implements PipeTransform { +export class AuthorityNamePipe implements PipeTransform { transform(value: string | null = null): string { - return toAcmeName(value) + return toAuthorityName(value) } } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts index e6bbe3e85..65ff42b08 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts @@ -28,14 +28,14 @@ import { FormComponent, FormContext, } 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 { 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 { toAcmeName } from 'src/app/utils/acme' +import { toAuthorityName } from 'src/app/utils/acme' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { InterfaceActionsComponent } from './actions.component' import { ClearnetAddress } from './interface.utils' @@ -43,7 +43,7 @@ import { MaskPipe } from './mask.pipe' type ClearnetForm = { domain: string - acme: string + authority: string } @Component({ @@ -85,11 +85,15 @@ type ClearnetForm = { }} } - +
@for (address of clearnet(); track $index) { - - - `, - 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() - - open = false -} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/table.component.ts deleted file mode 100644 index 34541293e..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/table.component.ts +++ /dev/null @@ -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: ` -
- {{ interface.value().addSsl ? (address.acme | acme) : '-' }} + {{ + interface.value().addSsl + ? (address.authority | authorityName) + : '-' + }} {{ address.url | mask }} () readonly isPublic = input.required() - readonly acme = toSignal( + readonly authorityUrls = toSignal( inject>(PatchDB) .watch$('serverInfo', 'network', 'acme') .pipe(map(acme => Object.keys(acme))), @@ -237,16 +241,16 @@ export class InterfaceClearnetComponent { default: null, patterns: [utils.Patterns.domain], }) - const acme = ISB.Value.select({ - name: 'ACME Provider', + const authority = ISB.Value.select({ + name: 'Certificate Authority', 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.', - values: this.acme().reduce>( + '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>( (obj, url) => ({ ...obj, - [url]: toAcmeName(url), + [url]: toAuthorityName(url), }), - { none: 'None (use system Root CA)' }, + { local: toAuthorityName(null) }, ), default: '', }) @@ -256,7 +260,7 @@ export class InterfaceClearnetComponent { data: { spec: await configBuilderToSpec( ISB.InputSpec.of( - this.interface.value().addSsl ? { domain, acme } : { domain }, + this.interface.value().addSsl ? { domain, authority } : { domain }, ), ), buttons: [ @@ -272,11 +276,11 @@ export class InterfaceClearnetComponent { private async save(domainInfo: ClearnetForm): Promise { const loader = this.loader.open('Saving').subscribe() - const { domain, acme } = domainInfo + const { domain, authority } = domainInfo const params = { domain, - acme: acme === 'none' ? null : acme, + acme: authority === 'local' ? null : authority, private: false, } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index 7d366df10..22de11903 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -72,7 +72,7 @@ export function getAddresses( url, disabled: !h.public, isDomain: hostnameKind == 'domain', - acme: + authority: hostnameKind == 'domain' ? host.domains[h.hostname.domain]?.acme || null : null, @@ -118,7 +118,7 @@ export type MappedServiceInterface = T.ServiceInterface & { export type ClearnetAddress = { url: string - acme: string | null + authority: string | null isDomain: boolean disabled: boolean } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/item.component.ts deleted file mode 100644 index 0894d37cc..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/item.component.ts +++ /dev/null @@ -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: ` - {{ acme().name }}{{ acme().contact.join(', ') }} - - - - - - - -
- @for (acme of acmes(); track $index) { - - } @empty { - - - - } -
- @if (acmes()) { - - {{ 'No saved providers' | i18n }} - - } @else { -
{{ 'Loading' | i18n }}
- } -
- `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - TuiSkeleton, - i18nPipe, - TableComponent, - PlaceholderComponent, - AcmeItemComponent, - ], -}) -export class AcmeTableComponent { - readonly acmes = input() -} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/acme.service.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/authority.service.ts similarity index 81% rename from web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/acme.service.ts rename to web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/authority.service.ts index 64d79c5d9..52de892d7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/acme/acme.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/authority.service.ts @@ -13,18 +13,19 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { PatchDB } from 'patch-db-client' 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 { toAcmeName } from 'src/app/utils/acme' -export type ACMEInfo = { +export type Authority = { + url: string | null name: string - url: string - contact: readonly string[] + contact: readonly string[] | null } +export type RemoteAuthority = Authority & { url: string } + @Injectable() -export class AcmeService { +export class AuthorityService { private readonly patch = inject>(PatchDB) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) @@ -33,31 +34,36 @@ export class AcmeService { private readonly i18n = inject(i18nPipe) private readonly dialog = inject(DialogService) - readonly acmes = toSignal( + readonly authorities = toSignal( this.patch.watch$('serverInfo', 'network', 'acme').pipe( - map(acme => - Object.keys(acme).map(url => ({ + map(acme => [ + { + url: null, + name: toAuthorityName(null), + contact: null, + }, + ...Object.keys(acme).map(url => ({ url, - name: toAcmeName(url), + name: toAuthorityName(url), contact: acme[url]?.contact.map(mailto => mailto.replace('mailto:', '')) || - [], + null, })), - ), + ]), ), ) - async add(acmes: ACMEInfo[]) { - const availableAcme = knownACME.filter( - acme => !acmes.map(a => a.url).includes(acme.url), + async add(authorities: Authority[]) { + const availableAuthorities = knownAuthorities.filter( + ca => !authorities.map(a => a.url).includes(ca.url), ) const addSpec = ISB.InputSpec.of({ provider: ISB.Value.union({ name: 'Provider', - default: (availableAcme[0]?.url as any) || 'other', + default: (availableAuthorities[0]?.url as any) || 'other', variants: ISB.Variants.of({ - ...availableAcme.reduce( + ...availableAuthorities.reduce( (obj, curr) => ({ ...obj, [curr.url]: { @@ -85,7 +91,7 @@ export class AcmeService { }) this.formDialog.open(FormComponent, { - label: 'Add ACME Provider', + label: 'Add Certificate Authority', data: { spec: await configBuilderToSpec(addSpec), buttons: [ @@ -105,13 +111,13 @@ export class AcmeService { }) } - async edit({ url, contact }: ACMEInfo) { + async edit({ url, contact }: RemoteAuthority) { const editSpec = ISB.InputSpec.of({ contact: this.emailListSpec(), }) this.formDialog.open(FormComponent, { - label: 'Edit ACME Provider', + label: 'Edit Contact Info', data: { spec: await configBuilderToSpec(editSpec), buttons: [ @@ -126,7 +132,7 @@ export class AcmeService { }) } - remove({ url }: ACMEInfo) { + remove({ url }: RemoteAuthority) { this.dialog .openConfirm({ label: 'Are you sure?', size: 's' }) .pipe(filter(Boolean)) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/item.component.ts new file mode 100644 index 000000000..c1aa0ab7b --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/item.component.ts @@ -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) { + {{ authority.name }} + {{ authority.url || '-' }} + {{ authority.contact ? authority.contact.join(', ') : '-' }} + + + + + + + } @else { + + + {{ 'Download your Root CA' | i18n }} + + + } + + + + } + `, + 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() + + open = false +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/table.component.ts new file mode 100644 index 000000000..a1e8cbae2 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/authorities/table.component.ts @@ -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: ` + + @for (authority of authorityService.authorities(); track $index) { + + } @empty { + + } +
+
{{ 'Loading' | i18n }}
+
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiSkeleton, i18nPipe, TableComponent, AuthorityItemComponent], +}) +export class AuthoritiesTableComponent { + protected readonly authorityService = inject(AuthorityService) +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts index c31c24640..61f2c208e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains.component.ts @@ -4,10 +4,10 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core' import { TuiHeader } from '@taiga-ui/layout' import { TitleDirective } from 'src/app/services/title.service' -import { AcmeService } from './acme/acme.service' -import { DomainsService } from './domains/domains.service' +import { AuthorityService } from './authorities/authority.service' +import { DomainService } from './domains/domain.service' import { DomainsTableComponent } from './domains/table.component' -import { AcmeTableComponent } from './acme/table.component' +import { AuthoritiesTableComponent } from './authorities/table.component' @Component({ template: ` @@ -21,9 +21,9 @@ import { AcmeTableComponent } from './acme/table.component'

{{ 'Domains' | i18n }}

- {{ 'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.' + | i18n }}

- {{ 'ACME Providers' | i18n }} - @if (acmeService.acmes(); as acmes) { + {{ 'Certificate Authorities' | i18n }} + @if (authorityService.authorities(); as authorities) { }
- +
{{ 'Domains' | i18n }} - @if (domainsService.data(); as value) { + @if (domainService.data(); as value) { }
- +
`, changeDetection: ChangeDetectionStrategy.OnPush, @@ -85,11 +85,11 @@ import { AcmeTableComponent } from './acme/table.component' i18nPipe, DocsLinkDirective, DomainsTableComponent, - AcmeTableComponent, + AuthoritiesTableComponent, ], - providers: [AcmeService, DomainsService], + providers: [AuthorityService, DomainService], }) export default class SystemDomainsComponent { - protected readonly acmeService = inject(AcmeService) - protected readonly domainsService = inject(DomainsService) + protected readonly authorityService = inject(AuthorityService) + protected readonly domainService = inject(DomainService) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domains.service.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts similarity index 81% rename from web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domains.service.ts rename to web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts index acdd391c2..59f6a3ec1 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domains.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/domain.service.ts @@ -14,12 +14,12 @@ 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 { toAcmeName } from 'src/app/utils/acme' +import { toAuthorityName } from 'src/app/utils/acme' // @TODO translations @Injectable() -export class DomainsService { +export class DomainService { private readonly patch = inject>(PatchDB) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) @@ -46,32 +46,32 @@ export class DomainsService { { domain: 'blog.mydomain.com', gateway: { - id: '', + id: 'wireguard1', name: 'StartTunnel', }, - acme: { - url: '', - name: `Lert's Encrypt`, + authority: { + url: 'https://acme-v02.api.letsencrypt.org/directory', + name: `Let's Encrypt`, }, }, { domain: 'store.mydomain.com', gateway: { - id: '', + id: 'eth0', name: 'Ethernet', }, - acme: { - url: null, - name: 'System', + authority: { + url: 'local', + name: toAuthorityName(null), }, }, ], - acme: Object.keys(network.acme).reduce>( + authorities: Object.keys(network.acme).reduce>( (obj, url) => ({ ...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, patterns: [utils.Patterns.domain], }), - ...this.gatewaysAndAcme(), + ...this.gatewaysAndAuthorities(), }) this.formDialog.open(FormComponent, { @@ -107,11 +107,11 @@ export class DomainsService { async edit(domain: any) { const editSpec = ISB.InputSpec.of({ - ...this.gatewaysAndAcme(), + ...this.gatewaysAndAuthorities(), }) this.formDialog.open(FormComponent, { - label: 'Edit Domain' as any, // @TODO translation + label: 'Edit Domain', data: { spec: await configBuilderToSpec(editSpec), buttons: [ @@ -126,7 +126,7 @@ export class DomainsService { ], value: { gateway: domain.gateway.id, - acme: domain.acme.url, + authority: domain.authority.url, }, }, }) @@ -172,7 +172,7 @@ export class DomainsService { } } - private gatewaysAndAcme() { + private gatewaysAndAuthorities() { return { gateway: ISB.Value.select({ name: 'Gateway', @@ -181,11 +181,11 @@ export class DomainsService { values: this.data()!.gateways, default: '', }), - acme: ISB.Value.select({ - name: 'Default ACME', + authority: ISB.Value.select({ + name: 'Default Certificate Authority', description: - 'Select the default ACME provider for this domain. This can be overridden on a case-by-case basis.', - values: this.data()!.acme, + '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: '', }), } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/item.component.ts index ad035b910..8694a0cf5 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/item.component.ts @@ -11,66 +11,68 @@ import { TuiDropdown, TuiTextfield, } from '@taiga-ui/core' -import { DomainsService } from './domains.service' +import { DomainService } from './domain.service' @Component({ selector: 'tr[domain]', template: ` - {{ domain().domain }} - {{ domain().gateway.name }} - {{ domain().acme.name }} - - - - - - - - - - - + @if (domain(); as domain) { + {{ domain.domain }} + {{ domain.gateway.name }} + {{ domain.authority.name }} + + + + + + + + + + + + } `, styles: ` td:last-child { @@ -91,8 +93,8 @@ import { DomainsService } from './domains.service' changeDetection: ChangeDetectionStrategy.OnPush, imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield], }) -export class DomainsItemComponent { - protected readonly domainsService = inject(DomainsService) +export class DomainItemComponent { + protected readonly domainService = inject(DomainService) readonly domain = input.required() diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/table.component.ts index b82485ebf..3be3ca335 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/domains/domains/table.component.ts @@ -1,20 +1,23 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { ChangeDetectionStrategy, Component, inject } 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 { DomainsItemComponent } from './item.component' +import { DomainItemComponent } from './item.component' +import { DomainService } from './domain.service' @Component({ selector: 'domains-table', template: ` - - @for (domain of domains(); track $index) { +
+ @for (domain of domainService.data()?.domains; track $index) { } @empty {
- @if (domains()) { + @if (domainService.data()?.domains) { {{ 'No domains' | i18n }} @@ -32,10 +35,9 @@ import { DomainsItemComponent } from './item.component' i18nPipe, TableComponent, PlaceholderComponent, - DomainsItemComponent, + DomainItemComponent, ], }) export class DomainsTableComponent { - // @TODO Alex proper types - readonly domains = input() + protected readonly domainService = inject(DomainService) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts index d33a5a98f..3a68683b4 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/sessions/table.component.ts @@ -12,7 +12,6 @@ import { TuiIcon } from '@taiga-ui/core' import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit' import { TableComponent } from 'src/app/routes/portal/components/table.component' import { Session } from 'src/app/services/api/api.types' -import { toAcmeName } from 'src/app/utils/acme' import { PlatformInfoPipe } from './platform-info.pipe' import { i18nPipe } from '@start9labs/shared' @@ -182,6 +181,4 @@ export class SessionsTableComponent implements OnChanges { this.selected.update(selected => [...selected, session]) } } - - protected readonly toAcmeName = toAcmeName } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/system.const.ts b/web/projects/ui/src/app/routes/portal/routes/system/system.const.ts index 07a3485b2..2571c49e6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/system.const.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/system.const.ts @@ -38,16 +38,16 @@ export const SYSTEM_MENU = [ }, ], [ - { - icon: '@tui.globe', - item: 'Domains', - link: 'domains', - }, { icon: '@tui.door-open', item: 'Gateways', link: 'gateways', }, + { + icon: '@tui.globe', + item: 'Domains', + link: 'domains', + }, ], [ { diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 60f6d9478..07c0a8d9b 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -24,7 +24,7 @@ import { AuthService } from '../auth.service' import { T } from '@start9labs/start-sdk' import { MarketplacePkg } from '@start9labs/marketplace' 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' @@ -1396,7 +1396,7 @@ export class MockApiService extends ApiService { op: PatchOp.ADD, path: `/serverInfo/acme`, value: { - [toAcmeUrl(params.provider)]: { contact: params.contact }, + [toAuthorityUrl(params.provider)]: { contact: params.contact }, }, }, ] diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 40937ad49..a9568e7cb 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -1,6 +1,6 @@ import { DataModel } from 'src/app/services/patch-db/data-model' 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 export const mockPatchData: DataModel = { @@ -28,7 +28,7 @@ export const mockPatchData: DataModel = { lastRegion: null, }, acme: { - [knownACME[0].url]: { + [knownAuthorities[0].url]: { contact: ['mailto:support@start9.com'], }, }, @@ -160,6 +160,20 @@ export const mockPatchData: DataModel = { 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, diff --git a/web/projects/ui/src/app/utils/acme.ts b/web/projects/ui/src/app/utils/acme.ts index c3a2dee4b..c18d35b86 100644 --- a/web/projects/ui/src/app/utils/acme.ts +++ b/web/projects/ui/src/app/utils/acme.ts @@ -1,12 +1,14 @@ -export function toAcmeName(url: string | null): string | 'System CA' { - return knownACME.find(acme => acme.url === url)?.name || url || 'System CA' +export function toAuthorityName(url: string | null): string | 'Local Root CA' { + return ( + knownAuthorities.find(ca => ca.url === url)?.name || url || 'Local Root CA' + ) } -export function toAcmeUrl(name: string): string { - return knownACME.find(acme => acme.name === name)?.url || name +export function toAuthorityUrl(name: string): string { + return knownAuthorities.find(ca => ca.name === name)?.url || name } -export const knownACME = [ +export const knownAuthorities = [ { name: `Let's Encrypt`, url: 'https://acme-v02.api.letsencrypt.org/directory',