mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
start service interface page, WIP
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
} from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
import { InterfaceComponent } from '../interface.component'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
@@ -114,7 +114,7 @@ import { InterfaceComponent } from './interface.component'
|
||||
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceActionsComponent {
|
||||
export class AddressActionsComponent {
|
||||
private readonly document = inject(DOCUMENT)
|
||||
|
||||
readonly isMobile = inject(TUI_IS_MOBILE)
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { MappedServiceInterface } from '../interface.utils'
|
||||
import { AddressActionsComponent } from './actions.component'
|
||||
|
||||
@Component({
|
||||
selector: 'section[addresses]',
|
||||
template: `
|
||||
<header>{{ 'Addresses' | i18n }}</header>
|
||||
@if (addresses().common.length) {
|
||||
<section class="g-card">
|
||||
<header>{{ 'Common' | i18n }}</header>
|
||||
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
||||
@for (address of addresses().common; track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.eye"
|
||||
(click)="instructions()"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ address.type }}</td>
|
||||
<td>{{ address.gateway }}</td>
|
||||
<td>{{ address.url }}</td>
|
||||
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</section>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
{{ 'No addresses' | i18n }}
|
||||
</app-placeholder>
|
||||
}
|
||||
|
||||
@if (addresses().uncommon.length) {
|
||||
<section class="g-card">
|
||||
<header>{{ 'Uncommon' | i18n }}</header>
|
||||
<table [appTable]="[null, 'Type', 'Gateway', 'URL', null]">
|
||||
@for (address of addresses().uncommon; track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.eye"
|
||||
(click)="instructions()"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ address.type }}</td>
|
||||
<td>{{ address.gateway }}</td>
|
||||
<td>{{ address.url }}</td>
|
||||
<td actions [disabled]="!isRunning()" [href]="address.url"></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
AddressActionsComponent,
|
||||
TuiButton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceAddressesComponent {
|
||||
readonly addresses = input.required<MappedServiceInterface['addresses']>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
instructions() {}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
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 {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiLink,
|
||||
} from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { ClearnetDomain } from './interface.utils'
|
||||
|
||||
@Component({
|
||||
selector: 'section[clearnetDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Clearnet Domains' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
@if (clearnetDomains().length) {
|
||||
<table [appTable]="['Domain', 'Certificate Authority', 'Type', null]">
|
||||
@for (domain of clearnetDomains(); track $index) {
|
||||
<tr>
|
||||
<td>{{ domain.fqdn }}</td>
|
||||
<td>{{ domain.authority }}</td>
|
||||
<td>{{ domain.public ? 'public' : 'private' }}</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 size="s" *tuiTextfieldDropdown>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.pencil"
|
||||
(click)="edit(domain)"
|
||||
>
|
||||
{{ 'Edit' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.trash"
|
||||
class="g-negative"
|
||||
(click)="remove(domain.fqdn)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
{{ 'No clearnet domains' | i18n }}
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiLink,
|
||||
TuiAppearance,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetDomainsComponent {
|
||||
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 clearnetDomains = input.required<readonly ClearnetDomain[]>()
|
||||
|
||||
open = false
|
||||
|
||||
async add() {}
|
||||
|
||||
async edit(domain: ClearnetDomain) {}
|
||||
|
||||
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()
|
||||
const params = { 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
DialogService,
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
TuiNotification,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { defaultIfEmpty, firstValueFrom, map } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { AuthorityNamePipe } from 'src/app/routes/portal/components/interfaces/acme.pipe'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { toAuthorityName } from 'src/app/utils/acme'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { ClearnetAddress } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
|
||||
type ClearnetForm = {
|
||||
domain: string
|
||||
authority: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'section[clearnet]',
|
||||
template: `
|
||||
<header>
|
||||
Clearnet
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
{{
|
||||
'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.'
|
||||
| i18n
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/clearnet.html"
|
||||
>
|
||||
{{ 'Learn more' | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
@if (clearnet().length) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
@if (clearnet().length) {
|
||||
@if (!isPublic()) {
|
||||
<tui-notification appearance="negative" [style.margin-bottom]="'1rem'">
|
||||
{{
|
||||
'To publish clearnet domains, you must click "Make Public", above.'
|
||||
| i18n
|
||||
}}
|
||||
</tui-notification>
|
||||
}
|
||||
<table [appTable]="['Certificate Authority', 'URL', null]">
|
||||
@for (address of clearnet(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="12">
|
||||
{{
|
||||
interface.value().addSsl
|
||||
? (address.authority | authorityName)
|
||||
: '-'
|
||||
}}
|
||||
</td>
|
||||
<td [style.order]="-1">{{ address.url | mask }}</td>
|
||||
<td
|
||||
actions
|
||||
[href]="address.url"
|
||||
[disabled]="!isRunning() || !isPublic()"
|
||||
>
|
||||
@if (address.isDomain) {
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="flat-grayscale"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (address.isDomain) {
|
||||
<button
|
||||
tuiOption
|
||||
tuiAppearance="action-destructive"
|
||||
iconStart="@tui.trash"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
{{ 'No public addresses' | i18n }}
|
||||
<button tuiButton iconStart="@tui.plus" (click)="add()">
|
||||
{{ 'Add domain' | i18n }}
|
||||
</button>
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host-context(tui-root._mobile) {
|
||||
td {
|
||||
font-weight: bold;
|
||||
color: var(--tui-text-primary);
|
||||
|
||||
&:first-child {
|
||||
font-weight: normal;
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TuiDataList,
|
||||
TuiAppearance,
|
||||
PlaceholderComponent,
|
||||
TableComponent,
|
||||
MaskPipe,
|
||||
AuthorityNamePipe,
|
||||
InterfaceActionsComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
TuiNotification,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceClearnetComponent {
|
||||
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)
|
||||
|
||||
readonly interface = inject(InterfaceComponent)
|
||||
|
||||
readonly clearnet = input.required<readonly ClearnetAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
readonly isPublic = input.required<boolean>()
|
||||
|
||||
readonly authorityUrls = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'acme')
|
||||
.pipe(map(acme => Object.keys(acme))),
|
||||
{ initialValue: [] },
|
||||
)
|
||||
|
||||
async remove({ url }: ClearnetAddress) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
content: 'Are you sure you want to delete this address?',
|
||||
},
|
||||
})
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)) {
|
||||
url = 'http://' + url
|
||||
}
|
||||
|
||||
const params = { domain: new URL(url).hostname }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
async add() {
|
||||
const domain = ISB.Value.text({
|
||||
name: 'Domain',
|
||||
description: 'The domain or subdomain you want to use',
|
||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
||||
required: true,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
})
|
||||
const authority = ISB.Value.select({
|
||||
name: 'Certificate Authority',
|
||||
description:
|
||||
'Select which Certificate authority to use for obtaining your SSL certificate. Add new authority in the System tab. Optionally use your local= Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
||||
values: this.authorityUrls().reduce<Record<string, string>>(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAuthorityName(url),
|
||||
}),
|
||||
{ local: toAuthorityName(null) },
|
||||
),
|
||||
default: '',
|
||||
})
|
||||
|
||||
this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, {
|
||||
label: 'Select domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of(
|
||||
this.interface.value().addSsl ? { domain, authority } : { domain },
|
||||
),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(domainInfo: ClearnetForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
const { domain, authority } = domainInfo
|
||||
|
||||
const params = {
|
||||
domain,
|
||||
acme: authority === 'local' ? null : authority,
|
||||
private: false,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { TuiSwitch } from '@taiga-ui/kit'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'section[gateways]',
|
||||
template: `
|
||||
<header>{{ 'Gateways' | i18n }}</header>
|
||||
<ul>
|
||||
@for (gateway of gateways(); track $index) {
|
||||
<li>
|
||||
{{ gateway.name }}
|
||||
<input
|
||||
type="checkbox"
|
||||
tuiSwitch
|
||||
[style.margin-inline-start]="'auto'"
|
||||
[showIcons]="false"
|
||||
[ngModel]="gateway.enabled"
|
||||
(ngModelChange)="onToggle(gateway)"
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
<ul></ul>
|
||||
</ul>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, FormsModule, TuiSwitch, i18nPipe],
|
||||
})
|
||||
export class InterfaceGatewaysComponent {
|
||||
readonly gateways = input.required<any>()
|
||||
|
||||
async onToggle(event: any) {}
|
||||
}
|
||||
@@ -1,43 +1,28 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { InterfaceClearnetComponent } from 'src/app/routes/portal/components/interfaces/clearnet.component'
|
||||
import { InterfaceLocalComponent } from 'src/app/routes/portal/components/interfaces/local.component'
|
||||
import { InterfaceTorComponent } from 'src/app/routes/portal/components/interfaces/tor.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { MappedServiceInterface } from './interface.utils'
|
||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||
import { InterfaceClearnetDomainsComponent } from './clearnet-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-interface',
|
||||
selector: 'service-interface',
|
||||
template: `
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
[appearance]="value().public ? 'primary-destructive' : 'primary-success'"
|
||||
[iconStart]="value().public ? '@tui.globe-lock' : '@tui.globe'"
|
||||
(click)="toggle()"
|
||||
>
|
||||
{{ value().public ? ('Make private' | i18n) : ('Make public' | i18n) }}
|
||||
</button>
|
||||
<section class="g-card" [gateways]="value().gateways"></section>
|
||||
<section class="g-card" [torDomains]="value().torDomains"></section>
|
||||
<section
|
||||
[clearnet]="value().addresses.clearnet"
|
||||
[isPublic]="value().public"
|
||||
[isRunning]="isRunning()"
|
||||
class="g-card"
|
||||
[clearnetDomains]="value().clearnetDomains"
|
||||
></section>
|
||||
<section [tor]="value().addresses.tor" [isRunning]="isRunning()"></section>
|
||||
<section
|
||||
[local]="value().addresses.local"
|
||||
[isRunning]="isRunning()"
|
||||
class="g-card"
|
||||
[addresses]="value().addresses"
|
||||
[isRunning]="true"
|
||||
></section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 56rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
@@ -48,54 +33,18 @@ import { MappedServiceInterface } from './interface.utils'
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: -0.5rem auto 0 0;
|
||||
}
|
||||
`,
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
imports: [
|
||||
InterfaceClearnetComponent,
|
||||
InterfaceTorComponent,
|
||||
InterfaceLocalComponent,
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
InterfaceGatewaysComponent,
|
||||
InterfaceTorDomainsComponent,
|
||||
InterfaceClearnetDomainsComponent,
|
||||
InterfaceAddressesComponent,
|
||||
],
|
||||
})
|
||||
export class InterfaceComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly packageId = input('')
|
||||
readonly value = input.required<MappedServiceInterface>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
async toggle() {
|
||||
const loader = this.loader
|
||||
.open(`Making ${this.value().public ? 'private' : 'public'}`)
|
||||
.subscribe()
|
||||
|
||||
const params = {
|
||||
internalPort: this.value().addressInfo.internalPort,
|
||||
public: !this.value().public,
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.packageId()) {
|
||||
await this.api.pkgBindingSetPubic({
|
||||
...params,
|
||||
host: this.value().addressInfo.hostId,
|
||||
package: this.packageId(),
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingSetPubic(params)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
export abstract class AddressesService {
|
||||
abstract static: boolean
|
||||
abstract add(): Promise<void>
|
||||
abstract remove(): Promise<void>
|
||||
}
|
||||
|
||||
export function getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
config: ConfigService,
|
||||
): {
|
||||
clearnet: ClearnetAddress[]
|
||||
local: LocalAddress[]
|
||||
tor: TorAddress[]
|
||||
} {
|
||||
): MappedServiceInterface['addresses'] {
|
||||
const addressInfo = serviceInterface.addressInfo
|
||||
const hostnames =
|
||||
host.hostnameInfo[addressInfo.internalPort]?.filter(
|
||||
@@ -46,60 +36,75 @@ export function getAddresses(
|
||||
}
|
||||
}
|
||||
|
||||
const clearnet: ClearnetAddress[] = []
|
||||
const local: LocalAddress[] = []
|
||||
const tor: TorAddress[] = []
|
||||
const common: Address[] = [
|
||||
{
|
||||
type: 'Local',
|
||||
description: '',
|
||||
gateway: 'Wire Conenction 1',
|
||||
url: 'https://test.local:1234',
|
||||
},
|
||||
{
|
||||
type: 'IPv4 (LAN)',
|
||||
description: '',
|
||||
gateway: 'Wire Connction 1',
|
||||
url: 'https://192.168.1.10.local:1234',
|
||||
},
|
||||
]
|
||||
const uncommon: Address[] = [
|
||||
{
|
||||
type: 'IPv4 (WAN)',
|
||||
description: '',
|
||||
gateway: 'Wire Conenction 1',
|
||||
url: 'https://72.72.72.72',
|
||||
},
|
||||
]
|
||||
|
||||
hostnames.forEach(h => {
|
||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||
// hostnames.forEach(h => {
|
||||
// const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||
|
||||
addresses.forEach(url => {
|
||||
if (h.kind === 'onion') {
|
||||
tor.push({
|
||||
protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)
|
||||
? new URL(url).protocol.replace(':', '').toUpperCase()
|
||||
: null,
|
||||
url,
|
||||
})
|
||||
} else {
|
||||
const hostnameKind = h.hostname.kind
|
||||
// addresses.forEach(url => {
|
||||
// if (h.kind === 'onion') {
|
||||
// tor.push({
|
||||
// protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)
|
||||
// ? new URL(url).protocol.replace(':', '').toUpperCase()
|
||||
// : null,
|
||||
// url,
|
||||
// })
|
||||
// } else {
|
||||
// const hostnameKind = h.hostname.kind
|
||||
|
||||
if (
|
||||
h.public ||
|
||||
(hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public)
|
||||
) {
|
||||
clearnet.push({
|
||||
url,
|
||||
disabled: !h.public,
|
||||
isDomain: hostnameKind == 'domain',
|
||||
authority:
|
||||
hostnameKind == 'domain'
|
||||
? host.domains[h.hostname.domain]?.acme || null
|
||||
: null,
|
||||
})
|
||||
} else {
|
||||
local.push({
|
||||
nid:
|
||||
hostnameKind === 'local'
|
||||
? 'Local'
|
||||
: `${h.gatewayId} (${hostnameKind})`,
|
||||
url,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// if (
|
||||
// h.public ||
|
||||
// (hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public)
|
||||
// ) {
|
||||
// clearnet.push({
|
||||
// url,
|
||||
// disabled: !h.public,
|
||||
// isDomain: hostnameKind == 'domain',
|
||||
// authority:
|
||||
// hostnameKind == 'domain'
|
||||
// ? host.domains[h.hostname.domain]?.acme || null
|
||||
// : null,
|
||||
// })
|
||||
// } else {
|
||||
// local.push({
|
||||
// nid:
|
||||
// hostnameKind === 'local'
|
||||
// ? 'Local'
|
||||
// : `${h.gatewayId} (${hostnameKind})`,
|
||||
// url,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
return {
|
||||
clearnet: clearnet.filter(
|
||||
common: common.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex(t => t.url === value.url),
|
||||
),
|
||||
local: local.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex(t => t.url === value.url),
|
||||
),
|
||||
tor: tor.filter(
|
||||
uncommon: uncommon.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex(t => t.url === value.url),
|
||||
),
|
||||
@@ -107,28 +112,28 @@ export function getAddresses(
|
||||
}
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
addSsl?: T.AddSslOptions | null
|
||||
public: boolean
|
||||
gateways: {
|
||||
id: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
torDomains: string[]
|
||||
clearnetDomains: ClearnetDomain[]
|
||||
addresses: {
|
||||
clearnet: ClearnetAddress[]
|
||||
local: LocalAddress[]
|
||||
tor: TorAddress[]
|
||||
common: Address[]
|
||||
uncommon: Address[]
|
||||
}
|
||||
}
|
||||
|
||||
export type ClearnetAddress = {
|
||||
url: string
|
||||
export type ClearnetDomain = {
|
||||
fqdn: string
|
||||
authority: string | null
|
||||
isDomain: boolean
|
||||
disabled: boolean
|
||||
public: boolean
|
||||
}
|
||||
|
||||
export type LocalAddress = {
|
||||
export type Address = {
|
||||
type: string
|
||||
gateway: string
|
||||
url: string
|
||||
nid: string
|
||||
}
|
||||
|
||||
export type TorAddress = {
|
||||
url: string
|
||||
protocol: string | null
|
||||
description: string
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { TuiIcon, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { LocalAddress } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'section[local]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Local' | i18n }}
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
{{
|
||||
'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.'
|
||||
| i18n
|
||||
}}
|
||||
<a tuiLink docsLink path="/user-manual/connecting-locally.html">
|
||||
{{ 'Learn More' | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</header>
|
||||
<table [appTable]="['Network Interface', 'URL', null]">
|
||||
@for (address of local(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="12">{{ address.nid }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td actions [href]="address.url" [disabled]="!isRunning()"></td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TableComponent,
|
||||
InterfaceActionsComponent,
|
||||
MaskPipe,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceLocalComponent {
|
||||
readonly local = input.required<readonly LocalAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'interface-status',
|
||||
template: `
|
||||
<tui-badge
|
||||
size="l"
|
||||
[iconStart]="public() ? '@tui.globe' : '@tui.lock'"
|
||||
[appearance]="public() ? 'positive' : 'negative'"
|
||||
>
|
||||
{{ public() ? ('Public' | i18n) : ('Private' | i18n) }}
|
||||
</tui-badge>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiBadge, i18nPipe],
|
||||
})
|
||||
export class InterfaceStatusComponent {
|
||||
readonly public = input(false)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
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 { TuiAppearance, TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'section[torDomains]',
|
||||
template: `
|
||||
<header>
|
||||
<!-- @TODO translation -->
|
||||
Tor Domains
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/tor.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
@if (torDomains().length) {
|
||||
<table [appTable]="['Domain', null]">
|
||||
@for (domain of torDomains(); track $index) {
|
||||
<tr>
|
||||
<td>{{ domain }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove(domain)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
{{ 'No Tor domains' | i18n }}
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiLink,
|
||||
TuiAppearance,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceTorDomainsComponent {
|
||||
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 torDomains = input.required<readonly string[]>()
|
||||
|
||||
async remove(domain: string) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { onion: domain }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async add() {
|
||||
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
|
||||
label: 'New Tor domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: this.i18n.transform('Private Key (optional)')!,
|
||||
description: this.i18n.transform(
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.',
|
||||
),
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
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 {
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiLink,
|
||||
TuiOption,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiTooltip } from '@taiga-ui/kit'
|
||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { InterfaceActionsComponent } from './actions.component'
|
||||
import { TorAddress } from './interface.utils'
|
||||
import { MaskPipe } from './mask.pipe'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'section[tor]',
|
||||
template: `
|
||||
<header>
|
||||
Tor
|
||||
<tui-icon [tuiTooltip]="tooltip" />
|
||||
<ng-template #tooltip>
|
||||
{{
|
||||
'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.'
|
||||
| i18n
|
||||
}}
|
||||
<a tuiLink docsLink path="/user-manual/connecting-remotely/tor.html">
|
||||
{{ 'Learn More' | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
@if (tor().length) {
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
@if (tor().length) {
|
||||
<table [appTable]="['Protocol', 'URL', null]">
|
||||
@for (address of tor(); track $index) {
|
||||
<tr>
|
||||
<td [style.width.rem]="12">{{ address.protocol || '-' }}</td>
|
||||
<td>{{ address.url | mask }}</td>
|
||||
<td actions [href]="address.url" [disabled]="!isRunning()">
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="flat-grayscale"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiOption
|
||||
tuiAppearance="action-destructive"
|
||||
iconStart="@tui.trash"
|
||||
(click)="remove(address)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
} @else {
|
||||
<app-placeholder icon="@tui.app-window">
|
||||
{{ 'No onion addresses' | i18n }}
|
||||
<button tuiButton iconStart="@tui.plus" (click)="add()">
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</app-placeholder>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
[tuiFade] {
|
||||
white-space: nowrap;
|
||||
max-width: 30rem;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiIcon,
|
||||
TuiTooltip,
|
||||
TuiLink,
|
||||
TuiAppearance,
|
||||
TuiOption,
|
||||
TableComponent,
|
||||
PlaceholderComponent,
|
||||
MaskPipe,
|
||||
InterfaceActionsComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceTorComponent {
|
||||
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 tor = input.required<readonly TorAddress[]>()
|
||||
readonly isRunning = input.required<boolean>()
|
||||
|
||||
async remove({ url }: TorAddress) {
|
||||
const confirm = await firstValueFrom(
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
content: 'Are you sure you want to delete this address?',
|
||||
},
|
||||
})
|
||||
.pipe(defaultIfEmpty(false)),
|
||||
)
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { onion: new URL(url).hostname }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async add() {
|
||||
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
|
||||
label: 'New onion address',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: this.i18n.transform('Private Key (optional)')!,
|
||||
description: this.i18n.transform(
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
||||
),
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
handler: async value => this.save(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(form: OnionForm): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
let onion = form.key
|
||||
? await this.api.addTorKey({ key: form.key })
|
||||
: await this.api.generateTorKey({})
|
||||
onion = `${onion}.onion`
|
||||
|
||||
if (this.interface.packageId) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value().addressInfo.hostId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiIcon } from '@taiga-ui/core'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
@@ -21,13 +21,6 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
<td>
|
||||
<tui-badge size="m" [appearance]="appearance">{{ info.type }}</tui-badge>
|
||||
</td>
|
||||
<td [style.text-align]="'center'">
|
||||
@if (info.public) {
|
||||
<tui-icon class="g-positive" icon="@tui.globe" />
|
||||
} @else {
|
||||
<tui-icon class="g-negative" icon="@tui.lock" />
|
||||
}
|
||||
</td>
|
||||
<td class="g-secondary" [style.grid-area]="'2 / span 4'">
|
||||
{{ info.description }}
|
||||
</td>
|
||||
@@ -86,7 +79,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, TuiBadge, TuiIcon, RouterLink],
|
||||
imports: [TuiButton, TuiBadge, RouterLink],
|
||||
})
|
||||
export class ServiceInterfaceItemComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
@@ -94,7 +87,6 @@ export class ServiceInterfaceItemComponent {
|
||||
|
||||
@Input({ required: true })
|
||||
info!: T.ServiceInterface & {
|
||||
public: boolean
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
|
||||
@@ -2,27 +2,23 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiTable } from '@taiga-ui/addon-table'
|
||||
import { tuiDefaultSort } from '@taiga-ui/cdk'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||
import { ServiceInterfaceItemComponent } from './interface-item.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'service-interfaces',
|
||||
template: `
|
||||
<header>{{ 'Interfaces' | i18n }}</header>
|
||||
<header>{{ 'Service Interfaces' | i18n }}</header>
|
||||
<table tuiTable class="g-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th tuiTh>{{ 'Name' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Type' | i18n }}</th>
|
||||
<th tuiTh [style.text-align]="'center'">{{ 'Hosting' | i18n }}</th>
|
||||
<th tuiTh>{{ 'Description' | i18n }}</th>
|
||||
<th tuiTh></th>
|
||||
</tr>
|
||||
@@ -49,8 +45,6 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
imports: [ServiceInterfaceItemComponent, TuiTable, i18nPipe],
|
||||
})
|
||||
export class ServiceInterfacesComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
readonly pkg = input.required<PackageDataEntry>()
|
||||
readonly disabled = input(false)
|
||||
|
||||
@@ -58,14 +52,8 @@ export class ServiceInterfacesComponent {
|
||||
Object.entries(serviceInterfaces)
|
||||
.sort((a, b) => tuiDefaultSort(a[1], b[1]))
|
||||
.map(([id, value]) => {
|
||||
const host = hosts[value.addressInfo.hostId]
|
||||
const port = value.addressInfo.internalPort
|
||||
|
||||
return {
|
||||
...value,
|
||||
addSsl: host?.bindings[port]?.options.addSsl,
|
||||
public: !!host?.bindings[port]?.net.public,
|
||||
addresses: host ? getAddresses(value, host, this.config) : {},
|
||||
routerLink: `./interface/${id}`,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,6 @@ import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
@@ -28,10 +27,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ interface()?.name }}
|
||||
<interface-status
|
||||
[style.margin-left.rem]="0.5"
|
||||
[public]="!!interface()?.public"
|
||||
/>
|
||||
</ng-container>
|
||||
<tui-breadcrumbs size="l">
|
||||
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
|
||||
@@ -47,12 +42,11 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
<tui-badge size="l" [appearance]="getAppearance(value.type)">
|
||||
{{ value.type }}
|
||||
</tui-badge>
|
||||
<interface-status [public]="value.public" />
|
||||
</h3>
|
||||
<p tuiSubtitle>{{ value.description }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<app-interface
|
||||
<service-interface
|
||||
[packageId]="pkgId"
|
||||
[value]="value"
|
||||
[isRunning]="isRunning()"
|
||||
@@ -86,7 +80,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
TuiBreadcrumbs,
|
||||
TuiItem,
|
||||
TuiLink,
|
||||
InterfaceStatusComponent,
|
||||
i18nPipe,
|
||||
TuiBadge,
|
||||
TuiHeader,
|
||||
@@ -127,9 +120,10 @@ export default class ServiceInterfaceRoute {
|
||||
|
||||
return {
|
||||
...item,
|
||||
addSsl: host?.bindings[port]?.options.addSsl,
|
||||
public: !!host?.bindings[port]?.net.public,
|
||||
addresses: getAddresses(item, host, this.config),
|
||||
gateways: [],
|
||||
torDomains: [],
|
||||
clearnetDomains: [],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { AuthorityService } from './authority.service'
|
||||
import { AuthoritiesTableComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ 'Certificate Authorities' | i18n }}
|
||||
</ng-container>
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Certificate Authorities' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/authorities.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
@if (authorityService.authorities(); as authorities) {
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="authorityService.add(authorities)"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
<authorities-table />
|
||||
</section>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiLink,
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
AuthoritiesTableComponent,
|
||||
],
|
||||
providers: [AuthorityService],
|
||||
})
|
||||
export default class SystemAuthoritiesComponent {
|
||||
protected readonly authorityService = inject(AuthorityService)
|
||||
}
|
||||
@@ -14,7 +14,6 @@ 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 { parse } from 'tldts'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { DNS } from './dns.component'
|
||||
@@ -29,10 +28,6 @@ export type MappedDomain = {
|
||||
name: string | null
|
||||
ipInfo: T.IpInfo | null
|
||||
}
|
||||
authority: {
|
||||
url: string | null
|
||||
name: string | null
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -64,19 +59,8 @@ export class DomainService {
|
||||
id: gateway,
|
||||
ipInfo: gateways[gateway]?.ipInfo || null,
|
||||
},
|
||||
authority: {
|
||||
url: acme,
|
||||
name: toAuthorityName(acme),
|
||||
},
|
||||
}) as MappedDomain,
|
||||
),
|
||||
authorities: Object.keys(acme).reduce<Record<string, string>>(
|
||||
(obj, url) => ({
|
||||
...obj,
|
||||
[url]: toAuthorityName(url),
|
||||
}),
|
||||
{ local: toAuthorityName(null) },
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -91,7 +75,7 @@ export class DomainService {
|
||||
default: null,
|
||||
patterns: [utils.Patterns.domain],
|
||||
}),
|
||||
...this.gatewaysAndAuthorities(),
|
||||
...this.gatewaysSpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -105,7 +89,6 @@ export class DomainService {
|
||||
this.save({
|
||||
fqdn: input.fqdn,
|
||||
gateway: input.gateway,
|
||||
acme: input.authority === 'local' ? null : input.authority,
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -115,7 +98,7 @@ export class DomainService {
|
||||
|
||||
async edit(domain: MappedDomain) {
|
||||
const editSpec = ISB.InputSpec.of({
|
||||
...this.gatewaysAndAuthorities(),
|
||||
...this.gatewaysSpec(),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
@@ -129,13 +112,11 @@ export class DomainService {
|
||||
this.save({
|
||||
fqdn: domain.fqdn,
|
||||
gateway: input.gateway,
|
||||
acme: input.authority === 'local' ? null : input.authority,
|
||||
}),
|
||||
},
|
||||
],
|
||||
value: {
|
||||
gateway: domain.gateway.id,
|
||||
authority: domain.authority.url || 'local',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -178,22 +159,14 @@ export class DomainService {
|
||||
}
|
||||
}
|
||||
|
||||
private gatewaysAndAuthorities() {
|
||||
private gatewaysSpec() {
|
||||
return {
|
||||
gateway: ISB.Value.select({
|
||||
name: 'Gateway',
|
||||
description:
|
||||
'Select the public gateway for this domain. Whichever gateway you select is the IP address that will be exposed to the Internet.',
|
||||
description: 'Select which gateway to use for this domain.',
|
||||
values: this.data()!.gateways,
|
||||
default: '',
|
||||
}),
|
||||
authority: ISB.Value.select({
|
||||
name: 'Default Certificate Authority',
|
||||
description:
|
||||
'Select the default certificate authority that will sign certificates for this domain. You can override this on a case-by-case basis.',
|
||||
values: this.data()!.authorities,
|
||||
default: '',
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { AuthorityService } from './authorities/authority.service'
|
||||
import { DomainService } from './domains/domain.service'
|
||||
import { DomainsTableComponent } from './domains/table.component'
|
||||
import { AuthoritiesTableComponent } from './authorities/table.component'
|
||||
import { DomainService } from './domain.service'
|
||||
import { DomainsTableComponent } from './table.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -17,48 +14,18 @@ import { AuthoritiesTableComponent } from './authorities/table.component'
|
||||
</a>
|
||||
{{ 'Domains' | i18n }}
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'Domains' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{
|
||||
'Adding a domain to StartOS means you can use it and its subdomains to host service interfaces on the public Internet.'
|
||||
| i18n
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/domains.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Certificate Authorities' | i18n }}
|
||||
@if (authorityService.authorities(); as authorities) {
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="authorityService.add(authorities)"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
<authorities-table />
|
||||
</section>
|
||||
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Domains' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/domains.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
@if (domainService.data(); as value) {
|
||||
<button
|
||||
tuiButton
|
||||
@@ -77,19 +44,15 @@ import { AuthoritiesTableComponent } from './authorities/table.component'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
TuiButton,
|
||||
TuiTitle,
|
||||
TuiHeader,
|
||||
TuiLink,
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
DomainsTableComponent,
|
||||
AuthoritiesTableComponent,
|
||||
],
|
||||
providers: [AuthorityService, DomainService],
|
||||
providers: [DomainService],
|
||||
})
|
||||
export default class SystemDomainsComponent {
|
||||
protected readonly authorityService = inject(AuthorityService)
|
||||
protected readonly domainService = inject(DomainService)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { DomainService, MappedDomain } from './domain.service'
|
||||
@if (domain(); as domain) {
|
||||
<td>{{ domain.fqdn }}</td>
|
||||
<td [style.order]="-1">{{ domain.gateway.ipInfo?.name || '-' }}</td>
|
||||
<td>{{ domain.authority.name }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -9,9 +9,7 @@ import { DomainService } from './domain.service'
|
||||
@Component({
|
||||
selector: 'domains-table',
|
||||
template: `
|
||||
<table
|
||||
[appTable]="['Domain', 'Gateway', 'Default Certificate Authority', null]"
|
||||
>
|
||||
<table [appTable]="['Domain', 'Gateway', null]">
|
||||
@for (domain of domainService.data()?.domains; track $index) {
|
||||
<tr [domain]="domain"></tr>
|
||||
} @empty {
|
||||
@@ -31,31 +31,21 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
</a>
|
||||
{{ 'Email' | i18n }}
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'Email' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{
|
||||
'Connecting an external SMTP server allows StartOS and your installed services to send you emails.'
|
||||
| i18n
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/smtp.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
@if (form$ | async; as form) {
|
||||
<form [formGroup]="form">
|
||||
<header tuiHeader="body-l">
|
||||
<h3 tuiTitle>
|
||||
<b>{{ 'SMTP Credentials' | i18n }}</b>
|
||||
<b>
|
||||
{{ 'SMTP Credentials' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/smtp.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
</b>
|
||||
</h3>
|
||||
</header>
|
||||
@if (spec | async; as resolved) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GatewaysTableComponent } from './table.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { map } from 'rxjs'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
import { GatewayPlus } from './item.component'
|
||||
@@ -28,30 +27,18 @@ import { GatewayPlus } from './item.component'
|
||||
</a>
|
||||
{{ 'Gateways' | i18n }}
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'Gateways' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{
|
||||
'Gateways connect your server to the Internet. They process outbound traffic, and under certain conditions, they also permit inbound traffic.'
|
||||
| i18n
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/gateways.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<section class="g-card">
|
||||
<header>
|
||||
{{ 'Gateways' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/gateways.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
@@ -70,7 +57,6 @@ import { GatewayPlus } from './item.component'
|
||||
CommonModule,
|
||||
TuiButton,
|
||||
GatewaysTableComponent,
|
||||
TuiHeader,
|
||||
TitleDirective,
|
||||
i18nPipe,
|
||||
TuiLink,
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
TuiButtonSelect,
|
||||
TuiDataListWrapper,
|
||||
} from '@taiga-ui/kit'
|
||||
import { TuiCell, tuiCellOptionsProvider, TuiHeader } from '@taiga-ui/layout'
|
||||
import { TuiCell, tuiCellOptionsProvider } from '@taiga-ui/layout'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter } from 'rxjs'
|
||||
@@ -59,14 +59,6 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
</a>
|
||||
{{ 'General Settings' | i18n }}
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'General Settings' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{ 'Manage your overall setup and preferences' | i18n }}
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
@if (server(); as server) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.zap" />
|
||||
@@ -138,16 +130,6 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.award" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Root Certificate Authority' | i18n }}</strong>
|
||||
<span tuiSubtitle>{{ 'Download your Root CA' | i18n }}</span>
|
||||
</span>
|
||||
<button tuiButton iconStart="@tui.download" (click)="downloadCA()">
|
||||
{{ 'Download' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.monitor" />
|
||||
<span tuiTitle>
|
||||
@@ -205,8 +187,6 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
src="assets/img/icons/snek.png"
|
||||
/>
|
||||
}
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a id="download-ca" href="/static/local-root-ca.crt"></a>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
@@ -239,7 +219,6 @@ import { SystemWipeComponent } from './wipe.component'
|
||||
RouterLink,
|
||||
i18nPipe,
|
||||
TuiTitle,
|
||||
TuiHeader,
|
||||
TuiCell,
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
@@ -347,10 +326,6 @@ export default class SystemGeneralComponent {
|
||||
.subscribe(() => this.resetTor(this.wipe))
|
||||
}
|
||||
|
||||
downloadCA() {
|
||||
this.document.getElementById('download-ca')?.click()
|
||||
}
|
||||
|
||||
async tryToggleKiosk() {
|
||||
if (
|
||||
this.server()?.kiosk &&
|
||||
|
||||
@@ -31,13 +31,10 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'Change Password' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{ 'Change your StartOS master password.' | i18n }}
|
||||
<strong>
|
||||
{{
|
||||
'You will still need your current password to decrypt existing backups!'
|
||||
| i18n
|
||||
}}
|
||||
</strong>
|
||||
{{
|
||||
'You will still need your current password to decrypt existing backups!'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { from, map, merge, Observable, Subject } from 'rxjs'
|
||||
import { Session } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -21,17 +20,7 @@ import { SessionsTableComponent } from './table.component'
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||
{{ 'Active Sessions' | i18n }}
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ 'Active Sessions' | i18n }}</h3>
|
||||
<p tuiSubtitle>
|
||||
{{
|
||||
'A session is a device that is currently logged into StartOS. For best security, terminate sessions you do not recognize or no longer use.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<section class="g-card">
|
||||
<header>{{ 'Current session' | i18n }}</header>
|
||||
<div [single]="true" [sessions]="current$ | async"></div>
|
||||
@@ -62,8 +51,6 @@ import { SessionsTableComponent } from './table.component'
|
||||
SessionsTableComponent,
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -14,8 +14,7 @@ import {
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiHeader } from '@taiga-ui/layout'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { filter, from, merge, Subject } from 'rxjs'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { SSHKey } from 'src/app/services/api/api.types'
|
||||
@@ -33,30 +32,18 @@ import { SSHTableComponent } from './table.component'
|
||||
</a>
|
||||
SSH
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>SSH</h3>
|
||||
<p tuiSubtitle>
|
||||
{{
|
||||
'By default, you can SSH into your server from any device using your master password. Optionally add SSH public keys to grant specific devices access without needing to enter a password.'
|
||||
| i18n
|
||||
}}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/ssh.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
[textContent]="'View instructions' | i18n"
|
||||
></a>
|
||||
</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
@let keys = keys$ | async;
|
||||
<section class="g-card">
|
||||
<header>
|
||||
Saved Keys
|
||||
{{ 'SSH Keys' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
docsLink
|
||||
path="/user-manual/ssh.html"
|
||||
appearance="action-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
[pseudo]="true"
|
||||
></a>
|
||||
<button
|
||||
tuiButton
|
||||
size="xs"
|
||||
@@ -95,8 +82,6 @@ import { SSHTableComponent } from './table.component'
|
||||
SSHTableComponent,
|
||||
RouterLink,
|
||||
TitleDirective,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
TuiLink,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
@@ -118,7 +103,7 @@ export default class SystemSSHComponent {
|
||||
|
||||
async add(all: readonly SSHKey[]) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add SSH Public Key',
|
||||
label: 'Add SSH key',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(SSHSpec),
|
||||
buttons: [
|
||||
|
||||
@@ -47,7 +47,7 @@ import { SSHKey } from 'src/app/services/api/api.types'
|
||||
} @empty {
|
||||
@if (keys()) {
|
||||
<tr>
|
||||
<td colspan="5">{{ 'No keys' | i18n }}</td>
|
||||
<td colspan="5">{{ 'No SSH keys' | i18n }}</td>
|
||||
</tr>
|
||||
} @else {
|
||||
@for (i of ['', '']; track $index) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
@@ -26,19 +25,17 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
{{ 'Back' | i18n }}
|
||||
</a>
|
||||
{{ iface.name }}
|
||||
<interface-status [style.margin-left.rem]="0.5" [public]="public()" />
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>
|
||||
{{ iface.name }}
|
||||
<interface-status [public]="public()" />
|
||||
</h3>
|
||||
<p tuiSubtitle>{{ iface.description }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
@if (ui(); as ui) {
|
||||
<app-interface [value]="ui" [isRunning]="true" />
|
||||
<service-interface [value]="ui" [isRunning]="true" />
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
@@ -50,7 +47,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
TitleDirective,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
InterfaceStatusComponent,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
@@ -81,17 +77,14 @@ export default class StartOsUiComponent {
|
||||
.watch$('serverInfo', 'network', 'host')
|
||||
.pipe(
|
||||
map(host => {
|
||||
const port = this.iface.addressInfo.internalPort
|
||||
|
||||
return {
|
||||
...this.iface,
|
||||
addSsl: host.bindings[port]?.options.addSsl,
|
||||
public: !!host.bindings[port]?.net.public,
|
||||
addresses: getAddresses(this.iface, host, this.config),
|
||||
gateways: [],
|
||||
torDomains: [],
|
||||
clearnetDomains: [],
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
readonly public = computed((ui = this.ui()) => !!ui?.public)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { map } from 'rxjs'
|
||||
<span tuiTitle>
|
||||
<span>
|
||||
{{ page.item | i18n }}
|
||||
@if (page.item === 'General' && badge()) {
|
||||
@if (page.item === 'General Settings' && badge()) {
|
||||
<tui-badge-notification>{{ badge() }}</tui-badge-notification>
|
||||
}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { i18nKey } from '@start9labs/shared'
|
||||
|
||||
export const SYSTEM_MENU = [
|
||||
[
|
||||
{
|
||||
icon: '@tui.settings',
|
||||
item: 'General',
|
||||
icon: '@tui.wrench',
|
||||
item: 'General Settings',
|
||||
link: 'general',
|
||||
},
|
||||
],
|
||||
@@ -43,6 +41,11 @@ export const SYSTEM_MENU = [
|
||||
item: 'Gateways',
|
||||
link: 'gateways',
|
||||
},
|
||||
{
|
||||
icon: '@tui.award',
|
||||
item: 'Certificate Authorities',
|
||||
link: 'authorities',
|
||||
},
|
||||
{
|
||||
icon: '@tui.globe',
|
||||
item: 'Domains',
|
||||
@@ -57,7 +60,7 @@ export const SYSTEM_MENU = [
|
||||
},
|
||||
{
|
||||
icon: '@tui.terminal',
|
||||
item: 'SSH' as i18nKey,
|
||||
item: 'SSH Keys',
|
||||
link: 'ssh',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,6 +71,12 @@ export default [
|
||||
path: 'gateways',
|
||||
loadComponent: () => import('./routes/gateways/gateways.component'),
|
||||
},
|
||||
{
|
||||
path: 'authorities',
|
||||
title: titleResolver,
|
||||
loadComponent: () =>
|
||||
import('./routes/authorities/authorities.component'),
|
||||
},
|
||||
{
|
||||
path: 'domains',
|
||||
title: titleResolver,
|
||||
|
||||
@@ -239,7 +239,6 @@ export namespace RR {
|
||||
export type AddDomainReq = {
|
||||
fqdn: string
|
||||
gateway: string
|
||||
acme: string | null
|
||||
} // net.domain.add
|
||||
export type AddDomainRes = null
|
||||
|
||||
@@ -251,7 +250,7 @@ export namespace RR {
|
||||
export type TestDomainReq = {
|
||||
fqdn: string
|
||||
gateway: string
|
||||
} // net.domain.test
|
||||
} // net.domain.test-dns
|
||||
export type TestDomainRes = {
|
||||
root: boolean
|
||||
wildcard: boolean
|
||||
@@ -293,12 +292,13 @@ export namespace RR {
|
||||
export type GenerateTorKeyReq = {} // net.tor.key.generate
|
||||
export type AddTorKeyRes = string // onion address without .onion suffix
|
||||
|
||||
export type ServerBindingSetPublicReq = {
|
||||
// server.host.binding.set-public
|
||||
internalPort: number
|
||||
public: boolean | null // default true
|
||||
export type ServerBindingToggleGatewayReq = {
|
||||
// server.host.binding.set-gateway-enabled
|
||||
gateway: T.GatewayId
|
||||
internalPort: 80
|
||||
enabled: boolean
|
||||
}
|
||||
export type BindingSetPublicRes = null
|
||||
export type ServerBindingToggleGatewayRes = null
|
||||
|
||||
export type ServerAddOnionReq = {
|
||||
// server.host.address.onion.add
|
||||
@@ -311,23 +311,25 @@ export namespace RR {
|
||||
|
||||
export type OsUiAddDomainReq = {
|
||||
// server.host.address.domain.add
|
||||
domain: string // FQDN
|
||||
fqdn: string // FQDN
|
||||
private: boolean
|
||||
acme: string | null // Url | null
|
||||
acme: string | null // URL. null means local Root CA
|
||||
}
|
||||
export type OsUiAddDomainRes = null
|
||||
|
||||
export type OsUiRemoveDomainReq = {
|
||||
// server.host.address.domain.remove
|
||||
domain: string // FQDN
|
||||
fqdn: string // FQDN
|
||||
}
|
||||
export type OsUiRemoveDomainRes = null
|
||||
|
||||
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
|
||||
// package.host.binding.set-public
|
||||
export type PkgBindingToggleGatewayReq = ServerBindingToggleGatewayReq & {
|
||||
// package.host.binding.set-gateway-enabled
|
||||
internalPort: number
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgBindingToggleGatewayRes = null
|
||||
|
||||
export type PkgAddOnionReq = ServerAddOnionReq & {
|
||||
// package.host.address.onion.add
|
||||
|
||||
@@ -359,9 +359,9 @@ export abstract class ApiService {
|
||||
params: RR.GenerateTorKeyReq,
|
||||
): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
abstract serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes>
|
||||
|
||||
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
@@ -377,9 +377,9 @@ export abstract class ApiService {
|
||||
params: RR.OsUiRemoveDomainReq,
|
||||
): Promise<RR.OsUiRemoveDomainRes>
|
||||
|
||||
abstract pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
abstract pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes>
|
||||
|
||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
async testDomain(params: RR.TestDomainReq): Promise<RR.TestDomainRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.test', params })
|
||||
return this.rpcRequest({ method: 'net.domain.test-dns', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
@@ -638,11 +638,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.binding.set-public',
|
||||
method: 'server.host.binding.set-gateway-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -681,11 +681,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-public',
|
||||
method: 'package.host.binding.set-gateway-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -613,7 +613,6 @@ export class MockApiService extends ApiService {
|
||||
value: {
|
||||
[params.fqdn]: {
|
||||
gateway: params.gateway,
|
||||
acme: params.acme,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1369,16 +1368,16 @@ export class MockApiService extends ApiService {
|
||||
return 'abcdefghijklmnopqrstuv'
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/host/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
|
||||
value: params.enabled ? [params.gateway] : [],
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1443,7 +1442,7 @@ export class MockApiService extends ApiService {
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
[params.fqdn]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1455,7 +1454,7 @@ export class MockApiService extends ApiService {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
domain: params.fqdn,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
@@ -1476,7 +1475,7 @@ export class MockApiService extends ApiService {
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/domains/${params.domain}`,
|
||||
path: `/serverInfo/host/domains/${params.fqdn}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
@@ -1488,16 +1487,16 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
|
||||
value: params.enabled ? [] : [params.gateway],
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1560,7 +1559,7 @@ export class MockApiService extends ApiService {
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
[params.fqdn]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1572,7 +1571,7 @@ export class MockApiService extends ApiService {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
domain: params.fqdn,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
@@ -1593,7 +1592,7 @@ export class MockApiService extends ApiService {
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.fqdn}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
|
||||
Reference in New Issue
Block a user