From d5bb537368d253c52cf2b4c64da61100218c9d05 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 13 Aug 2025 13:27:05 -0600 Subject: [PATCH] dns --- .../shared/src/i18n/dictionaries/de.ts | 1 + .../shared/src/i18n/dictionaries/en.ts | 3 +- .../shared/src/i18n/dictionaries/es.ts | 1 + .../shared/src/i18n/dictionaries/fr.ts | 1 + .../shared/src/i18n/dictionaries/pl.ts | 1 + .../routes/system/routes/dns/dns.component.ts | 194 ++++++++++++++++++ .../portal/routes/system/system.const.ts | 7 + .../portal/routes/system/system.routes.ts | 5 + .../ui/src/app/services/api/api.types.ts | 15 +- .../app/services/api/embassy-api.service.ts | 4 +- .../services/api/embassy-live-api.service.ts | 11 +- .../services/api/embassy-mock-api.service.ts | 17 +- 12 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 7934dcfd1..dec1340ab 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -525,4 +525,5 @@ export default { 556: '', 557: '', 558: '', + 559: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index 5175bc3ac..6290173b4 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -523,5 +523,6 @@ export const ENGLISH = { 'Address details': 555, 'Private Domains': 556, 'No private domains': 557, - 'New private domain': 558 + 'New private domain': 558, + 'DNS Servers': 559 } as const diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index da7a060da..5383bb07c 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -525,4 +525,5 @@ export default { 556: '', 557: '', 558: '', + 559: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index 3da50f08e..17b4cd60a 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -525,4 +525,5 @@ export default { 556: '', 557: '', 558: '', + 559: '', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 02ea43e2d..fdcbd1c96 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -525,4 +525,5 @@ export default { 556: '', 557: '', 558: '', + 559: '', } satisfies i18n diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts new file mode 100644 index 000000000..59086b878 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts @@ -0,0 +1,194 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { RouterLink } from '@angular/router' +import { + DocsLinkDirective, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' +import { ISB } from '@start9labs/start-sdk' +import { TuiButton, TuiTitle } from '@taiga-ui/core' +import { TuiHeader } from '@taiga-ui/layout' +import { PatchDB } from 'patch-db-client' +import { combineLatest, first, switchMap } from 'rxjs' +import { FormModule } from 'src/app/routes/portal/components/form/form.module' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormService } from 'src/app/services/form.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { TitleDirective } from 'src/app/services/title.service' +import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' + +@Component({ + template: ` + + + {{ 'Back' | i18n }} + + {{ 'DNS Servers' | i18n }} + + @if (data(); as d) { +
+
+

+ + {{ 'DNS Servers' | i18n }} + + {{ 'Documentation' | i18n }} + + +

+
+ + + + @if (d.warn.length; as length) { +

+ Warning. StartOS is currently using {{ d.warn.join(', ') }} for DNS. + Therefore, {{ length > 1 ? 'they' : 'it' }} cannot use StartOS for + DNS. This is circular. If you want to use StartOS as the DNS server + for {{ d.warn.join(', ') }} for private domain resolution, you must + set custom DNS servers above. +

+ } + +
+ +
+ + } + `, + styles: ` + :host { + max-width: 36rem; + } + + form header, + form footer { + margin: 1rem 0; + display: flex; + gap: 1rem; + } + + footer { + justify-content: flex-end; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + FormModule, + TuiButton, + TuiHeader, + TuiTitle, + RouterLink, + TitleDirective, + i18nPipe, + DocsLinkDirective, + ], +}) +export default class SystemDnsComponent { + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + private readonly formService = inject(FormService) + private readonly patch = inject>(PatchDB) + private readonly api = inject(ApiService) + private readonly i18n = inject(i18nPipe) + + private readonly dnsSpec = ISB.InputSpec.of({ + strategy: ISB.Value.union({ + name: 'DNS Servers', + default: 'defaults', + variants: ISB.Variants.of({ + defaults: { + name: 'Default', + spec: ISB.InputSpec.of({ + servers: ISB.Value.list( + ISB.List.text( + { + name: 'Default DNS Servers', + }, + {}, + ), + ), + }), + }, + custom: { + name: 'Custom', + spec: ISB.InputSpec.of({ + servers: ISB.Value.list( + ISB.List.text( + { + name: 'DNS Servers', + minLength: 1, + maxLength: 3, + }, + { placeholder: '1.1.1.1' }, + ), + ), + }), + }, + }), + }), + }) + + readonly data = toSignal( + combineLatest([ + this.patch.watch$('packageData').pipe(first()), + this.patch.watch$('serverInfo', 'network'), + ]).pipe( + switchMap(async ([pkgs, { gateways, dns }]) => { + const spec = await configBuilderToSpec(this.dnsSpec) + + const selection = dns.static ? 'custom' : 'defaults' + + const form = this.formService.createForm(spec, { + strategy: { selection, value: dns.servers }, + }) + + return { + spec, + form, + warn: + (Object.values(pkgs).some(p => p) || []) && + Object.values(gateways) + .filter(g => dns.servers.includes(g.ipInfo?.lanIp)) + .map(g => g.ipInfo?.name), + } + }), + ), + ) + + async save(value: typeof this.dnsSpec._TYPE): Promise { + const loader = this.loader.open('Saving').subscribe() + + try { + await this.api.setDns({ + servers: value.strategy.value.servers, + static: value.strategy.selection === 'custom', + }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } +} 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 974b0f40a..313481e6b 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 @@ -1,3 +1,5 @@ +import { i18nKey } from '@start9labs/shared' + export const SYSTEM_MENU = [ [ { @@ -46,6 +48,11 @@ export const SYSTEM_MENU = [ item: 'Certificate Authorities', link: 'authorities', }, + { + icon: '@tui.globe', + item: 'DNS' as i18nKey, + link: 'dns', + }, ], [ { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts b/web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts index 8e3622843..983a6f7f2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts @@ -77,6 +77,11 @@ export default [ loadComponent: () => import('./routes/authorities/authorities.component'), }, + { + path: 'dns', + title: titleResolver, + loadComponent: () => import('./routes/dns/dns.component'), + }, ], }, ] satisfies Routes diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index ea9019513..48bb3843e 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -104,11 +104,16 @@ export namespace RR { export type DiskRepairReq = {} // server.disk.repair export type DiskRepairRes = null - export type TestDnsReq = { + export type SetDnsReq = { + servers: string[] + static: boolean + } // net.dns.set + export type SetDnsRes = null + + export type QueryDnsReq = { fqdn: string - gateway: T.GatewayId // string - } // net.dns.test - export type TestDnsRes = string | null + } // net.dns.query + export type QueryDnsRes = string | null export type ResetTorReq = { wipeState: boolean @@ -301,7 +306,7 @@ export namespace RR { gateway: T.GatewayId acme: string | null // URL. null means local Root CA } - export type OsUiAddPublicDomainRes = TestDnsRes + export type OsUiAddPublicDomainRes = QueryDnsRes export type OsUiRemovePublicDomainReq = { // server.host.address.domain.public.remove diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 32acc8d94..80b2368af 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -122,7 +122,9 @@ export abstract class ApiService { abstract toggleKiosk(enable: boolean): Promise - abstract testDns(params: RR.TestDnsReq): Promise + abstract setDns(params: RR.SetDnsReq): Promise + + abstract queryDns(params: RR.QueryDnsReq): Promise abstract resetTor(params: RR.ResetTorReq): Promise diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 62330e4f1..49e841a59 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -267,9 +267,16 @@ export class LiveApiService extends ApiService { }) } - async testDns(params: RR.TestDnsReq): Promise { + async setDns(params: RR.SetDnsReq): Promise { return this.rpcRequest({ - method: 'net.dns.test', + method: 'net.dns.set', + params, + }) + } + + async queryDns(params: RR.QueryDnsReq): Promise { + return this.rpcRequest({ + method: 'net.dns.query', params, }) } 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 9db0e5fff..f5cf9b560 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 @@ -462,7 +462,22 @@ export class MockApiService extends ApiService { return null } - async testDns(params: RR.TestDnsReq): Promise { + async setDns(params: RR.SetDnsReq): Promise { + await pauseFor(2000) + + const patch: ReplaceOperation[] = [ + { + op: PatchOp.REPLACE, + path: '/serverInfo/network/dns', + value: params, + }, + ] + this.mockRevision(patch) + + return null + } + + async queryDns(params: RR.QueryDnsReq): Promise { await pauseFor(2000) return null