domains mostly finished

This commit is contained in:
Matt Hill
2025-08-05 17:29:48 -06:00
parent 3835562200
commit d8d1009417
30 changed files with 427 additions and 410 deletions

19
web/package-lock.json generated
View File

@@ -62,6 +62,7 @@
"patch-db-client": "file:../patch-db/client", "patch-db-client": "file:../patch-db/client",
"pbkdf2": "^3.1.2", "pbkdf2": "^3.1.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tldts": "^7.0.11",
"ts-matches": "^6.3.2", "ts-matches": "^6.3.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -12185,6 +12186,24 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"node_modules/tldts": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.11.tgz",
"integrity": "sha512-7k7JV/LZpGhFUu2t+YDaMZ1wdPPRNpaCYNQ0NQbSLY3Rbgy+XbCdkXyqRiS9TLXiYAsrv0yiA0OvnxmgRFCdNA==",
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.11"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.11.tgz",
"integrity": "sha512-65eeOpBwWBabh0XqT+zB0vEllq/V3XcrF2fhgMXWWFfNw1yxEjeYg9Vv/B/UNozd0CTR/TohO1ubfn6O6mBW3w==",
"license": "MIT"
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View File

@@ -52,6 +52,7 @@
"@taiga-ui/addon-table": "4.47.0", "@taiga-ui/addon-table": "4.47.0",
"@taiga-ui/cdk": "4.47.0", "@taiga-ui/cdk": "4.47.0",
"@taiga-ui/core": "4.47.0", "@taiga-ui/core": "4.47.0",
"@taiga-ui/dompurify": "4.1.11",
"@taiga-ui/event-plugins": "4.6.0", "@taiga-ui/event-plugins": "4.6.0",
"@taiga-ui/experimental": "4.47.0", "@taiga-ui/experimental": "4.47.0",
"@taiga-ui/icons": "4.47.0", "@taiga-ui/icons": "4.47.0",
@@ -59,7 +60,6 @@
"@taiga-ui/layout": "4.47.0", "@taiga-ui/layout": "4.47.0",
"@taiga-ui/legacy": "4.47.0", "@taiga-ui/legacy": "4.47.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@taiga-ui/dompurify": "4.1.11",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
@@ -68,8 +68,8 @@
"core-js": "^3.42.0", "core-js": "^3.42.0",
"cron": "^2.2.0", "cron": "^2.2.0",
"cronstrue": "^2.21.0", "cronstrue": "^2.21.0",
"dompurify": "^3.1.7",
"deep-equality-data-structures": "1.5.1", "deep-equality-data-structures": "1.5.1",
"dompurify": "^3.1.7",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6", "fuse.js": "^6.4.6",
"jose": "^4.9.0", "jose": "^4.9.0",
@@ -83,17 +83,18 @@
"patch-db-client": "file:../patch-db/client", "patch-db-client": "file:../patch-db/client",
"pbkdf2": "^3.1.2", "pbkdf2": "^3.1.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tldts": "^7.0.11",
"ts-matches": "^6.3.2", "ts-matches": "^6.3.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"zone.js": "^0.15.0" "zone.js": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-experts/hawkeye": "^1.7.2",
"@angular/build": "^20.1.0", "@angular/build": "^20.1.0",
"@angular/cli": "^20.1.0", "@angular/cli": "^20.1.0",
"@angular/compiler-cli": "^20.1.0", "@angular/compiler-cli": "^20.1.0",
"@angular/language-service": "^20.1.0", "@angular/language-service": "^20.1.0",
"@angular-experts/hawkeye": "^1.7.2",
"@types/dompurify": "3.0.5", "@types/dompurify": "3.0.5",
"@types/estree": "^0.0.51", "@types/estree": "^0.0.51",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",

View File

@@ -112,7 +112,7 @@ export default {
109: 'Privat', 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.', 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', 111: 'Keine Onion-Adressen',
112: 'Neue Onion-Adresse', 112: 'Neue onion-adresse',
113: 'Privater Schlüssel (optional)', 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: '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.',
115: 'Verarbeite 10.000 Logs', 115: 'Verarbeite 10.000 Logs',
@@ -303,7 +303,7 @@ export default {
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',
310: 'Fertig', 310: 'Fertig',
311: 'Master-Passwort erforderlich', 311: 'Master-passwort erforderlich',
312: 'Geben Sie Ihr Master-Passwort ein, um diese Sicherung zu verschlüsseln.', 312: 'Geben Sie Ihr Master-Passwort ein, um diese Sicherung zu verschlüsseln.',
313: 'Master-Passwort', 313: 'Master-Passwort',
314: 'Master-Passwort eingeben', 314: 'Master-Passwort eingeben',
@@ -539,6 +539,5 @@ export default {
544: 'Domain bearbeiten', 544: 'Domain bearbeiten',
545: 'Keine Domains', 545: 'Keine Domains',
546: 'Anbieter', 546: 'Anbieter',
547: 'DNS anzeigen', 547: 'DNS verwalten',
548: 'DNS testen',
} satisfies i18n } satisfies i18n

View File

@@ -15,7 +15,7 @@ export const ENGLISH = {
'Change Password': 13, 'Change Password': 13,
'General Settings': 14, 'General Settings': 14,
'Manage your overall setup and preferences': 15, 'Manage your overall setup and preferences': 15,
'Browser Tab Title': 16, 'Browser tab title': 16,
'Language': 17, 'Language': 17,
'Disk Repair': 18, 'Disk Repair': 18,
'Attempt automatic repair': 19, 'Attempt automatic repair': 19,
@@ -103,7 +103,7 @@ export const ENGLISH = {
'You have unsaved changes. Are you sure you want to leave?': 101, 'You have unsaved changes. Are you sure you want to leave?': 101,
'Leave': 102, 'Leave': 102,
'Are you sure?': 103, 'Are you sure?': 103,
'Select Domain': 104, 'Select domain': 104,
'Local': 105, '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, '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, 'Learn More': 107,
@@ -111,7 +111,7 @@ export const ENGLISH = {
'Private': 109, '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, '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, 'No onion addresses': 111,
'New Onion Address': 112, 'New onion address': 112,
'Private Key (optional)': 113, '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) address. If not provided, a random key will be generated and used.': 114,
'Processing 10,000 logs': 115, 'Processing 10,000 logs': 115,
@@ -297,16 +297,16 @@ export const ENGLISH = {
'Contact': 303, // as in, "contact us" 'Contact': 303, // as in, "contact us"
'Edit': 304, 'Edit': 304,
'Add Certificate Authority': 305, 'Add Certificate Authority': 305,
'Edit Contact Info': 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,
'Done': 310, 'Done': 310,
'Master Password Needed': 311, 'Master password needed': 311,
'Enter your master password to encrypt this backup.': 312, 'Enter your master password to encrypt this backup.': 312,
'Master Password': 313, 'Master Password': 313,
'Enter master password': 314, 'Enter master password': 314,
'Original Password Needed': 315, 'Original password needed': 315,
'This backup was created with a different password. Enter the original password that was used to encrypt this backup.': 316, 'This backup was created with a different password. Enter the original password that was used to encrypt this backup.': 316,
'Original Password': 317, 'Original Password': 317,
'Enter original password': 318, 'Enter original password': 318,
@@ -363,7 +363,7 @@ export const ENGLISH = {
'Ready to restore': 369, 'Ready to restore': 369,
'Local Hostname': 370, 'Local Hostname': 370,
'Created': 371, 'Created': 371,
'Password Required': 372, 'Password required': 372,
'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.': 373, 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.': 373,
'Decrypting drive': 374, 'Decrypting drive': 374,
'Select services to restore': 375, 'Select services to restore': 375,
@@ -395,7 +395,7 @@ export const ENGLISH = {
'Terminate selected': 401, 'Terminate selected': 401,
'Terminating sessions': 402, 'Terminating sessions': 402,
'No sessions': 403, 'No sessions': 403,
'Password Needed': 404, 'Password needed': 404,
'Connected': 405, 'Connected': 405,
'Forget': 406, // as in, delete or remove 'Forget': 406, // as in, delete or remove
'WiFi Credentials': 407, 'WiFi Credentials': 407,
@@ -526,7 +526,7 @@ export const ENGLISH = {
'Finished': 532, // an in, complete 'Finished': 532, // an in, complete
'Gateways': 533, // as in, a device or software that connects two different networks '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, '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 'Add gateway': 535, // as in, add a new network gateway to StartOS
'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
@@ -535,9 +535,8 @@ export const ENGLISH = {
'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 Certificate Authority': 542, 'Default Certificate Authority': 542,
'Certificate Authority': 543, 'Certificate Authority': 543,
'Edit Domain': 544, 'Edit domain': 544,
'No domains': 545, 'No domains': 545,
'Provider': 546, 'Provider': 546,
'Show DNS': 547, 'Manage DNS': 547,
'Test DNS': 548,
} as const } as const

View File

@@ -112,7 +112,7 @@ export default {
109: 'Privado', 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.', 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', 111: 'Sin direcciones onion',
112: 'Nueva dirección Onion', 112: 'Nueva dirección onion',
113: 'Clave privada (opcional)', 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: '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.',
115: 'Procesando 10,000 registros', 115: 'Procesando 10,000 registros',
@@ -294,7 +294,6 @@ export default {
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: 'Agregar un dominio a StartOS significa que puedes usarlo y sus subdominios para alojar interfaces de servicios en Internet público.', 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',
@@ -540,6 +539,5 @@ export default {
544: 'Editar dominio', 544: 'Editar dominio',
545: 'Sin dominios', 545: 'Sin dominios',
546: 'Proveedor', 546: 'Proveedor',
547: 'Mostrar DNS', 547: 'Administrar DNS',
548: 'Probar DNS',
} satisfies i18n } satisfies i18n

View File

@@ -539,6 +539,5 @@ export default {
544: 'Modifier le domaine', 544: 'Modifier le domaine',
545: 'Aucun domaine', 545: 'Aucun domaine',
546: 'Fournisseur', 546: 'Fournisseur',
547: 'Afficher le DNS', 547: 'Gérer le DNS',
548: 'Tester le DNS',
} satisfies i18n } satisfies i18n

View File

@@ -112,7 +112,7 @@ export default {
109: 'Prywatny', 109: 'Prywatny',
110: 'Dodaj adres onion, aby anonimowo udostępnić ten interfejs w sieci Tor. Adresy onion są dostępne tylko przez sieć Tor.', 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', 111: 'Brak adresów onion',
112: 'Nowy adres Onion', 112: 'Nowy adres onion',
113: 'Klucz prywatny (opcjonalnie)', 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: '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.',
115: 'Przetwarzanie 10 000 logów', 115: 'Przetwarzanie 10 000 logów',
@@ -539,6 +539,5 @@ export default {
544: 'Edytuj domenę', 544: 'Edytuj domenę',
545: 'Brak domen', 545: 'Brak domen',
546: 'Dostawca', 546: 'Dostawca',
547: 'Pokaż DNS', 547: 'Zarządzaj DNS',
548: 'Test DNS',
} satisfies i18n } satisfies i18n

View File

@@ -12,6 +12,8 @@ import { distinctUntilChanged, map, merge, Subject } from 'rxjs'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
// @TODO translations
@Component({ @Component({
selector: 'refresh-alert', selector: 'refresh-alert',
template: ` template: `

View File

@@ -221,7 +221,7 @@ export class InterfaceClearnetComponent {
host: this.interface.value().addressInfo.hostId, host: this.interface.value().addressInfo.hostId,
}) })
} else { } else {
await this.api.serverRemoveDomain(params) await this.api.osUiRemoveDomain(params)
} }
return true return true
} catch (e: any) { } catch (e: any) {
@@ -256,7 +256,7 @@ export class InterfaceClearnetComponent {
}) })
this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, { this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, {
label: 'Select Domain', label: 'Select domain',
data: { data: {
spec: await configBuilderToSpec( spec: await configBuilderToSpec(
ISB.InputSpec.of( ISB.InputSpec.of(
@@ -292,7 +292,7 @@ export class InterfaceClearnetComponent {
host: this.interface.value().addressInfo.hostId, host: this.interface.value().addressInfo.hostId,
}) })
} else { } else {
await this.api.serverAddDomain(params) await this.api.osUiAddDomain(params)
} }
return true return true
} catch (e: any) { } catch (e: any) {

View File

@@ -179,7 +179,7 @@ export class InterfaceTorComponent {
async add() { async add() {
this.formDialog.open<FormContext<OnionForm>>(FormComponent, { this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
label: 'New Onion Address', label: 'New onion address',
data: { data: {
spec: await configBuilderToSpec( spec: await configBuilderToSpec(
ISB.InputSpec.of({ ISB.InputSpec.of({

View File

@@ -133,7 +133,7 @@ export class TabsComponent {
) )
more(content: TemplateRef<any>) { more(content: TemplateRef<any>) {
this.dialogs.open(content, { label: 'Start OS' }).subscribe({ this.dialogs.open(content, { label: 'StartOS' }).subscribe({
complete: () => this.update(), complete: () => this.update(),
}) })
} }

View File

@@ -128,7 +128,7 @@ export class BackupsBackupComponent {
this.dialog this.dialog
.openPrompt<string>({ .openPrompt<string>({
label: 'Master Password Needed', label: 'Master password needed',
data: { data: {
message: 'Enter your master password to encrypt this backup.', message: 'Enter your master password to encrypt this backup.',
label: 'Master Password', label: 'Master Password',
@@ -169,7 +169,7 @@ export class BackupsBackupComponent {
this.dialog this.dialog
.openPrompt<string>({ .openPrompt<string>({
label: 'Original Password Needed', label: 'Original password needed',
data: { data: {
message: message:
'This backup was created with a different password. Enter the original password that was used to encrypt this backup.', 'This backup was created with a different password. Enter the original password that was used to encrypt this backup.',

View File

@@ -55,7 +55,7 @@ export class BackupRestoreComponent {
onClick(serverId: string, { passwordHash }: StartOSDiskInfo) { onClick(serverId: string, { passwordHash }: StartOSDiskInfo) {
this.dialog this.dialog
.openPrompt<string>({ .openPrompt<string>({
label: 'Password Required', label: 'Password required',
data: { data: {
message: message:
'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.',

View File

@@ -117,7 +117,7 @@ export class AuthorityService {
}) })
this.formDialog.open(FormComponent, { this.formDialog.open(FormComponent, {
label: 'Edit Contact Info', label: 'Edit contact info',
data: { data: {
spec: await configBuilderToSpec(editSpec), spec: await configBuilderToSpec(editSpec),
buttons: [ buttons: [

View File

@@ -19,7 +19,9 @@ import { Authority, AuthorityService } from './authority.service'
@if (authority(); as authority) { @if (authority(); as authority) {
<td>{{ authority.name }}</td> <td>{{ authority.name }}</td>
<td>{{ authority.url || '-' }}</td> <td>{{ authority.url || '-' }}</td>
<td>{{ authority.contact ? authority.contact.join(', ') : '-' }}</td> <td class="hidden">
{{ authority.contact ? authority.contact.join(', ') : '-' }}
</td>
<td> <td>
<button <button
tuiIconButton tuiIconButton
@@ -73,7 +75,7 @@ import { Authority, AuthorityService } from './authority.service'
`, `,
styles: ` styles: `
td:last-child { td:last-child {
grid-area: 1 / 2 / 3; grid-area: 1 / 2 / 4;
align-self: center; align-self: center;
text-align: right; text-align: right;
} }
@@ -85,6 +87,10 @@ import { Authority, AuthorityService } from './authority.service'
font: var(--tui-font-text-m); font: var(--tui-font-text-m);
font-weight: bold; font-weight: bold;
} }
.hidden {
display: none;
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -0,0 +1,92 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MappedDomain } from './domain.service'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
@Component({
selector: 'dns',
template: `
<section class="g-card">
<header>
{{ $any('Using IP') | i18n }}
</header>
@let subdomain = context.data.subdomain;
@let wanIp = context.data.gateway.ipInfo?.wanIp || ('Error' | i18n);
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
<tr>
<td>A</td>
<td>{{ subdomain || '@' }}</td>
<td>{{ wanIp }}</td>
<td></td>
</tr>
<tr>
<td>A</td>
<td>
{{ subdomain ? '*.' + subdomain : '*' }}
</td>
<td>{{ wanIp }}</td>
<td></td>
</tr>
</table>
</section>
@if (context.data.gateway.ipInfo?.deviceType !== 'wireguard') {
<section class="g-card">
<header>
{{ $any('Using Dynamic DNS') | i18n }}
</header>
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
<tr>
<td>ALIAS</td>
<td>{{ subdomain || '@' }}</td>
<td>[Dynamic DNS Address]</td>
<td></td>
</tr>
<tr>
<td>ALIAS</td>
<td>{{ subdomain ? '*.' + subdomain : '*' }}</td>
<td>[Dynamic DNS Address]</td>
<td></td>
</tr>
</table>
</section>
}
<button tuiButton size="l" (click)="testDns()">
{{ 'Test' | i18n }}
</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, i18nPipe, TableComponent],
})
export class DnsComponent {
private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
readonly context = injectContext<TuiDialogContext<void, MappedDomain>>()
async testDns() {
const loader = this.loader.open().subscribe()
try {
await this.api.testDomain({
fqdn: this.context.data.fqdn,
gateway: this.context.data.gateway.id,
})
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
} finally {
loader.unsubscribe()
}
}
}
export const DNS = new PolymorpheusComponent(DnsComponent)

View File

@@ -6,7 +6,7 @@ import {
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { ISB, utils } from '@start9labs/start-sdk' import { ISB, T, utils } from '@start9labs/start-sdk'
import { filter, map } from 'rxjs' import { filter, map } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -15,9 +15,26 @@ 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 { toAuthorityName } from 'src/app/utils/acme' 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'
// @TODO translations // @TODO translations
export type MappedDomain = {
fqdn: string
subdomain: string | null
gateway: {
id: string
name: string | null
ipInfo: T.IpInfo | null
}
authority: {
url: string | null
name: string | null
}
}
@Injectable() @Injectable()
export class DomainService { export class DomainService {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
@@ -30,57 +47,45 @@ export class DomainService {
readonly data = toSignal( readonly data = toSignal(
this.patch.watch$('serverInfo', 'network').pipe( this.patch.watch$('serverInfo', 'network').pipe(
map(network => { map(({ networkInterfaces, domains, acme }) => ({
return { gateways: Object.entries(networkInterfaces).reduce<
gateways: Object.entries(network.networkInterfaces).reduce< Record<string, string>
Record<string, string> >(
>( (obj, [id, n]) => ({
(obj, [id, n]) => ({ ...obj,
...obj, [id]: n.ipInfo?.name || '',
[id]: n.ipInfo?.name || '', }),
}), {},
{}, ),
), domains: Object.entries(domains).map(
// @TODO use real data ([fqdn, { gateway, acme }]) =>
domains: [ ({
{ fqdn,
domain: 'blog.mydomain.com', subdomain: parse(fqdn).subdomain,
gateway: { gateway: {
id: 'wireguard1', id: gateway,
name: 'StartTunnel', ipInfo: networkInterfaces[gateway]?.ipInfo || null,
}, },
authority: { authority: {
url: 'https://acme-v02.api.letsencrypt.org/directory', url: acme,
name: `Let's Encrypt`, name: toAuthorityName(acme),
}, },
}, }) as MappedDomain,
{ ),
domain: 'store.mydomain.com', authorities: Object.keys(acme).reduce<Record<string, string>>(
gateway: { (obj, url) => ({
id: 'eth0', ...obj,
name: 'Ethernet', [url]: toAuthorityName(url),
}, }),
authority: { { local: toAuthorityName(null) },
url: 'local', ),
name: toAuthorityName(null), })),
},
},
],
authorities: Object.keys(network.acme).reduce<Record<string, string>>(
(obj, url) => ({
...obj,
[url]: toAuthorityName(url),
}),
{ local: toAuthorityName(null) },
),
}
}),
), ),
) )
async add() { async add() {
const addSpec = ISB.InputSpec.of({ const addSpec = ISB.InputSpec.of({
domain: ISB.Value.text({ fqdn: ISB.Value.text({
name: 'Domain', name: 'Domain',
description: description:
'Enter a domain/subdomain. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com. In any case, the domain you enter and all possible subdomains of the domain will be available for assignment in StartOS', 'Enter a domain/subdomain. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com. In any case, the domain you enter and all possible subdomains of the domain will be available for assignment in StartOS',
@@ -92,26 +97,31 @@ export class DomainService {
}) })
this.formDialog.open(FormComponent, { this.formDialog.open(FormComponent, {
label: 'Add Domain' as any, label: 'Add domain',
data: { data: {
spec: await configBuilderToSpec(addSpec), spec: await configBuilderToSpec(addSpec),
buttons: [ buttons: [
{ {
text: 'Save', text: 'Save',
handler: (input: typeof addSpec._TYPE) => this.save(input), handler: (input: typeof addSpec._TYPE) =>
this.save({
fqdn: input.fqdn,
gateway: input.gateway,
acme: input.authority === 'local' ? null : input.authority,
}),
}, },
], ],
}, },
}) })
} }
async edit(domain: any) { async edit(domain: MappedDomain) {
const editSpec = ISB.InputSpec.of({ const editSpec = ISB.InputSpec.of({
...this.gatewaysAndAuthorities(), ...this.gatewaysAndAuthorities(),
}) })
this.formDialog.open(FormComponent, { this.formDialog.open(FormComponent, {
label: 'Edit Domain', label: 'Edit domain',
data: { data: {
spec: await configBuilderToSpec(editSpec), spec: await configBuilderToSpec(editSpec),
buttons: [ buttons: [
@@ -119,20 +129,21 @@ export class DomainService {
text: 'Save', text: 'Save',
handler: (input: typeof editSpec._TYPE) => handler: (input: typeof editSpec._TYPE) =>
this.save({ this.save({
domain: domain.domain, fqdn: domain.fqdn,
...input, gateway: input.gateway,
acme: input.authority === 'local' ? null : input.authority,
}), }),
}, },
], ],
value: { value: {
gateway: domain.gateway.id, gateway: domain.gateway.id,
authority: domain.authority.url, authority: domain.authority.url || 'local',
}, },
}, },
}) })
} }
remove(domain: any) { remove(fqdn: string) {
this.dialog this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' }) .openConfirm({ label: 'Are you sure?', size: 's' })
.pipe(filter(Boolean)) .pipe(filter(Boolean))
@@ -140,7 +151,7 @@ export class DomainService {
const loader = this.loader.open('Deleting').subscribe() const loader = this.loader.open('Deleting').subscribe()
try { try {
// @TODO API await this.api.removeDomain({ fqdn })
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {
@@ -149,20 +160,17 @@ export class DomainService {
}) })
} }
showDns(domain: any) { showDns(domain: MappedDomain) {
// @TODO this.dialog
.openComponent(DNS, { label: 'Manage DNS', data: domain })
.subscribe()
} }
testDns(domain: any) { private async save(params: RR.AddDomainReq) {
// @TODO
}
// @TODO different endpoints for create and edit?
private async save(params: any) {
const loader = this.loader.open('Saving').subscribe() const loader = this.loader.open('Saving').subscribe()
try { try {
// @TODO API await this.api.addDomain(params)
return true return true
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -11,14 +11,14 @@ import {
TuiDropdown, TuiDropdown,
TuiTextfield, TuiTextfield,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { DomainService } from './domain.service' import { DomainService, MappedDomain } from './domain.service'
@Component({ @Component({
selector: 'tr[domain]', selector: 'tr[domain]',
template: ` template: `
@if (domain(); as domain) { @if (domain(); as domain) {
<td>{{ domain.domain }}</td> <td>{{ domain.fqdn }}</td>
<td [style.order]="-1">{{ domain.gateway.name }}</td> <td [style.order]="-1">{{ domain.gateway.ipInfo?.name || '-' }}</td>
<td>{{ domain.authority.name }}</td> <td>{{ domain.authority.name }}</td>
<td> <td>
<button <button
@@ -47,15 +47,7 @@ import { DomainService } from './domain.service'
iconStart="@tui.eye" iconStart="@tui.eye"
(click)="domainService.showDns(domain)" (click)="domainService.showDns(domain)"
> >
{{ 'Show DNS' | i18n }} {{ 'Manage DNS' | i18n }}
</button>
<button
tuiOption
new
iconStart="@tui.arrow-up-down"
(click)="domainService.testDns(domain)"
>
{{ 'Test DNS' | i18n }}
</button> </button>
</tui-opt-group> </tui-opt-group>
<tui-opt-group> <tui-opt-group>
@@ -64,7 +56,7 @@ import { DomainService } from './domain.service'
new new
iconStart="@tui.trash" iconStart="@tui.trash"
class="g-negative" class="g-negative"
(click)="domainService.remove(domain)" (click)="domainService.remove(domain.fqdn)"
> >
{{ 'Delete' | i18n }} {{ 'Delete' | i18n }}
</button> </button>
@@ -96,7 +88,7 @@ import { DomainService } from './domain.service'
export class DomainItemComponent { export class DomainItemComponent {
protected readonly domainService = inject(DomainService) protected readonly domainService = inject(DomainService)
readonly domain = input.required<any>() readonly domain = input.required<MappedDomain>()
open = false open = false
} }

View File

@@ -18,7 +18,7 @@ import { TitleDirective } from 'src/app/services/title.service'
import { TuiHeader } from '@taiga-ui/layout' import { TuiHeader } from '@taiga-ui/layout'
import { map } from 'rxjs' import { map } from 'rxjs'
import { ISB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
import { GatewayWithID } from './item.component' import { GatewayPlus } from './item.component'
@Component({ @Component({
template: ` template: `
@@ -87,19 +87,24 @@ export default class GatewaysComponent {
.watch$('serverInfo', 'network', 'networkInterfaces') .watch$('serverInfo', 'network', 'networkInterfaces')
.pipe( .pipe(
map(gateways => map(gateways =>
Object.entries(gateways).map( Object.entries(gateways)
([id, val]) => .filter(([_, val]) => !!val.ipInfo)
({ .map(
...val, ([id, val]) =>
id, ({
}) as GatewayWithID, ...val,
), id,
ipv4: val.ipInfo?.subnets
.filter(s => !s.includes('::'))
.map(s => s.split('/')[0]),
}) as GatewayPlus,
),
), ),
) )
async add() { async add() {
this.formDialog.open(FormComponent, { this.formDialog.open(FormComponent, {
label: 'Add Gateway', label: 'Add gateway',
data: { data: {
spec: await configBuilderToSpec(gatewaySpec), spec: await configBuilderToSpec(gatewaySpec),
buttons: [ buttons: [

View File

@@ -24,55 +24,64 @@ 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 { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
export type GatewayWithID = T.NetworkInterfaceInfo & { export type GatewayPlus = T.NetworkInterfaceInfo & {
id: string id: string
ipInfo: T.IpInfo ipInfo: T.IpInfo
ipv4: string[]
} }
@Component({ @Component({
selector: 'tr[proxy]', selector: 'tr[proxy]',
template: ` template: `
<td [style.grid-column]="'span 2'">{{ proxy().ipInfo.name }}</td> @if (proxy(); as proxy) {
<td class="type">{{ proxy().ipInfo.deviceType || '-' }}</td> <td [style.grid-column]="'span 2'">{{ proxy.ipInfo.name }}</td>
<td [style.order]="-2"> <td class="type">{{ proxy.ipInfo.deviceType || '-' }}</td>
{{ proxy().public ? ('Public' | i18n) : ('Private' | i18n) }} <td [style.order]="-2">
</td> {{ proxy.public ? ('Public' | i18n) : ('Private' | i18n) }}
<!-- // @TODO show both LAN IPs? --> </td>
<td class="lan">{{ proxy().ipInfo.subnets[0] }}</td> <td class="lan">{{ proxy.ipv4.join(', ') }}</td>
<td class="wan">{{ proxy().ipInfo.wanIp }}</td> <td
<td> class="wan"
<button [style.color]="
tuiIconButton proxy.ipInfo.wanIp ? 'var(--tui-text-warning)' : undefined
tuiDropdown "
size="s"
appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical"
[tuiAppearanceState]="open ? 'hover' : null"
[(tuiDropdownOpen)]="open"
> >
{{ 'More' | i18n }} {{ proxy.ipInfo.wanIp || ('Error' | i18n) }}
<tui-data-list size="s" *tuiTextfieldDropdown> </td>
<tui-opt-group> <td>
<button tuiOption new iconStart="@tui.pencil" (click)="rename()"> <button
{{ 'Rename' | i18n }} tuiIconButton
</button> tuiDropdown
</tui-opt-group> size="s"
@if (proxy().ipInfo.deviceType === 'wireguard') { appearance="flat-grayscale"
iconStart="@tui.ellipsis-vertical"
[tuiAppearanceState]="open ? 'hover' : null"
[(tuiDropdownOpen)]="open"
>
{{ 'More' | i18n }}
<tui-data-list size="s" *tuiTextfieldDropdown>
<tui-opt-group> <tui-opt-group>
<button <button tuiOption new iconStart="@tui.pencil" (click)="rename()">
tuiOption {{ 'Rename' | i18n }}
new
iconStart="@tui.trash"
class="g-negative"
(click)="remove()"
>
{{ 'Delete' | i18n }}
</button> </button>
</tui-opt-group> </tui-opt-group>
} @if (proxy.ipInfo.deviceType === 'wireguard') {
</tui-data-list> <tui-opt-group>
</button> <button
</td> tuiOption
new
iconStart="@tui.trash"
class="g-negative"
(click)="remove()"
>
{{ 'Delete' | i18n }}
</button>
</tui-opt-group>
}
</tui-data-list>
</button>
</td>
}
`, `,
styles: ` styles: `
td:last-child { td:last-child {
@@ -106,7 +115,7 @@ export type GatewayWithID = T.NetworkInterfaceInfo & {
grid-column: span 2; grid-column: span 2;
&::before { &::before {
content: 'LAN IPs: '; content: 'LAN IP: ';
color: var(--tui-text-primary); color: var(--tui-text-primary);
} }
} }
@@ -133,7 +142,7 @@ export class GatewaysItemComponent {
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)
readonly proxy = input.required<GatewayWithID>() readonly proxy = input.required<GatewayPlus>()
open = false open = false

View File

@@ -3,7 +3,7 @@ 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 { GatewaysItemComponent, GatewayWithID } from './item.component' import { GatewaysItemComponent, GatewayPlus } from './item.component'
@Component({ @Component({
selector: '[gateways]', selector: '[gateways]',
@@ -13,7 +13,7 @@ import { GatewaysItemComponent, GatewayWithID } from './item.component'
'Name', 'Name',
'Type', 'Type',
'Access', 'Access',
$any('LAN IPs'), $any('LAN IP'),
$any('WAN IP'), $any('WAN IP'),
null, null,
]" ]"
@@ -25,7 +25,7 @@ import { GatewaysItemComponent, GatewayWithID } from './item.component'
<td colspan="5"> <td colspan="5">
@if (gateways()) { @if (gateways()) {
<app-placeholder icon="@tui.door-closed-locked"> <app-placeholder icon="@tui.door-closed-locked">
<!-- @TODO Matt finalize text and add translations --> <!-- @TODO translation -->
No gateways No gateways
</app-placeholder> </app-placeholder>
} @else { } @else {
@@ -45,6 +45,6 @@ import { GatewaysItemComponent, GatewayWithID } from './item.component'
PlaceholderComponent, PlaceholderComponent,
], ],
}) })
export class GatewaysTableComponent<T extends GatewayWithID> { export class GatewaysTableComponent<T extends GatewayPlus> {
readonly gateways = input<readonly T[] | null>(null) readonly gateways = input<readonly T[] | null>(null)
} }

View File

@@ -102,7 +102,7 @@ import { SystemWipeComponent } from './wipe.component'
<div tuiCell tuiAppearance="outline-grayscale"> <div tuiCell tuiAppearance="outline-grayscale">
<tui-icon icon="@tui.app-window" /> <tui-icon icon="@tui.app-window" />
<span tuiTitle> <span tuiTitle>
<strong>{{ 'Browser Tab Title' | i18n }}</strong> <strong>{{ 'Browser tab title' | i18n }}</strong>
<span tuiSubtitle> <span tuiSubtitle>
{{ 'Customize the name appearing in your browser tab' | i18n }} {{ 'Customize the name appearing in your browser tab' | i18n }}
</span> </span>
@@ -302,7 +302,7 @@ export default class SystemGeneralComponent {
onTitle() { onTitle() {
const sub = this.dialog const sub = this.dialog
.openPrompt<string>({ .openPrompt<string>({
label: 'Browser Tab Title', label: 'Browser tab title',
data: { data: {
label: 'Device Name', label: 'Device Name',
message: message:

View File

@@ -144,7 +144,7 @@ export class WifiTableComponent {
await this.component.saveAndConnect(network.ssid) await this.component.saveAndConnect(network.ssid)
} else { } else {
this.formDialog.open<FormContext<WiFiForm>>(FormComponent, { this.formDialog.open<FormContext<WiFiForm>>(FormComponent, {
label: 'Password Needed', label: 'Password needed',
data: { data: {
spec: wifiSpec.spec, spec: wifiSpec.spec,
buttons: [ buttons: [

View File

@@ -234,7 +234,28 @@ export namespace RR {
} }
export type CreateBackupRes = null export type CreateBackupRes = null
// tunnel // network
export type AddDomainReq = {
fqdn: string
gateway: string
acme: string | null
} // net.domain.add
export type AddDomainRes = null
export type RemoveDomainReq = {
fqdn: string
} // net.domain.remove
export type RemoveDomainRes = null
export type TestDomainReq = {
fqdn: string
gateway: string
} // net.domain.test
export type TestDomainRes = {
root: boolean
wildcard: boolean
}
export type AddTunnelReq = { export type AddTunnelReq = {
name: string name: string
@@ -255,7 +276,7 @@ export namespace RR {
export type RemoveTunnelRes = null export type RemoveTunnelRes = null
export type InitAcmeReq = { export type InitAcmeReq = {
provider: 'letsencrypt' | 'letsencrypt-staging' | string provider: string
contact: string[] contact: string[]
} }
export type InitAcmeRes = null export type InitAcmeRes = null
@@ -288,19 +309,19 @@ export namespace RR {
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
export type RemoveOnionRes = null export type RemoveOnionRes = null
export type ServerAddDomainReq = { export type OsUiAddDomainReq = {
// server.host.address.domain.add // server.host.address.domain.add
domain: string // FQDN domain: string // FQDN
private: boolean private: boolean
acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null acme: string | null // Url | null
} }
export type AddDomainRes = null export type OsUiAddDomainRes = null
export type ServerRemoveDomainReq = { export type OsUiRemoveDomainReq = {
// server.host.address.domain.remove // server.host.address.domain.remove
domain: string // FQDN domain: string // FQDN
} }
export type RemoveDomainRes = null export type OsUiRemoveDomainRes = null
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & { export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
// package.host.binding.set-public // package.host.binding.set-public
@@ -316,17 +337,19 @@ export namespace RR {
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
export type PkgAddDomainReq = ServerAddDomainReq & { export type PkgAddDomainReq = OsUiAddDomainReq & {
// package.host.address.domain.add // package.host.address.domain.add
package: T.PackageId // string package: T.PackageId // string
host: T.HostId // string host: T.HostId // string
} }
export type PkgAddDomainRes = null
export type PkgRemoveDomainReq = ServerRemoveDomainReq & { export type PkgRemoveDomainReq = OsUiRemoveDomainReq & {
// package.host.address.domain.remove // package.host.address.domain.remove
package: T.PackageId // string package: T.PackageId // string
host: T.HostId // string host: T.HostId // string
} }
export type PkgRemoveDomainRes = null
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
export type GetPackageLogsRes = FetchLogsRes export type GetPackageLogsRes = FetchLogsRes
@@ -624,32 +647,6 @@ export type DependencyErrorTransitive = {
// @TODO 041 // @TODO 041
// export namespace RR041 { // export namespace RR041 {
// // ** domains **
// export type ClaimStart9ToReq = { gatewayId: string } // net.domain.me.claim
// export type ClaimStart9ToRes = null
// export type DeleteStart9ToReq = {} // net.domain.me.delete
// export type DeleteStart9ToRes = null
// export type AddDomainReq = {
// hostname: string
// provider: {
// name: string
// username: string | null
// password: string | null
// }
// gatewayId: string
// } // net.domain.add
// export type AddDomainRes = null
// export type DeleteDomainReq = { hostname: string } // net.domain.delete
// export type DeleteDomainRes = null
// // port forwards
// export type OverridePortReq = { target: number; port: number } // net.port-forwards.override
// export type OverridePortRes = null
// // ** automated backups ** // // ** automated backups **
@@ -731,20 +728,6 @@ export type DependencyErrorTransitive = {
// @TODO 041 types // @TODO 041 types
// export type AppMetrics = {
// memory: {
// percentageUsed: MetricData
// used: MetricData
// }
// cpu: {
// percentageUsed: MetricData
// }
// disk: {
// percentageUsed: MetricData
// used: MetricData
// }
// }
// export type RemoteBackupTarget = CifsBackupTarget | CloudBackupTarget // export type RemoteBackupTarget = CifsBackupTarget | CloudBackupTarget
// export type BackupTarget = RemoteBackupTarget | DiskBackupTarget // export type BackupTarget = RemoteBackupTarget | DiskBackupTarget

View File

@@ -184,33 +184,15 @@ export abstract class ApiService {
// @TODO 041 // @TODO 041
// abstract setOutboundProxy(
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes>
// ** domains ** // ** domains **
// @TODO 041 // @TODO 041
// abstract claimStart9ToDomain( abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes>
// params: RR.ClaimStart9ToReq,
// ): Promise<RR.ClaimStart9ToRes>
// abstract deleteStart9ToDomain( abstract removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes>
// params: RR.DeleteStart9ToReq,
// ): Promise<RR.DeleteStart9ToRes>
// abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> abstract testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes>
// abstract deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes>
// ** port forwards **
// @TODO 041
// abstract overridePortForward(
// params: RR.OverridePortReq,
// ): Promise<RR.OverridePortRes>
// wifi // wifi
@@ -387,13 +369,13 @@ export abstract class ApiService {
params: RR.ServerRemoveOnionReq, params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes> ): Promise<RR.RemoveOnionRes>
abstract serverAddDomain( abstract osUiAddDomain(
params: RR.ServerAddDomainReq, params: RR.OsUiAddDomainReq,
): Promise<RR.AddDomainRes> ): Promise<RR.OsUiAddDomainRes>
abstract serverRemoveDomain( abstract osUiRemoveDomain(
params: RR.ServerRemoveDomainReq, params: RR.OsUiRemoveDomainReq,
): Promise<RR.RemoveDomainRes> ): Promise<RR.OsUiRemoveDomainRes>
abstract pkgBindingSetPubic( abstract pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq, params: RR.PkgBindingSetPublicReq,
@@ -405,9 +387,9 @@ export abstract class ApiService {
params: RR.PkgRemoveOnionReq, params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes> ): Promise<RR.RemoveOnionRes>
abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes>
abstract pkgRemoveDomain( abstract pkgRemoveDomain(
params: RR.PkgRemoveDomainReq, params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> ): Promise<RR.PkgRemoveDomainRes>
} }

View File

@@ -358,41 +358,19 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'net.tunnel.remove', params }) return this.rpcRequest({ method: 'net.tunnel.remove', params })
} }
// async setOutboundProxy(
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes> {
// return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
// }
// domains // domains
// async claimStart9ToDomain( async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
// params: RR.ClaimStart9ToReq, return this.rpcRequest({ method: 'net.domain.add', params })
// ): Promise<RR.ClaimStart9ToRes> { }
// return this.rpcRequest({ method: 'net.domain.me.claim', params })
// }
// async deleteStart9ToDomain( async removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes> {
// params: RR.DeleteStart9ToReq, return this.rpcRequest({ method: 'net.domain.remove', params })
// ): Promise<RR.DeleteStart9ToRes> { }
// return this.rpcRequest({ method: 'net.domain.me.delete', params })
// }
// async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> { async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
// return this.rpcRequest({ method: 'net.domain.add', params }) return this.rpcRequest({ method: 'net.domain.test', params })
// } }
// async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
// return this.rpcRequest({ method: 'net.domain.delete', params })
// }
// port forwards
// async overridePortForward(
// params: RR.OverridePortReq,
// ): Promise<RR.OverridePortRes> {
// return this.rpcRequest({ method: 'net.port-forwards.override', params })
// }
// wifi // wifi
@@ -685,18 +663,18 @@ export class LiveApiService extends ApiService {
}) })
} }
async serverAddDomain( async osUiAddDomain(
params: RR.ServerAddDomainReq, params: RR.OsUiAddDomainReq,
): Promise<RR.AddDomainRes> { ): Promise<RR.OsUiAddDomainRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'server.host.address.domain.add', method: 'server.host.address.domain.add',
params, params,
}) })
} }
async serverRemoveDomain( async osUiRemoveDomain(
params: RR.ServerRemoveDomainReq, params: RR.OsUiRemoveDomainReq,
): Promise<RR.RemoveDomainRes> { ): Promise<RR.OsUiRemoveDomainRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'server.host.address.domain.remove', method: 'server.host.address.domain.remove',
params, params,
@@ -728,7 +706,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> { async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'package.host.address.domain.add', method: 'package.host.address.domain.add',
params, params,
@@ -737,7 +715,7 @@ export class LiveApiService extends ApiService {
async pkgRemoveDomain( async pkgRemoveDomain(
params: RR.PkgRemoveDomainReq, params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> { ): Promise<RR.PkgRemoveDomainRes> {
return this.rpcRequest({ return this.rpcRequest({
method: 'package.host.address.domain.remove', method: 'package.host.address.domain.remove',
params, params,

View File

@@ -601,113 +601,50 @@ export class MockApiService extends ApiService {
return null return null
} }
// async setOutboundProxy(
// params: RR.SetOutboundTunnelReq,
// ): Promise<RR.SetOutboundTunnelRes> {
// await pauseFor(2000)
// const patch: ReplaceOperation<string | null>[] = [
// {
// op: PatchOp.REPLACE,
// path: '/serverInfo/network/outboundInterface',
// value: params.id,
// },
// ]
// this.mockRevision(patch)
// return null
// }
// domains // domains
// async claimStart9ToDomain( async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
// params: RR.ClaimStart9ToReq, await pauseFor(2000)
// ): Promise<RR.ClaimStart9ToRes> {
// await pauseFor(2000)
// const patch = [ const patch = [
// { {
// op: PatchOp.REPLACE, op: PatchOp.REPLACE,
// path: '/serverInfo/network/start9To', path: `/serverInfo/network/domains`,
// value: { value: {
// subdomain: 'xyz', [params.fqdn]: {
// gatewayId: params.gatewayId, gateway: params.gateway,
// }, acme: params.acme,
// }, },
// ] },
// this.mockRevision(patch) },
]
this.mockRevision(patch)
// return null return null
// } }
// async deleteStart9ToDomain( async removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes> {
// params: RR.DeleteStart9ToReq, await pauseFor(2000)
// ): Promise<RR.DeleteStart9ToRes> { const patch = [
// await pauseFor(2000) {
// const patch = [ op: PatchOp.REPLACE,
// { path: '/serverInfo/network/domains',
// op: PatchOp.REPLACE, value: {},
// path: '/serverInfo/network/start9To', },
// value: null, ]
// }, this.mockRevision(patch)
// ]
// this.mockRevision(patch)
// return null return null
// } }
// async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> { async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
// await pauseFor(2000) await pauseFor(2000)
// const patch = [ return {
// { root: true,
// op: PatchOp.REPLACE, wildcard: true,
// path: `/serverInfo/network/domains`, }
// value: { }
// [params.hostname]: {
// gatewayId: params.gatewayId,
// provider: params.provider.name,
// },
// },
// },
// ]
// this.mockRevision(patch)
// return null
// }
// async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
// await pauseFor(2000)
// const patch = [
// {
// op: PatchOp.REPLACE,
// path: '/serverInfo/network/domains',
// value: {},
// },
// ]
// this.mockRevision(patch)
// return null
// }
// port forwards
// async overridePortForward(
// params: RR.OverridePortReq,
// ): Promise<RR.OverridePortRes> {
// await pauseFor(2000)
// const patch = [
// {
// op: PatchOp.REPLACE,
// path: '/serverInfo/network/wanConfig/forwards/0/override',
// value: params.port,
// },
// ]
// this.mockRevision(patch)
// return null
// }
// wifi // wifi
@@ -1496,7 +1433,9 @@ export class MockApiService extends ApiService {
return null return null
} }
async serverAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> { async osUiAddDomain(
params: RR.OsUiAddDomainReq,
): Promise<RR.OsUiAddDomainRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: Operation<any>[] = [ const patch: Operation<any>[] = [
@@ -1529,9 +1468,9 @@ export class MockApiService extends ApiService {
return null return null
} }
async serverRemoveDomain( async osUiRemoveDomain(
params: RR.PkgRemoveDomainReq, params: RR.OsUiRemoveDomainReq,
): Promise<RR.RemoveDomainRes> { ): Promise<RR.OsUiRemoveDomainRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: RemoveOperation[] = [ const patch: RemoveOperation[] = [
@@ -1613,7 +1552,7 @@ export class MockApiService extends ApiService {
return null return null
} }
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> { async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: Operation<any>[] = [ const patch: Operation<any>[] = [
@@ -1648,7 +1587,7 @@ export class MockApiService extends ApiService {
async pkgRemoveDomain( async pkgRemoveDomain(
params: RR.PkgRemoveDomainReq, params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> { ): Promise<RR.PkgRemoveDomainRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: RemoveOperation[] = [ const patch: RemoveOperation[] = [

View File

@@ -32,6 +32,16 @@ export const mockPatchData: DataModel = {
contact: ['mailto:support@start9.com'], contact: ['mailto:support@start9.com'],
}, },
}, },
domains: {
'cloud.private.com': {
gateway: 'eth0',
acme: null,
},
'public.com': {
gateway: 'wireguard1',
acme: 'https://acme-v02.api.letsencrypt.org/directory',
},
},
host: { host: {
bindings: { bindings: {
80: { 80: {

View File

@@ -1,7 +1,20 @@
import { Languages } from '@start9labs/shared' import { Languages } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData } export type DataModel = T.Public & {
ui: UIData
packageData: AllPackageData
serverInfo: T.ServerInfo & {
network: T.NetworkInfo & {
domains: {
[fqdn: string]: {
gateway: string
acme: string | null
}
}
}
}
}
export type UIData = { export type UIData = {
name: string | null name: string | null
@@ -11,22 +24,6 @@ export type UIData = {
language: Languages language: Languages
} }
export type NetworkInfo = T.NetworkInfo & {
// @TODO 041
// start9To: {
// subdomain: string
// gatewayId: string
// } | null
// domains: {
// [key: string]: Domain
// }
// wanConfig: {
// upnp: boolean
// forwards: PortForward[]
// }
// outboundProxy: string | null
}
export type PackageDataEntry<T extends StateInfo = StateInfo> = export type PackageDataEntry<T extends StateInfo = StateInfo> =
T.PackageDataEntry & { T.PackageDataEntry & {
stateInfo: T stateInfo: T

View File

@@ -50,7 +50,7 @@
// const options: Partial< // const options: Partial<
// TuiDialogOptions<FormContext<typeof config.validator._TYPE>> // TuiDialogOptions<FormContext<typeof config.validator._TYPE>>
// > = { // > = {
// label: 'Outbound Proxy', // label: 'Outbound proxy',
// data: { // data: {
// spec: await configBuilderToSpec(config), // spec: await configBuilderToSpec(config),
// buttons: [ // buttons: [