mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
MVP of service interface page
This commit is contained in:
@@ -4,31 +4,19 @@ import { TuiButton } from '@taiga-ui/core'
|
|||||||
import { TuiAccordion } from '@taiga-ui/experimental'
|
import { TuiAccordion } from '@taiga-ui/experimental'
|
||||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { MappedServiceInterface } from '../interface.utils'
|
import { MappedServiceInterface } from '../interface.service'
|
||||||
import { AddressActionsComponent } from './actions.component'
|
import { InterfaceAddressItemComponent } from './item.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'section[addresses]',
|
selector: 'section[addresses]',
|
||||||
template: `
|
template: `
|
||||||
<header>{{ 'Addresses' | i18n }}</header>
|
<header>{{ 'Addresses' | i18n }}</header>
|
||||||
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
||||||
@for (address of addresses().common; track $index) {
|
@for (
|
||||||
<tr>
|
address of addresses().common.concat(addresses().uncommon);
|
||||||
<td>
|
track $index
|
||||||
<button
|
) {
|
||||||
tuiIconButton
|
<tr [address]="address" [isRunning]="isRunning()"></tr>
|
||||||
appearance="flat-grayscale"
|
|
||||||
iconStart="@tui.eye"
|
|
||||||
(click)="instructions()"
|
|
||||||
>
|
|
||||||
{{ 'View instructions' | i18n }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>{{ address.type }}</td>
|
|
||||||
<td [style.order]="-1">{{ address.gateway }}</td>
|
|
||||||
<td>{{ address.url }}</td>
|
|
||||||
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
|
|
||||||
</tr>
|
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
@@ -38,35 +26,17 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</table>
|
<!-- @if (addresses().uncommon.length) {
|
||||||
|
<tui-accordion>
|
||||||
@if (addresses().uncommon.length) {
|
<button tuiAccordion>{{ 'Uncommon' | i18n }}</button>
|
||||||
<tui-accordion>
|
<tui-expand>
|
||||||
<button tuiAccordion>{{ 'Uncommon' | i18n }}</button>
|
|
||||||
<tui-expand>
|
|
||||||
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
|
||||||
@for (address of addresses().uncommon; track $index) {
|
@for (address of addresses().uncommon; track $index) {
|
||||||
<tr>
|
<tr [address]="address" [isRunning]="isRunning()"></tr>
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
tuiIconButton
|
|
||||||
appearance="flat-grayscale"
|
|
||||||
iconStart="@tui.eye"
|
|
||||||
(click)="instructions()"
|
|
||||||
>
|
|
||||||
{{ 'View instructions' | i18n }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>{{ address.type }}</td>
|
|
||||||
<td [style.order]="-1">{{ address.gateway }}</td>
|
|
||||||
<td>{{ address.url }}</td>
|
|
||||||
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
|
|
||||||
</tr>
|
|
||||||
}
|
}
|
||||||
</table>
|
</tui-expand>
|
||||||
</tui-expand>
|
</tui-accordion>
|
||||||
</tui-accordion>
|
} -->
|
||||||
}
|
</table>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
[tuiAccordion],
|
[tuiAccordion],
|
||||||
@@ -79,25 +49,13 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(tui-root._mobile) {
|
|
||||||
td:first-child {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:nth-child(2) {
|
|
||||||
font: var(--tui-font-text-m);
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--tui-text-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
host: { class: 'g-card' },
|
host: { class: 'g-card' },
|
||||||
imports: [
|
imports: [
|
||||||
TableComponent,
|
TableComponent,
|
||||||
PlaceholderComponent,
|
PlaceholderComponent,
|
||||||
i18nPipe,
|
i18nPipe,
|
||||||
AddressActionsComponent,
|
InterfaceAddressItemComponent,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiAccordion,
|
TuiAccordion,
|
||||||
],
|
],
|
||||||
@@ -106,6 +64,4 @@ import { AddressActionsComponent } from './actions.component'
|
|||||||
export class InterfaceAddressesComponent {
|
export class InterfaceAddressesComponent {
|
||||||
readonly addresses = input.required<MappedServiceInterface['addresses']>()
|
readonly addresses = input.required<MappedServiceInterface['addresses']>()
|
||||||
readonly isRunning = input.required<boolean>()
|
readonly isRunning = input.required<boolean>()
|
||||||
|
|
||||||
instructions() {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
input,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
|
||||||
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
|
import { DisplayAddress } from '../interface.service'
|
||||||
|
import { AddressActionsComponent } from './actions.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tr[address]',
|
||||||
|
template: `
|
||||||
|
@if (address(); as address) {
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
tuiIconButton
|
||||||
|
appearance="flat-grayscale"
|
||||||
|
iconStart="@tui.eye"
|
||||||
|
(click)="instructions(address.bullets)"
|
||||||
|
>
|
||||||
|
{{ 'View instructions' | i18n }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>{{ address.type }}</td>
|
||||||
|
<td [style.order]="-1">{{ address.gatewayName || '-' }}</td>
|
||||||
|
<td>{{ address.url }}</td>
|
||||||
|
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
:host-context(tui-root._mobile) {
|
||||||
|
td:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
font: var(--tui-font-text-m);
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--tui-text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
imports: [i18nPipe, AddressActionsComponent, TuiButton],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class InterfaceAddressItemComponent {
|
||||||
|
readonly address = input.required<DisplayAddress>()
|
||||||
|
readonly isRunning = input.required<boolean>()
|
||||||
|
readonly dialog = inject(DialogService)
|
||||||
|
|
||||||
|
instructions(bullets: string[]) {
|
||||||
|
this.dialog
|
||||||
|
.openAlert(
|
||||||
|
`<ul>${bullets.map(b => `<li>${b}</li>`).join('')}</ul>` as i18nKey,
|
||||||
|
{
|
||||||
|
label: 'About this address' as i18nKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { PlaceholderComponent } from 'src/app/routes/portal/components/placehold
|
|||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
|
|
||||||
import { DomainComponent } from './domain.component'
|
import { DomainComponent } from './domain.component'
|
||||||
import { ClearnetDomain } from './interface.utils'
|
import { ClearnetDomain } from './interface.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'section[clearnetDomains]',
|
selector: 'section[clearnetDomains]',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { TuiBadge } from '@taiga-ui/kit'
|
|||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { InterfaceComponent } from './interface.component'
|
import { InterfaceComponent } from './interface.component'
|
||||||
import { ClearnetDomain } from './interface.utils'
|
import { ClearnetDomain } from './interface.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[domain]',
|
selector: 'tr[domain]',
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
computed,
|
|
||||||
input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { TuiTitle } from '@taiga-ui/core'
|
import { TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiSwitch } from '@taiga-ui/kit'
|
import { TuiSwitch } from '@taiga-ui/kit'
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
import { InterfaceGateway } from './interface.utils'
|
import { InterfaceGateway } from './interface.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'section[gateways]',
|
selector: 'section[gateways]',
|
||||||
@@ -18,7 +13,7 @@ import { InterfaceGateway } from './interface.utils'
|
|||||||
<header>{{ 'Gateways' | i18n }}</header>
|
<header>{{ 'Gateways' | i18n }}</header>
|
||||||
@for (gateway of gateways(); track $index) {
|
@for (gateway of gateways(); track $index) {
|
||||||
<label tuiCell="s">
|
<label tuiCell="s">
|
||||||
<span tuiTitle>{{ gateway.name }}</span>
|
<span tuiTitle>{{ gateway.ipInfo.name }}</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
tuiSwitch
|
tuiSwitch
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||||
import { MappedServiceInterface } from './interface.utils'
|
import { MappedServiceInterface } from './interface.service'
|
||||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||||
import { InterfaceClearnetDomainsComponent } from './clearnet-domains.component'
|
import { InterfaceClearnetDomainsComponent } from './clearnet-domains.component'
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { inject, Injectable } from '@angular/core'
|
import { inject, Injectable } from '@angular/core'
|
||||||
import { T, utils } from '@start9labs/start-sdk'
|
import { T, utils } from '@start9labs/start-sdk'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
import { toAuthorityName } from 'src/app/utils/acme'
|
||||||
|
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||||
|
import { i18nKey } from '@start9labs/shared'
|
||||||
|
|
||||||
type AddressWithInfo = {
|
type AddressWithInfo = {
|
||||||
address: URL
|
url: URL
|
||||||
info: T.HostnameInfo
|
info: T.HostnameInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +17,7 @@ function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
|||||||
): -1 | 0 | 1 {
|
): -1 | 0 | 1 {
|
||||||
for (const pred of preds) {
|
for (const pred of preds) {
|
||||||
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
||||||
if (pred(x) && !pred(y)) return sign
|
if (pred(y) && !pred(x)) return sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -26,8 +29,7 @@ function filterTor(a: AddressWithInfo): a is TorAddress {
|
|||||||
}
|
}
|
||||||
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
|
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
|
||||||
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
||||||
if (x.address.protocol === 'http:' && y.address.protocol === 'https:')
|
if (y.url.protocol === 'http:' && x.url.protocol === 'https:') return sign
|
||||||
return sign
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -98,8 +100,161 @@ function cmpClearnet(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDisplayAddress(a: AddressWithInfo): Address {
|
// @TODO translations
|
||||||
throw new Error('@TODO: MattHill')
|
function toDisplayAddress(
|
||||||
|
{ info, url }: AddressWithInfo,
|
||||||
|
gateways: GatewayPlus[],
|
||||||
|
domains: Record<string, T.DomainConfig>,
|
||||||
|
): DisplayAddress {
|
||||||
|
let access: DisplayAddress['access']
|
||||||
|
let gatewayName: DisplayAddress['gatewayName']
|
||||||
|
let type: DisplayAddress['type']
|
||||||
|
let bullets: any[]
|
||||||
|
// let bullets: DisplayAddress['bullets']
|
||||||
|
|
||||||
|
const rootCaRequired = `Requires trusting your server's Root CA`
|
||||||
|
|
||||||
|
// ** Tor **
|
||||||
|
if (info.kind === 'onion') {
|
||||||
|
access = null
|
||||||
|
gatewayName = null
|
||||||
|
type = 'Tor'
|
||||||
|
bullets = [
|
||||||
|
'Connections can be slow or unreliable at times',
|
||||||
|
'Public if you share the address publicly, otherwise private',
|
||||||
|
'Requires using a Tor-enabled device or browser',
|
||||||
|
]
|
||||||
|
// Tor (HTTPS)
|
||||||
|
if (url.protocol.startsWith('https')) {
|
||||||
|
type = `${type} (HTTPS)`
|
||||||
|
bullets = [
|
||||||
|
'Only useful for clients that enforce HTTPS',
|
||||||
|
rootCaRequired,
|
||||||
|
...bullets,
|
||||||
|
]
|
||||||
|
// Tor (HTTP)
|
||||||
|
} else {
|
||||||
|
bullets.unshift('Ideal for anonymous, remote connectivity')
|
||||||
|
type = `${type} (HTTP)`
|
||||||
|
}
|
||||||
|
// ** Not Tor **
|
||||||
|
} else {
|
||||||
|
const port = info.hostname.sslPort || info.hostname.port
|
||||||
|
const gateway = gateways.find(g => g.id === info.gatewayId)!
|
||||||
|
gatewayName = gateway.ipInfo.name
|
||||||
|
|
||||||
|
const gatewayIpv4 = gateway.ipInfo.subnets[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 vpnAccess = 'Ideal for VPN access via your'
|
||||||
|
|
||||||
|
// * Local *
|
||||||
|
if (info.hostname.kind === 'local') {
|
||||||
|
type = 'Local'
|
||||||
|
access = 'private'
|
||||||
|
bullets = [
|
||||||
|
localIdeal,
|
||||||
|
'Not recommended for VPN access. VPNs do not support ".local" domains without extra configuration',
|
||||||
|
lanRequired,
|
||||||
|
rootCaRequired,
|
||||||
|
]
|
||||||
|
// * IPv4 *
|
||||||
|
} else if (info.hostname.kind === 'ipv4') {
|
||||||
|
type = 'IPv4'
|
||||||
|
if (info.public) {
|
||||||
|
access = 'public'
|
||||||
|
bullets = [
|
||||||
|
'Can be used for clearnet access',
|
||||||
|
'Not recommended in most cases',
|
||||||
|
rootCaRequired,
|
||||||
|
]
|
||||||
|
if (!gateway.public) {
|
||||||
|
bullets.push(
|
||||||
|
`Requires creating a port forwarding rule in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
access = 'private'
|
||||||
|
if (isWireguard) {
|
||||||
|
bullets = [`${vpnAccess} StartTunnel (or similar)`, rootCaRequired]
|
||||||
|
} else {
|
||||||
|
bullets = [
|
||||||
|
localIdeal,
|
||||||
|
`${vpnAccess} router's Wireguard server`,
|
||||||
|
lanRequired,
|
||||||
|
rootCaRequired,
|
||||||
|
staticRequired,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// * IPv6 *
|
||||||
|
} else if (info.hostname.kind === 'ipv6') {
|
||||||
|
type = 'IPv6'
|
||||||
|
access = 'private'
|
||||||
|
bullets = ['Can be used for local access', lanRequired, rootCaRequired]
|
||||||
|
// * Domain *
|
||||||
|
} else {
|
||||||
|
type = 'Domain'
|
||||||
|
const domain = domains[info.hostname.value]!
|
||||||
|
if (info.public) {
|
||||||
|
access = 'public'
|
||||||
|
bullets = [
|
||||||
|
`Requires creating DNS records for "${domains[info.hostname.value]?.root}", as shown in System -> Domains`,
|
||||||
|
`Requires creating a port forwarding rule in gateway "${gatewayName}": ${port} -> ${info.hostname.value}:${port === 443 ? 5443 : port}`,
|
||||||
|
]
|
||||||
|
if (domain.acme) {
|
||||||
|
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',
|
||||||
|
rootCaRequired,
|
||||||
|
...bullets,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
access = 'private'
|
||||||
|
const ipPortBad = 'when using IP addresses and ports is undesirable'
|
||||||
|
const customDnsRequired = `Requires creating custom DNS records for ${info.hostname.value} that resolve to ${gatewayIpv4}`
|
||||||
|
if (isWireguard) {
|
||||||
|
bullets = [
|
||||||
|
`${vpnAccess} StartTunnel (or similar) ${ipPortBad}`,
|
||||||
|
customDnsRequired,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
bullets = [
|
||||||
|
`${localIdeal} ${ipPortBad}`,
|
||||||
|
`${vpnAccess} router's Wireguard server ${ipPortBad}`,
|
||||||
|
customDnsRequired,
|
||||||
|
lanRequired,
|
||||||
|
staticRequired,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (domain.acme) {
|
||||||
|
bullets.push(rootCaRequired)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: url.href,
|
||||||
|
access,
|
||||||
|
gatewayName,
|
||||||
|
type,
|
||||||
|
bullets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClearnetDomains(host: T.Host): ClearnetDomain[] {
|
||||||
|
return Object.entries(host.domains).map(([fqdn, info]) => ({
|
||||||
|
fqdn,
|
||||||
|
authority: toAuthorityName(info.acme),
|
||||||
|
public: info.public,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -109,9 +264,10 @@ export class InterfaceService {
|
|||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
getAddresses(
|
getAddresses(
|
||||||
serverDomains: Record<string, T.DomainSettings>,
|
|
||||||
serviceInterface: T.ServiceInterface,
|
serviceInterface: T.ServiceInterface,
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
|
serverDomains: Record<string, T.DomainSettings>,
|
||||||
|
gateways: GatewayPlus[],
|
||||||
): MappedServiceInterface['addresses'] {
|
): MappedServiceInterface['addresses'] {
|
||||||
const hostnamesInfos = this.hostnameInfo(serviceInterface, host)
|
const hostnamesInfos = this.hostnameInfo(serviceInterface, host)
|
||||||
|
|
||||||
@@ -125,7 +281,7 @@ export class InterfaceService {
|
|||||||
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
|
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
|
||||||
utils
|
utils
|
||||||
.addressHostToUrl(serviceInterface.addressInfo, h)
|
.addressHostToUrl(serviceInterface.addressInfo, h)
|
||||||
.map(a => ({ address: new URL(a), info: h })),
|
.map(a => ({ url: new URL(a), info: h })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
||||||
@@ -147,10 +303,10 @@ export class InterfaceService {
|
|||||||
}, [] as AddressWithInfo[])
|
}, [] as AddressWithInfo[])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
common: bestAddrs.map(toDisplayAddress),
|
common: bestAddrs.map(a => toDisplayAddress(a, gateways, host.domains)),
|
||||||
uncommon: allAddressesWithInfo
|
uncommon: allAddressesWithInfo
|
||||||
.filter(a => !bestAddrs.includes(a))
|
.filter(a => !bestAddrs.includes(a))
|
||||||
.map(toDisplayAddress),
|
.map(a => toDisplayAddress(a, gateways, host.domains)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,28 +453,35 @@ export type MappedServiceInterface = T.ServiceInterface & {
|
|||||||
torDomains: string[]
|
torDomains: string[]
|
||||||
clearnetDomains: ClearnetDomain[]
|
clearnetDomains: ClearnetDomain[]
|
||||||
addresses: {
|
addresses: {
|
||||||
common: Address[]
|
common: DisplayAddress[]
|
||||||
uncommon: Address[]
|
uncommon: DisplayAddress[]
|
||||||
}
|
}
|
||||||
isOs: boolean
|
isOs: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InterfaceGateway = {
|
export type InterfaceGateway = GatewayPlus & {
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
public: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export type InterfaceGateway = {
|
||||||
|
// id: string
|
||||||
|
// name: string
|
||||||
|
// enabled: boolean
|
||||||
|
// public: boolean
|
||||||
|
// type: T.NetworkInterfaceType
|
||||||
|
// lanIpv4: string | null
|
||||||
|
// }
|
||||||
|
|
||||||
export type ClearnetDomain = {
|
export type ClearnetDomain = {
|
||||||
fqdn: string
|
fqdn: string
|
||||||
authority: string | null
|
authority: string | null
|
||||||
public: boolean
|
public: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Address = {
|
export type DisplayAddress = {
|
||||||
type: string
|
type: string
|
||||||
gateway: string
|
access: 'public' | 'private' | null
|
||||||
|
gatewayName: string | null
|
||||||
url: string
|
url: string
|
||||||
description: string
|
bullets: i18nKey[]
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import { TuiButton } from '@taiga-ui/core'
|
|||||||
import { TuiBadge } from '@taiga-ui/kit'
|
import { TuiBadge } from '@taiga-ui/kit'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { InterfaceService } from '../../../components/interfaces/interface.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[serviceInterface]',
|
selector: 'tr[serviceInterface]',
|
||||||
@@ -82,7 +83,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
imports: [TuiButton, TuiBadge, RouterLink],
|
imports: [TuiButton, TuiBadge, RouterLink],
|
||||||
})
|
})
|
||||||
export class ServiceInterfaceItemComponent {
|
export class ServiceInterfaceItemComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly interfaceService = inject(InterfaceService)
|
||||||
private readonly document = inject(DOCUMENT)
|
private readonly document = inject(DOCUMENT)
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
@@ -110,7 +111,7 @@ export class ServiceInterfaceItemComponent {
|
|||||||
get href() {
|
get href() {
|
||||||
const host = this.pkg.hosts[this.info.addressInfo.hostId]
|
const host = this.pkg.hosts[this.info.addressInfo.hostId]
|
||||||
if (!host) return ''
|
if (!host) return ''
|
||||||
return this.config.launchableAddress(this.info, host)
|
return this.interfaceService.launchableAddress(this.info, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
openUI() {
|
openUI() {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { i18nPipe } from '@start9labs/shared'
|
|||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { tuiPure } from '@taiga-ui/cdk'
|
import { tuiPure } from '@taiga-ui/cdk'
|
||||||
import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
|
import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { InterfaceService } from '../../../components/interfaces/interface.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-ui-launch',
|
selector: 'app-ui-launch',
|
||||||
@@ -59,7 +59,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
|||||||
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe],
|
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe],
|
||||||
})
|
})
|
||||||
export class UILaunchComponent {
|
export class UILaunchComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly interfaceService = inject(InterfaceService)
|
||||||
private readonly document = inject(DOCUMENT)
|
private readonly document = inject(DOCUMENT)
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -88,7 +88,7 @@ export class UILaunchComponent {
|
|||||||
getHref(ui: T.ServiceInterface): string {
|
getHref(ui: T.ServiceInterface): string {
|
||||||
const host = this.pkg.hosts[ui.addressInfo.hostId]
|
const host = this.pkg.hosts[ui.addressInfo.hostId]
|
||||||
if (!host) return ''
|
if (!host) return ''
|
||||||
return this.config.launchableAddress(ui, host)
|
return this.interfaceService.launchableAddress(ui, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
openUI(ui: T.ServiceInterface) {
|
openUI(ui: T.ServiceInterface) {
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
|
|||||||
import { TuiHeader } from '@taiga-ui/layout'
|
import { TuiHeader } from '@taiga-ui/layout'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
import {
|
||||||
|
getClearnetDomains,
|
||||||
|
InterfaceService,
|
||||||
|
} from '../../../components/interfaces/interface.service'
|
||||||
|
import { GatewayService } from 'src/app/services/gateway.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -25,15 +29,15 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">
|
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">
|
||||||
{{ 'Back' | i18n }}
|
{{ 'Back' | i18n }}
|
||||||
</a>
|
</a>
|
||||||
{{ interface()?.name }}
|
{{ serviceInterface()?.name }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<tui-breadcrumbs size="l">
|
<tui-breadcrumbs size="l">
|
||||||
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
|
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
|
||||||
{{ 'Dashboard' | i18n }}
|
{{ 'Dashboard' | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<span *tuiItem class="g-primary">{{ interface()?.name }}</span>
|
<span *tuiItem class="g-primary">{{ serviceInterface()?.name }}</span>
|
||||||
</tui-breadcrumbs>
|
</tui-breadcrumbs>
|
||||||
@if (interface(); as value) {
|
@if (serviceInterface(); as value) {
|
||||||
<header tuiHeader [style.margin-bottom.rem]="1">
|
<header tuiHeader [style.margin-bottom.rem]="1">
|
||||||
<hgroup tuiTitle>
|
<hgroup tuiTitle>
|
||||||
<h3>
|
<h3>
|
||||||
@@ -71,6 +75,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
`,
|
`,
|
||||||
host: { class: 'g-subpage' },
|
host: { class: 'g-subpage' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [GatewayService],
|
||||||
imports: [
|
imports: [
|
||||||
InterfaceComponent,
|
InterfaceComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
@@ -86,43 +91,59 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class ServiceInterfaceRoute {
|
export default class ServiceInterfaceRoute {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly interfaceService = inject(InterfaceService)
|
||||||
|
private readonly gatewayService = inject(GatewayService)
|
||||||
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
|
||||||
readonly pkgId = getPkgId()
|
readonly pkgId = getPkgId()
|
||||||
readonly interfaceId = input('')
|
readonly interfaceId = input('')
|
||||||
|
|
||||||
readonly pkg = toSignal(
|
readonly pkg = toSignal(this.patch.watch$('packageData', this.pkgId))
|
||||||
inject<PatchDB<DataModel>>(PatchDB).watch$('packageData', this.pkgId),
|
|
||||||
|
readonly domains = toSignal(
|
||||||
|
this.patch.watch$('serverInfo', 'network', 'domains'),
|
||||||
)
|
)
|
||||||
|
|
||||||
readonly isRunning = computed(() => {
|
readonly isRunning = computed(() => {
|
||||||
return this.pkg()?.status.main === 'running'
|
return this.pkg()?.status.main === 'running'
|
||||||
})
|
})
|
||||||
|
|
||||||
readonly interface = computed(() => {
|
readonly serviceInterface = computed(() => {
|
||||||
const pkg = this.pkg()
|
const pkg = this.pkg()
|
||||||
const id = this.interfaceId()
|
const id = this.interfaceId()
|
||||||
|
const domains = this.domains()
|
||||||
|
|
||||||
if (!pkg || !id) {
|
if (!pkg || !id || !domains) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { serviceInterfaces, hosts } = pkg
|
const { serviceInterfaces, hosts } = pkg
|
||||||
const item = serviceInterfaces[this.interfaceId()]
|
const iFace = serviceInterfaces[this.interfaceId()]
|
||||||
const key = item?.addressInfo.hostId || ''
|
const key = iFace?.addressInfo.hostId || ''
|
||||||
const host = hosts[key]
|
const host = hosts[key]
|
||||||
const port = item?.addressInfo.internalPort
|
const port = iFace?.addressInfo.internalPort
|
||||||
|
|
||||||
if (!host || !item || !port) {
|
if (!host || !iFace || !port) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gateways = this.gatewayService.gateways() || []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...iFace,
|
||||||
addresses: this.config.getAddresses(item, host),
|
addresses: this.interfaceService.getAddresses(
|
||||||
gateways: [],
|
iFace,
|
||||||
torDomains: [],
|
host,
|
||||||
clearnetDomains: [],
|
domains,
|
||||||
|
gateways,
|
||||||
|
),
|
||||||
|
gateways:
|
||||||
|
gateways.map(g => ({
|
||||||
|
enabled: true,
|
||||||
|
...g,
|
||||||
|
})) || [],
|
||||||
|
torDomains: host.onions.map(o => `${o}.onion`),
|
||||||
|
clearnetDomains: getClearnetDomains(host),
|
||||||
isOs: false,
|
isOs: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class DomainService {
|
|||||||
|
|
||||||
readonly data = toSignal(
|
readonly data = toSignal(
|
||||||
this.patch.watch$('serverInfo', 'network').pipe(
|
this.patch.watch$('serverInfo', 'network').pipe(
|
||||||
map(({ gateways, domains, acme }) => ({
|
map(({ gateways, domains }) => ({
|
||||||
gateways: Object.entries(gateways).reduce<Record<string, string>>(
|
gateways: Object.entries(gateways).reduce<Record<string, string>>(
|
||||||
(obj, [id, n]) => ({
|
(obj, [id, n]) => ({
|
||||||
...obj,
|
...obj,
|
||||||
@@ -51,7 +51,7 @@ export class DomainService {
|
|||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
domains: Object.entries(domains).map(
|
domains: Object.entries(domains).map(
|
||||||
([fqdn, { gateway, acme }]) =>
|
([fqdn, { gateway }]) =>
|
||||||
({
|
({
|
||||||
fqdn,
|
fqdn,
|
||||||
subdomain: parse(fqdn).subdomain,
|
subdomain: parse(fqdn).subdomain,
|
||||||
|
|||||||
@@ -8,17 +8,13 @@ import {
|
|||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { TuiButton } from '@taiga-ui/core'
|
import { TuiButton } from '@taiga-ui/core'
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { GatewaysTableComponent } from './table.component'
|
import { GatewaysTableComponent } from './table.component'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { map } from 'rxjs'
|
|
||||||
import { ISB } from '@start9labs/start-sdk'
|
import { ISB } from '@start9labs/start-sdk'
|
||||||
import { GatewayPlus } from './item.component'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -52,7 +48,7 @@ import { GatewayPlus } from './item.component'
|
|||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div [gateways]="gateways$ | async"></div>
|
<gateways-table />
|
||||||
</section>
|
</section>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -72,25 +68,6 @@ export default class GatewaysComponent {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
|
||||||
readonly gateways$ = inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
.watch$('serverInfo', 'network', 'gateways')
|
|
||||||
.pipe(
|
|
||||||
map(gateways =>
|
|
||||||
Object.entries(gateways)
|
|
||||||
.filter(([_, val]) => !!val.ipInfo)
|
|
||||||
.map(
|
|
||||||
([id, val]) =>
|
|
||||||
({
|
|
||||||
...val,
|
|
||||||
id,
|
|
||||||
ipv4: val.ipInfo?.subnets
|
|
||||||
.filter(s => !s.includes('::'))
|
|
||||||
.map(s => s.split('/')[0]),
|
|
||||||
}) as GatewayPlus,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async add() {
|
async add() {
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add gateway',
|
label: 'Add gateway',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
i18nPipe,
|
i18nPipe,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { ISB, T } from '@start9labs/start-sdk'
|
import { ISB } from '@start9labs/start-sdk'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiDataList,
|
TuiDataList,
|
||||||
@@ -23,12 +23,7 @@ import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
|||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||||
export type GatewayPlus = T.NetworkInterfaceInfo & {
|
|
||||||
id: string
|
|
||||||
ipInfo: T.IpInfo
|
|
||||||
ipv4: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tr[gateway]',
|
selector: 'tr[gateway]',
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { i18nPipe } from '@start9labs/shared'
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { GatewaysItemComponent, GatewayPlus } from './item.component'
|
import { GatewaysItemComponent } from './item.component'
|
||||||
|
import { GatewayService } from 'src/app/services/gateway.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: '[gateways]',
|
selector: 'gateways-table',
|
||||||
template: `
|
template: `
|
||||||
<table
|
<table
|
||||||
[appTable]="[
|
[appTable]="[
|
||||||
@@ -17,7 +18,7 @@ import { GatewaysItemComponent, GatewayPlus } from './item.component'
|
|||||||
null,
|
null,
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@for (gateway of gateways(); track $index) {
|
@for (gateway of gatewayService.gateways(); track $index) {
|
||||||
<tr [gateway]="gateway"></tr>
|
<tr [gateway]="gateway"></tr>
|
||||||
} @empty {
|
} @empty {
|
||||||
<tr>
|
<tr>
|
||||||
@@ -29,8 +30,9 @@ import { GatewaysItemComponent, GatewayPlus } from './item.component'
|
|||||||
</table>
|
</table>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [GatewayService],
|
||||||
imports: [TuiSkeleton, i18nPipe, TableComponent, GatewaysItemComponent],
|
imports: [TuiSkeleton, i18nPipe, TableComponent, GatewaysItemComponent],
|
||||||
})
|
})
|
||||||
export class GatewaysTableComponent<T extends GatewayPlus> {
|
export class GatewaysTableComponent {
|
||||||
readonly gateways = input<readonly T[] | null>(null)
|
protected readonly gatewayService = inject(GatewayService)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import { TuiHeader } from '@taiga-ui/layout'
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { map } from 'rxjs'
|
import { map } from 'rxjs'
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
import {
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
getClearnetDomains,
|
||||||
|
InterfaceService,
|
||||||
|
} from 'src/app/routes/portal/components/interfaces/interface.service'
|
||||||
|
import { GatewayService } from 'src/app/services/gateway.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
|
|
||||||
@@ -40,6 +43,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
`,
|
`,
|
||||||
host: { class: 'g-subpage' },
|
host: { class: 'g-subpage' },
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [GatewayService],
|
||||||
imports: [
|
imports: [
|
||||||
InterfaceComponent,
|
InterfaceComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
@@ -51,7 +55,8 @@ import { TitleDirective } from 'src/app/services/title.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class StartOsUiComponent {
|
export default class StartOsUiComponent {
|
||||||
private readonly config = inject(ConfigService)
|
private readonly interfaceService = inject(InterfaceService)
|
||||||
|
private readonly gatewayService = inject(GatewayService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
readonly iface: T.ServiceInterface = {
|
readonly iface: T.ServiceInterface = {
|
||||||
@@ -72,27 +77,31 @@ export default class StartOsUiComponent {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly ui = toSignal(
|
readonly network = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo', 'network'),
|
||||||
.watch$('serverInfo', 'network', 'host')
|
|
||||||
.pipe(
|
|
||||||
map(host => {
|
|
||||||
return {
|
|
||||||
...this.iface,
|
|
||||||
addresses: getAddresses(this.iface, host, this.config),
|
|
||||||
gateways: [
|
|
||||||
{
|
|
||||||
id: 'eth0',
|
|
||||||
name: 'Wired Connection 1',
|
|
||||||
public: false,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
torDomains: [],
|
|
||||||
clearnetDomains: [],
|
|
||||||
isOs: true,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readonly ui = computed(() => {
|
||||||
|
const network = this.network()
|
||||||
|
const gateways = this.gatewayService.gateways()
|
||||||
|
|
||||||
|
if (!network || !gateways) return
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.iface,
|
||||||
|
addresses: this.interfaceService.getAddresses(
|
||||||
|
this.iface,
|
||||||
|
network.host,
|
||||||
|
network.domains,
|
||||||
|
gateways,
|
||||||
|
),
|
||||||
|
gateways: gateways.map(g => ({
|
||||||
|
enabled: true,
|
||||||
|
...g,
|
||||||
|
})),
|
||||||
|
torDomains: network.host.onions.map(o => `${o}.onion`),
|
||||||
|
clearnetDomains: getClearnetDomains(network.host),
|
||||||
|
isOs: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,9 @@ export const mockPatchData: DataModel = {
|
|||||||
domains: {
|
domains: {
|
||||||
'cloud.private.com': {
|
'cloud.private.com': {
|
||||||
gateway: 'eth0',
|
gateway: 'eth0',
|
||||||
acme: null,
|
|
||||||
},
|
},
|
||||||
'public.com': {
|
'public.com': {
|
||||||
gateway: 'wireguard1',
|
gateway: 'wireguard1',
|
||||||
acme: 'https://acme-v02.api.letsencrypt.org/directory',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
|
|||||||
36
web/projects/ui/src/app/services/gateway.service.ts
Normal file
36
web/projects/ui/src/app/services/gateway.service.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { inject, Injectable } from '@angular/core'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { map } from 'rxjs/operators'
|
||||||
|
import { DataModel } from './patch-db/data-model'
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
|
||||||
|
export type GatewayPlus = T.NetworkInterfaceInfo & {
|
||||||
|
id: string
|
||||||
|
ipInfo: T.IpInfo
|
||||||
|
ipv4: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GatewayService {
|
||||||
|
readonly gateways = toSignal(
|
||||||
|
inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
.watch$('serverInfo', 'network', 'gateways')
|
||||||
|
.pipe(
|
||||||
|
map(gateways =>
|
||||||
|
Object.entries(gateways)
|
||||||
|
.filter(([_, val]) => !!val.ipInfo)
|
||||||
|
.map(
|
||||||
|
([id, val]) =>
|
||||||
|
({
|
||||||
|
...val,
|
||||||
|
id,
|
||||||
|
ipv4: val.ipInfo?.subnets
|
||||||
|
.filter(s => !s.includes('::'))
|
||||||
|
.map(s => s.split('/')[0]),
|
||||||
|
}) as GatewayPlus,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,16 +4,6 @@ import { T } from '@start9labs/start-sdk'
|
|||||||
export type DataModel = T.Public & {
|
export type DataModel = T.Public & {
|
||||||
ui: UIData
|
ui: UIData
|
||||||
packageData: AllPackageData
|
packageData: AllPackageData
|
||||||
serverInfo: T.ServerInfo & {
|
|
||||||
network: T.NetworkInfo & {
|
|
||||||
domains: {
|
|
||||||
[fqdn: string]: {
|
|
||||||
gateway: string
|
|
||||||
acme: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UIData = {
|
export type UIData = {
|
||||||
|
|||||||
Reference in New Issue
Block a user