mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
proxies (#2376)
* proxies * OS outbound proxy. ugly, needs work * abstract interface address management * clearnet and outbound proxies for services * clean up * router tab * smart launching of UIs * update sdk types * display outbound proxy on service show and rework menu
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
import { AppComponent } from './app.component'
|
||||
import { RoutingModule } from './routing.module'
|
||||
import { OSWelcomePageModule } from './common/os-welcome/os-welcome.module'
|
||||
import { QRComponentModule } from './common/qr/qr.module'
|
||||
import { PreloaderModule } from './app/preloader/preloader.module'
|
||||
import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
@@ -70,6 +71,7 @@ import { environment } from '../environments/environment'
|
||||
registrationStrategy: 'registerWhenStable:30000',
|
||||
}),
|
||||
LoadingModule,
|
||||
QRComponentModule,
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -69,6 +69,7 @@ const ICONS = [
|
||||
'pulse',
|
||||
'push-outline',
|
||||
'qr-code-outline',
|
||||
'radio-outline',
|
||||
'receipt-outline',
|
||||
'refresh',
|
||||
'reload',
|
||||
@@ -81,7 +82,8 @@ const ICONS = [
|
||||
'save-outline',
|
||||
'server-outline',
|
||||
'settings-outline',
|
||||
'shield-checkmark-outline',
|
||||
'shield-outline',
|
||||
'shuffle-outline',
|
||||
'stop-outline',
|
||||
'stopwatch-outline',
|
||||
'storefront-outline',
|
||||
|
||||
@@ -74,6 +74,6 @@ export class TargetSelectPage {
|
||||
styleUrls: ['./target-select.page.scss'],
|
||||
})
|
||||
export class TargetStatusComponent {
|
||||
@Input() type!: BackupType
|
||||
@Input() target!: BackupTarget
|
||||
@Input({ required: true }) type!: BackupType
|
||||
@Input({ required: true }) target!: BackupTarget
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const googleDriveSpec = Config.of({
|
||||
name: 'Private Key File',
|
||||
description:
|
||||
'Your Google Drive service account private key file (.json file)',
|
||||
required: true,
|
||||
required: { default: null },
|
||||
extensions: ['json'],
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ export class MarketplaceShowControlsComponent {
|
||||
@Input()
|
||||
url?: string
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
@Input()
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MarketplaceShowDependentComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
readonly dependentInfo?: DependentInfo =
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
styleUrls: ['marketplace-status.component.scss'],
|
||||
})
|
||||
export class MarketplaceStatusComponent {
|
||||
@Input() version!: string
|
||||
|
||||
@Input({ required: true }) version!: string
|
||||
@Input() localPkg?: PackageDataEntry
|
||||
|
||||
PackageState = PackageState
|
||||
|
||||
@@ -188,7 +188,7 @@ interface LocalAction {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppActionsItemComponent {
|
||||
@Input() action!: LocalAction
|
||||
@Input({ required: true }) action!: LocalAction
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppInterfacePage } from './app-interface.page'
|
||||
import { InterfaceAddressesComponentModule } from 'src/app/common/interface-addresses/interface-addresses.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppInterfacePage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
InterfaceAddressesComponentModule,
|
||||
],
|
||||
declarations: [AppInterfacePage],
|
||||
})
|
||||
export class AppInterfacePageModule {}
|
||||
@@ -0,0 +1,20 @@
|
||||
<ng-container *ngIf="interfaceInfo$ | async as interfaceInfo">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ interfaceInfo.name }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="with-widgets">
|
||||
<div class="cap-width">
|
||||
<interface-addresses
|
||||
[packageContext]="{ packageId: pkgId, interfaceId }"
|
||||
[addressInfo]="interfaceInfo.addressInfo"
|
||||
[isUi]="interfaceInfo.type === 'ui'"
|
||||
></interface-addresses>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-interface',
|
||||
templateUrl: './app-interface.page.html',
|
||||
styleUrls: ['./app-interface.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppInterfacePage {
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
readonly interfaceId = this.route.snapshot.paramMap.get('interfaceId')!
|
||||
|
||||
readonly interfaceInfo$ = this.patch.watch$(
|
||||
'package-data',
|
||||
this.pkgId,
|
||||
'installed',
|
||||
'interfaceInfo',
|
||||
this.interfaceId,
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<ion-item>
|
||||
<ion-icon
|
||||
slot="start"
|
||||
size="large"
|
||||
[name]="addressInfo.ui ? 'desktop-outline' : 'terminal-outline'"
|
||||
></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ addressInfo.name }}</h1>
|
||||
<h2>{{ addressInfo.description }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div style="padding-left: 64px">
|
||||
<ion-item *ngFor="let address of addressInfo.addresses">
|
||||
<ion-label>
|
||||
<h2>{{ address | addressType }}</h2>
|
||||
<p>{{ address }}</p>
|
||||
</ion-label>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="addressInfo.ui" fill="clear" (click)="launch(address)">
|
||||
<ion-icon size="small" slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="showQR(address)">
|
||||
<ion-icon
|
||||
size="small"
|
||||
slot="icon-only"
|
||||
name="qr-code-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(address)">
|
||||
<ion-icon size="small" slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-item>
|
||||
</div>
|
||||
@@ -1,32 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
import {
|
||||
AppInterfacesItemComponent,
|
||||
AppInterfacesPage,
|
||||
} from './app-interfaces.page'
|
||||
import { UiPipesModule } from '../ui-pipes/ui.module'
|
||||
import { QRComponent } from './qr.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppInterfacesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
UiPipesModule,
|
||||
QrCodeModule,
|
||||
],
|
||||
declarations: [AppInterfacesPage, AppInterfacesItemComponent, QRComponent],
|
||||
})
|
||||
export class AppInterfacesPageModule {}
|
||||
@@ -1,19 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Interfaces</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<ion-item-group>
|
||||
<div
|
||||
*ngFor="let addressInfo of (addressInfo$ | async)"
|
||||
style="margin-bottom: 30px"
|
||||
>
|
||||
<app-interfaces-item [addressInfo]="addressInfo"></app-interfaces-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
p {
|
||||
font-family: 'Courier New';
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { getPkgId, CopyService } from '@start9labs/shared'
|
||||
import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { QRComponent } from './qr.component'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
|
||||
@Component({
|
||||
selector: 'app-interfaces',
|
||||
templateUrl: './app-interfaces.page.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppInterfacesPage {
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
readonly addressInfo$ = this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'address-info')
|
||||
.pipe(
|
||||
map(addressInfo =>
|
||||
Object.values(addressInfo).sort((a, b) => a.name.localeCompare(b.name)),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-interfaces-item',
|
||||
templateUrl: './app-interfaces-item.component.html',
|
||||
styleUrls: ['./app-interfaces.page.scss'],
|
||||
})
|
||||
export class AppInterfacesItemComponent {
|
||||
@Input()
|
||||
addressInfo!: AddressInfo
|
||||
|
||||
constructor(
|
||||
private readonly dialogs: TuiDialogService,
|
||||
readonly copyService: CopyService,
|
||||
) {}
|
||||
|
||||
launch(url: string): void {
|
||||
window.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
showQR(data: string) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(QRComponent), {
|
||||
size: 'auto',
|
||||
data,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppListIconComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PkgInfo
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
@@ -20,19 +20,27 @@
|
||||
></status>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="pkg.entry.installed as installed">
|
||||
<ion-button
|
||||
*ngIf="installed['address-info'] | hasUi"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="openPopover($event)"
|
||||
[disabled]="status !== 'running'"
|
||||
<ng-container
|
||||
*ngIf="installed['interfaceInfo'] | launchableInterfaces as launchable"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<launch-menu
|
||||
#launchMenu
|
||||
[addressInfo]="installed['address-info']"
|
||||
></launch-menu>
|
||||
<ion-button
|
||||
*ngIf="launchable.length"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="
|
||||
launchable.length > 1
|
||||
? openPopover($event)
|
||||
: launchUI(launchable[0].address, $event)
|
||||
"
|
||||
[disabled]="status !== 'running'"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<launch-menu
|
||||
#launchMenu
|
||||
[launchableInterfaces]="launchable"
|
||||
></launch-menu>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { LaunchMenuComponent } from '../../launch-menu/launch-menu.component'
|
||||
import { LaunchMenuComponent } from './launch-menu/launch-menu.component'
|
||||
import { PackageMainStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-pkg',
|
||||
@@ -17,7 +19,7 @@ import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
export class AppListPkgComponent {
|
||||
@ViewChild('launchMenu') launchMenu!: LaunchMenuComponent
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PkgInfo
|
||||
|
||||
get status(): PackageMainStatus {
|
||||
@@ -26,10 +28,18 @@ export class AppListPkgComponent {
|
||||
)
|
||||
}
|
||||
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
openPopover(e: Event): void {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.launchMenu.event = e
|
||||
this.launchMenu.isOpen = true
|
||||
}
|
||||
|
||||
launchUI(address: string, e: Event) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.document.defaultView?.open(address, '_blank', 'noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<ion-popover
|
||||
#popover
|
||||
(didDismiss)="popover.isOpen = false"
|
||||
mode="ios"
|
||||
type="event"
|
||||
>
|
||||
<ng-template>
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
button
|
||||
*ngFor="let iface of launchableInterfaces"
|
||||
detail="false"
|
||||
(click)="launchUI(iface.address)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>{{ iface.name }}</h2>
|
||||
<p>{{ iface.address }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline" size="small"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
</ion-popover>
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { LaunchableInterface } from '../launchable-interfaces.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'launch-menu',
|
||||
@@ -17,8 +18,8 @@ import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
export class LaunchMenuComponent {
|
||||
@ViewChild('popover') popover!: HTMLIonPopoverElement
|
||||
|
||||
@Input()
|
||||
addressInfo!: InstalledPackageInfo['address-info']
|
||||
@Input({ required: true })
|
||||
launchableInterfaces!: LaunchableInterface[]
|
||||
|
||||
set isOpen(open: boolean) {
|
||||
this.popover.isOpen = open
|
||||
@@ -1,12 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { UiPipesModule } from '../ui-pipes/ui.module'
|
||||
import { LaunchMenuComponent } from './launch-menu.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [LaunchMenuComponent],
|
||||
imports: [CommonModule, IonicModule, UiPipesModule],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [LaunchMenuComponent],
|
||||
})
|
||||
export class LaunchMenuComponentModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'launchableInterfaces',
|
||||
})
|
||||
export class LaunchableInterfacesPipe implements PipeTransform {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
transform(
|
||||
interfaceInfo: InstalledPackageInfo['interfaceInfo'],
|
||||
): LaunchableInterface[] {
|
||||
return Object.values(interfaceInfo)
|
||||
.filter(info => info.type === 'ui')
|
||||
.map(info => ({
|
||||
name: info.name,
|
||||
address: this.config.launchableAddress(info),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export type LaunchableInterface = {
|
||||
name: string
|
||||
address: string
|
||||
}
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module'
|
||||
import { WidgetListComponentModule } from 'src/app/common/widget-list/widget-list.component.module'
|
||||
import { StatusComponentModule } from '../status/status.component.module'
|
||||
import { UiPipesModule } from '../ui-pipes/ui.module'
|
||||
import { AppListIconComponent } from './app-list-icon/app-list-icon.component'
|
||||
import { AppListPkgComponent } from './app-list-pkg/app-list-pkg.component'
|
||||
import { PackageInfoPipe } from './package-info.pipe'
|
||||
import { LaunchMenuComponentModule } from '../launch-menu/launch-menu.module'
|
||||
import { LaunchMenuComponentModule } from './app-list-pkg/launch-menu/launch-menu.module'
|
||||
import { LaunchableInterfacesPipe } from './app-list-pkg/launchable-interfaces.pipe'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -31,7 +31,6 @@ const routes: Routes = [
|
||||
StatusComponentModule,
|
||||
EmverPipesModule,
|
||||
TextSpinnerComponentModule,
|
||||
UiPipesModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
BadgeMenuComponentModule,
|
||||
@@ -45,6 +44,7 @@ const routes: Routes = [
|
||||
AppListIconComponent,
|
||||
AppListPkgComponent,
|
||||
PackageInfoPipe,
|
||||
LaunchableInterfacesPipe,
|
||||
],
|
||||
})
|
||||
export class AppListPageModule {}
|
||||
|
||||
@@ -10,21 +10,23 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { StatusComponentModule } from '../status/status.component.module'
|
||||
import { AppConfigPageModule } from './modals/app-config/app-config.module'
|
||||
import { UiPipesModule } from '../ui-pipes/ui.module'
|
||||
import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component'
|
||||
import { AppShowProgressComponent } from './components/app-show-progress/app-show-progress.component'
|
||||
import { AppShowStatusComponent } from './components/app-show-status/app-show-status.component'
|
||||
import { AppShowDependenciesComponent } from './components/app-show-dependencies/app-show-dependencies.component'
|
||||
import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.component'
|
||||
import {
|
||||
AppShowInterfacesComponent,
|
||||
InterfaceInfoPipe,
|
||||
} from './components/app-show-interfaces/app-show-interfaces.component'
|
||||
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
||||
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
import { InsecureWarningComponentModule } from 'src/app/common/insecure-warning/insecure-warning.module'
|
||||
import { LaunchMenuComponentModule } from '../launch-menu/launch-menu.module'
|
||||
import { LaunchMenuComponentModule } from '../app-list/app-list-pkg/launch-menu/launch-menu.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -38,7 +40,6 @@ const routes: Routes = [
|
||||
AppShowPage,
|
||||
HealthColorPipe,
|
||||
ProgressDataPipe,
|
||||
ToButtonsPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
AppShowHeaderComponent,
|
||||
@@ -46,8 +47,10 @@ const routes: Routes = [
|
||||
AppShowStatusComponent,
|
||||
AppShowDependenciesComponent,
|
||||
AppShowMenuComponent,
|
||||
AppShowInterfacesComponent,
|
||||
AppShowHealthChecksComponent,
|
||||
AppShowAdditionalComponent,
|
||||
InterfaceInfoPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -56,7 +59,6 @@ const routes: Routes = [
|
||||
RouterModule.forChild(routes),
|
||||
AppConfigPageModule,
|
||||
EmverPipesModule,
|
||||
UiPipesModule,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
InsecureWarningComponentModule,
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** interfaces ** -->
|
||||
<app-show-interfaces [pkg]="pkg.installed!"></app-show-interfaces>
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
@@ -38,7 +40,7 @@
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
|
||||
<app-show-menu [pkg]="pkg"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowAdditionalComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -8,6 +8,6 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowHeaderComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isEmptyObject } from '@start9labs/shared'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowHealthChecksComponent {
|
||||
@Input() pkgId!: string
|
||||
@Input({ required: true }) pkgId!: string
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<ion-item-divider>Interfaces</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
*ngFor="let info of pkg.interfaceInfo | interfaceInfo"
|
||||
[routerLink]="['interfaces', info.id]"
|
||||
>
|
||||
<ion-icon slot="start" [name]="info.icon" [color]="info.color"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ info.name }}</h2>
|
||||
<p>{{ info.description }}</p>
|
||||
<p>
|
||||
<ion-text [color]="info.color">{{ info.typeDetail }}</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
*ngIf="info.type === 'ui'"
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
(click)="launchUI(info, $event)"
|
||||
[disabled]="pkg.status.main.status !== 'running'"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,83 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import {
|
||||
InstalledPackageInfo,
|
||||
InterfaceInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-interfaces',
|
||||
templateUrl: './app-show-interfaces.component.html',
|
||||
styleUrls: ['./app-show-interfaces.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowInterfacesComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: InstalledPackageInfo
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
|
||||
launchUI(info: InterfaceInfo, e: Event) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.document.defaultView?.open(
|
||||
this.config.launchableAddress(info),
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'interfaceInfo',
|
||||
})
|
||||
export class InterfaceInfoPipe implements PipeTransform {
|
||||
transform(info: InstalledPackageInfo['interfaceInfo']) {
|
||||
return Object.entries(info).map(([id, val]) => {
|
||||
let color: string
|
||||
let icon: string
|
||||
let typeDetail: string
|
||||
|
||||
switch (val.type) {
|
||||
case 'ui':
|
||||
color = 'primary'
|
||||
icon = 'desktop-outline'
|
||||
typeDetail = 'User Interface (UI)'
|
||||
break
|
||||
case 'p2p':
|
||||
color = 'secondary'
|
||||
icon = 'people-outline'
|
||||
typeDetail = 'Peer-To-Peer Interface (P2P)'
|
||||
break
|
||||
case 'api':
|
||||
color = 'tertiary'
|
||||
icon = 'terminal-outline'
|
||||
typeDetail = 'Application Program Interface (API)'
|
||||
break
|
||||
case 'other':
|
||||
color = 'dark'
|
||||
icon = 'cube-outline'
|
||||
typeDetail = 'Unknown Interface Type'
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
color,
|
||||
icon,
|
||||
typeDetail,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,96 @@
|
||||
<ion-item-divider>Menu</ion-item-divider>
|
||||
|
||||
<!-- instructions -->
|
||||
<ion-item
|
||||
*ngFor="let button of buttons"
|
||||
button
|
||||
detail
|
||||
(click)="button.action()"
|
||||
[disabled]="button.disabled"
|
||||
[ngClass]="{ highlighted: button.highlighted$ | async }"
|
||||
(click)="presentModalInstructions()"
|
||||
[ngClass]="{ highlighted: highlighted$ | async }"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-icon slot="start" name="list-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
<h2>Instructions</h2>
|
||||
<p>Understand how to use {{ pkg.manifest.title }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- config -->
|
||||
<ion-item button detail (click)="openConfig()">
|
||||
<ion-icon slot="start" name="options-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Config</h2>
|
||||
<p>Customize {{ pkg.manifest.title }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- credentials -->
|
||||
<ion-item button detail (click)="navigate('credentials')">
|
||||
<ion-icon slot="start" name="key-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Credentials</h2>
|
||||
<p>Password, keys, or other credentials of interest</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- actions -->
|
||||
<ion-item button detail (click)="navigate('actions')">
|
||||
<ion-icon slot="start" name="flash-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Actions</h2>
|
||||
<p>Uninstall and other commands specific to {{ pkg.manifest.title }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- outbound proxy -->
|
||||
<ion-item button detail (click)="setOutboundProxy()">
|
||||
<ion-icon slot="start" name="shield-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Outbound Proxy</h2>
|
||||
<p>Proxy all outbound traffic from {{ pkg.manifest.title }}</p>
|
||||
<p *ngIf="{ value: pkg.installed?.outboundProxy } as proxy">
|
||||
<ion-text [color]="proxy.value ? 'success' : 'warning'">
|
||||
{{
|
||||
!proxy.value
|
||||
? 'None'
|
||||
: proxy.value === 'primary'
|
||||
? 'System Primary'
|
||||
: proxy.value === 'mirror'
|
||||
? 'Mirror P2P'
|
||||
: proxy.value.proxyId
|
||||
}}
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- logs -->
|
||||
<ion-item button detail (click)="navigate('logs')">
|
||||
<ion-icon slot="start" name="receipt-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Logs</h2>
|
||||
<p>Raw, unfiltered logs</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- marketplace -->
|
||||
<ion-item
|
||||
*ngIf="pkg.installed?.['marketplace-url'] as url; else sideloaded"
|
||||
button
|
||||
detail
|
||||
(click)="navigate('/marketplace/' + pkg.manifest.id, { url })"
|
||||
>
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Marketplace Listing</h2>
|
||||
<p>View service in the marketplace</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-template #sideloaded>
|
||||
<ion-item button detail disabled (click)="({})">
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Marketplace Listing</h2>
|
||||
<p>This package was not installed from the marketplace</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { Button } from '../../pipes/to-buttons.pipe'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import { from, map } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ActivatedRoute, Params } from '@angular/router'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../../modals/app-config/app-config.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-menu',
|
||||
@@ -8,6 +25,68 @@ import { Button } from '../../pipes/to-buttons.pipe'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowMenuComponent {
|
||||
@Input()
|
||||
buttons: Button[] = []
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
get highlighted$() {
|
||||
return this.patch
|
||||
.watch$('ui', 'ack-instructions', this.pkg.manifest.id)
|
||||
.pipe(map(seen => !seen))
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly api: ApiService,
|
||||
readonly patch: PatchDB<DataModel>,
|
||||
private readonly proxyService: ProxyService,
|
||||
) {}
|
||||
|
||||
async presentModalInstructions() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
this.api
|
||||
.setDbValue<boolean>(['ack-instructions', id], true)
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
label: 'Instructions',
|
||||
size: 'l',
|
||||
data: {
|
||||
content: from(
|
||||
this.api.getStatic(
|
||||
`/public/package-data/${id}/${version}/INSTRUCTIONS.md`,
|
||||
),
|
||||
),
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
openConfig() {
|
||||
this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: `${this.pkg.manifest.title} configuration`,
|
||||
data: { pkgId: this.pkg.manifest.id },
|
||||
})
|
||||
}
|
||||
|
||||
setOutboundProxy() {
|
||||
this.proxyService.presentModalSetOutboundProxy({
|
||||
packageId: this.pkg.manifest.id,
|
||||
outboundProxy: this.pkg.installed!.outboundProxy,
|
||||
hasP2P: Object.values(this.pkg.installed!.interfaceInfo).some(
|
||||
i => i.type === 'p2p',
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
navigate(path: string, qp?: Params) {
|
||||
return this.navCtrl.navigateForward([path], {
|
||||
relativeTo: this.route,
|
||||
queryParams: qp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import { ProgressData } from 'src/app/types/progress-data'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowProgressComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
progressData!: ProgressData
|
||||
|
||||
get unpackingBuffer(): number {
|
||||
|
||||
@@ -48,19 +48,6 @@
|
||||
<ion-icon slot="start" name="construct-outline"></ion-icon>
|
||||
Configure
|
||||
</ion-button>
|
||||
|
||||
<ion-button
|
||||
*ngIf="addressInfo | hasUi"
|
||||
class="action-button"
|
||||
color="primary"
|
||||
[disabled]="status.primary !== 'running'"
|
||||
(click)="openPopover($event)"
|
||||
>
|
||||
<ion-icon slot="start" name="open-outline"></ion-icon>
|
||||
Open UI
|
||||
</ion-button>
|
||||
|
||||
<launch-menu #launchMenu [addressInfo]="addressInfo"></launch-menu>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
StatusRendering,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import {
|
||||
AddressInfo,
|
||||
DataModel,
|
||||
InterfaceInfo,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { LaunchMenuComponent } from '../../../launch-menu/launch-menu.component'
|
||||
import { LaunchMenuComponent } from '../../../app-list/app-list-pkg/launch-menu/launch-menu.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-status',
|
||||
@@ -41,10 +41,10 @@ import { LaunchMenuComponent } from '../../../launch-menu/launch-menu.component'
|
||||
export class AppShowStatusComponent {
|
||||
@ViewChild('launchMenu') launchMenu!: LaunchMenuComponent
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
status!: PackageStatus
|
||||
|
||||
@Input()
|
||||
@@ -66,8 +66,8 @@ export class AppShowStatusComponent {
|
||||
return this.pkg.manifest.id
|
||||
}
|
||||
|
||||
get addressInfo(): Record<string, AddressInfo> {
|
||||
return this.pkg.installed!['address-info']
|
||||
get interfaceInfo(): Record<string, InterfaceInfo> {
|
||||
return this.pkg.installed!['interfaceInfo']
|
||||
}
|
||||
|
||||
get isConfigured(): boolean {
|
||||
@@ -90,11 +90,6 @@ export class AppShowStatusComponent {
|
||||
return PrimaryRendering[this.status.primary]
|
||||
}
|
||||
|
||||
openPopover(e: Event): void {
|
||||
this.launchMenu.event = e
|
||||
this.launchMenu.isOpen = true
|
||||
}
|
||||
|
||||
presentModalConfig(): void {
|
||||
this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: `${this.pkg.manifest.title} configuration`,
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../modals/app-config/app-config.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { from, map, Observable } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
|
||||
export interface Button {
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
action: Function
|
||||
highlighted$?: Observable<boolean>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toButtons',
|
||||
})
|
||||
export class ToButtonsPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Button[] {
|
||||
const pkgTitle = pkg.manifest.title
|
||||
|
||||
return [
|
||||
// instructions
|
||||
{
|
||||
action: () => this.presentModalInstructions(pkg),
|
||||
title: 'Instructions',
|
||||
description: `Understand how to use ${pkgTitle}`,
|
||||
icon: 'list-outline',
|
||||
highlighted$: this.patch
|
||||
.watch$('ui', 'ack-instructions', pkg.manifest.id)
|
||||
.pipe(map(seen => !seen)),
|
||||
},
|
||||
// config
|
||||
{
|
||||
action: () =>
|
||||
this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: `${pkg.manifest.title} configuration`,
|
||||
data: { pkgId: pkg.manifest.id },
|
||||
}),
|
||||
title: 'Config',
|
||||
description: `Customize ${pkgTitle}`,
|
||||
icon: 'options-outline',
|
||||
},
|
||||
// credentials
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['credentials'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
title: 'Credentials',
|
||||
description: 'Password, keys, or other credentials of interest',
|
||||
icon: 'key-outline',
|
||||
},
|
||||
// actions
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['actions'], { relativeTo: this.route }),
|
||||
title: 'Actions',
|
||||
description: `Uninstall and other commands specific to ${pkgTitle}`,
|
||||
icon: 'flash-outline',
|
||||
},
|
||||
// interfaces
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['interfaces'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
title: 'Interfaces',
|
||||
description: 'User and machine access points',
|
||||
icon: 'desktop-outline',
|
||||
},
|
||||
// logs
|
||||
{
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
title: 'Logs',
|
||||
description: 'Raw, unfiltered service logs',
|
||||
icon: 'receipt-outline',
|
||||
},
|
||||
// view in marketplace
|
||||
this.viewInMarketplaceButton(pkg),
|
||||
]
|
||||
}
|
||||
|
||||
private async presentModalInstructions(pkg: PackageDataEntry) {
|
||||
const { id, version } = pkg.manifest
|
||||
|
||||
this.apiService
|
||||
.setDbValue<boolean>(['ack-instructions', id], true)
|
||||
.catch(e => console.error('Failed to mark instructions as seen', e))
|
||||
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(MarkdownComponent), {
|
||||
label: 'Instructions',
|
||||
size: 'l',
|
||||
data: {
|
||||
content: from(
|
||||
this.apiService.getStatic(
|
||||
`/public/package-data/${id}/${version}/INSTRUCTIONS.md`,
|
||||
),
|
||||
),
|
||||
},
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private viewInMarketplaceButton(pkg: PackageDataEntry): Button {
|
||||
const url = pkg.installed?.['marketplace-url']
|
||||
const queryParams = url ? { url } : {}
|
||||
|
||||
let button: Button = {
|
||||
title: 'Marketplace Listing',
|
||||
icon: 'storefront-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward([`marketplace/${pkg.manifest.id}`], {
|
||||
queryParams,
|
||||
}),
|
||||
disabled: false,
|
||||
description: 'View service in the marketplace',
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
button.disabled = true
|
||||
button.description = 'This package was not installed from the marketplace'
|
||||
button.action = () => {}
|
||||
}
|
||||
|
||||
return button
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<ion-popover
|
||||
#popover
|
||||
(didDismiss)="popover.isOpen = false"
|
||||
mode="ios"
|
||||
type="event"
|
||||
>
|
||||
<ng-template>
|
||||
<ion-content>
|
||||
<ion-item-group>
|
||||
<ng-container *ngFor="let address of addressInfo | uiAddresses">
|
||||
<ion-item-divider>{{ address.name }}</ion-item-divider>
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
*ngFor="let address of address.addresses"
|
||||
(click)="launchUI(address)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>{{ address | addressType }}</h2>
|
||||
<p>{{ address }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline" size="small"></ion-icon>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
</ion-popover>
|
||||
@@ -24,13 +24,6 @@ const routes: Routes = [
|
||||
m => m.AppActionsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/interfaces',
|
||||
loadChildren: () =>
|
||||
import('./app-interfaces/app-interfaces.module').then(
|
||||
m => m.AppInterfacesPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/logs',
|
||||
loadChildren: () =>
|
||||
@@ -43,6 +36,13 @@ const routes: Routes = [
|
||||
m => m.AppCredentialsPageModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':pkgId/interfaces/:interfaceId',
|
||||
loadChildren: () =>
|
||||
import('./app-interface/app-interface.module').then(
|
||||
m => m.AppInterfacePageModule,
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -16,7 +16,7 @@ export class StatusComponent {
|
||||
PS = PrimaryStatus
|
||||
PR = PrimaryRendering
|
||||
|
||||
@Input() rendering!: StatusRendering
|
||||
@Input({ required: true }) rendering!: StatusRendering
|
||||
@Input() size?: string
|
||||
@Input() style?: string = 'regular'
|
||||
@Input() weight?: string = 'normal'
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { UiPipe, UiAddressesPipe, AddressTypePipe } from './ui.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [UiPipe, UiAddressesPipe, AddressTypePipe],
|
||||
exports: [UiPipe, UiAddressesPipe, AddressTypePipe],
|
||||
})
|
||||
export class UiPipesModule {}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
import { hasUi } from 'src/app/services/config.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'hasUi',
|
||||
})
|
||||
export class UiPipe implements PipeTransform {
|
||||
transform(addressInfo: InstalledPackageInfo['address-info']): boolean {
|
||||
return hasUi(addressInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'uiAddresses',
|
||||
})
|
||||
export class UiAddressesPipe implements PipeTransform {
|
||||
transform(
|
||||
addressInfo: InstalledPackageInfo['address-info'],
|
||||
): { name: string; addresses: string[] }[] {
|
||||
return Object.values(addressInfo)
|
||||
.filter(info => info.ui)
|
||||
.map(info => ({
|
||||
name: info.name,
|
||||
addresses: info.addresses,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'addressType',
|
||||
})
|
||||
export class AddressTypePipe implements PipeTransform {
|
||||
transform(address: string): string {
|
||||
if (isValidIpv4(address)) return 'IPv4'
|
||||
if (isValidIpv6(address)) return 'IPv6'
|
||||
|
||||
const hostname = new URL(address).hostname
|
||||
if (hostname.endsWith('.onion')) return 'Tor'
|
||||
if (hostname.endsWith('.local')) return 'Local'
|
||||
|
||||
return 'Custom'
|
||||
}
|
||||
}
|
||||
|
||||
function isValidIpv4(address: string): boolean {
|
||||
const regexExp =
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||
return regexExp.test(address)
|
||||
}
|
||||
|
||||
function isValidIpv6(address: string): boolean {
|
||||
const regexExp =
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi
|
||||
return regexExp.test(address)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
|
||||
import { Proxy } from 'src/app/services/patch-db/data-model'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
|
||||
const auth = Config.of({
|
||||
username: Value.text({
|
||||
@@ -14,89 +16,137 @@ const auth = Config.of({
|
||||
}),
|
||||
})
|
||||
|
||||
const strategyUnion = Value.union(
|
||||
{
|
||||
name: 'Networking Strategy',
|
||||
required: { default: 'router' },
|
||||
},
|
||||
Variants.of({
|
||||
router: {
|
||||
name: 'Router',
|
||||
spec: Config.of({
|
||||
ip: Value.select({
|
||||
name: 'IP Strategy',
|
||||
description: `
|
||||
<h5>IPv6 Only</h5><b>Pros</b>: Ready for IPv6 Internet. Enhanced privacy, as IPv6 addresses are less correlated with geographic area
|
||||
<b>Cons</b>: Your website is only accessible to people who's ISP supports IPv6
|
||||
<h5>IPv6 and IPv4</h5><b>Pros</b>: Ready for IPv6 Internet. Anyone can access your website
|
||||
<b>Cons</b>: IPv4 addresses are closely correlated with geographic areas
|
||||
<h5>IPv4 Only</h5><b>Pros</b>: Anyone can access your website
|
||||
<b>Cons</b>: IPv4 addresses are closely correlated with geographic areas
|
||||
`,
|
||||
required: { default: 'ipv6' },
|
||||
values: {
|
||||
ipv6: 'IPv6 Only',
|
||||
both: 'IPv6 and IPv4',
|
||||
ipv4: 'IPv4 Only',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
reverseProxy: {
|
||||
name: 'Reverse Proxy',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
}),
|
||||
)
|
||||
function getStrategyUnion(proxies: Proxy[]) {
|
||||
const inboundProxies = proxies
|
||||
.filter(p => p.type === 'inbound-outbound')
|
||||
.reduce((prev, curr) => {
|
||||
return {
|
||||
[curr.id]: curr.name,
|
||||
...prev,
|
||||
}
|
||||
}, {})
|
||||
|
||||
export const start9MeSpec = Config.of({
|
||||
strategy: strategyUnion,
|
||||
})
|
||||
|
||||
export const customSpec = Config.of({
|
||||
hostname: Value.text({
|
||||
name: 'Hostname',
|
||||
required: { default: null },
|
||||
placeholder: 'yourdomain.com',
|
||||
}),
|
||||
provider: Value.union(
|
||||
return Value.union(
|
||||
{
|
||||
name: 'Dynamic DNS Provider',
|
||||
required: { default: 'start9' },
|
||||
name: 'Networking Strategy',
|
||||
required: { default: null },
|
||||
description: `<h5>Local</h5>Select this option if you do not mind exposing your home/business IP address to the Internet. This option requires configuring router settings, which StartOS can do automatically if you have an OpenWRT router
|
||||
<h5>Proxy</h5>Select this option is you prefer to hide your home/business IP address from the Internet. This option requires running your own Virtual Private Server (VPS) <i>or</i> paying service provider such as Static Wire
|
||||
`,
|
||||
},
|
||||
Variants.of({
|
||||
start9: {
|
||||
name: 'Start9',
|
||||
spec: Config.of({}),
|
||||
local: {
|
||||
name: 'Local',
|
||||
spec: Config.of({
|
||||
ipStrategy: Value.select({
|
||||
name: 'IP Strategy',
|
||||
description: `<h5>IPv6 Only (recommended)</h5><b>Requirements</b>:<ol><li>ISP IPv6 support</li><li>OpenWRT (recommended) or Linksys router</li></ol><b>Pros</b>: Ready for IPv6 Internet. Enhanced privacy. Run multiple clearnet servers from the same network
|
||||
<b>Cons</b>: Interfaces using this domain will only be accessible to people whose ISP supports IPv6
|
||||
<h5>IPv6 and IPv4</h5><b>Pros</b>: Ready for IPv6 Internet. Accessible by anyone
|
||||
<b>Cons</b>: Less private, as IPv4 addresses are closely correlated with geographic areas. Cannot run multiple clearnet servers from the same network
|
||||
<h5>IPv4 Only</h5><b>Pros</b>: Accessible by anyone
|
||||
<b>Cons</b>: Less private, as IPv4 addresses are closely correlated with geographic areas. Cannot run multiple clearnet servers from the same network
|
||||
`,
|
||||
required: { default: 'ipv6' },
|
||||
values: {
|
||||
ipv6: 'IPv6 Only',
|
||||
ipv4: 'IPv4 Only',
|
||||
dualstack: 'IPv6 and IPv4',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
duckdns: {
|
||||
name: 'Duck DNS',
|
||||
spec: auth,
|
||||
},
|
||||
dyn: {
|
||||
name: 'DynDNS',
|
||||
spec: auth,
|
||||
},
|
||||
easydns: {
|
||||
name: 'easyDNS',
|
||||
spec: auth,
|
||||
},
|
||||
googledomains: {
|
||||
name: 'Google Domains',
|
||||
spec: auth,
|
||||
},
|
||||
namecheap: {
|
||||
name: 'Namecheap (IPv4 only)',
|
||||
spec: auth,
|
||||
},
|
||||
zoneedit: {
|
||||
name: 'Zoneedit',
|
||||
spec: auth,
|
||||
proxy: {
|
||||
name: 'Proxy',
|
||||
spec: Config.of({
|
||||
proxyStrategy: Value.union(
|
||||
{
|
||||
name: 'Proxy Strategy',
|
||||
required: { default: 'primary' },
|
||||
description: `<h5>Primary</h5>Use the <i>Primary Inbound</i> proxy from your proxy settings. If you do not have any inbound proxies, no proxy will be used
|
||||
<h5>Other</h5>Use a specific proxy from your proxy settings
|
||||
`,
|
||||
},
|
||||
Variants.of({
|
||||
primary: {
|
||||
name: 'Primary',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
other: {
|
||||
name: 'Specific',
|
||||
spec: Config.of({
|
||||
proxyId: Value.select({
|
||||
name: 'Select Proxy',
|
||||
required: { default: null },
|
||||
values: inboundProxies,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
strategy: strategyUnion,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export type Start9MeSpec = typeof start9MeSpec.validator._TYPE
|
||||
export type CustomSpec = typeof customSpec.validator._TYPE
|
||||
export async function getStart9ToSpec(proxies: Proxy[]) {
|
||||
return configBuilderToSpec(
|
||||
Config.of({
|
||||
strategy: getStrategyUnion(proxies),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCustomSpec(proxies: Proxy[]) {
|
||||
return configBuilderToSpec(
|
||||
Config.of({
|
||||
hostname: Value.text({
|
||||
name: 'Hostname',
|
||||
required: { default: null },
|
||||
placeholder: 'yourdomain.com',
|
||||
}),
|
||||
provider: Value.union(
|
||||
{
|
||||
name: 'Dynamic DNS Provider',
|
||||
required: { default: 'start9' },
|
||||
},
|
||||
Variants.of({
|
||||
start9: {
|
||||
name: 'Start9',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
njalla: {
|
||||
name: 'Njalla',
|
||||
spec: auth,
|
||||
},
|
||||
duckdns: {
|
||||
name: 'Duck DNS',
|
||||
spec: auth,
|
||||
},
|
||||
dyn: {
|
||||
name: 'DynDNS',
|
||||
spec: auth,
|
||||
},
|
||||
easydns: {
|
||||
name: 'easyDNS',
|
||||
spec: auth,
|
||||
},
|
||||
zoneedit: {
|
||||
name: 'Zoneedit',
|
||||
spec: auth,
|
||||
},
|
||||
googledomains: {
|
||||
name: 'Google Domains (IPv4 or IPv6)',
|
||||
spec: auth,
|
||||
},
|
||||
namecheap: {
|
||||
name: 'Namecheap (IPv4 only)',
|
||||
spec: auth,
|
||||
},
|
||||
}),
|
||||
),
|
||||
strategy: getStrategyUnion(proxies),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,21 +10,20 @@
|
||||
<ion-content class="ion-padding-top with-widgets">
|
||||
<div class="ion-padding-start ion-padding-end">
|
||||
<tui-notification>
|
||||
Adding domains to StartOS enables you to access your server and service
|
||||
interfaces over clearnet.
|
||||
Adding domains permits accessing your server and services over clearnet.
|
||||
<a [href]="docsUrl" target="_blank" rel="noreferrer">View instructions</a>
|
||||
</tui-notification>
|
||||
</div>
|
||||
|
||||
<ion-item-group *ngIf="domains$ | async as domains">
|
||||
<ion-item-divider>
|
||||
Start9.me
|
||||
Start9.to
|
||||
<ion-button
|
||||
*ngIf="!domains.start9Me"
|
||||
*ngIf="!domains.start9To"
|
||||
class="ion-padding-start"
|
||||
strong
|
||||
size="small"
|
||||
(click)="presentModalClaimStart9Me()"
|
||||
(click)="presentModalClaimStart9To()"
|
||||
>
|
||||
<ion-icon slot="start" name="add-outline"></ion-icon>
|
||||
Claim
|
||||
@@ -35,26 +34,27 @@
|
||||
<ion-grid class="ion-padding">
|
||||
<ion-row class="grid-headings">
|
||||
<ion-col size="2">Domain</ion-col>
|
||||
<ion-col size="2">Added</ion-col>
|
||||
<ion-col size="2.5">Added</ion-col>
|
||||
<ion-col size="2">DDNS Provider</ion-col>
|
||||
<ion-col size="1.5">Network Strategy</ion-col>
|
||||
<ion-col size="2">IP Strategy</ion-col>
|
||||
<ion-col size="1.5">In Use</ion-col>
|
||||
<ion-col size="1"></ion-col>
|
||||
<ion-col size="2">Network Strategy</ion-col>
|
||||
<ion-col size="1.5">Used By</ion-col>
|
||||
<ion-col size="2"></ion-col>
|
||||
</ion-row>
|
||||
<ion-row
|
||||
*ngIf="domains.start9Me as start9Me"
|
||||
*ngIf="domains.start9To as start9To"
|
||||
class="ion-align-items-center grid-row-border"
|
||||
>
|
||||
<ion-col size="2">{{ start9Me.value }}</ion-col>
|
||||
<ion-col size="2">{{ start9Me.createdAt| date: 'short' }}</ion-col>
|
||||
<ion-col size="2">{{ start9To.value }}</ion-col>
|
||||
<ion-col size="2.5">{{ start9To.createdAt| date: 'short' }}</ion-col>
|
||||
<ion-col size="2">Start9</ion-col>
|
||||
<ion-col size="1.5">{{ start9Me.networkStrategy }}</ion-col>
|
||||
<ion-col size="2">{{ start9Me.ipStrategy || 'N/A' }}</ion-col>
|
||||
<ion-col size="1.5" *ngIf="start9Me.usedBy as usedBy">
|
||||
<ion-col size="2">
|
||||
{{ $any(start9To.networkStrategy).ipStrategy ||
|
||||
$any(start9To.networkStrategy).proxyId || 'Primary Proxy' }}
|
||||
</ion-col>
|
||||
<ion-col size="1.5" *ngIf="start9To.usedBy as usedBy">
|
||||
<a
|
||||
*ngIf="usedBy.length as qty; else unused"
|
||||
(click)="presentAlertUsedBy(start9Me.value, usedBy)"
|
||||
(click)="presentAlertUsedBy(start9To.value, usedBy)"
|
||||
>
|
||||
{{ qty }} Interfaces
|
||||
</a>
|
||||
@@ -62,9 +62,9 @@
|
||||
<span>N/A</span>
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
<ion-col size="1">
|
||||
<ion-col size="2">
|
||||
<ion-buttons style="float: right">
|
||||
<ion-button size="small" (click)="presentAlertDeleteStart9Me()">
|
||||
<ion-button size="small" (click)="presentAlertDeleteStart9To()">
|
||||
<ion-icon name="trash"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
@@ -90,22 +90,23 @@
|
||||
<ion-grid class="ion-padding">
|
||||
<ion-row class="grid-headings">
|
||||
<ion-col size="2">Domain</ion-col>
|
||||
<ion-col size="2">Added</ion-col>
|
||||
<ion-col size="2.5">Added</ion-col>
|
||||
<ion-col size="2">DDNS Provider</ion-col>
|
||||
<ion-col size="1.5">Network Strategy</ion-col>
|
||||
<ion-col size="2">IP Strategy</ion-col>
|
||||
<ion-col size="1.5">In Use</ion-col>
|
||||
<ion-col size="1"></ion-col>
|
||||
<ion-col size="2">Network Strategy</ion-col>
|
||||
<ion-col size="1.5">Used By</ion-col>
|
||||
<ion-col size="2"></ion-col>
|
||||
</ion-row>
|
||||
<ion-row
|
||||
*ngFor="let domain of domains.custom"
|
||||
class="ion-align-items-center grid-row-border"
|
||||
>
|
||||
<ion-col size="2">{{ domain.value }}</ion-col>
|
||||
<ion-col size="2">{{ domain.createdAt| date: 'short' }}</ion-col>
|
||||
<ion-col size="2.5">{{ domain.createdAt| date: 'short' }}</ion-col>
|
||||
<ion-col size="2">{{ domain.provider }}</ion-col>
|
||||
<ion-col size="1.5">{{ domain.networkStrategy }}</ion-col>
|
||||
<ion-col size="2">{{ domain.ipStrategy || 'N/A' }}</ion-col>
|
||||
<ion-col size="2">
|
||||
{{ $any(domain.networkStrategy).ipStrategy ||
|
||||
$any(domain.networkStrategy).proxyId || 'Primary Proxy' }}
|
||||
</ion-col>
|
||||
<ion-col size="1.5" *ngIf="domain.usedBy as usedBy">
|
||||
<a
|
||||
*ngIf="usedBy.length as qty; else unused"
|
||||
@@ -117,7 +118,7 @@
|
||||
<span>N/A</span>
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
<ion-col size="1">
|
||||
<ion-col size="2">
|
||||
<ion-buttons style="float: right">
|
||||
<ion-button
|
||||
size="small"
|
||||
|
||||
@@ -2,20 +2,13 @@ import { Component } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { combineLatest, filter, first, map, switchMap } from 'rxjs'
|
||||
import { filter, firstValueFrom, map } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel, Domain } from 'src/app/services/patch-db/data-model'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
start9MeSpec,
|
||||
Start9MeSpec,
|
||||
customSpec,
|
||||
CustomSpec,
|
||||
} from './domain.const'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { FormContext, FormPage } from '../../../modals/form/form.page'
|
||||
import { getClearnetAddress } from 'src/app/util/clearnetAddress'
|
||||
import { getCustomSpec, getStart9ToSpec } from './domain.const'
|
||||
|
||||
@Component({
|
||||
selector: 'domains',
|
||||
@@ -25,46 +18,19 @@ import { getClearnetAddress } from 'src/app/util/clearnetAddress'
|
||||
export class DomainsPage {
|
||||
readonly docsUrl = 'https://docs.start9.com/latest/user-manual/domains'
|
||||
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
readonly pkgs$ = this.patch.watch$('package-data').pipe(first())
|
||||
readonly domains$ = this.patch.watch$('server-info', 'network').pipe(
|
||||
map(network => {
|
||||
const start9ToSubdomain = network.start9ToSubdomain
|
||||
const start9To = !start9ToSubdomain
|
||||
? null
|
||||
: {
|
||||
...start9ToSubdomain,
|
||||
value: `${start9ToSubdomain.value}.start9.to`,
|
||||
provider: 'Start9',
|
||||
}
|
||||
|
||||
readonly domains$ = this.connectionService.websocketConnected$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(() =>
|
||||
combineLatest([this.server$, this.pkgs$]).pipe(
|
||||
map(([{ ui, network }, packageData]) => {
|
||||
const start9MeSubdomain = network.start9MeSubdomain
|
||||
const start9Me = !start9MeSubdomain
|
||||
? null
|
||||
: {
|
||||
value: `${start9MeSubdomain.value}.start9.me`,
|
||||
createdAt: start9MeSubdomain.createdAt,
|
||||
provider: 'Start9',
|
||||
networkStrategy: start9MeSubdomain.networkStrategy,
|
||||
ipStrategy: start9MeSubdomain.ipStrategy,
|
||||
usedBy: usedBy(
|
||||
start9MeSubdomain.value,
|
||||
getClearnetAddress('https', ui.domainInfo),
|
||||
packageData,
|
||||
),
|
||||
}
|
||||
const custom = network.domains.map(domain => ({
|
||||
value: domain.value,
|
||||
createdAt: domain.createdAt,
|
||||
provider: domain.provider,
|
||||
networkStrategy: domain.networkStrategy,
|
||||
ipStrategy: domain.ipStrategy,
|
||||
usedBy: usedBy(
|
||||
domain.value,
|
||||
getClearnetAddress('https', ui.domainInfo),
|
||||
packageData,
|
||||
),
|
||||
}))
|
||||
|
||||
return { start9Me, custom }
|
||||
}),
|
||||
),
|
||||
),
|
||||
return { start9To, custom: network.domains }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
@@ -73,16 +39,23 @@ export class DomainsPage {
|
||||
private readonly api: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
async presentModalAdd() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<CustomSpec>>> = {
|
||||
const proxies = await firstValueFrom(
|
||||
this.patch.watch$('server-info', 'network', 'proxies'),
|
||||
)
|
||||
|
||||
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
||||
label: 'Custom Domain',
|
||||
data: {
|
||||
spec: await customSpec.build({} as any),
|
||||
spec: await getCustomSpec(proxies),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage proxies',
|
||||
link: '/system/proxies',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.save(value),
|
||||
@@ -93,15 +66,23 @@ export class DomainsPage {
|
||||
this.formDialog.open(FormPage, options)
|
||||
}
|
||||
|
||||
async presentModalClaimStart9Me() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<Start9MeSpec>>> = {
|
||||
label: 'start9.me',
|
||||
async presentModalClaimStart9To() {
|
||||
const proxies = await firstValueFrom(
|
||||
this.patch.watch$('server-info', 'network', 'proxies'),
|
||||
)
|
||||
|
||||
const options: Partial<TuiDialogOptions<FormContext<any>>> = {
|
||||
label: 'start9.to',
|
||||
data: {
|
||||
spec: await start9MeSpec.build({} as any),
|
||||
spec: await getStart9ToSpec(proxies),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage proxies',
|
||||
link: '/system/proxies',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => this.claimStart9MeDomain(value),
|
||||
handler: async value => this.claimStart9ToDomain(value),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -124,26 +105,26 @@ export class DomainsPage {
|
||||
.subscribe(() => this.delete(hostname))
|
||||
}
|
||||
|
||||
presentAlertDeleteStart9Me() {
|
||||
presentAlertDeleteStart9To() {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Delete start9.me domain?',
|
||||
content: 'Delete start9.to domain?',
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.deleteStart9MeDomain())
|
||||
.subscribe(() => this.deleteStart9ToDomain())
|
||||
}
|
||||
|
||||
presentAlertUsedBy(domain: string, usedBy: string[]) {
|
||||
presentAlertUsedBy(domain: string, usedBy: Domain['usedBy']) {
|
||||
this.dialogs
|
||||
.open(
|
||||
`${domain} is currently being used by:<ul>${usedBy.map(
|
||||
u => `<li>${u}</li>`,
|
||||
`${domain} is currently being used by:<ul>${usedBy.map(u =>
|
||||
u.interfaces.map(i => `<li>${u.service.title} - ${i.title}</li>`),
|
||||
)}</ul>`,
|
||||
{
|
||||
label: 'Used by',
|
||||
@@ -153,17 +134,23 @@ export class DomainsPage {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async claimStart9MeDomain(value: Start9MeSpec): Promise<boolean> {
|
||||
private async claimStart9ToDomain(value: any): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
const networkStrategy = value.strategy.unionSelectKey
|
||||
const strategy = value.strategy.unionValueKey
|
||||
|
||||
const networkStrategy =
|
||||
value.strategy.unionSelectKey === 'local'
|
||||
? { ipStrategy: strategy.ipStrategy }
|
||||
: {
|
||||
proxyId:
|
||||
strategy.proxyStrategy.unionSelectKey === 'primary'
|
||||
? null
|
||||
: strategy.proxyStrategy.unionValueKey.proxyId,
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.claimStart9MeDomain({
|
||||
networkStrategy,
|
||||
ipStrategy:
|
||||
networkStrategy === 'router' ? value.strategy.unionValueKey.ip : null,
|
||||
})
|
||||
await this.api.claimStart9ToDomain({ networkStrategy })
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
@@ -173,12 +160,23 @@ export class DomainsPage {
|
||||
}
|
||||
}
|
||||
|
||||
private async save(value: CustomSpec): Promise<boolean> {
|
||||
private async save(value: any): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
const networkStrategy = value.strategy.unionSelectKey
|
||||
const providerName = value.provider.unionSelectKey
|
||||
|
||||
const strategy = value.strategy.unionValueKey
|
||||
|
||||
const networkStrategy =
|
||||
value.strategy.unionSelectKey === 'local'
|
||||
? { ipStrategy: strategy.ipStrategy }
|
||||
: {
|
||||
proxyId:
|
||||
strategy.proxyStrategy.unionSelectKey === 'primary'
|
||||
? null
|
||||
: strategy.proxyStrategy.unionValueKey.proxyId,
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.addDomain({
|
||||
hostname: value.hostname,
|
||||
@@ -194,8 +192,6 @@ export class DomainsPage {
|
||||
: value.provider.unionValueKey.password,
|
||||
},
|
||||
networkStrategy,
|
||||
ipStrategy:
|
||||
networkStrategy === 'router' ? value.strategy.unionValueKey.ip : null,
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
@@ -218,11 +214,11 @@ export class DomainsPage {
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteStart9MeDomain(): Promise<void> {
|
||||
private async deleteStart9ToDomain(): Promise<void> {
|
||||
const loader = this.loader.open('Deleting...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.deleteStart9MeDomain({})
|
||||
await this.api.deleteStart9ToDomain({})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -230,21 +226,3 @@ export class DomainsPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function usedBy(
|
||||
domain: string,
|
||||
serverUi: string | null,
|
||||
pkgs: DataModel['package-data'],
|
||||
): string[] {
|
||||
const list = []
|
||||
if (serverUi && serverUi.includes(domain)) list.push('StartOS Web Interface')
|
||||
return list.concat(
|
||||
Object.values(pkgs)
|
||||
.filter(pkg =>
|
||||
Object.values(pkg.installed?.['address-info'] || {}).some(ai =>
|
||||
ai.addresses.some(a => a.includes(domain)),
|
||||
),
|
||||
)
|
||||
.map(pkg => pkg.manifest.title),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { OSAddressesPage, OsClearnetPipe } from './os-addresses.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: OSAddressesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
|
||||
declarations: [OSAddressesPage, OsClearnetPipe],
|
||||
})
|
||||
export class OSAddressesPageModule {}
|
||||
@@ -1,228 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="system"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>StartOS Web Interface</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="with-widgets">
|
||||
<div *ngIf="server$ | async as server" class="cap-width">
|
||||
<!-- clearnet -->
|
||||
<ion-item-divider style="--padding-top: 0">Clearnet</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Clearnet provides a fast and convenient experience. It not not
|
||||
provide anonymity, and the addresses can be discovered and accessed
|
||||
by anyone.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/os-addresses#clearnet"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="server.ui.domainInfo as domainInfo; else noClearnet">
|
||||
<ion-item *ngIf="domainInfo | osClearnetPipe as clearnetAddress">
|
||||
<ion-label>
|
||||
<h2>Clearnet</h2>
|
||||
<p>{{ clearnetAddress }}</p>
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentModalAddClearnet(server)">
|
||||
Update
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-start"
|
||||
(click)="presentAlertRemoveClearnet()"
|
||||
color="danger"
|
||||
>
|
||||
Remove
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="launch(clearnetAddress)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="open-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
fill="clear"
|
||||
(click)="copyService.copy(clearnetAddress)"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="copy-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-template #noClearnet>
|
||||
<div class="ion-padding">
|
||||
<ion-button strong (click)="presentModalAddClearnet(server)">
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
Add Clearnet Address
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- tor -->
|
||||
<ion-item-divider>Tor</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Tor offers privacy and anonymity at the expense of speed and
|
||||
reliability. A Tor-enabled browser is required to use a Tor address.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/os-addresses#tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="server.ui.torHostname as torHostname">
|
||||
<ion-label class="break-all">
|
||||
<h2>Tor</h2>
|
||||
<p>{{ torHostname }}</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="launch(torHostname)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="open-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(torHostname)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="copy-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- local -->
|
||||
<ion-item-divider>LAN</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
LAN offers a fast and private experience. These addresses can only
|
||||
be accessed from a device connected to the same LAN as your server,
|
||||
either directly or using a VPN.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/os-addresses#local"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
<div class="ion-padding-top ion-padding-bottom">
|
||||
<ion-button
|
||||
(click)="installCert()"
|
||||
[disabled]="!(crtName$ | async)"
|
||||
strong
|
||||
>
|
||||
<ion-icon slot="start" name="download-outline"></ion-icon>
|
||||
Download Root CA
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="server.ui.lanHostname as lanHostname">
|
||||
<ion-label class="break-all">
|
||||
<h2>Local</h2>
|
||||
<p>{{ lanHostname }}</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="launch(lanHostname)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="open-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(lanHostname)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="copy-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let iface of server.ui.ipInfo | keyvalue">
|
||||
<ion-item *ngIf="iface.value.ipv4 as ipv4">
|
||||
<ion-label>
|
||||
<h2>{{ iface.key }} (IPv4)</h2>
|
||||
<p>{{ ipv4 }}</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="launch(ipv4)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="open-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(ipv4)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="copy-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="iface.value.ipv6 as ipv6">
|
||||
<ion-label>
|
||||
<h2>{{ iface.key }} (IPv6)</h2>
|
||||
<p>{{ ipv6 }}</p>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button fill="clear" (click)="launch(ipv6)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="open-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(ipv6)">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
name="copy-outline"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
</div>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/public/eos/local.crt"
|
||||
[download]="crtName$ | async"
|
||||
></a>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
TuiButtonModule,
|
||||
TuiDataListModule,
|
||||
TuiHostedDropdownModule,
|
||||
TuiNotificationModule,
|
||||
TuiSvgModule,
|
||||
TuiWrapperModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiBadgeModule, TuiInputModule, TuiToggleModule } from '@taiga-ui/kit'
|
||||
import { ProxiesPage } from './proxies.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProxiesPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
FormsModule,
|
||||
TuiNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiInputModule,
|
||||
TuiToggleModule,
|
||||
TuiWrapperModule,
|
||||
TuiBadgeModule,
|
||||
TuiSvgModule,
|
||||
TuiHostedDropdownModule,
|
||||
TuiDataListModule,
|
||||
],
|
||||
declarations: [ProxiesPage],
|
||||
})
|
||||
export class ProxiesPageModule {}
|
||||
@@ -0,0 +1,144 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/system"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Proxies</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<div class="ion-padding-start ion-padding-end">
|
||||
<tui-notification>
|
||||
Currently, StartOS only supports Wireguard proxies, which can be used for:
|
||||
<ol>
|
||||
<li>
|
||||
Proxying
|
||||
<i>outbound</i>
|
||||
traffic to mask your home/business IP from other servers accessed by
|
||||
your server/services
|
||||
</li>
|
||||
<li>
|
||||
Proxying
|
||||
<i>inbound</i>
|
||||
traffic to mask your home/business IP from anyone accessing your
|
||||
server/services over clearnet
|
||||
</li>
|
||||
<li>
|
||||
Creating a Virtual Local Area Network (VLAN) to enable private, remote
|
||||
VPN access to your server/services
|
||||
</li>
|
||||
</ol>
|
||||
<a [href]="docsUrl" target="_blank" rel="noreferrer">View instructions</a>
|
||||
</tui-notification>
|
||||
</div>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
Proxies
|
||||
<ion-button
|
||||
class="ion-padding-start"
|
||||
strong
|
||||
size="small"
|
||||
(click)="presentModalAdd()"
|
||||
>
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
Add Proxy
|
||||
</ion-button>
|
||||
</ion-item-divider>
|
||||
|
||||
<div class="grid-fixed">
|
||||
<ion-grid class="ion-padding">
|
||||
<ion-row class="grid-headings">
|
||||
<ion-col size="2">Name</ion-col>
|
||||
<ion-col size="2">Created</ion-col>
|
||||
<ion-col size="2">Type</ion-col>
|
||||
<ion-col size="3">Primary</ion-col>
|
||||
<ion-col size="2">Used By</ion-col>
|
||||
<ion-col size="1"></ion-col>
|
||||
</ion-row>
|
||||
<ion-row
|
||||
*ngFor="let proxy of proxies$ | async"
|
||||
class="ion-align-items-center grid-row-border"
|
||||
>
|
||||
<ion-col size="2">{{ proxy.name }}</ion-col>
|
||||
<ion-col size="2">{{ proxy.createdAt| date: 'short' }}</ion-col>
|
||||
<ion-col size="2">{{ proxy.type }}</ion-col>
|
||||
<ion-col size="3">
|
||||
<tui-badge
|
||||
*ngIf="proxy.primaryInbound"
|
||||
status="success"
|
||||
value="Inbound"
|
||||
style="margin-right: 4px"
|
||||
></tui-badge>
|
||||
<tui-badge
|
||||
*ngIf="proxy.primaryOutbound"
|
||||
status="info"
|
||||
value="Outbound"
|
||||
></tui-badge>
|
||||
</ion-col>
|
||||
<ion-col size="2" *ngIf="proxy.usedBy as usedBy">
|
||||
<a
|
||||
*ngIf="usedBy.domains.length || usedBy.services.length; else unused"
|
||||
(click)="presentAlertUsedBy(proxy.name, usedBy)"
|
||||
>
|
||||
{{ usedBy.domains.length + usedBy.services.length }} Connections
|
||||
</a>
|
||||
<ng-template #unused>
|
||||
<span>N/A</span>
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
<ion-col size="1">
|
||||
<tui-hosted-dropdown
|
||||
style="float: right"
|
||||
tuiDropdownAlign="left"
|
||||
[sided]="true"
|
||||
[content]="dropdown"
|
||||
[(open)]="menuOpen"
|
||||
>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="flat"
|
||||
tuiHostedDropdownHost
|
||||
size="s"
|
||||
[icon]="icon"
|
||||
></button>
|
||||
<ng-template #icon>
|
||||
<tui-svg src="tuiIconMoreHorizontal" class="icon"></tui-svg>
|
||||
</ng-template>
|
||||
</tui-hosted-dropdown>
|
||||
<ng-template #dropdown let-close="close">
|
||||
<tui-data-list>
|
||||
<tui-opt-group>
|
||||
<button
|
||||
*ngIf="!proxy.primaryInbound && proxy.type === 'inbound-outbound'"
|
||||
tuiOption
|
||||
(click)="update({ primaryInbound: true })"
|
||||
>
|
||||
Make Primary Inbound
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!proxy.primaryOutbound && (proxy.type === 'inbound-outbound' || proxy.type === 'outbound')"
|
||||
tuiOption
|
||||
(click)="update({ primaryOutbound: true })"
|
||||
>
|
||||
Make Primary Outbound
|
||||
</button>
|
||||
<button tuiOption (click)="presentModalRename(proxy)">
|
||||
Rename
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
<tui-opt-group>
|
||||
<button tuiOption (click)="presentAlertDelete(proxy.id)">
|
||||
Delete
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
</tui-data-list>
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,180 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import {
|
||||
TuiDialogOptions,
|
||||
TuiDialogService,
|
||||
TuiHostedDropdownComponent,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel, Proxy } from 'src/app/services/patch-db/data-model'
|
||||
import { FormContext, FormPage } from '../../../modals/form/form.page'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
|
||||
@Component({
|
||||
selector: 'proxies',
|
||||
templateUrl: './proxies.page.html',
|
||||
styleUrls: ['./proxies.page.scss'],
|
||||
})
|
||||
export class ProxiesPage {
|
||||
@ViewChild(TuiHostedDropdownComponent)
|
||||
menuComponent?: TuiHostedDropdownComponent
|
||||
|
||||
menuOpen = false
|
||||
|
||||
readonly docsUrl = 'https://docs.start9.com/latest/user-manual/vpns/'
|
||||
|
||||
readonly proxies$ = this.patch.watch$('server-info', 'network', 'proxies')
|
||||
|
||||
constructor(
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
async presentModalAdd() {
|
||||
const options: Partial<TuiDialogOptions<FormContext<WireguardSpec>>> = {
|
||||
label: 'Add Proxy',
|
||||
data: {
|
||||
spec: await wireguardSpec.build({} as any),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: value => this.save(value).then(() => true),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormPage, options)
|
||||
}
|
||||
|
||||
async presentModalRename(proxy: Proxy) {
|
||||
const options: Partial<TuiDialogOptions<FormContext<{ name: string }>>> = {
|
||||
label: `Rename ${proxy.name}`,
|
||||
data: {
|
||||
spec: {
|
||||
name: await Value.text({
|
||||
name: 'Name',
|
||||
required: { default: proxy.name },
|
||||
}).build({} as any),
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: value => this.update(value).then(() => true),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormPage, options)
|
||||
}
|
||||
|
||||
presentAlertDelete(id: string) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Delete proxy? This action cannot be undone.',
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
this.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
presentAlertUsedBy(name: string, usedBy: Proxy['usedBy']) {
|
||||
let message = `Proxy "${name}" is currently used by:`
|
||||
if (usedBy.domains.length) {
|
||||
message = `${message}<h2>Domains (inbound)</h2><ul>${usedBy.domains.map(
|
||||
d => `<li>${d}</li>`,
|
||||
)}</ul>`
|
||||
}
|
||||
if (usedBy.services.length) {
|
||||
message = `${message}<h2>Services (outbound)</h2>${usedBy.services.map(
|
||||
s => `<li>${s.title}</li>`,
|
||||
)}`
|
||||
}
|
||||
|
||||
this.dialogs
|
||||
.open(message, {
|
||||
label: 'Used by',
|
||||
size: 's',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private async save(value: WireguardSpec): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.addProxy({
|
||||
name: value.name,
|
||||
config: value.config?.filePath || '',
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
value: Partial<{
|
||||
name: string
|
||||
primaryInbound: true
|
||||
primaryOutbound: true
|
||||
}>,
|
||||
): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.updateProxy(value)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async delete(id: string): Promise<void> {
|
||||
const loader = this.loader.open('Deleting...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.deleteProxy({ id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wireguardSpec = Config.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'A friendly name to help you remember and identify this proxy',
|
||||
required: { default: null },
|
||||
}),
|
||||
config: Value.file({
|
||||
name: 'Wiregaurd Config',
|
||||
required: { default: null },
|
||||
extensions: ['.conf'],
|
||||
}),
|
||||
})
|
||||
|
||||
type WireguardSpec = typeof wireguardSpec.validator._TYPE
|
||||
@@ -2,14 +2,14 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { PortForwardsPage } from './port-forwards.page'
|
||||
import { RouterPage } from './router.page'
|
||||
import { PrimaryIpPipeModule } from 'src/app/common/primary-ip/primary-ip.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PortForwardsPage,
|
||||
component: RouterPage,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -21,6 +21,6 @@ const routes: Routes = [
|
||||
PrimaryIpPipeModule,
|
||||
FormsModule,
|
||||
],
|
||||
declarations: [PortForwardsPage],
|
||||
declarations: [RouterPage],
|
||||
})
|
||||
export class PortForwardsPageModule {}
|
||||
export class RouterPageModule {}
|
||||
@@ -5,12 +5,12 @@ import { LoadingService, CopyService, ErrorService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'port-forwards',
|
||||
templateUrl: './port-forwards.page.html',
|
||||
styleUrls: ['./port-forwards.page.scss'],
|
||||
selector: 'router',
|
||||
templateUrl: './router.page.html',
|
||||
styleUrls: ['./router.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PortForwardsPage {
|
||||
export class RouterPage {
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
editing: Record<string, boolean> = {}
|
||||
overrides: Record<string, number> = {}
|
||||
@@ -66,6 +66,16 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</p>
|
||||
<!-- "Outbound Proxy" button only -->
|
||||
<p *ngIf="button.title === 'Outbound Proxy'">
|
||||
<ion-text
|
||||
[color]="!server.network.outboundProxy ? 'warning' : 'success'"
|
||||
>
|
||||
{{ !server.network.outboundProxy ? 'None' :
|
||||
server.network.outboundProxy === 'primary' ? 'System Primary' :
|
||||
server.network.outboundProxy.proxyId }}
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
@@ -46,7 +47,7 @@ export class ServerShowPage {
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly api: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@@ -56,6 +57,7 @@ export class ServerShowPage {
|
||||
private readonly alerts: TuiAlertService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly proxyService: ProxyService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
|
||||
@@ -156,7 +158,7 @@ export class ServerShowPage {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.resetPassword({
|
||||
await this.api.resetPassword({
|
||||
'old-password': value.currentPassword,
|
||||
'new-password': value.newPassword1,
|
||||
})
|
||||
@@ -256,7 +258,7 @@ export class ServerShowPage {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.setDbValue<string | null>(['name'], value)
|
||||
await this.api.setDbValue<string | null>(['name'], value)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
@@ -264,7 +266,7 @@ export class ServerShowPage {
|
||||
|
||||
// should wipe cache independent of actual BE logout
|
||||
private logout() {
|
||||
this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||
this.authService.setUnverified()
|
||||
}
|
||||
|
||||
@@ -273,7 +275,7 @@ export class ServerShowPage {
|
||||
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.restartServer({})
|
||||
await this.api.restartServer({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
@@ -287,7 +289,7 @@ export class ServerShowPage {
|
||||
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.shutdownServer({})
|
||||
await this.api.shutdownServer({})
|
||||
this.presentAlertInProgress(
|
||||
action,
|
||||
'.<br /><br /><b>You will need to physically power cycle the device to regain connectivity.</b>',
|
||||
@@ -304,7 +306,7 @@ export class ServerShowPage {
|
||||
const loader = this.loader.open(`Beginning ${action}...`).subscribe()
|
||||
|
||||
try {
|
||||
await this.embassyApi.systemRebuild({})
|
||||
await this.api.systemRebuild({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
@@ -375,14 +377,6 @@ export class ServerShowPage {
|
||||
detail: false,
|
||||
disabled$: this.eosService.updatingOrBackingUp$,
|
||||
},
|
||||
{
|
||||
title: 'Browser Tab Title',
|
||||
description: `Customize the display name of your browser tab`,
|
||||
icon: 'pricetag-outline',
|
||||
action: () => this.setBrowserTab(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
description:
|
||||
@@ -425,21 +419,9 @@ export class ServerShowPage {
|
||||
},
|
||||
],
|
||||
Network: [
|
||||
{
|
||||
title: 'StartOS Web Interface',
|
||||
description: 'Addresses for accessing this StartOS web interface',
|
||||
icon: 'desktop-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['addresses'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Domains',
|
||||
description:
|
||||
'Add domains to your server to enable clearnet connections',
|
||||
description: 'Manage domains for clearnet connectivity',
|
||||
icon: 'globe-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['domains'], { relativeTo: this.route }),
|
||||
@@ -447,12 +429,20 @@ export class ServerShowPage {
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Port Forwards',
|
||||
description:
|
||||
'A list of ports that should be forwarded through your router',
|
||||
icon: 'trail-sign-outline',
|
||||
title: 'Proxies',
|
||||
description: 'Manage proxies for inbound and outbound connections',
|
||||
icon: 'shuffle-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['port-forwards'], {
|
||||
this.navCtrl.navigateForward(['proxies'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Router Config',
|
||||
description: 'Connect or configure your router for clearnet',
|
||||
icon: 'radio-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['router-config'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
@@ -468,7 +458,36 @@ export class ServerShowPage {
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
Security: [
|
||||
'User Interface': [
|
||||
{
|
||||
title: 'Browser Tab Title',
|
||||
description: `Customize the display name of your browser tab`,
|
||||
icon: 'pricetag-outline',
|
||||
action: () => this.setBrowserTab(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Web Addresses',
|
||||
description: 'View and manage web addresses for accessing this UI',
|
||||
icon: 'desktop-outline',
|
||||
action: () =>
|
||||
this.navCtrl.navigateForward(['interfaces', 'ui'], {
|
||||
relativeTo: this.route,
|
||||
}),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
],
|
||||
'Privacy and Security': [
|
||||
{
|
||||
title: 'Outbound Proxy',
|
||||
description: 'Proxy outbound traffic from the StartOS main process',
|
||||
icon: 'shield-outline',
|
||||
action: () => this.proxyService.presentModalSetOutboundProxy(),
|
||||
detail: false,
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
description:
|
||||
@@ -493,7 +512,7 @@ export class ServerShowPage {
|
||||
],
|
||||
Logs: [
|
||||
{
|
||||
title: 'System Resources',
|
||||
title: 'Activity Monitor',
|
||||
description: 'CPU, disk, memory, and other useful metrics',
|
||||
icon: 'pulse',
|
||||
action: () =>
|
||||
|
||||
@@ -10,18 +10,14 @@ const routes: Routes = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'addresses',
|
||||
path: 'interfaces/ui',
|
||||
loadChildren: () =>
|
||||
import('./os-addresses/os-addresses.module').then(
|
||||
m => m.OSAddressesPageModule,
|
||||
),
|
||||
import('./ui-details/ui-details.module').then(m => m.UIDetailsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'port-forwards',
|
||||
path: 'router-config',
|
||||
loadChildren: () =>
|
||||
import('./port-forwards/port-forwards.module').then(
|
||||
m => m.PortForwardsPageModule,
|
||||
),
|
||||
import('./router/router.module').then(m => m.RouterPageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
@@ -71,6 +67,11 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./domains/domains.module').then(m => m.DomainsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'proxies',
|
||||
loadChildren: () =>
|
||||
import('./proxies/proxies.module').then(m => m.ProxiesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'ssh',
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { UIDetailsPage } from './ui-details.page'
|
||||
import { InterfaceAddressesComponentModule } from 'src/app/common/interface-addresses/interface-addresses.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: UIDetailsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
InterfaceAddressesComponentModule,
|
||||
],
|
||||
declarations: [UIDetailsPage],
|
||||
})
|
||||
export class UIDetailsPageModule {}
|
||||
@@ -0,0 +1,14 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="system"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>StartOS UI</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="with-widgets">
|
||||
<div *ngIf="ui$ | async as ui" class="cap-width">
|
||||
<interface-addresses [addressInfo]="ui" [isUi]="true"></interface-addresses>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'ui-details',
|
||||
templateUrl: './ui-details.page.html',
|
||||
styleUrls: ['./ui-details.page.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UIDetailsPage {
|
||||
readonly ui$ = this.patch.watch$('server-info', 'ui')
|
||||
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { ERRORS } from '../form-group/form-group.component'
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
spec!: ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
|
||||
@@ -28,7 +28,7 @@ export class FormControlComponent<
|
||||
T extends ValueSpec,
|
||||
V,
|
||||
> extends AbstractTuiNullableControl<V> {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
spec!: T
|
||||
|
||||
@ViewChild('warning')
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
spec!: ValueSpecObject
|
||||
|
||||
@Input()
|
||||
|
||||
@@ -28,7 +28,7 @@ import { tuiPure } from '@taiga-ui/cdk'
|
||||
],
|
||||
})
|
||||
export class FormUnionComponent implements OnChanges {
|
||||
@Input()
|
||||
@Input({ required: true })
|
||||
spec!: ValueSpecUnion
|
||||
|
||||
selectSpec!: ValueSpecSelect
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<ion-item>
|
||||
<ion-label class="break-all">
|
||||
<h2>{{ label }}</h2>
|
||||
<p>{{ hostname }}</p>
|
||||
<ng-content></ng-content>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<ion-button *ngIf="isUi" fill="clear" (click)="launch(hostname)">
|
||||
<ion-icon slot="icon-only" size="small" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="showQR(hostname)">
|
||||
<ion-icon size="small" slot="icon-only" name="qr-code-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" (click)="copyService.copy(hostname)">
|
||||
<ion-icon slot="icon-only" size="small" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
@@ -0,0 +1,125 @@
|
||||
<ng-container *ngIf="network$ | async as network">
|
||||
<!-- clearnet -->
|
||||
<ion-item-divider style="--padding-top: 0">Clearnet</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Add clearnet to expose this interface to the public Internet.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#clearnet"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngIf="addressInfo.domainInfo as domainInfo; else noClearnet">
|
||||
<interface-addresses-item
|
||||
label="Clearnet"
|
||||
[hostname]="domainInfo | interfaceClearnetPipe"
|
||||
[isUi]="isUi"
|
||||
>
|
||||
<div class="ion-padding-top">
|
||||
<ion-button (click)="presentModalAddClearnet(network)">
|
||||
Update
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-start"
|
||||
(click)="presentAlertRemoveClearnet()"
|
||||
color="danger"
|
||||
>
|
||||
Remove
|
||||
</ion-button>
|
||||
</div>
|
||||
</interface-addresses-item>
|
||||
</ng-container>
|
||||
<ng-template #noClearnet>
|
||||
<div class="ion-padding">
|
||||
<ion-button strong (click)="presentModalAddClearnet(network)">
|
||||
<ion-icon slot="start" name="add"></ion-icon>
|
||||
Add Clearnet
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- tor -->
|
||||
<ion-item-divider>Tor</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Use a Tor-enabled browser to access this address. Tor connections can
|
||||
be slow and unreliable.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<interface-addresses-item
|
||||
label="Tor"
|
||||
[hostname]="addressInfo.torHostname"
|
||||
[isUi]="isUi"
|
||||
></interface-addresses-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- local -->
|
||||
<ion-item-divider>Local</ion-item-divider>
|
||||
<ion-item-group>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>
|
||||
Local addresses can only be accessed while connected to the same Local
|
||||
Area Network (LAN) as your server, either directly or using a VPN.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/interface-addresses#local"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</h2>
|
||||
<div *ngIf="!packageContext" class="ion-padding-top ion-padding-bottom">
|
||||
<ion-button (click)="installCert()" strong>
|
||||
<ion-icon slot="start" name="download-outline"></ion-icon>
|
||||
Download Root CA
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<interface-addresses-item
|
||||
label="Local"
|
||||
[hostname]="addressInfo.lanHostname"
|
||||
[isUi]="isUi"
|
||||
></interface-addresses-item>
|
||||
<ng-container *ngFor="let iface of addressInfo.ipInfo | keyvalue">
|
||||
<interface-addresses-item
|
||||
*ngIf="iface.value.ipv4 as ipv4"
|
||||
[label]="iface.key + ' (IPv4)'"
|
||||
[hostname]="ipv4"
|
||||
[isUi]="isUi"
|
||||
></interface-addresses-item>
|
||||
<interface-addresses-item
|
||||
*ngIf="iface.value.ipv6 as ipv6"
|
||||
[label]="iface.key + ' (IPv6)'"
|
||||
[hostname]="ipv6"
|
||||
[isUi]="isUi"
|
||||
></interface-addresses-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- hidden element for downloading cert -->
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/public/eos/local.crt"
|
||||
[download]="addressInfo.lanHostname + '.crt'"
|
||||
></a>
|
||||
</ng-container>
|
||||
@@ -1,25 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { LoadingService, CopyService, ErrorService } from '@start9labs/shared'
|
||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map } from 'rxjs'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
DomainInfo,
|
||||
AddressInfo,
|
||||
DataModel,
|
||||
DomainInfo,
|
||||
NetworkInfo,
|
||||
ServerInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormContext, FormPage } from '../../../modals/form/form.page'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { getClearnetAddress } from 'src/app/util/clearnetAddress'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { FormContext, FormPage } from 'src/app/apps/ui/modals/form/form.page'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { QRComponent } from 'src/app/common/qr/qr.component'
|
||||
|
||||
export type ClearnetForm = {
|
||||
domain: string
|
||||
@@ -27,39 +34,37 @@ export type ClearnetForm = {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'os-addresses',
|
||||
templateUrl: './os-addresses.page.html',
|
||||
styleUrls: ['./os-addresses.page.scss'],
|
||||
selector: 'interface-addresses',
|
||||
templateUrl: './interface-addresses.component.html',
|
||||
styleUrls: ['./interface-addresses.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OSAddressesPage {
|
||||
readonly server$ = this.patch.watch$('server-info')
|
||||
export class InterfaceAddressesComponent {
|
||||
@Input() packageContext?: {
|
||||
packageId: string
|
||||
interfaceId: string
|
||||
}
|
||||
@Input({ required: true }) addressInfo!: AddressInfo
|
||||
@Input({ required: true }) isUi!: boolean
|
||||
|
||||
readonly crtName$ = this.server$.pipe(
|
||||
map(server => `${server.ui.lanHostname}.crt`),
|
||||
)
|
||||
readonly network$ = this.patch.watch$('server-info', 'network')
|
||||
|
||||
constructor(
|
||||
readonly copyService: CopyService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
|
||||
launch(url: string): void {
|
||||
this.document.defaultView?.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
installCert(): void {
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
async presentModalAddClearnet(server: ServerInfo) {
|
||||
const domainInfo = server.ui.domainInfo
|
||||
async presentModalAddClearnet(networkInfo: NetworkInfo) {
|
||||
const domainInfo = this.addressInfo.domainInfo
|
||||
const options: Partial<TuiDialogOptions<FormContext<ClearnetForm>>> = {
|
||||
label: 'Select Domain/Subdomain',
|
||||
data: {
|
||||
@@ -67,7 +72,7 @@ export class OSAddressesPage {
|
||||
domain: domainInfo?.domain || '',
|
||||
subdomain: domainInfo?.subdomain || '',
|
||||
},
|
||||
spec: await this.getClearnetSpec(server.network),
|
||||
spec: await getClearnetSpec(networkInfo),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage domains',
|
||||
@@ -102,7 +107,14 @@ export class OSAddressesPage {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setServerClearnetAddress({ domainInfo })
|
||||
if (this.packageContext) {
|
||||
await this.api.setInterfaceClearnetAddress({
|
||||
...this.packageContext,
|
||||
domainInfo,
|
||||
})
|
||||
} else {
|
||||
await this.api.setServerClearnetAddress({ domainInfo })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
@@ -116,52 +128,86 @@ export class OSAddressesPage {
|
||||
const loader = this.loader.open('Removing...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setServerClearnetAddress({ domainInfo: null })
|
||||
if (this.packageContext) {
|
||||
await this.api.setInterfaceClearnetAddress({
|
||||
...this.packageContext,
|
||||
domainInfo: null,
|
||||
})
|
||||
} else {
|
||||
await this.api.setServerClearnetAddress({ domainInfo: null })
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getClearnetSpec({
|
||||
domains,
|
||||
start9MeSubdomain,
|
||||
}: NetworkInfo): Promise<InputSpec> {
|
||||
const start9MeDomain = `${start9MeSubdomain?.value}.start9.me`
|
||||
const base = start9MeSubdomain ? { [start9MeDomain]: start9MeDomain } : {}
|
||||
function getClearnetSpec({
|
||||
domains,
|
||||
start9ToSubdomain,
|
||||
}: NetworkInfo): Promise<InputSpec> {
|
||||
const start9ToDomain = `${start9ToSubdomain?.value}.start9.to`
|
||||
const base = start9ToSubdomain ? { [start9ToDomain]: start9ToDomain } : {}
|
||||
|
||||
return configBuilderToSpec(
|
||||
Config.of({
|
||||
domain: Value.dynamicSelect(() => {
|
||||
return {
|
||||
name: 'Domain',
|
||||
required: { default: null },
|
||||
values: domains.reduce((prev, curr) => {
|
||||
return {
|
||||
[curr.value]: curr.value,
|
||||
...prev,
|
||||
}
|
||||
}, base),
|
||||
}
|
||||
}),
|
||||
subdomain: Value.text({
|
||||
name: 'Subdomain',
|
||||
required: false,
|
||||
}),
|
||||
const values = domains.reduce((prev, curr) => {
|
||||
return {
|
||||
[curr.value]: curr.value,
|
||||
...prev,
|
||||
}
|
||||
}, base)
|
||||
|
||||
return configBuilderToSpec(
|
||||
Config.of({
|
||||
domain: Value.select({
|
||||
name: 'Domain',
|
||||
required: { default: null },
|
||||
values,
|
||||
}),
|
||||
)
|
||||
subdomain: Value.text({
|
||||
name: 'Subdomain',
|
||||
required: false,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'interface-addresses-item',
|
||||
templateUrl: './interface-addresses-item.component.html',
|
||||
styleUrls: ['./interface-addresses.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceAddressItemComponent {
|
||||
@Input({ required: true }) label!: string
|
||||
@Input({ required: true }) hostname!: string
|
||||
@Input({ required: true }) isUi!: boolean
|
||||
|
||||
constructor(
|
||||
readonly copyService: CopyService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
|
||||
launch(url: string): void {
|
||||
this.document.defaultView?.open(url, '_blank', 'noreferrer')
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
showQR(data: string) {
|
||||
this.dialogs
|
||||
.open(new PolymorpheusComponent(QRComponent), {
|
||||
size: 'auto',
|
||||
data,
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'osClearnetPipe',
|
||||
name: 'interfaceClearnetPipe',
|
||||
})
|
||||
export class OsClearnetPipe implements PipeTransform {
|
||||
export class InterfaceClearnetPipe implements PipeTransform {
|
||||
transform(clearnet: DomainInfo): string {
|
||||
return getClearnetAddress('https', clearnet)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import {
|
||||
InterfaceAddressesComponent,
|
||||
InterfaceAddressItemComponent,
|
||||
InterfaceClearnetPipe,
|
||||
} from './interface-addresses.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, IonicModule],
|
||||
declarations: [
|
||||
InterfaceAddressesComponent,
|
||||
InterfaceAddressItemComponent,
|
||||
InterfaceClearnetPipe,
|
||||
],
|
||||
exports: [InterfaceAddressesComponent],
|
||||
})
|
||||
export class InterfaceAddressesComponentModule {}
|
||||
@@ -46,13 +46,15 @@ export class LogsComponent {
|
||||
@ViewChild(IonContent)
|
||||
private content?: IonContent
|
||||
|
||||
@Input() followLogs!: (
|
||||
@Input({ required: true }) followLogs!: (
|
||||
params: RR.FollowServerLogsReq,
|
||||
) => Promise<RR.FollowServerLogsRes>
|
||||
@Input() fetchLogs!: (params: ServerLogsReq) => Promise<LogsRes>
|
||||
@Input() context!: string
|
||||
@Input() defaultBack!: string
|
||||
@Input() pageTitle!: string
|
||||
@Input({ required: true }) fetchLogs!: (
|
||||
params: ServerLogsReq,
|
||||
) => Promise<LogsRes>
|
||||
@Input({ required: true }) context!: string
|
||||
@Input({ required: true }) defaultBack!: string
|
||||
@Input({ required: true }) pageTitle!: string
|
||||
|
||||
loading = true
|
||||
infiniteStatus: 0 | 1 | 2 = 0
|
||||
|
||||
12
frontend/projects/ui/src/app/common/qr/qr.module.ts
Normal file
12
frontend/projects/ui/src/app/common/qr/qr.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { QrCodeModule } from 'ng-qrcode'
|
||||
|
||||
import { QRComponent } from './qr.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [QRComponent],
|
||||
imports: [CommonModule, QrCodeModule],
|
||||
exports: [QRComponent],
|
||||
})
|
||||
export class QRComponentModule {}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AnyLinkComponent implements OnInit {
|
||||
@Input() link!: string
|
||||
@Input({ required: true }) link!: string
|
||||
@Input() qp?: Record<string, string>
|
||||
externalLink = false
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetCardComponent {
|
||||
@Input() cardDetails!: Card
|
||||
@Input() containerDimensions!: Dimension
|
||||
@Input({ required: true }) cardDetails!: Card
|
||||
@Input({ required: true }) containerDimensions!: Dimension
|
||||
@ViewChild('outerWrapper') outerWrapper: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@ViewChild('innerWrapper') innerWrapper: ElementRef<HTMLElement> =
|
||||
|
||||
@@ -1260,25 +1260,40 @@ export module Mock {
|
||||
},
|
||||
'dependency-errors': {},
|
||||
},
|
||||
'address-info': {
|
||||
interfaceInfo: {
|
||||
rpc: {
|
||||
name: 'Bitcoin RPC',
|
||||
description: `Bitcoin's RPC interface`,
|
||||
addresses: [
|
||||
'http://bitcoind-rpc-address.onion',
|
||||
'https://bitcoind-rpc-address.local',
|
||||
'https://192.168.1.1:8332',
|
||||
],
|
||||
ui: true,
|
||||
addressInfo: {
|
||||
ipInfo: {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '192.168.1.1:8333',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD:8333',
|
||||
},
|
||||
},
|
||||
lanHostname: 'adjective-noun:8333',
|
||||
torHostname: 'bitcoind-rpc-address.onion',
|
||||
domainInfo: null,
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
p2p: {
|
||||
name: 'Bitcoin P2P',
|
||||
description: `Bitcoin's P2P interface`,
|
||||
addresses: [
|
||||
'bitcoin://bitcoind-rpc-address.onion',
|
||||
'bitcoin://192.168.1.1:8333',
|
||||
],
|
||||
ui: true,
|
||||
addressInfo: {
|
||||
ipInfo: {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '192.168.1.1:8332',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD:8332',
|
||||
},
|
||||
},
|
||||
lanHostname: 'adjective-noun:8332',
|
||||
torHostname: 'bitcoind-p2p-address.onion',
|
||||
domainInfo: null,
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
},
|
||||
'current-dependencies': {},
|
||||
@@ -1286,6 +1301,7 @@ export module Mock {
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
'has-config': true,
|
||||
outboundProxy: null,
|
||||
},
|
||||
actions: {
|
||||
resync: {
|
||||
@@ -1336,15 +1352,23 @@ export module Mock {
|
||||
},
|
||||
'dependency-errors': {},
|
||||
},
|
||||
'address-info': {
|
||||
interfaceInfo: {
|
||||
rpc: {
|
||||
name: 'Proxy RPC addresses',
|
||||
description: `Use these addresses to access Proxy's RPC interface`,
|
||||
addresses: [
|
||||
'http://bitcoinproxy-rpc-address.onion',
|
||||
'https://bitcoinproxy-rpc-address.local',
|
||||
],
|
||||
ui: false,
|
||||
addressInfo: {
|
||||
ipInfo: {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '192.168.1.1:8459',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD:8459',
|
||||
},
|
||||
},
|
||||
lanHostname: 'adjective-noun.local:8459',
|
||||
torHostname: 'btcrpc-proxy-address.onion',
|
||||
domainInfo: null,
|
||||
},
|
||||
type: 'api',
|
||||
},
|
||||
},
|
||||
'current-dependencies': {
|
||||
@@ -1361,6 +1385,7 @@ export module Mock {
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
'has-config': true,
|
||||
outboundProxy: null,
|
||||
},
|
||||
actions: {},
|
||||
}
|
||||
@@ -1384,26 +1409,40 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
},
|
||||
'address-info': {
|
||||
interfaceInfo: {
|
||||
ui: {
|
||||
name: 'Web UI',
|
||||
description: 'The browser web interface for LND',
|
||||
addresses: [
|
||||
'http://lnd-ui-address.onion',
|
||||
'https://lnd-ui-address.local',
|
||||
'https://192.168.1.1:3449',
|
||||
],
|
||||
ui: true,
|
||||
addressInfo: {
|
||||
ipInfo: {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '192.168.1.1:7171',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD:7171',
|
||||
},
|
||||
},
|
||||
lanHostname: 'adjective-noun.local:7171',
|
||||
torHostname: 'lnd-ui-address.onion',
|
||||
domainInfo: null,
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
grpc: {
|
||||
name: 'gRPC',
|
||||
description: 'For connecting to LND gRPC interface',
|
||||
addresses: [
|
||||
'http://lnd-grpc-address.onion',
|
||||
'https://lnd-grpc-address.local',
|
||||
'https://192.168.1.1:3449',
|
||||
],
|
||||
ui: true,
|
||||
addressInfo: {
|
||||
ipInfo: {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '192.168.1.1:9191',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD:9191',
|
||||
},
|
||||
},
|
||||
lanHostname: 'adjective-noun.local:9191',
|
||||
torHostname: 'lnd-grpc-address.onion',
|
||||
domainInfo: null,
|
||||
},
|
||||
type: 'p2p',
|
||||
},
|
||||
},
|
||||
'current-dependencies': {
|
||||
@@ -1417,7 +1456,7 @@ export module Mock {
|
||||
'dependency-info': {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
@@ -1427,6 +1466,7 @@ export module Mock {
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
'has-config': true,
|
||||
outboundProxy: null,
|
||||
},
|
||||
actions: {},
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ import {
|
||||
DataModel,
|
||||
DependencyError,
|
||||
DomainInfo,
|
||||
NetworkStrategy,
|
||||
OsOutboundProxy,
|
||||
ServiceOutboundProxy,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
import {
|
||||
CustomSpec,
|
||||
Start9MeSpec,
|
||||
} from 'src/app/apps/ui/pages/system/domains/domain.const'
|
||||
|
||||
export module RR {
|
||||
// DB
|
||||
@@ -89,6 +88,11 @@ export module RR {
|
||||
} // server.experimental.zram
|
||||
export type ToggleZramRes = null
|
||||
|
||||
export type SetOsOutboundProxyReq = {
|
||||
proxy: OsOutboundProxy
|
||||
} // server.proxy.set-outbound
|
||||
export type SetOsOutboundProxyRes = null
|
||||
|
||||
// sessions
|
||||
|
||||
export type GetSessionsReq = {} // sessions.list
|
||||
@@ -114,16 +118,31 @@ export module RR {
|
||||
export type DeleteAllNotificationsReq = { before: number } // notification.delete-before
|
||||
export type DeleteAllNotificationsRes = null
|
||||
|
||||
// network
|
||||
|
||||
export type AddProxyReq = {
|
||||
name: string
|
||||
config: string
|
||||
} // net.proxy.add
|
||||
export type AddProxyRes = null
|
||||
|
||||
export type UpdateProxyReq = {
|
||||
name?: string
|
||||
primaryInbound?: true
|
||||
primaryOutbound?: true
|
||||
} // net.proxy.update
|
||||
export type UpdateProxyRes = null
|
||||
|
||||
export type DeleteProxyReq = { id: string } // net.proxy.delete
|
||||
export type DeleteProxyRes = null
|
||||
|
||||
// domains
|
||||
|
||||
export type ClaimStart9MeReq = {
|
||||
networkStrategy: string
|
||||
ipStrategy: string | null
|
||||
} // net.domain.me.claim
|
||||
export type ClaimStart9MeRes = null
|
||||
export type ClaimStart9ToReq = { networkStrategy: NetworkStrategy } // net.domain.me.claim
|
||||
export type ClaimStart9ToRes = null
|
||||
|
||||
export type DeleteStart9MeReq = {} // net.domain.me.delete
|
||||
export type DeleteStart9MeRes = null
|
||||
export type DeleteStart9ToReq = {} // net.domain.me.delete
|
||||
export type DeleteStart9ToRes = null
|
||||
|
||||
export type AddDomainReq = {
|
||||
hostname: string
|
||||
@@ -132,8 +151,7 @@ export module RR {
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
networkStrategy: string
|
||||
ipStrategy: string | null
|
||||
networkStrategy: NetworkStrategy
|
||||
} // net.domain.add
|
||||
export type AddDomainRes = null
|
||||
|
||||
@@ -347,6 +365,18 @@ export module RR {
|
||||
}
|
||||
export type SideloadPacakgeRes = string //guid
|
||||
|
||||
export type SetInterfaceClearnetAddressReq = SetServerClearnetAddressReq & {
|
||||
packageId: string
|
||||
interfaceId: string
|
||||
} // package.interface.set-clearnet
|
||||
export type SetInterfaceClearnetAddressRes = null
|
||||
|
||||
export type SetServiceOutboundProxyReq = {
|
||||
packageId: string
|
||||
proxy: ServiceOutboundProxy
|
||||
} // package.proxy.set-outbound
|
||||
export type SetServiceOutboundProxyRes = null
|
||||
|
||||
// marketplace
|
||||
|
||||
export type EnvInfo = {
|
||||
|
||||
@@ -125,6 +125,10 @@ export abstract class ApiService {
|
||||
|
||||
abstract toggleZram(params: RR.ToggleZramReq): Promise<RR.ToggleZramRes>
|
||||
|
||||
abstract setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes>
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
abstract marketplaceProxy<T>(
|
||||
@@ -150,15 +154,23 @@ export abstract class ApiService {
|
||||
params: RR.DeleteAllNotificationsReq,
|
||||
): Promise<RR.DeleteAllNotificationsRes>
|
||||
|
||||
// network
|
||||
|
||||
abstract addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes>
|
||||
|
||||
abstract updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes>
|
||||
|
||||
abstract deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes>
|
||||
|
||||
// domains
|
||||
|
||||
abstract claimStart9MeDomain(
|
||||
params: RR.ClaimStart9MeReq,
|
||||
): Promise<RR.ClaimStart9MeRes>
|
||||
abstract claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes>
|
||||
|
||||
abstract deleteStart9MeDomain(
|
||||
params: RR.DeleteStart9MeReq,
|
||||
): Promise<RR.DeleteStart9MeRes>
|
||||
abstract deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes>
|
||||
|
||||
abstract addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes>
|
||||
|
||||
@@ -322,4 +334,12 @@ export abstract class ApiService {
|
||||
abstract getSetupStatus(): Promise<SetupStatus | null>
|
||||
|
||||
abstract followLogs(): Promise<string>
|
||||
|
||||
abstract setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes>
|
||||
|
||||
abstract setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes>
|
||||
}
|
||||
|
||||
@@ -233,6 +233,12 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.experimental.zram', params })
|
||||
}
|
||||
|
||||
async setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes> {
|
||||
return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async marketplaceProxy<T>(
|
||||
@@ -288,17 +294,31 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
// network
|
||||
|
||||
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.add', params })
|
||||
}
|
||||
|
||||
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.update', params })
|
||||
}
|
||||
|
||||
async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
return this.rpcRequest({ method: 'net.proxy.delete', params })
|
||||
}
|
||||
|
||||
// domains
|
||||
|
||||
async claimStart9MeDomain(
|
||||
params: RR.ClaimStart9MeReq,
|
||||
): Promise<RR.ClaimStart9MeRes> {
|
||||
async claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.me.claim', params })
|
||||
}
|
||||
|
||||
async deleteStart9MeDomain(
|
||||
params: RR.DeleteStart9MeReq,
|
||||
): Promise<RR.DeleteStart9MeRes> {
|
||||
async deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes> {
|
||||
return this.rpcRequest({ method: 'net.domain.me.delete', params })
|
||||
}
|
||||
|
||||
@@ -544,6 +564,18 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
||||
return this.rpcRequest({ method: 'package.interface.set-clearnet', params })
|
||||
}
|
||||
|
||||
async setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
|
||||
}
|
||||
|
||||
async getSetupStatus() {
|
||||
return this.rpcRequest<SetupStatus | null>({
|
||||
method: 'setup.status',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackageState,
|
||||
Proxy,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
@@ -371,6 +372,21 @@ export class MockApiService extends ApiService {
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
async setOsOutboundProxy(
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/network/outboundProxy',
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async marketplaceProxy(
|
||||
@@ -439,36 +455,97 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// network
|
||||
|
||||
async addProxy(params: RR.AddProxyReq): Promise<RR.AddProxyRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const type: Proxy['type'] = 'inbound-outbound'
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/network/proxies',
|
||||
value: [
|
||||
{
|
||||
id: 'abcd-efgh-ijkl-mnop',
|
||||
name: params.name,
|
||||
createdAt: new Date(),
|
||||
type,
|
||||
endpoint: '10.25.2.17',
|
||||
usedBy: {
|
||||
domains: [],
|
||||
services: [],
|
||||
},
|
||||
primaryInbound: type === 'inbound-outbound' ? true : false,
|
||||
primaryOutbound:
|
||||
type === 'inbound-outbound' || type === 'outbound' ? true : false,
|
||||
// primaryInbound: false,
|
||||
// primaryOutbound: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const value = params.name || params.primaryInbound || params.primaryOutbound
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/server-info/network/proxies/0/${Object.keys(params)[0]}`,
|
||||
value,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/network/proxies',
|
||||
value: [],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
// domains
|
||||
|
||||
async claimStart9MeDomain(
|
||||
params: RR.ClaimStart9MeReq,
|
||||
): Promise<RR.ClaimStart9MeRes> {
|
||||
async claimStart9ToDomain(
|
||||
params: RR.ClaimStart9ToReq,
|
||||
): Promise<RR.ClaimStart9ToRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/network/start9MeSubdomain',
|
||||
path: '/server-info/network/start9ToSubdomain',
|
||||
value: {
|
||||
value: 'xyz',
|
||||
createdAt: new Date(),
|
||||
networkStrategy: params.networkStrategy,
|
||||
ipStrategy: params.ipStrategy,
|
||||
usedBy: [],
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
async deleteStart9MeDomain(
|
||||
params: RR.DeleteStart9MeReq,
|
||||
): Promise<RR.DeleteStart9MeRes> {
|
||||
async deleteStart9ToDomain(
|
||||
params: RR.DeleteStart9ToReq,
|
||||
): Promise<RR.DeleteStart9ToRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/network/start9MeSubdomain',
|
||||
path: '/server-info/network/start9ToSubdomain',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
@@ -485,10 +562,10 @@ export class MockApiService extends ApiService {
|
||||
value: [
|
||||
{
|
||||
value: params.hostname,
|
||||
createdAt: new Date(),
|
||||
provider: params.provider.name,
|
||||
networkStrategy: params.networkStrategy,
|
||||
ipStrategy: params.ipStrategy,
|
||||
createdAt: new Date(),
|
||||
usedBy: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1109,6 +1186,34 @@ export class MockApiService extends ApiService {
|
||||
return 'fake-guid'
|
||||
}
|
||||
|
||||
async setInterfaceClearnetAddress(
|
||||
params: RR.SetInterfaceClearnetAddressReq,
|
||||
): Promise<RR.SetInterfaceClearnetAddressRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${params.packageId}/installed/interfaceInfo/${params.interfaceId}/addressInfo/domainInfo`,
|
||||
value: params.domainInfo,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
async setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/package-data/${params.packageId}/installed/outboundProxy`,
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
}
|
||||
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
const progress = { ...PROGRESS }
|
||||
const phases = [
|
||||
|
||||
@@ -45,11 +45,6 @@ export const mockPatchData: DataModel = {
|
||||
eth0: {
|
||||
wireless: false,
|
||||
ipv4: '10.0.0.1',
|
||||
ipv6: null,
|
||||
},
|
||||
wlan0: {
|
||||
wireless: true,
|
||||
ipv4: '10.0.90.12',
|
||||
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD',
|
||||
},
|
||||
},
|
||||
@@ -57,7 +52,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
network: {
|
||||
domains: [],
|
||||
start9MeSubdomain: null,
|
||||
start9ToSubdomain: null,
|
||||
wifi: {
|
||||
enabled: false,
|
||||
lastRegion: null,
|
||||
@@ -85,6 +80,12 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
],
|
||||
},
|
||||
proxies: [],
|
||||
primaryProxies: {
|
||||
inbound: null,
|
||||
outbound: null,
|
||||
},
|
||||
outboundProxy: null,
|
||||
},
|
||||
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
'unread-notification-count': 4,
|
||||
@@ -105,7 +106,6 @@ export const mockPatchData: DataModel = {
|
||||
from: '',
|
||||
login: '',
|
||||
password: '',
|
||||
tls: true,
|
||||
},
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import {
|
||||
InstalledPackageInfo,
|
||||
PackageMainStatus,
|
||||
InterfaceInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
|
||||
const {
|
||||
@@ -30,8 +30,6 @@ export class ConfigService {
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = (window as any)['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
|
||||
isTor(): boolean {
|
||||
return (
|
||||
@@ -39,23 +37,65 @@ export class ConfigService {
|
||||
)
|
||||
}
|
||||
|
||||
isLan(): boolean {
|
||||
isLocal(): boolean {
|
||||
return (
|
||||
this.hostname.endsWith('.local') || (useMocks && mocks.maskAs === 'local')
|
||||
)
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return (
|
||||
this.hostname === 'localhost' ||
|
||||
this.hostname.endsWith('.local') ||
|
||||
(useMocks && mocks.maskAs === 'lan')
|
||||
(useMocks && mocks.maskAs === 'localhost')
|
||||
)
|
||||
}
|
||||
|
||||
isIpv4(): boolean {
|
||||
return isValidIpv4(this.hostname) || (useMocks && mocks.maskAs === 'ipv4')
|
||||
}
|
||||
|
||||
isIpv6(): boolean {
|
||||
return isValidIpv6(this.hostname) || (useMocks && mocks.maskAs === 'ipv6')
|
||||
}
|
||||
|
||||
isClearnet(): boolean {
|
||||
return (
|
||||
(useMocks && mocks.maskAs === 'clearnet') ||
|
||||
(!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isIpv4() &&
|
||||
!this.isIpv6())
|
||||
)
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
|
||||
launchableAddress(info: InterfaceInfo): string {
|
||||
return this.isTor()
|
||||
? info.addressInfo.torHostname
|
||||
: this.isLocalhost()
|
||||
? `https://${info.addressInfo.lanHostname}`
|
||||
: this.isLocal() || this.isIpv4() || this.isIpv6()
|
||||
? `https://${this.hostname}`
|
||||
: info.addressInfo.domainInfo?.subdomain
|
||||
? `https://${info.addressInfo.domainInfo.subdomain}${info.addressInfo.domainInfo.domain}`
|
||||
: `https://${info.addressInfo.domainInfo?.domain}`
|
||||
}
|
||||
}
|
||||
|
||||
export function hasUi(
|
||||
addressInfo: InstalledPackageInfo['address-info'],
|
||||
): boolean {
|
||||
return !!Object.values(addressInfo).find(a => a.ui)
|
||||
export function isValidIpv4(address: string): boolean {
|
||||
const regexExp =
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||
return regexExp.test(address)
|
||||
}
|
||||
|
||||
export function isValidIpv6(address: string): boolean {
|
||||
const regexExp =
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi
|
||||
return regexExp.test(address)
|
||||
}
|
||||
|
||||
export function removeProtocol(str: string): string {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Url } from '@start9labs/shared'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import { BackupJob } from '../api/api.types'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
import { NetworkInterfaceType } from '@start9labs/start-sdk/lib/util/utils'
|
||||
|
||||
export interface DataModel {
|
||||
'server-info': ServerInfo
|
||||
@@ -55,7 +56,7 @@ export interface ServerInfo {
|
||||
id: string
|
||||
version: string
|
||||
country: string
|
||||
ui: StartOsUiInfo
|
||||
ui: AddressInfo
|
||||
network: NetworkInfo
|
||||
'last-backup': string | null
|
||||
'unread-notification-count': number
|
||||
@@ -69,21 +70,20 @@ export interface ServerInfo {
|
||||
'password-hash': string
|
||||
}
|
||||
|
||||
export type StartOsUiInfo = {
|
||||
ipInfo: IpInfo
|
||||
lanHostname: string
|
||||
torHostname: string
|
||||
domainInfo: DomainInfo | null
|
||||
}
|
||||
|
||||
export type NetworkInfo = {
|
||||
wifi: WiFiInfo
|
||||
start9MeSubdomain: Omit<Domain, 'provider'> | null
|
||||
start9ToSubdomain: Omit<Domain, 'provider'> | null
|
||||
domains: Domain[]
|
||||
wanConfig: {
|
||||
upnp: boolean
|
||||
forwards: PortForward[]
|
||||
}
|
||||
proxies: Proxy[]
|
||||
outboundProxy: OsOutboundProxy
|
||||
primaryProxies: {
|
||||
inbound: string | null
|
||||
outbound: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type DomainInfo = {
|
||||
@@ -91,6 +91,10 @@ export type DomainInfo = {
|
||||
subdomain: string | null
|
||||
}
|
||||
|
||||
export type InboundProxy = { proxyId: string } | 'primary' | null
|
||||
export type OsOutboundProxy = InboundProxy
|
||||
export type ServiceOutboundProxy = OsOutboundProxy | 'mirror'
|
||||
|
||||
export type PortForward = {
|
||||
assigned: number
|
||||
override: number | null
|
||||
@@ -105,10 +109,32 @@ export type WiFiInfo = {
|
||||
|
||||
export type Domain = {
|
||||
value: string
|
||||
provider: string
|
||||
networkStrategy: string
|
||||
ipStrategy: string
|
||||
createdAt: string
|
||||
provider: string
|
||||
networkStrategy: NetworkStrategy
|
||||
usedBy: {
|
||||
service: { id: string | null; title: string } // null means startos
|
||||
interfaces: { id: string | null; title: string }[] // null means startos
|
||||
}[]
|
||||
}
|
||||
|
||||
export type NetworkStrategy =
|
||||
| { proxyId: string | null } // null means system primary
|
||||
| { ipStrategy: 'ipv4' | 'ipv6' | 'dualstack' }
|
||||
|
||||
export type Proxy = {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string
|
||||
type: 'outbound' | 'inbound-outbound' | 'vlan' | { error: string }
|
||||
endpoint: string
|
||||
// below is overlay only
|
||||
usedBy: {
|
||||
services: { id: string | null; title: string }[] // implies outbound - null means startos
|
||||
domains: string[] // implies inbound
|
||||
}
|
||||
primaryInbound: boolean
|
||||
primaryOutbound: boolean
|
||||
}
|
||||
|
||||
export interface IpInfo {
|
||||
@@ -199,21 +225,29 @@ export interface InstalledPackageInfo {
|
||||
'installed-at': string
|
||||
'current-dependencies': Record<string, CurrentDependencyInfo>
|
||||
'dependency-info': Record<string, { title: string; icon: Url }>
|
||||
'address-info': Record<string, AddressInfo>
|
||||
interfaceInfo: Record<string, InterfaceInfo>
|
||||
'marketplace-url': string | null
|
||||
'developer-key': string
|
||||
'has-config': boolean
|
||||
outboundProxy: ServiceOutboundProxy
|
||||
}
|
||||
|
||||
export interface CurrentDependencyInfo {
|
||||
'health-checks': string[] // array of health check IDs
|
||||
}
|
||||
|
||||
export interface AddressInfo {
|
||||
export interface InterfaceInfo {
|
||||
name: string
|
||||
description: string
|
||||
addresses: Url[]
|
||||
ui: boolean
|
||||
type: NetworkInterfaceType
|
||||
addressInfo: AddressInfo
|
||||
}
|
||||
|
||||
export interface AddressInfo {
|
||||
ipInfo: IpInfo
|
||||
lanHostname: string
|
||||
torHostname: string
|
||||
domainInfo: DomainInfo | null
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
|
||||
161
frontend/projects/ui/src/app/services/proxy.service.ts
Normal file
161
frontend/projects/ui/src/app/services/proxy.service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
OsOutboundProxy,
|
||||
ServiceOutboundProxy,
|
||||
} from './patch-db/data-model'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormContext, FormPage } from '../apps/ui/modals/form/form.page'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProxyService {
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly api: ApiService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
||||
async presentModalSetOutboundProxy(serviceContext?: {
|
||||
packageId: string
|
||||
outboundProxy: ServiceOutboundProxy
|
||||
hasP2P: boolean
|
||||
}) {
|
||||
const network = await firstValueFrom(
|
||||
this.patch.watch$('server-info', 'network'),
|
||||
)
|
||||
|
||||
const outboundProxy = serviceContext?.outboundProxy
|
||||
|
||||
const defaultValue = !outboundProxy
|
||||
? 'none'
|
||||
: outboundProxy === 'primary'
|
||||
? 'primary'
|
||||
: outboundProxy === 'mirror'
|
||||
? 'mirror'
|
||||
: 'other'
|
||||
|
||||
let variants: Record<string, { name: string; spec: Config<any> }> = {}
|
||||
|
||||
if (serviceContext) {
|
||||
variants['mirror'] = {
|
||||
name: 'Mirror P2P Interface',
|
||||
spec: Config.of({}),
|
||||
}
|
||||
}
|
||||
|
||||
variants = {
|
||||
...variants,
|
||||
primary: {
|
||||
name: 'Use System Primary',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: Config.of({
|
||||
proxyId: Value.select({
|
||||
name: 'Select Specific Proxy',
|
||||
required: {
|
||||
default:
|
||||
outboundProxy && typeof outboundProxy !== 'string'
|
||||
? outboundProxy.proxyId
|
||||
: null,
|
||||
},
|
||||
values: network.proxies
|
||||
.filter(
|
||||
p => p.type === 'outbound' || p.type === 'inbound-outbound',
|
||||
)
|
||||
.reduce((prev, curr) => {
|
||||
return {
|
||||
[curr.id]: curr.name,
|
||||
...prev,
|
||||
}
|
||||
}, {}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
none: {
|
||||
name: 'None',
|
||||
spec: Config.of({}),
|
||||
},
|
||||
}
|
||||
|
||||
const config = Config.of({
|
||||
proxy: Value.union(
|
||||
{
|
||||
name: 'Select Proxy',
|
||||
required: { default: defaultValue },
|
||||
description: `
|
||||
<h5>Use System Primary</h5>The primary <i>inbound</i> proxy will be used. If you do not have a primary inbound proxy, no proxy will be used
|
||||
<h5>Mirror Primary Interface</h5>If you have an inbound proxy enabled for the primary interface, outbound traffic will flow through the same proxy
|
||||
<h5>Other</h5>The specific proxy you select will be used, overriding the default
|
||||
`,
|
||||
disabled: serviceContext?.hasP2P ? [] : ['mirror'],
|
||||
},
|
||||
Variants.of(variants),
|
||||
),
|
||||
})
|
||||
|
||||
const options: Partial<
|
||||
TuiDialogOptions<FormContext<typeof config.validator._TYPE>>
|
||||
> = {
|
||||
label: 'Outbound Proxy',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(config),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Manage proxies',
|
||||
link: '/system/proxies',
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async value => {
|
||||
const proxy =
|
||||
value.proxy.unionSelectKey === 'none'
|
||||
? null
|
||||
: value.proxy.unionSelectKey === 'primary'
|
||||
? 'primary'
|
||||
: value.proxy.unionSelectKey === 'mirror'
|
||||
? 'mirror'
|
||||
: { proxyId: value.proxy.unionValueKey.proxyId }
|
||||
await this.saveOutboundProxy(proxy, serviceContext?.packageId)
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
this.formDialog.open(FormPage, options)
|
||||
}
|
||||
|
||||
private async saveOutboundProxy(
|
||||
proxy: OsOutboundProxy | ServiceOutboundProxy,
|
||||
packageId?: string,
|
||||
) {
|
||||
const loader = this.loader.open(`Saving`).subscribe()
|
||||
|
||||
try {
|
||||
if (packageId) {
|
||||
await this.api.setServiceOutboundProxy({ packageId, proxy })
|
||||
} else {
|
||||
await this.api.setOsOutboundProxy({ proxy: proxy as OsOutboundProxy })
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
|
||||
export async function configBuilderToSpec(
|
||||
builder:
|
||||
| Config<Record<string, unknown>, unknown, unknown>
|
||||
| Config<Record<string, unknown>, never, never>,
|
||||
| Config<Record<string, unknown>, unknown>
|
||||
| Config<Record<string, unknown>, never>,
|
||||
) {
|
||||
return builder.build({} as any)
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ h2 {
|
||||
scrollbar-width: none;
|
||||
|
||||
ion-grid {
|
||||
min-width: 840px;
|
||||
min-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user