mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
nix StartOS domains, implement public and private domains at interface scope
This commit is contained in:
@@ -90,13 +90,12 @@ export default {
|
||||
88: 'Aktionen',
|
||||
89: 'nicht empfohlen',
|
||||
90: 'Root-CA ist vertrauenswürdig!',
|
||||
96: 'Domain hinzufügen',
|
||||
96: '',
|
||||
97: 'Wird entfernt',
|
||||
100: 'Nicht gespeicherte Änderungen',
|
||||
101: 'Sie haben nicht gespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?',
|
||||
102: 'Verlassen',
|
||||
103: 'Sind Sie sicher?',
|
||||
104: 'Domain auswählen',
|
||||
108: 'Öffentlich',
|
||||
109: 'privat',
|
||||
111: 'Keine Onion-Domains',
|
||||
@@ -519,11 +518,13 @@ export default {
|
||||
546: 'Anbieter',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
551: '',
|
||||
552: '',
|
||||
553: '',
|
||||
554: '',
|
||||
555: '',
|
||||
556: '',
|
||||
557: '',
|
||||
558: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -89,13 +89,12 @@ export const ENGLISH = {
|
||||
'Actions': 88, // as in, actions available to the user
|
||||
'not recommended': 89,
|
||||
'Root CA Trusted!': 90,
|
||||
'Add domain': 96,
|
||||
'Add public domain': 96,
|
||||
'Removing': 97,
|
||||
'Unsaved changes': 100,
|
||||
'You have unsaved changes. Are you sure you want to leave?': 101,
|
||||
'Leave': 102,
|
||||
'Are you sure?': 103,
|
||||
'Select domain': 104,
|
||||
'public': 108,
|
||||
'private': 109,
|
||||
'No Tor domains': 111,
|
||||
@@ -513,16 +512,18 @@ export const ENGLISH = {
|
||||
'Domain': 540, // as in, an internat domain name
|
||||
'Gateway': 541, // as in, a device or software that connects two different networks
|
||||
'Certificate Authority': 543,
|
||||
'Edit domain': 544,
|
||||
'Edit public domain': 544,
|
||||
'No public domains': 545,
|
||||
'Provider': 546,
|
||||
'View DNS': 547,
|
||||
'Clearnet Domains': 548,
|
||||
'No clearnet domains': 549,
|
||||
'New public domain': 548,
|
||||
'Addresses': 550,
|
||||
'Common': 551,
|
||||
'Uncommon': 552,
|
||||
'No addresses': 553,
|
||||
'Change CA': 554,
|
||||
'Address details': 555,
|
||||
'Private Domains': 556,
|
||||
'No private domains': 557,
|
||||
'New private domain': 558
|
||||
} as const
|
||||
|
||||
@@ -90,13 +90,12 @@ export default {
|
||||
88: 'Acciones',
|
||||
89: 'no recomendado',
|
||||
90: '¡CA raíz confiable!',
|
||||
96: 'Agregar dominio',
|
||||
96: '',
|
||||
97: 'Eliminando',
|
||||
100: 'Cambios no guardados',
|
||||
101: 'Tienes cambios no guardados. ¿Estás seguro de que deseas salir?',
|
||||
102: 'Salir',
|
||||
103: '¿Estás seguro?',
|
||||
104: 'Seleccionar dominio',
|
||||
108: 'público',
|
||||
109: 'privado',
|
||||
111: 'Sin dominios onion',
|
||||
@@ -519,11 +518,13 @@ export default {
|
||||
546: 'Proveedor',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
551: '',
|
||||
552: '',
|
||||
553: '',
|
||||
554: '',
|
||||
555: '',
|
||||
556: '',
|
||||
557: '',
|
||||
558: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,13 +90,12 @@ export default {
|
||||
88: 'Actions',
|
||||
89: 'non recommandé',
|
||||
90: 'Certificat racine approuvé !',
|
||||
96: 'Ajouter un domaine',
|
||||
96: '',
|
||||
97: 'Suppression',
|
||||
100: 'Modifications non enregistrées',
|
||||
101: 'Vous avez des modifications non enregistrées. Voulez-vous vraiment quitter ?',
|
||||
102: 'Quitter',
|
||||
103: 'Êtes-vous sûr ?',
|
||||
104: 'Sélectionner un domaine',
|
||||
108: 'public',
|
||||
109: 'privé',
|
||||
111: 'Aucune domaine onion',
|
||||
@@ -519,11 +518,13 @@ export default {
|
||||
546: 'Fournisseur',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
551: '',
|
||||
552: '',
|
||||
553: '',
|
||||
554: '',
|
||||
555: '',
|
||||
556: '',
|
||||
557: '',
|
||||
558: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -90,13 +90,12 @@ export default {
|
||||
88: 'Akcje',
|
||||
89: 'niezalecane',
|
||||
90: 'Główny certyfikat CA zaufany!',
|
||||
96: 'Dodaj domenę',
|
||||
96: '',
|
||||
97: 'Usuwanie',
|
||||
100: 'Niezapisane zmiany',
|
||||
101: 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić tę stronę?',
|
||||
102: 'Opuść',
|
||||
103: 'Czy jesteś pewien?',
|
||||
104: 'Wybierz domenę',
|
||||
108: 'publiczny',
|
||||
109: 'prywatny',
|
||||
111: 'Brak domeny onion',
|
||||
@@ -519,11 +518,13 @@ export default {
|
||||
546: 'Dostawca',
|
||||
547: '',
|
||||
548: '',
|
||||
549: '',
|
||||
550: '',
|
||||
551: '',
|
||||
552: '',
|
||||
553: '',
|
||||
554: '',
|
||||
555: '',
|
||||
556: '',
|
||||
557: '',
|
||||
558: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
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 { InterfaceClearnetDomainsItemComponent } from './item.component'
|
||||
import { ClearnetDomain } from '../interface.service'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormComponent } from '../../form.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { map } from 'rxjs'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'section[clearnetDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Clearnet Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
|
||||
@for (domain of clearnetDomains(); track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
@if (clearnetDomains()) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No clearnet domains' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 3;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
InterfaceClearnetDomainsItemComponent,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetDomainsComponent {
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly clearnetDomains = input.required<
|
||||
readonly ClearnetDomain[] | undefined
|
||||
>()
|
||||
|
||||
private readonly domains = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network', 'domains'),
|
||||
)
|
||||
|
||||
private readonly acme = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network', 'acme').pipe(
|
||||
map(acme =>
|
||||
Object.keys(acme).reduce<Record<string, string>>(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAuthorityName(url),
|
||||
}),
|
||||
{ local: toAuthorityName(null) },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
type: ISB.Value.union({
|
||||
name: 'Type',
|
||||
default: 'public',
|
||||
description:
|
||||
'- **Public**: the domain can be accessed by anyone with an Internet connection.\n- **Private**: the domain can only be accessed by people connected to the same Local Area Network (LAN) as the server, either physically or via VPN.',
|
||||
variants: ISB.Variants.of({
|
||||
public: {
|
||||
name: 'Public',
|
||||
spec: ISB.InputSpec.of({
|
||||
domain: ISB.Value.select({
|
||||
name: 'Domain',
|
||||
default: '',
|
||||
values: Object.keys(this.domains() || {}).reduce<
|
||||
Record<string, string>
|
||||
>(
|
||||
(obj, domain) => ({
|
||||
...obj,
|
||||
[domain]: domain,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
subdomain: ISB.Value.text({
|
||||
name: 'Subdomain',
|
||||
description: 'Optionally enter a subdomain',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [], // @TODO subdomain pattern
|
||||
}),
|
||||
...this.acmeSpec(true),
|
||||
}),
|
||||
},
|
||||
private: {
|
||||
name: 'Private',
|
||||
spec: ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
...this.acmeSpec(false),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(addSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (input: typeof addSpec._TYPE) => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const type = input.type.selection
|
||||
const params = {
|
||||
private: type === 'private',
|
||||
fqdn:
|
||||
type === 'public'
|
||||
? `${input.type.value.subdomain}.${input.type.value.domain}`
|
||||
: input.type.value.fqdn,
|
||||
acme:
|
||||
input.type.value.authority === 'local'
|
||||
? null
|
||||
: input.type.value.authority,
|
||||
}
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgAddDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiAddDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private acmeSpec(isPublic: boolean) {
|
||||
return {
|
||||
authority: ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select the Certificate Authority that will issue the SSL/TLS certificate for this domain',
|
||||
values: this.acme()!,
|
||||
default: isPublic ? '' : 'local',
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { ClearnetDomain } from '../interface.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
<td>{{ domain().fqdn }}</td>
|
||||
<td>{{ domain().authority }}</td>
|
||||
<td>
|
||||
@if (domain().public) {
|
||||
<tui-badge size="s" appearance="primary-success">
|
||||
{{ 'public' | i18n }}
|
||||
</tui-badge>
|
||||
} @else {
|
||||
<tui-badge size="s" appearance="primary-destructive">
|
||||
{{ 'private' | i18n }}
|
||||
</tui-badge>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove()"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
order: -1;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
grid-area: 1 / 3 / 3;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
tui-badge {
|
||||
vertical-align: bottom;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
i18nPipe,
|
||||
TuiTextfield,
|
||||
TuiBadge,
|
||||
],
|
||||
})
|
||||
export class InterfaceClearnetDomainsItemComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly domain = input.required<ClearnetDomain>()
|
||||
|
||||
remove() {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { fqdn: this.domain().fqdn }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiRemoveDomain(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,23 @@ import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { MappedServiceInterface } from './interface.service'
|
||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||
import { InterfaceClearnetDomainsComponent } from './clearnet-domains/clearnet-domains.component'
|
||||
import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'service-interface',
|
||||
template: `
|
||||
<!-- @TODO Alex / Matt translation in all nested components -->
|
||||
<div [style.display]="'grid'">
|
||||
<section
|
||||
[gateways]="value()?.gateways"
|
||||
[isOs]="!!value()?.isOs"
|
||||
></section>
|
||||
<section [torDomains]="value()?.torDomains"></section>
|
||||
<section [clearnetDomains]="value()?.clearnetDomains"></section>
|
||||
<section [privateDomains]="value()?.privateDomains"></section>
|
||||
<section [publicDomains]="value()?.publicDomains"></section>
|
||||
</div>
|
||||
<hr [style.width.rem]="10" />
|
||||
<section [addresses]="value()?.addresses" [isRunning]="true"></section>
|
||||
@@ -49,7 +52,8 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
imports: [
|
||||
InterfaceGatewaysComponent,
|
||||
InterfaceTorDomainsComponent,
|
||||
InterfaceClearnetDomainsComponent,
|
||||
PublicDomainsComponent,
|
||||
InterfacePrivateDomainsComponent,
|
||||
InterfaceAddressesComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { T, utils } from '@start9labs/start-sdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { PublicDomain } from './public-domains/pd.service'
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
|
||||
type AddressWithInfo = {
|
||||
@@ -104,7 +105,8 @@ function cmpClearnet(
|
||||
function toDisplayAddress(
|
||||
{ info, url }: AddressWithInfo,
|
||||
gateways: GatewayPlus[],
|
||||
domains: Record<string, T.DomainConfig>,
|
||||
publicDomains: Record<string, T.DomainConfig>,
|
||||
privateDomains: string[],
|
||||
): DisplayAddress {
|
||||
let access: DisplayAddress['access']
|
||||
let gatewayName: DisplayAddress['gatewayName']
|
||||
@@ -145,13 +147,13 @@ function toDisplayAddress(
|
||||
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||
gatewayName = gateway.ipInfo.name
|
||||
|
||||
const gatewayIpv4 = gateway.ipv4[0]
|
||||
const gatewayLanIpv4 = gateway.lanIpv4[0]
|
||||
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
|
||||
|
||||
const localIdeal = 'Ideal for local access'
|
||||
const lanRequired =
|
||||
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN'
|
||||
const staticRequired = `Requires setting a static IP address for ${gatewayIpv4} in your gateway`
|
||||
const staticRequired = `Requires setting a static IP address for ${gatewayLanIpv4} in your gateway`
|
||||
const vpnAccess = 'Ideal for VPN access via your'
|
||||
|
||||
// * Local *
|
||||
@@ -201,18 +203,17 @@ function toDisplayAddress(
|
||||
// * Domain *
|
||||
} else {
|
||||
type = 'Domain'
|
||||
const domain = domains[info.hostname.value]!
|
||||
if (info.public) {
|
||||
access = 'public'
|
||||
bullets = [
|
||||
`Requires DNS record(s) for ${domains[info.hostname.value]?.root}, as shown in System -> Domains`,
|
||||
`Requires a DNS record for ${info.hostname.value} that resolves to ${gateway.ipInfo.wanIp}`,
|
||||
`Requires port forwarding in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||
]
|
||||
if (domain.acme) {
|
||||
if (publicDomains[info.hostname.value]!) {
|
||||
bullets.unshift('Ideal for public access via the Internet')
|
||||
} else {
|
||||
bullets = [
|
||||
'Can be used for personal access via the public Internet. VPN is more secure',
|
||||
'Can be used for personal access via the public Internet. VPN is more private and secure',
|
||||
rootCaRequired,
|
||||
...bullets,
|
||||
]
|
||||
@@ -220,24 +221,23 @@ function toDisplayAddress(
|
||||
} else {
|
||||
access = 'private'
|
||||
const ipPortBad = 'when using IP addresses and ports is undesirable'
|
||||
const customDnsRequired = `Requires DNS record for ${info.hostname.value} that resolve to ${gatewayIpv4}`
|
||||
const customDnsRequired = `Requires a DNS record for ${info.hostname.value} that resolves to ${gatewayLanIpv4}`
|
||||
if (isWireguard) {
|
||||
bullets = [
|
||||
`${vpnAccess} StartTunnel (or similar) ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
]
|
||||
} else {
|
||||
bullets = [
|
||||
`${localIdeal} ${ipPortBad}`,
|
||||
`${vpnAccess} router's Wireguard server ${ipPortBad}`,
|
||||
customDnsRequired,
|
||||
rootCaRequired,
|
||||
lanRequired,
|
||||
staticRequired,
|
||||
]
|
||||
}
|
||||
if (domain.acme) {
|
||||
bullets.push(rootCaRequired)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,11 +251,10 @@ function toDisplayAddress(
|
||||
}
|
||||
}
|
||||
|
||||
export function getClearnetDomains(host: T.Host): ClearnetDomain[] {
|
||||
return Object.entries(host.domains).map(([fqdn, info]) => ({
|
||||
export function getPublicDomains(publicDomains: any): PublicDomain[] {
|
||||
return Object.entries(publicDomains).map(([fqdn, info]) => ({
|
||||
fqdn,
|
||||
authority: toAuthorityName(info.acme),
|
||||
public: info.public,
|
||||
...info,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -305,10 +304,19 @@ export class InterfaceService {
|
||||
}, [] as AddressWithInfo[])
|
||||
|
||||
return {
|
||||
common: bestAddrs.map(a => toDisplayAddress(a, gateways, host.domains)),
|
||||
common: bestAddrs.map(a =>
|
||||
toDisplayAddress(a, gateways, host.publicDomains, host.privateDomains),
|
||||
),
|
||||
uncommon: allAddressesWithInfo
|
||||
.filter(a => !bestAddrs.includes(a))
|
||||
.map(a => toDisplayAddress(a, gateways, host.domains)),
|
||||
.map(a =>
|
||||
toDisplayAddress(
|
||||
a,
|
||||
gateways,
|
||||
host.publicDomains,
|
||||
host.privateDomains,
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +461,8 @@ export class InterfaceService {
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
gateways: InterfaceGateway[]
|
||||
torDomains: string[]
|
||||
clearnetDomains: ClearnetDomain[]
|
||||
publicDomains: PublicDomain[]
|
||||
privateDomains: string[]
|
||||
addresses: {
|
||||
common: DisplayAddress[]
|
||||
uncommon: DisplayAddress[]
|
||||
@@ -465,12 +474,6 @@ export type InterfaceGateway = GatewayPlus & {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ClearnetDomain = {
|
||||
fqdn: string
|
||||
authority: string
|
||||
public: boolean
|
||||
}
|
||||
|
||||
export type DisplayAddress = {
|
||||
type: string
|
||||
access: 'public' | 'private' | null
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'section[privateDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Private Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-locally.html"
|
||||
fragment="private-domains"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
@for (domain of privateDomains(); track $index) {
|
||||
<div tuiCell="s">
|
||||
<span tuiTitle>{{ domain }}</span>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove(domain)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
} @empty {
|
||||
@if (privateDomains()) {
|
||||
<app-placeholder icon="@tui.globe-lock">
|
||||
{{ 'No private domains' | i18n }}
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<label tuiCell="s">
|
||||
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 2;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiCell,
|
||||
TuiTitle,
|
||||
TuiButton,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfacePrivateDomainsComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly privateDomains = input.required<readonly string[] | undefined>()
|
||||
|
||||
async add() {
|
||||
this.formDialog.open<FormContext<{ fqdn: string }>>(FormComponent, {
|
||||
label: 'New private domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a fully qualified domain name. Since the domain is for private use, it can be any domain you want, even one you do not control.',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
handler: async value => this.save(value.fqdn),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async remove(fqdn: string) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemovePrivateDomain({
|
||||
fqdn,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiRemovePrivateDomain({ fqdn })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async save(fqdn: string): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddPrivateDomain({
|
||||
fqdn,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiAddPrivateDomain({ fqdn })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ErrorService, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiIcon } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiButtonLoading,
|
||||
TuiSwitch,
|
||||
tuiSwitchOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { parse } from 'tldts'
|
||||
import { GatewayWithId } from './pd.service'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'dns',
|
||||
template: `
|
||||
<p>{{ context.data.message }}</p>
|
||||
|
||||
@let wanIp = context.data.gateway.ipInfo.wanIp || ('Error' | i18n);
|
||||
|
||||
@if (context.data.gateway.ipInfo.deviceType !== 'wireguard') {
|
||||
<label>
|
||||
IP
|
||||
<input
|
||||
type="checkbox"
|
||||
tuiSwitch
|
||||
[(ngModel)]="ddns"
|
||||
(ngModelChange)="pass.set(undefined)"
|
||||
/>
|
||||
Dynamic DNS
|
||||
</label>
|
||||
}
|
||||
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
@for (row of rows(); track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
@if (pass() === true) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else if (pass() === false) {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
}
|
||||
{{ ddns ? 'ALIAS' : 'A' }}
|
||||
</td>
|
||||
<td>{{ row.host }}</td>
|
||||
<td>{{ ddns ? '[DDNS Address]' : wanIp }}</td>
|
||||
<td>{{ row.purpose }}</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
||||
<footer class="g-buttons">
|
||||
<button tuiButton [loading]="loading()" (click)="testDns()">
|
||||
{{ 'Test' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
styles: `
|
||||
label {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
`,
|
||||
providers: [
|
||||
tuiSwitchOptionsProvider({
|
||||
appearance: () => 'primary',
|
||||
icon: () => '',
|
||||
}),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
TuiSwitch,
|
||||
FormsModule,
|
||||
TuiButtonLoading,
|
||||
TuiIcon,
|
||||
],
|
||||
})
|
||||
export class DnsComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly ddns = false
|
||||
|
||||
readonly context =
|
||||
injectContext<
|
||||
TuiDialogContext<
|
||||
void,
|
||||
{ fqdn: string; gateway: GatewayWithId; message: string }
|
||||
>
|
||||
>()
|
||||
|
||||
readonly loading = signal(false)
|
||||
readonly pass = signal<boolean | undefined>(undefined)
|
||||
|
||||
readonly rows = computed<{ host: string; purpose: string }[]>(() => {
|
||||
const { domain, subdomain } = parse(this.context.data.fqdn)
|
||||
|
||||
if (!subdomain) {
|
||||
return [
|
||||
{
|
||||
host: '@',
|
||||
purpose: domain!,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const segments = subdomain.split('.')
|
||||
|
||||
return [
|
||||
{
|
||||
host: subdomain,
|
||||
purpose: `only ${subdomain}`,
|
||||
},
|
||||
...segments.map((_, i) => {
|
||||
const parent = segments.slice(i + 1).join('.')
|
||||
return {
|
||||
host: `*.${parent}`,
|
||||
purpose: `subdomains of ${parent}`,
|
||||
}
|
||||
}),
|
||||
{
|
||||
host: '*',
|
||||
purpose: `subdomains of ${domain}`,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async testDns() {
|
||||
this.pass.set(undefined)
|
||||
this.loading.set(true)
|
||||
|
||||
try {
|
||||
const ip = await this.api.testDns({
|
||||
fqdn: this.context.data.fqdn,
|
||||
gateway: this.context.data.gateway.id,
|
||||
})
|
||||
|
||||
this.pass.set(ip === this.context.data.gateway.ipInfo.wanIp)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DNS = new PolymorpheusComponent(DnsComponent)
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
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 { PublicDomainsItemComponent } from './pd.item.component'
|
||||
import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
|
||||
@Component({
|
||||
selector: 'section[publicDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Public Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
@if (service.data()) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="service.add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
<table [appTable]="['Domain', 'Gateway', 'Certificate Authority', null]">
|
||||
@for (domain of publicDomains(); track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
@if (publicDomains()) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No public domains' | i18n }}
|
||||
</app-placeholder>
|
||||
</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (_ of [0]; track $index) {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 3;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
providers: [PublicDomainService],
|
||||
imports: [
|
||||
TuiButton,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
PublicDomainsItemComponent,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PublicDomainsComponent {
|
||||
readonly service = inject(PublicDomainService)
|
||||
|
||||
readonly publicDomains = input.required<readonly PublicDomain[] | undefined>()
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe, i18nKey } from '@start9labs/shared'
|
||||
import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
import { PublicDomain, PublicDomainService } from './pd.service'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
<td>{{ domain().fqdn }}</td>
|
||||
<td>{{ domain().gateway }}</td>
|
||||
<td>{{ authority() }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
tuiDropdown
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
>
|
||||
{{ 'More' | i18n }}
|
||||
<tui-data-list *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.eye"
|
||||
(click)="
|
||||
service.showDns(domain().fqdn, domain().gateway, dnsMessage())
|
||||
"
|
||||
>
|
||||
{{ 'View DNS' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="service.edit(domain())"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="service.remove(domain().fqdn)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</button>
|
||||
</td>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
order: -1;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
grid-area: 1 / 3 / 3;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
tui-badge {
|
||||
vertical-align: bottom;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiDataList, TuiDropdown, i18nPipe, TuiTextfield],
|
||||
})
|
||||
export class PublicDomainsItemComponent {
|
||||
protected readonly service = inject(PublicDomainService)
|
||||
|
||||
open = false
|
||||
|
||||
readonly domain = input.required<PublicDomain>()
|
||||
|
||||
readonly authority = computed(() => toAuthorityName(this.domain().acme))
|
||||
readonly dnsMessage = computed<i18nKey>(
|
||||
() =>
|
||||
`Create one of the DNS records below to cause ${this.domain().fqdn} to resolve to ${this.domain().gateway.ipInfo.wanIp}` as i18nKey,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
import { DNS } from './dns.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
export type PublicDomain = {
|
||||
fqdn: string
|
||||
gateway: GatewayPlus
|
||||
acme: string | null
|
||||
}
|
||||
|
||||
export type GatewayWithId = T.NetworkInterfaceInfo & {
|
||||
id: string
|
||||
ipInfo: T.IpInfo
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PublicDomainService {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly data = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network').pipe(
|
||||
map(({ gateways, acme }) => ({
|
||||
gateways: Object.entries(gateways)
|
||||
.filter(([_, g]) => g.ipInfo)
|
||||
.map(([id, g]) => ({ id, ...g })) as GatewayWithId[],
|
||||
authorities: Object.keys(acme).reduce<Record<string, string>>(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAuthorityName(url),
|
||||
}),
|
||||
{ local: toAuthorityName(null) },
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a fully qualified domain name. For example, if you control domain.com, you could enter domain.com or subdomain.domain.com or another.subdomain.domain.com.',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add public domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(addSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (input: typeof addSpec._TYPE) =>
|
||||
this.save(input.fqdn, input.gateway, input.authority),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async edit(domain: PublicDomain) {
|
||||
const editSpec = ISB.InputSpec.of({
|
||||
...this.gatewayAndAuthoritySpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit public domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(editSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: ({ gateway, authority }: typeof editSpec._TYPE) =>
|
||||
this.save(domain.fqdn, gateway, authority),
|
||||
},
|
||||
],
|
||||
value: {
|
||||
gateway: domain.gateway.id,
|
||||
authority: domain.acme,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
remove(fqdn: string) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Deleting').subscribe()
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemovePublicDomain({
|
||||
fqdn,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.osUiRemovePublicDomain({ fqdn })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showDns(fqdn: string, gateway: GatewayWithId, message: i18nKey) {
|
||||
this.dialog
|
||||
.openComponent(DNS, {
|
||||
label: 'DNS Records' as i18nKey,
|
||||
size: 'l',
|
||||
data: {
|
||||
fqdn,
|
||||
gateway,
|
||||
message,
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async save(
|
||||
fqdn: string,
|
||||
gatewayId: string,
|
||||
authority: 'local' | string,
|
||||
) {
|
||||
const gateway = this.data()!.gateways.find(g => (g.id = gatewayId))!
|
||||
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
const params = {
|
||||
fqdn,
|
||||
gateway: gatewayId,
|
||||
acme: authority === 'local' ? null : authority,
|
||||
}
|
||||
try {
|
||||
let ip: string | null
|
||||
if (this.interface.packageId()) {
|
||||
ip = await this.api.pkgAddPublicDomain({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
ip = await this.api.osUiAddPublicDomain(params)
|
||||
}
|
||||
|
||||
const wanIp = gateway.ipInfo.wanIp
|
||||
let message = `Create one of the DNS records below to cause ${fqdn} to resolve to ${wanIp}`
|
||||
|
||||
if (!ip) {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`No DNS detected for ${fqdn}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
} else if (ip === wanIp) {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.showDns(
|
||||
fqdn,
|
||||
gateway,
|
||||
`Invalid DNS. ${fqdn} is currently resolving to ${ip}. ${message}` as i18nKey,
|
||||
),
|
||||
250,
|
||||
)
|
||||
} else {
|
||||
setTimeout(
|
||||
() =>
|
||||
this.dialog.openAlert(
|
||||
`${fqdn} is successfully resolving to ${wanIp}` as i18nKey,
|
||||
{ label: 'DNS detected!' as i18nKey, appearance: 'positive' },
|
||||
),
|
||||
250,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private gatewayAndAuthoritySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Gateway',
|
||||
description: 'Select a gateway to use for this domain.',
|
||||
values: data.gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
[gateway.id]: gateway.ipInfo!.name,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
default: '',
|
||||
disabled: data.gateways
|
||||
.filter(
|
||||
g => !g.ipInfo.wanIp || g.ipInfo.wanIp.split('.').at(-1) === '100',
|
||||
)
|
||||
.map(g => g.id),
|
||||
})),
|
||||
authority: ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select the Certificate Authority that will issue the SSL/TLS certificate for this domain',
|
||||
values: data.authorities,
|
||||
default: '',
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,19 +156,19 @@ export class InterfaceTorDomainsComponent {
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
handler: async value => this.save(value),
|
||||
handler: async value => this.save(value.key),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
private async save(key?: string): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
let onion = key
|
||||
? await this.api.addTorKey({ key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import {
|
||||
getClearnetDomains,
|
||||
getPublicDomains,
|
||||
InterfaceService,
|
||||
} from '../../../components/interfaces/interface.service'
|
||||
import { GatewayService } from 'src/app/services/gateway.service'
|
||||
@@ -143,7 +143,8 @@ export default class ServiceInterfaceRoute {
|
||||
...g,
|
||||
})) || [],
|
||||
torDomains: host.onions.map(o => `${o}.onion`),
|
||||
clearnetDomains: getClearnetDomains(host),
|
||||
publicDomains: getPublicDomains(host.domains.public),
|
||||
privateDomains: host.domains.private,
|
||||
isOs: false,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ErrorService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiIcon } from '@taiga-ui/core'
|
||||
import {
|
||||
TuiButtonLoading,
|
||||
TuiSwitch,
|
||||
tuiSwitchOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { parse } from 'tldts'
|
||||
import { MappedDomain } from './domain.service'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
@Component({
|
||||
selector: 'dns',
|
||||
template: `
|
||||
@let wanIp = context.data.gateway.ipInfo?.wanIp || ('Error' | i18n);
|
||||
|
||||
@if (context.data.gateway.ipInfo?.deviceType !== 'wireguard') {
|
||||
<label>
|
||||
IP
|
||||
<input
|
||||
type="checkbox"
|
||||
tuiSwitch
|
||||
[(ngModel)]="ddns"
|
||||
(ngModelChange)="reset()"
|
||||
/>
|
||||
Dynamic DNS
|
||||
</label>
|
||||
}
|
||||
|
||||
<table [appTable]="['Type', $any('Host'), 'Value', 'Purpose']">
|
||||
@if (ddns) {
|
||||
<tr>
|
||||
<td>
|
||||
@if (root() !== undefined; as $implicit) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="test"
|
||||
[ngTemplateOutletContext]="{ $implicit }"
|
||||
/>
|
||||
}
|
||||
ALIAS
|
||||
</td>
|
||||
<td>{{ subdomain() || '@' }}</td>
|
||||
<td>[DDNS Address]</td>
|
||||
<td>{{ purpose().root }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@if (wildcard() !== undefined; as $implicit) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="test"
|
||||
[ngTemplateOutletContext]="{ $implicit }"
|
||||
/>
|
||||
}
|
||||
ALIAS
|
||||
</td>
|
||||
<td>{{ subdomain() ? '*.' + subdomain() : '*' }}</td>
|
||||
<td>[DDNS Address]</td>
|
||||
<td>{{ purpose().wildcard }}</td>
|
||||
</tr>
|
||||
} @else {
|
||||
<tr>
|
||||
<td>
|
||||
@if (root() !== undefined; as $implicit) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="test"
|
||||
[ngTemplateOutletContext]="{ $implicit }"
|
||||
/>
|
||||
}
|
||||
A
|
||||
</td>
|
||||
<td>{{ subdomain() || '@' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td>{{ purpose().root }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@if (wildcard() !== undefined; as $implicit) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="test"
|
||||
[ngTemplateOutletContext]="{ $implicit }"
|
||||
/>
|
||||
}
|
||||
A
|
||||
</td>
|
||||
<td>{{ subdomain() ? '*.' + subdomain() : '*' }}</td>
|
||||
<td>{{ wanIp }}</td>
|
||||
<td>{{ purpose().wildcard }}</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
||||
<ng-template #test let-result>
|
||||
@if (result) {
|
||||
<tui-icon class="g-positive" icon="@tui.check" />
|
||||
} @else {
|
||||
<tui-icon class="g-negative" icon="@tui.x" />
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<footer class="g-buttons">
|
||||
<button tuiButton [loading]="loading()" (click)="testDns()">
|
||||
{{ 'Test' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
`,
|
||||
styles: `
|
||||
label {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
tui-icon {
|
||||
font-size: 1rem;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
`,
|
||||
providers: [
|
||||
tuiSwitchOptionsProvider({
|
||||
appearance: () => 'primary',
|
||||
icon: () => '',
|
||||
}),
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
TuiSwitch,
|
||||
FormsModule,
|
||||
TuiButtonLoading,
|
||||
NgTemplateOutlet,
|
||||
TuiIcon,
|
||||
],
|
||||
})
|
||||
export class DnsComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
ddns = false
|
||||
|
||||
readonly context = injectContext<TuiDialogContext<void, MappedDomain>>()
|
||||
|
||||
readonly subdomain = computed(() => parse(this.context.data.fqdn).subdomain)
|
||||
readonly loading = signal(false)
|
||||
readonly root = signal<boolean | undefined>(undefined)
|
||||
readonly wildcard = signal<boolean | undefined>(undefined)
|
||||
|
||||
readonly purpose = computed(() => ({
|
||||
root: this.context.data.fqdn,
|
||||
wildcard: `subdomains of ${this.context.data.fqdn}`,
|
||||
}))
|
||||
|
||||
async testDns() {
|
||||
this.reset()
|
||||
this.loading.set(true)
|
||||
|
||||
try {
|
||||
await this.api
|
||||
.testDomain({
|
||||
fqdn: this.context.data.fqdn,
|
||||
gateway: this.context.data.gateway.id,
|
||||
})
|
||||
.then(({ root, wildcard }) => {
|
||||
this.root.set(root)
|
||||
this.wildcard.set(wildcard)
|
||||
})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.root.set(undefined)
|
||||
this.wildcard.set(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export const DNS = new PolymorpheusComponent(DnsComponent)
|
||||
@@ -1,188 +0,0 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { DNS } from './dns.component'
|
||||
|
||||
// @TODO translations
|
||||
|
||||
export type MappedDomain = {
|
||||
fqdn: string
|
||||
gateway: {
|
||||
id: string
|
||||
name: string | null
|
||||
ipInfo: T.IpInfo | null
|
||||
}
|
||||
}
|
||||
|
||||
type GatewayWithId = T.NetworkInterfaceInfo & {
|
||||
id: string
|
||||
ipInfo: T.IpInfo & {
|
||||
wanIp: string
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DomainService {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
readonly data = toSignal(
|
||||
this.patch.watch$('serverInfo', 'network').pipe(
|
||||
map(({ gateways, domains }) => ({
|
||||
gateways: Object.entries(gateways)
|
||||
.filter(([_, g]) => g.ipInfo && g.ipInfo.wanIp)
|
||||
.map(([id, g]) => ({ id, ...g })) as GatewayWithId[],
|
||||
domains: Object.entries(domains).map(
|
||||
([fqdn, { gateway }]) =>
|
||||
({
|
||||
fqdn,
|
||||
gateway: {
|
||||
id: gateway,
|
||||
ipInfo: gateways[gateway]?.ipInfo || null,
|
||||
},
|
||||
}) as MappedDomain,
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
async add() {
|
||||
const addSpec = ISB.InputSpec.of({
|
||||
fqdn: ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description:
|
||||
'Enter a fully qualified domain name. 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',
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
...this.gatewaysSpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(addSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (input: typeof addSpec._TYPE) =>
|
||||
this.save({
|
||||
fqdn: input.fqdn,
|
||||
gateway: input.gateway,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async edit(domain: MappedDomain) {
|
||||
const editSpec = ISB.InputSpec.of({
|
||||
...this.gatewaysSpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(editSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (input: typeof editSpec._TYPE) =>
|
||||
this.save({
|
||||
fqdn: domain.fqdn,
|
||||
gateway: input.gateway,
|
||||
}),
|
||||
},
|
||||
],
|
||||
value: {
|
||||
gateway: domain.gateway.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
remove(fqdn: string) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Deleting').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeDomain({ fqdn })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showDns(domain: MappedDomain) {
|
||||
this.dialog
|
||||
.openComponent(DNS, {
|
||||
label: 'DNS Records' as i18nKey,
|
||||
size: 'l',
|
||||
data: domain,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async save(params: RR.AddDomainReq) {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addDomain(params)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private gatewaysSpec() {
|
||||
const gateways = this.data()?.gateways || []
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: 'Gateway',
|
||||
description: 'Select a gateway to use for this domain.',
|
||||
values: gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
[gateway.id]: gateway.ipInfo!.name,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
default: '',
|
||||
disabled: gateways
|
||||
.filter(g => g.ipInfo.wanIp.split('.').at(-1) === '100')
|
||||
.map(g => g.id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { DomainService } from './domain.service'
|
||||
import { DomainsTableComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ 'Public Domains' | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Public Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
size="xs"
|
||||
docsLink
|
||||
path="/user-manual/domains.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
@if (domainService.data(); as value) {
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="domainService.add()"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
<domains-table />
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 48rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
DomainsTableComponent,
|
||||
],
|
||||
providers: [DomainService],
|
||||
})
|
||||
export default class SystemDomainsComponent {
|
||||
protected readonly domainService = inject(DomainService)
|
||||
}
|
||||
@@ -1,88 +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 { DomainService, MappedDomain } from './domain.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[domain]',
|
||||
template: `
|
||||
@if (domain(); as domain) {
|
||||
<td>{{ domain.fqdn }}</td>
|
||||
<td>{{ domain.gateway.ipInfo?.name || '-' }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
tuiDropdown
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.ellipsis-vertical"
|
||||
[tuiAppearanceState]="open ? 'hover' : null"
|
||||
[(tuiDropdownOpen)]="open"
|
||||
>
|
||||
{{ 'More' | i18n }}
|
||||
<tui-data-list *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.eye"
|
||||
(click)="domainService.showDns(domain)"
|
||||
>
|
||||
{{ 'View DNS' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="domainService.edit(domain)"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="domainService.remove(domain.fqdn)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</button>
|
||||
</td>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
td:last-child {
|
||||
grid-area: 1 / 2 / 4;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
grid-template-columns: 1fr min-content;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, i18nPipe, TuiDropdown, TuiDataList, TuiTextfield],
|
||||
})
|
||||
export class DomainItemComponent {
|
||||
protected readonly domainService = inject(DomainService)
|
||||
|
||||
readonly domain = input.required<MappedDomain>()
|
||||
|
||||
open = false
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 { DomainItemComponent } from './item.component'
|
||||
import { DomainService } from './domain.service'
|
||||
|
||||
@Component({
|
||||
selector: 'domains-table',
|
||||
template: `
|
||||
<table [appTable]="['Domain', 'Gateway', null]">
|
||||
@for (domain of domainService.data()?.domains; track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td [attr.colspan]="3">
|
||||
@if (domainService.data()?.domains) {
|
||||
<app-placeholder icon="@tui.globe">
|
||||
{{ 'No public domains' | i18n }}
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiSkeleton,
|
||||
i18nPipe,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
DomainItemComponent,
|
||||
],
|
||||
})
|
||||
export class DomainsTableComponent {
|
||||
protected readonly domainService = inject(DomainService)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
-
|
||||
}
|
||||
</td>
|
||||
<td class="lan">{{ gateway.ipv4.join(', ') }}</td>
|
||||
<td class="lan">{{ gateway.lanIpv4.join(', ') }}</td>
|
||||
<td
|
||||
class="wan"
|
||||
[style.color]="
|
||||
|
||||
@@ -11,10 +11,9 @@ import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import {
|
||||
getClearnetDomains,
|
||||
getPublicDomains,
|
||||
InterfaceService,
|
||||
} from 'src/app/routes/portal/components/interfaces/interface.service'
|
||||
import { GatewayService } from 'src/app/services/gateway.service'
|
||||
@@ -98,7 +97,8 @@ export default class StartOsUiComponent {
|
||||
...g,
|
||||
})),
|
||||
torDomains: network.host.onions.map(o => `${o}.onion`),
|
||||
clearnetDomains: getClearnetDomains(network.host),
|
||||
publicDomains: getPublicDomains(network.host.domains.public),
|
||||
privateDomains: network.host.domains.private,
|
||||
isOs: true,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -46,11 +46,6 @@ export const SYSTEM_MENU = [
|
||||
item: 'Certificate Authorities',
|
||||
link: 'authorities',
|
||||
},
|
||||
{
|
||||
icon: '@tui.globe',
|
||||
item: 'Public Domains',
|
||||
link: 'domains',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
|
||||
@@ -77,11 +77,6 @@ export default [
|
||||
loadComponent: () =>
|
||||
import('./routes/authorities/authorities.component'),
|
||||
},
|
||||
{
|
||||
path: 'domains',
|
||||
title: titleResolver,
|
||||
loadComponent: () => import('./routes/domains/domains.component'),
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies Routes
|
||||
|
||||
@@ -2070,7 +2070,10 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
@@ -2170,7 +2173,10 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
@@ -2193,7 +2199,10 @@ export namespace Mock {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
|
||||
@@ -104,6 +104,12 @@ export namespace RR {
|
||||
export type DiskRepairReq = {} // server.disk.repair
|
||||
export type DiskRepairRes = null
|
||||
|
||||
export type TestDnsReq = {
|
||||
fqdn: string
|
||||
gateway: T.GatewayId // string
|
||||
} // net.dns.test
|
||||
export type TestDnsRes = string | null
|
||||
|
||||
export type ResetTorReq = {
|
||||
wipeState: boolean
|
||||
reason: string
|
||||
@@ -236,26 +242,6 @@ export namespace RR {
|
||||
|
||||
// network
|
||||
|
||||
export type AddDomainReq = {
|
||||
fqdn: string
|
||||
gateway: string
|
||||
} // 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-dns
|
||||
export type TestDomainRes = {
|
||||
root: boolean
|
||||
wildcard: boolean
|
||||
}
|
||||
|
||||
export type AddTunnelReq = {
|
||||
name: string
|
||||
config: string // file contents
|
||||
@@ -309,19 +295,31 @@ export namespace RR {
|
||||
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
|
||||
export type RemoveOnionRes = null
|
||||
|
||||
export type OsUiAddDomainReq = {
|
||||
// server.host.address.domain.add
|
||||
export type OsUiAddPublicDomainReq = {
|
||||
// server.host.address.domain.public.add
|
||||
fqdn: string // FQDN
|
||||
private: boolean
|
||||
gateway: T.GatewayId
|
||||
acme: string | null // URL. null means local Root CA
|
||||
}
|
||||
export type OsUiAddDomainRes = null
|
||||
export type OsUiAddPublicDomainRes = TestDnsRes
|
||||
|
||||
export type OsUiRemoveDomainReq = {
|
||||
// server.host.address.domain.remove
|
||||
export type OsUiRemovePublicDomainReq = {
|
||||
// server.host.address.domain.public.remove
|
||||
fqdn: string // FQDN
|
||||
}
|
||||
export type OsUiRemoveDomainRes = null
|
||||
export type OsUiRemovePublicDomainRes = null
|
||||
|
||||
export type OsUiAddPrivateDomainReq = {
|
||||
// server.host.address.domain.private.add
|
||||
fqdn: string // FQDN
|
||||
}
|
||||
export type OsUiAddPrivateDomainRes = null
|
||||
|
||||
export type OsUiRemovePrivateDomainReq = {
|
||||
// server.host.address.domain.private.remove
|
||||
fqdn: string // FQDN
|
||||
}
|
||||
export type OsUiRemovePrivateDomainRes = null
|
||||
|
||||
export type PkgBindingToggleGatewayReq = ServerBindingToggleGatewayReq & {
|
||||
// package.host.binding.set-gateway-enabled
|
||||
@@ -336,22 +334,31 @@ export namespace RR {
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
|
||||
|
||||
export type PkgAddDomainReq = OsUiAddDomainReq & {
|
||||
// package.host.address.domain.add
|
||||
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
||||
// package.host.address.domain.public.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgAddDomainRes = null
|
||||
export type PkgAddPublicDomainRes = OsUiAddPublicDomainRes
|
||||
|
||||
export type PkgRemoveDomainReq = OsUiRemoveDomainReq & {
|
||||
// package.host.address.domain.remove
|
||||
export type PkgRemovePublicDomainReq = OsUiRemovePublicDomainReq & {
|
||||
// package.host.address.domain.public.remove
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgRemoveDomainRes = null
|
||||
export type PkgRemovePublicDomainRes = OsUiRemovePublicDomainRes
|
||||
|
||||
export type PkgAddPrivateDomainReq = OsUiAddPrivateDomainReq & {
|
||||
// package.host.address.domain.private.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgAddPrivateDomainRes = OsUiAddPrivateDomainRes
|
||||
|
||||
export type PkgRemovePrivateDomainReq = PkgAddPrivateDomainReq
|
||||
export type PkgRemovePrivateDomainRes = OsUiRemovePrivateDomainRes
|
||||
|
||||
export type GetPackageLogsReq = FetchLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = FetchLogsRes
|
||||
|
||||
@@ -122,9 +122,9 @@ export abstract class ApiService {
|
||||
|
||||
abstract toggleKiosk(enable: boolean): Promise<null>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
abstract testDns(params: RR.TestDnsReq): Promise<RR.TestDnsRes>
|
||||
|
||||
// @TODO 041
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
// smtp
|
||||
|
||||
@@ -182,18 +182,8 @@ export abstract class ApiService {
|
||||
|
||||
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
|
||||
|
||||
// @TODO 041
|
||||
|
||||
// ** domains **
|
||||
|
||||
// @TODO 041
|
||||
|
||||
abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes>
|
||||
|
||||
abstract testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes>
|
||||
|
||||
// wifi
|
||||
|
||||
abstract enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes>
|
||||
@@ -369,13 +359,21 @@ export abstract class ApiService {
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract osUiAddDomain(
|
||||
params: RR.OsUiAddDomainReq,
|
||||
): Promise<RR.OsUiAddDomainRes>
|
||||
abstract osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
): Promise<RR.OsUiAddPublicDomainRes>
|
||||
|
||||
abstract osUiRemoveDomain(
|
||||
params: RR.OsUiRemoveDomainReq,
|
||||
): Promise<RR.OsUiRemoveDomainRes>
|
||||
abstract osUiRemovePublicDomain(
|
||||
params: RR.OsUiRemovePublicDomainReq,
|
||||
): Promise<RR.OsUiRemovePublicDomainRes>
|
||||
|
||||
abstract osUiAddPrivateDomain(
|
||||
params: RR.OsUiAddPrivateDomainReq,
|
||||
): Promise<RR.OsUiAddPrivateDomainRes>
|
||||
|
||||
abstract osUiRemovePrivateDomain(
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes>
|
||||
|
||||
abstract pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
@@ -387,9 +385,19 @@ export abstract class ApiService {
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes>
|
||||
abstract pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
): Promise<RR.PkgAddPublicDomainRes>
|
||||
|
||||
abstract pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.PkgRemoveDomainRes>
|
||||
abstract pkgRemovePublicDomain(
|
||||
params: RR.PkgRemovePublicDomainReq,
|
||||
): Promise<RR.PkgRemovePublicDomainRes>
|
||||
|
||||
abstract pkgAddPrivateDomain(
|
||||
params: RR.PkgAddPrivateDomainReq,
|
||||
): Promise<RR.PkgAddPrivateDomainRes>
|
||||
|
||||
abstract pkgRemovePrivateDomain(
|
||||
params: RR.PkgRemovePrivateDomainReq,
|
||||
): Promise<RR.PkgRemovePrivateDomainRes>
|
||||
}
|
||||
|
||||
@@ -267,6 +267,13 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async testDns(params: RR.TestDnsReq): Promise<RR.TestDnsRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.dns.test',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.reset', params })
|
||||
}
|
||||
@@ -358,20 +365,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'net.tunnel.remove', params })
|
||||
}
|
||||
|
||||
// domains
|
||||
|
||||
async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.add', params })
|
||||
}
|
||||
|
||||
async removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.remove', params })
|
||||
}
|
||||
|
||||
async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.test-dns', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
@@ -663,20 +656,38 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async osUiAddDomain(
|
||||
params: RR.OsUiAddDomainReq,
|
||||
): Promise<RR.OsUiAddDomainRes> {
|
||||
async osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
): Promise<RR.OsUiAddPublicDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.add',
|
||||
method: 'server.host.address.domain.public.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async osUiRemoveDomain(
|
||||
params: RR.OsUiRemoveDomainReq,
|
||||
): Promise<RR.OsUiRemoveDomainRes> {
|
||||
async osUiRemovePublicDomain(
|
||||
params: RR.OsUiRemovePublicDomainReq,
|
||||
): Promise<RR.OsUiRemovePublicDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.remove',
|
||||
method: 'server.host.address.domain.public.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async osUiAddPrivateDomain(
|
||||
params: RR.OsUiAddPrivateDomainReq,
|
||||
): Promise<RR.OsUiAddPrivateDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.private.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async osUiRemovePrivateDomain(
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.private.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -706,18 +717,38 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes> {
|
||||
async pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
): Promise<RR.PkgAddPublicDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.add',
|
||||
method: 'package.host.address.domain.public.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.PkgRemoveDomainRes> {
|
||||
async pkgRemovePublicDomain(
|
||||
params: RR.PkgRemovePublicDomainReq,
|
||||
): Promise<RR.PkgRemovePublicDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.remove',
|
||||
method: 'package.host.address.domain.public.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddPrivateDomain(
|
||||
params: RR.PkgAddPrivateDomainReq,
|
||||
): Promise<RR.PkgAddPrivateDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.private.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemovePrivateDomain(
|
||||
params: RR.PkgRemovePrivateDomainReq,
|
||||
): Promise<RR.PkgRemovePrivateDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.private.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -462,6 +462,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async testDns(params: RR.TestDnsReq): Promise<RR.TestDnsRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
@@ -601,50 +607,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// domains
|
||||
|
||||
async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/network/domains`,
|
||||
value: {
|
||||
[params.fqdn]: {
|
||||
gateway: params.gateway,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async removeDomain(params: RR.RemoveDomainReq): Promise<RR.RemoveDomainRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/domains',
|
||||
value: {},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
return {
|
||||
root: true,
|
||||
wildcard: true,
|
||||
}
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
@@ -1432,19 +1394,72 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async osUiAddDomain(
|
||||
params: RR.OsUiAddDomainReq,
|
||||
): Promise<RR.OsUiAddDomainRes> {
|
||||
async osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
): Promise<RR.OsUiAddPublicDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/domains`,
|
||||
path: `/serverInfo/host/domains/public`,
|
||||
value: {
|
||||
[params.fqdn]: { public: !params.private, acme: params.acme },
|
||||
[params.fqdn]: { gateway: params.gateway, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.fqdn,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async osUiRemovePublicDomain(
|
||||
params: RR.OsUiRemovePublicDomainReq,
|
||||
): Promise<RR.OsUiRemovePublicDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/domains/public/${params.fqdn}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async osUiAddPrivateDomain(
|
||||
params: RR.OsUiAddPrivateDomainReq,
|
||||
): Promise<RR.OsUiAddPrivateDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/host/domains/private`,
|
||||
value: [params.fqdn],
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
@@ -1467,15 +1482,16 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async osUiRemoveDomain(
|
||||
params: RR.OsUiRemoveDomainReq,
|
||||
): Promise<RR.OsUiRemoveDomainRes> {
|
||||
async osUiRemovePrivateDomain(
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/domains/${params.fqdn}`,
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/host/domains/private`,
|
||||
value: [],
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
@@ -1551,17 +1567,72 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.PkgAddDomainRes> {
|
||||
async pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
): Promise<RR.PkgAddPublicDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/public`,
|
||||
value: {
|
||||
[params.fqdn]: { public: !params.private, acme: params.acme },
|
||||
[params.fqdn]: { gateway: params.gateway, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.fqdn,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemovePublicDomain(
|
||||
params: RR.PkgRemovePublicDomainReq,
|
||||
): Promise<RR.PkgRemovePublicDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/public/${params.fqdn}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddPrivateDomain(
|
||||
params: RR.PkgAddPrivateDomainReq,
|
||||
): Promise<RR.PkgAddPrivateDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/private`,
|
||||
value: [params.fqdn],
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
@@ -1584,15 +1655,16 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.PkgRemoveDomainRes> {
|
||||
async pkgRemovePrivateDomain(
|
||||
params: RR.PkgRemovePrivateDomainReq,
|
||||
): Promise<RR.PkgRemovePrivateDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.fqdn}`,
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/private`,
|
||||
value: [],
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
|
||||
@@ -32,14 +32,6 @@ export const mockPatchData: DataModel = {
|
||||
contact: ['mailto:support@start9.com'],
|
||||
},
|
||||
},
|
||||
domains: {
|
||||
'cloud.private.com': {
|
||||
gateway: 'eth0',
|
||||
},
|
||||
'public.com': {
|
||||
gateway: 'wireguard1',
|
||||
},
|
||||
},
|
||||
host: {
|
||||
bindings: {
|
||||
80: {
|
||||
@@ -60,7 +52,10 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
},
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
onions: ['myveryownspecialtoraddress'],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
@@ -344,7 +339,10 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
@@ -444,7 +442,10 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
@@ -467,7 +468,10 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
domains: {
|
||||
public: {},
|
||||
private: {},
|
||||
},
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { toSignal } from '@angular/core/rxjs-interop'
|
||||
export type GatewayPlus = T.NetworkInterfaceInfo & {
|
||||
id: string
|
||||
ipInfo: T.IpInfo
|
||||
ipv4: string[]
|
||||
lanIpv4: string[]
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -25,7 +25,7 @@ export class GatewayService {
|
||||
({
|
||||
...val,
|
||||
id,
|
||||
ipv4: val.ipInfo?.subnets
|
||||
lanIpv4: val.ipInfo?.subnets
|
||||
.filter(s => !s.includes('::'))
|
||||
.map(s => s.split('/')[0]),
|
||||
}) as GatewayPlus,
|
||||
|
||||
Reference in New Issue
Block a user