diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d0ea5b0e3..5fff0f288 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -22,7 +22,7 @@
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0",
"@start9labs/emver": "^0.1.5",
- "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc5",
+ "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
"@taiga-ui/addon-charts": "3.38.0",
"@taiga-ui/cdk": "3.38.0",
"@taiga-ui/core": "3.38.0",
@@ -3976,11 +3976,12 @@
"integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg=="
},
"node_modules/@start9labs/start-sdk": {
- "version": "0.4.0-rev0.lib0.rc5",
- "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0-rev0.lib0.rc5.tgz",
- "integrity": "sha512-2hAJE1id0VgpU8DJt/I+m/IEePmnspzF8BxUoLO3C+ZgyOZU1tEri1f9QCsS6OLn3J11xPlpY1VuSjP5CyHC+Q==",
+ "version": "0.4.0-rev0.lib0.rc8.beta2",
+ "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0-rev0.lib0.rc8.beta2.tgz",
+ "integrity": "sha512-2jo8gF/lOvzuOKKntPuQyejwDAY6Uxaz4KKqm2awoYN6Ycn1TrYud0KAdSjKFYDCKmJI/guQNej0XGVJe0B1XQ==",
"dependencies": {
"@iarna/toml": "^2.2.5",
+ "isomorphic-fetch": "^3.0.0",
"ts-matches": "^5.4.1",
"yaml": "^2.2.2"
}
@@ -6793,7 +6794,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
- "dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
@@ -6803,7 +6803,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -8836,6 +8835,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/isomorphic-fetch": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
+ "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
+ "dependencies": {
+ "node-fetch": "^2.6.1",
+ "whatwg-fetch": "^3.4.1"
+ }
+ },
"node_modules/istanbul-lib-coverage": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
@@ -10431,6 +10439,25 @@
"dev": true,
"optional": true
},
+ "node_modules/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -13848,6 +13875,11 @@
"node": ">=6"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -14436,6 +14468,11 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
"node_modules/webpack": {
"version": "5.88.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
@@ -14844,6 +14881,20 @@
"node": ">=0.8.0"
}
},
+ "node_modules/whatwg-fetch": {
+ "version": "3.6.17",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz",
+ "integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 247cb2985..c37ed294d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -70,7 +70,7 @@
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
- "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc5",
+ "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
diff --git a/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts
index ff4a1aeae..b8e13c7ad 100644
--- a/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts
+++ b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts
@@ -12,7 +12,7 @@ export class StoreIconComponent {
url = ''
@Input()
size?: string
- @Input()
+ @Input({ required: true })
marketplace!: MarketplaceConfig
get icon() {
diff --git a/frontend/projects/marketplace/src/pages/list/item/item.component.ts b/frontend/projects/marketplace/src/pages/list/item/item.component.ts
index 36398efe6..d6d924acb 100644
--- a/frontend/projects/marketplace/src/pages/list/item/item.component.ts
+++ b/frontend/projects/marketplace/src/pages/list/item/item.component.ts
@@ -7,6 +7,6 @@ import { MarketplacePkg } from '../../../types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
}
diff --git a/frontend/projects/marketplace/src/pages/show/about/about.component.ts b/frontend/projects/marketplace/src/pages/show/about/about.component.ts
index 6626d4fbe..55d95bbfc 100644
--- a/frontend/projects/marketplace/src/pages/show/about/about.component.ts
+++ b/frontend/projects/marketplace/src/pages/show/about/about.component.ts
@@ -8,6 +8,6 @@ import { MarketplacePkg } from '../../../types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent {
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
}
diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts
index 6814aad6c..4a19abb2c 100644
--- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts
+++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts
@@ -31,7 +31,7 @@ import { AbstractMarketplaceService } from '../../../services/marketplace.servic
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdditionalComponent {
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
@Output()
diff --git a/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts
index a6ecb103f..40e7c3ff0 100644
--- a/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts
+++ b/frontend/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts
@@ -7,7 +7,7 @@ import { MarketplacePkg } from '../../../types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
getImg(key: string): string {
diff --git a/frontend/projects/marketplace/src/pages/show/package/package.component.ts b/frontend/projects/marketplace/src/pages/show/package/package.component.ts
index 08da8aa51..8b8ebc591 100644
--- a/frontend/projects/marketplace/src/pages/show/package/package.component.ts
+++ b/frontend/projects/marketplace/src/pages/show/package/package.component.ts
@@ -8,6 +8,6 @@ import { MarketplacePkg } from '../../../types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PackageComponent {
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
}
diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts
index 66cab3eff..c0042cd95 100644
--- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts
+++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts
@@ -128,7 +128,7 @@ export class RecoverPage {
styleUrls: ['./recover.page.scss'],
})
export class DriveStatusComponent {
- @Input() hasValidBackup!: boolean
+ @Input({ required: true }) hasValidBackup!: boolean
}
interface MappedDisk {
diff --git a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts
index 124afbfea..6d8fb54d1 100644
--- a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts
+++ b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.ts
@@ -5,7 +5,7 @@ import { Component, Input } from '@angular/core'
templateUrl: 'download-doc.component.html',
})
export class DownloadDocComponent {
- @Input() lanAddress!: string
+ @Input({ required: true }) lanAddress!: string
get crtName(): string {
const hostname = new URL(this.lanAddress).hostname
diff --git a/frontend/projects/shared/src/types/workspace-config.ts b/frontend/projects/shared/src/types/workspace-config.ts
index 2da7d3f8d..6a7669b98 100644
--- a/frontend/projects/shared/src/types/workspace-config.ts
+++ b/frontend/projects/shared/src/types/workspace-config.ts
@@ -12,7 +12,7 @@ export type WorkspaceConfig = {
}
marketplace: MarketplaceConfig
mocks: {
- maskAs: 'tor' | 'lan'
+ maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet'
skipStartupAlerts: boolean
}
}
diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts
index c3f9c6fb1..9db493975 100644
--- a/frontend/projects/ui/src/app/app.module.ts
+++ b/frontend/projects/ui/src/app/app.module.ts
@@ -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],
diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts
index cfa9e0321..de77bb500 100644
--- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts
+++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts
@@ -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',
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts
index 353fcdc14..8a2ca1733 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts
@@ -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
}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/types/target-types.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/types/target-types.ts
index 4e38f7570..fa129fdef 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/backups/types/target-types.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/types/target-types.ts
@@ -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'],
}),
})
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts
index 9dc921ea8..4c9465ad9 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts
@@ -41,7 +41,7 @@ export class MarketplaceShowControlsComponent {
@Input()
url?: string
- @Input()
+ @Input({ required: true })
pkg!: MarketplacePkg
@Input()
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-dependent/marketplace-show-dependent.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-dependent/marketplace-show-dependent.component.ts
index 76c648867..3e28b783b 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-dependent/marketplace-show-dependent.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-dependent/marketplace-show-dependent.component.ts
@@ -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 =
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-status/marketplace-status.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-status/marketplace-status.component.ts
index 05e36471b..3c4ad7c8a 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-status/marketplace-status.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-status/marketplace-status.component.ts
@@ -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
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts
index 42e354d06..4e9f90475 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts
@@ -188,7 +188,7 @@ interface LocalAction {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppActionsItemComponent {
- @Input() action!: LocalAction
+ @Input({ required: true }) action!: LocalAction
}
@Pipe({
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.module.ts
new file mode 100644
index 000000000..7d418114b
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.html
new file mode 100644
index 000000000..21f4b6c18
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ {{ interfaceInfo.name }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.ts
new file mode 100644
index 000000000..a9d57952a
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interface/app-interface.page.ts
@@ -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,
+ ) {}
+}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html
deleted file mode 100644
index c04293e09..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
- {{ addressInfo.name }}
- {{ addressInfo.description }}
-
-
-
-
-
- {{ address | addressType }}
- {{ address }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.module.ts
deleted file mode 100644
index 966905eae..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.module.ts
+++ /dev/null
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.html
deleted file mode 100644
index 3178172a8..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
- Interfaces
-
-
-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.scss
deleted file mode 100644
index 79823db59..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-p {
- font-family: 'Courier New';
-}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts
deleted file mode 100644
index 52537ac0e..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts
+++ /dev/null
@@ -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,
- ) {}
-}
-
-@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()
- }
-}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-icon/app-list-icon.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-icon/app-list-icon.component.ts
index ccc4cd0c8..2692a91fe 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-icon/app-list-icon.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-icon/app-list-icon.component.ts
@@ -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$
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.html
index 6cd49a9f8..52c2bf14a 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.html
@@ -20,19 +20,27 @@
>
-
-
-
-
+ 1
+ ? openPopover($event)
+ : launchUI(launchable[0].address, $event)
+ "
+ [disabled]="status !== 'running'"
+ >
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.ts
index 902391c28..dcb792718 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/app-list-pkg.component.ts
@@ -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')
+ }
}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.html
new file mode 100644
index 000000000..c54aa4b25
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ {{ iface.name }}
+ {{ iface.address }}
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.scss b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.scss
similarity index 100%
rename from frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.scss
rename to frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.scss
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.ts
similarity index 85%
rename from frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.ts
rename to frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.ts
index 1c22bf891..b6f4bfd22 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.component.ts
@@ -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
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.module.ts
similarity index 74%
rename from frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.module.ts
rename to frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.module.ts
index ccbe8fa7d..eba343572 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launch-menu/launch-menu.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launchable-interfaces.pipe.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launchable-interfaces.pipe.ts
new file mode 100644
index 000000000..8da2a0319
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list-pkg/launchable-interfaces.pipe.ts
@@ -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
+}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list.module.ts
index b1c8b5ec8..b0dbbc7af 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-list/app-list.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts
index ee45f5f2b..60d7e114a 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.module.ts
@@ -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,
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.page.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.page.html
index cbbaf402c..4629a85eb 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.page.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/app-show.page.html
@@ -27,6 +27,8 @@
>
+
+
-
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts
index a61101cd0..bd35ee3ae 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts
@@ -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(
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-header/app-show-header.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-header/app-show-header.component.ts
index fd234a9e9..0d37b9af1 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-header/app-show-header.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-header/app-show-header.component.ts
@@ -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
}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-health-checks/app-show-health-checks.component.ts
index e6cb90951..f2b5e96ce 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-health-checks/app-show-health-checks.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-health-checks/app-show-health-checks.component.ts
@@ -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$
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.html
new file mode 100644
index 000000000..737b5bbc1
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.html
@@ -0,0 +1,25 @@
+Interfaces
+
+
+
+ {{ info.name }}
+ {{ info.description }}
+
+ {{ info.typeDetail }}
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.scss b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.ts
new file mode 100644
index 000000000..d248a0a3a
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-interfaces/app-show-interfaces.component.ts
@@ -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,
+ }
+ })
+ }
+}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.html
index d872fdd52..161f0ecbf 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.html
@@ -1,15 +1,96 @@
Menu
+
+
-
+
- {{ button.title }}
- {{ button.description }}
+ Instructions
+ Understand how to use {{ pkg.manifest.title }}
+
+
+
+
+
+ Config
+ Customize {{ pkg.manifest.title }}
+
+
+
+
+
+
+
+ Credentials
+ Password, keys, or other credentials of interest
+
+
+
+
+
+
+
+ Actions
+ Uninstall and other commands specific to {{ pkg.manifest.title }}
+
+
+
+
+
+
+
+ Outbound Proxy
+ Proxy all outbound traffic from {{ pkg.manifest.title }}
+
+
+ {{
+ !proxy.value
+ ? 'None'
+ : proxy.value === 'primary'
+ ? 'System Primary'
+ : proxy.value === 'mirror'
+ ? 'Mirror P2P'
+ : proxy.value.proxyId
+ }}
+
+
+
+
+
+
+
+
+
+ Logs
+ Raw, unfiltered logs
+
+
+
+
+
+
+
+ Marketplace Listing
+ View service in the marketplace
+
+
+
+
+
+
+ Marketplace Listing
+ This package was not installed from the marketplace
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.ts
index 866d3e01f..237429bd2 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-menu/app-show-menu.component.ts
@@ -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,
+ private readonly proxyService: ProxyService,
+ ) {}
+
+ async presentModalInstructions() {
+ const { id, version } = this.pkg.manifest
+
+ this.api
+ .setDbValue(['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(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,
+ })
+ }
}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-progress/app-show-progress.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-progress/app-show-progress.component.ts
index 8ee7b750a..f902d42f0 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-progress/app-show-progress.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-progress/app-show-progress.component.ts
@@ -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 {
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.html
index 5cace82aa..87cb77384 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.html
@@ -48,19 +48,6 @@
Configure
-
-
-
- Open UI
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts
index 91eb1f7f0..938f1e398 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts
@@ -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 {
- return this.pkg.installed!['address-info']
+ get interfaceInfo(): Record {
+ 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(AppConfigPage, {
label: `${this.pkg.manifest.title} configuration`,
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts
deleted file mode 100644
index cf8252b75..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts
+++ /dev/null
@@ -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
- 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,
- ) {}
-
- 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(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(['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
- }
-}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.html
deleted file mode 100644
index 474a9e7e4..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/launch-menu/launch-menu.component.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
- {{ address.name }}
-
-
- {{ address | addressType }}
- {{ address }}
-
-
-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/services.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/services.module.ts
index 858d4d9a7..6f2b8c9ff 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/services.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/services.module.ts
@@ -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({
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/status/status.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/status/status.component.ts
index 28077098b..0535030b2 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/status/status.component.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/services/status/status.component.ts
@@ -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'
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.module.ts
deleted file mode 100644
index 24d4f0009..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.module.ts
+++ /dev/null
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.pipe.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.pipe.ts
deleted file mode 100644
index cce274115..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/services/ui-pipes/ui.pipe.ts
+++ /dev/null
@@ -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)
-}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts
index 63ad3777e..2d93ab758 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts
@@ -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: `
-IPv6 Only Pros : Ready for IPv6 Internet. Enhanced privacy, as IPv6 addresses are less correlated with geographic area
-Cons : Your website is only accessible to people who's ISP supports IPv6
-IPv6 and IPv4 Pros : Ready for IPv6 Internet. Anyone can access your website
-Cons : IPv4 addresses are closely correlated with geographic areas
-IPv4 Only Pros : Anyone can access your website
-Cons : 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: `Local 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
+Proxy 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) or 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: `IPv6 Only (recommended) Requirements :ISP IPv6 support OpenWRT (recommended) or Linksys router Pros : Ready for IPv6 Internet. Enhanced privacy. Run multiple clearnet servers from the same network
+Cons : Interfaces using this domain will only be accessible to people whose ISP supports IPv6
+IPv6 and IPv4 Pros : Ready for IPv6 Internet. Accessible by anyone
+Cons : Less private, as IPv4 addresses are closely correlated with geographic areas. Cannot run multiple clearnet servers from the same network
+IPv4 Only Pros : Accessible by anyone
+Cons : 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: `Primary Use the Primary Inbound proxy from your proxy settings. If you do not have any inbound proxies, no proxy will be used
+Other 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),
+ }),
+ )
+}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html
index 3bb2065bb..a14af00d1 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html
@@ -10,21 +10,20 @@
- 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.
View instructions
- Start9.me
+ Start9.to
Claim
@@ -35,26 +34,27 @@
Domain
- Added
+ Added
DDNS Provider
- Network Strategy
- IP Strategy
- In Use
-
+ Network Strategy
+ Used By
+
- {{ start9Me.value }}
- {{ start9Me.createdAt| date: 'short' }}
+ {{ start9To.value }}
+ {{ start9To.createdAt| date: 'short' }}
Start9
- {{ start9Me.networkStrategy }}
- {{ start9Me.ipStrategy || 'N/A' }}
-
+
+ {{ $any(start9To.networkStrategy).ipStrategy ||
+ $any(start9To.networkStrategy).proxyId || 'Primary Proxy' }}
+
+
{{ qty }} Interfaces
@@ -62,9 +62,9 @@
N/A
-
+
-
+
@@ -90,22 +90,23 @@
Domain
- Added
+ Added
DDNS Provider
- Network Strategy
- IP Strategy
- In Use
-
+ Network Strategy
+ Used By
+
{{ domain.value }}
- {{ domain.createdAt| date: 'short' }}
+ {{ domain.createdAt| date: 'short' }}
{{ domain.provider }}
- {{ domain.networkStrategy }}
- {{ domain.ipStrategy || 'N/A' }}
+
+ {{ $any(domain.networkStrategy).ipStrategy ||
+ $any(domain.networkStrategy).proxyId || 'Primary Proxy' }}
+
N/A
-
+
{
+ 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,
) {}
async presentModalAdd() {
- const options: Partial>> = {
+ const proxies = await firstValueFrom(
+ this.patch.watch$('server-info', 'network', 'proxies'),
+ )
+
+ const options: Partial>> = {
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>> = {
- label: 'start9.me',
+ async presentModalClaimStart9To() {
+ const proxies = await firstValueFrom(
+ this.patch.watch$('server-info', 'network', 'proxies'),
+ )
+
+ const options: Partial>> = {
+ 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:${usedBy.map(
- u => `${u} `,
+ `${domain} is currently being used by:${usedBy.map(u =>
+ u.interfaces.map(i => `${u.service.title} - ${i.title} `),
)} `,
{
label: 'Used by',
@@ -153,17 +134,23 @@ export class DomainsPage {
.subscribe()
}
- private async claimStart9MeDomain(value: Start9MeSpec): Promise {
+ private async claimStart9ToDomain(value: any): Promise {
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 {
+ private async save(value: any): Promise {
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 {
+ private async deleteStart9ToDomain(): Promise {
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),
- )
-}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts
deleted file mode 100644
index 5eb444af1..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts
+++ /dev/null
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html
deleted file mode 100644
index 1f24cbd57..000000000
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
- StartOS Web Interface
-
-
-
-
-
-
-
Clearnet
-
-
-
-
- Clearnet provides a fast and convenient experience. It not not
- provide anonymity, and the addresses can be discovered and accessed
- by anyone.
-
- View instructions
-
-
-
-
-
-
-
- Clearnet
- {{ clearnetAddress }}
-
-
- Update
-
-
- Remove
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add Clearnet Address
-
-
-
-
-
-
-
Tor
-
-
-
-
- Tor offers privacy and anonymity at the expense of speed and
- reliability. A Tor-enabled browser is required to use a Tor address.
-
- View instructions
-
-
-
-
-
-
- Tor
- {{ torHostname }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
LAN
-
-
-
-
- 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.
-
- View instructions
-
-
-
-
-
- Download Root CA
-
-
-
-
-
-
- Local
- {{ lanHostname }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ iface.key }} (IPv4)
- {{ ipv4 }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ iface.key }} (IPv6)
- {{ ipv6 }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.module.ts
new file mode 100644
index 000000000..fe6410830
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.html
new file mode 100644
index 000000000..133765a87
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.html
@@ -0,0 +1,144 @@
+
+
+
+
+
+ Proxies
+
+
+
+
+
+
+ Currently, StartOS only supports Wireguard proxies, which can be used for:
+
+
+ Proxying
+ outbound
+ traffic to mask your home/business IP from other servers accessed by
+ your server/services
+
+
+ Proxying
+ inbound
+ traffic to mask your home/business IP from anyone accessing your
+ server/services over clearnet
+
+
+ Creating a Virtual Local Area Network (VLAN) to enable private, remote
+ VPN access to your server/services
+
+
+ View instructions
+
+
+
+
+
+ Proxies
+
+
+ Add Proxy
+
+
+
+
+
+
+ Name
+ Created
+ Type
+ Primary
+ Used By
+
+
+
+ {{ proxy.name }}
+ {{ proxy.createdAt| date: 'short' }}
+ {{ proxy.type }}
+
+
+
+
+
+
+ {{ usedBy.domains.length + usedBy.services.length }} Connections
+
+
+ N/A
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make Primary Inbound
+
+
+ Make Primary Outbound
+
+
+ Rename
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.ts
new file mode 100644
index 000000000..6ba862396
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/proxies/proxies.page.ts
@@ -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,
+ private readonly formDialog: FormDialogService,
+ ) {}
+
+ async presentModalAdd() {
+ const options: Partial>> = {
+ 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>> = {
+ 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}Domains (inbound) ${usedBy.domains.map(
+ d => `${d} `,
+ )} `
+ }
+ if (usedBy.services.length) {
+ message = `${message}Services (outbound) ${usedBy.services.map(
+ s => `${s.title} `,
+ )}`
+ }
+
+ this.dialogs
+ .open(message, {
+ label: 'Used by',
+ size: 's',
+ })
+ .subscribe()
+ }
+
+ private async save(value: WireguardSpec): Promise {
+ 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 {
+ 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 {
+ 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
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.module.ts
similarity index 75%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts
rename to frontend/projects/ui/src/app/apps/ui/pages/system/router/router.module.ts
index b81c53fc3..1f745495b 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.html
similarity index 100%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html
rename to frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.html
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.scss
similarity index 100%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss
rename to frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.scss
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.ts
similarity index 89%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts
rename to frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.ts
index fcb977bba..c02027ab1 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/router/router.page.ts
@@ -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 = {}
overrides: Record = {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html
index 9c9dc22fb..9c8f86dd9 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html
@@ -66,6 +66,16 @@
+
+
+
+ {{ !server.network.outboundProxy ? 'None' :
+ server.network.outboundProxy === 'primary' ? 'System Primary' :
+ server.network.outboundProxy.proxyId }}
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts
index feeedb260..350078449 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts
@@ -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,
@@ -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(['name'], value)
+ await this.api.setDbValue(['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,
'.You will need to physically power cycle the device to regain connectivity. ',
@@ -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: () =>
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts
index 77bb24b69..b7096753f 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts
@@ -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: () =>
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.module.ts
new file mode 100644
index 000000000..c347dd42b
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.html
new file mode 100644
index 000000000..0b7f18ccb
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ StartOS UI
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.ts
new file mode 100644
index 000000000..51c1297a6
--- /dev/null
+++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ui-details/ui-details.page.ts
@@ -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) {}
+}
diff --git a/frontend/projects/ui/src/app/common/form/form-array/form-array.component.ts b/frontend/projects/ui/src/app/common/form/form-array/form-array.component.ts
index f65f55d24..40f441a63 100644
--- a/frontend/projects/ui/src/app/common/form/form-array/form-array.component.ts
+++ b/frontend/projects/ui/src/app/common/form/form-array/form-array.component.ts
@@ -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')
diff --git a/frontend/projects/ui/src/app/common/form/form-control/form-control.component.ts b/frontend/projects/ui/src/app/common/form/form-control/form-control.component.ts
index 413a52f2a..1396822f5 100644
--- a/frontend/projects/ui/src/app/common/form/form-control/form-control.component.ts
+++ b/frontend/projects/ui/src/app/common/form/form-control/form-control.component.ts
@@ -28,7 +28,7 @@ export class FormControlComponent<
T extends ValueSpec,
V,
> extends AbstractTuiNullableControl {
- @Input()
+ @Input({ required: true })
spec!: T
@ViewChild('warning')
diff --git a/frontend/projects/ui/src/app/common/form/form-object/form-object.component.ts b/frontend/projects/ui/src/app/common/form/form-object/form-object.component.ts
index cb8a5c9e9..459fc01c5 100644
--- a/frontend/projects/ui/src/app/common/form/form-object/form-object.component.ts
+++ b/frontend/projects/ui/src/app/common/form/form-object/form-object.component.ts
@@ -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()
diff --git a/frontend/projects/ui/src/app/common/form/form-union/form-union.component.ts b/frontend/projects/ui/src/app/common/form/form-union/form-union.component.ts
index 6e8251df8..f438fab87 100644
--- a/frontend/projects/ui/src/app/common/form/form-union/form-union.component.ts
+++ b/frontend/projects/ui/src/app/common/form/form-union/form-union.component.ts
@@ -28,7 +28,7 @@ import { tuiPure } from '@taiga-ui/cdk'
],
})
export class FormUnionComponent implements OnChanges {
- @Input()
+ @Input({ required: true })
spec!: ValueSpecUnion
selectSpec!: ValueSpecSelect
diff --git a/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses-item.component.html b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses-item.component.html
new file mode 100644
index 000000000..3b5669a64
--- /dev/null
+++ b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses-item.component.html
@@ -0,0 +1,18 @@
+
+
+ {{ label }}
+ {{ hostname }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.html b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.html
new file mode 100644
index 000000000..74a32d577
--- /dev/null
+++ b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.html
@@ -0,0 +1,125 @@
+
+
+ Clearnet
+
+
+
+
+ Add clearnet to expose this interface to the public Internet.
+
+ View instructions
+
+
+
+
+
+
+
+
+ Update
+
+
+ Remove
+
+
+
+
+
+
+
+
+ Add Clearnet
+
+
+
+
+
+
+ Tor
+
+
+
+
+ Use a Tor-enabled browser to access this address. Tor connections can
+ be slow and unreliable.
+
+ View instructions
+
+
+
+
+
+
+
+
+ Local
+
+
+
+
+ Local addresses can only be accessed while connected to the same Local
+ Area Network (LAN) as your server, either directly or using a VPN.
+
+ View instructions
+
+
+
+
+
+ Download Root CA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.scss
similarity index 100%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss
rename to frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.scss
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.ts
similarity index 53%
rename from frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts
rename to frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.ts
index 36c5c16b3..623bd1d50 100644
--- a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts
+++ b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.component.ts
@@ -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,
private readonly errorService: ErrorService,
private readonly api: ApiService,
private readonly dialogs: TuiDialogService,
+ private readonly patch: PatchDB,
@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>> = {
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 {
- const start9MeDomain = `${start9MeSubdomain?.value}.start9.me`
- const base = start9MeSubdomain ? { [start9MeDomain]: start9MeDomain } : {}
+function getClearnetSpec({
+ domains,
+ start9ToSubdomain,
+}: NetworkInfo): Promise {
+ 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)
}
diff --git a/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.module.ts b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.module.ts
new file mode 100644
index 000000000..5ca092095
--- /dev/null
+++ b/frontend/projects/ui/src/app/common/interface-addresses/interface-addresses.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/common/logs/logs.component.ts b/frontend/projects/ui/src/app/common/logs/logs.component.ts
index 75aa90b48..195767962 100644
--- a/frontend/projects/ui/src/app/common/logs/logs.component.ts
+++ b/frontend/projects/ui/src/app/common/logs/logs.component.ts
@@ -46,13 +46,15 @@ export class LogsComponent {
@ViewChild(IonContent)
private content?: IonContent
- @Input() followLogs!: (
+ @Input({ required: true }) followLogs!: (
params: RR.FollowServerLogsReq,
) => Promise
- @Input() fetchLogs!: (params: ServerLogsReq) => Promise
- @Input() context!: string
- @Input() defaultBack!: string
- @Input() pageTitle!: string
+ @Input({ required: true }) fetchLogs!: (
+ params: ServerLogsReq,
+ ) => Promise
+ @Input({ required: true }) context!: string
+ @Input({ required: true }) defaultBack!: string
+ @Input({ required: true }) pageTitle!: string
loading = true
infiniteStatus: 0 | 1 | 2 = 0
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts b/frontend/projects/ui/src/app/common/qr/qr.component.ts
similarity index 100%
rename from frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts
rename to frontend/projects/ui/src/app/common/qr/qr.component.ts
diff --git a/frontend/projects/ui/src/app/common/qr/qr.module.ts b/frontend/projects/ui/src/app/common/qr/qr.module.ts
new file mode 100644
index 000000000..aa5086b28
--- /dev/null
+++ b/frontend/projects/ui/src/app/common/qr/qr.module.ts
@@ -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 {}
diff --git a/frontend/projects/ui/src/app/common/widget-list/any-link/any-link.component.ts b/frontend/projects/ui/src/app/common/widget-list/any-link/any-link.component.ts
index 40a602f17..0e8d6f67d 100644
--- a/frontend/projects/ui/src/app/common/widget-list/any-link/any-link.component.ts
+++ b/frontend/projects/ui/src/app/common/widget-list/any-link/any-link.component.ts
@@ -12,7 +12,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnyLinkComponent implements OnInit {
- @Input() link!: string
+ @Input({ required: true }) link!: string
@Input() qp?: Record
externalLink = false
diff --git a/frontend/projects/ui/src/app/common/widget-list/widget-card/widget-card.component.ts b/frontend/projects/ui/src/app/common/widget-list/widget-card/widget-card.component.ts
index 61626fcb2..5b1ba93e9 100644
--- a/frontend/projects/ui/src/app/common/widget-list/widget-card/widget-card.component.ts
+++ b/frontend/projects/ui/src/app/common/widget-list/widget-card/widget-card.component.ts
@@ -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 =
{} as ElementRef
@ViewChild('innerWrapper') innerWrapper: ElementRef =
diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts
index 387b149b9..916a0ede4 100644
--- a/frontend/projects/ui/src/app/services/api/api.fixures.ts
+++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts
@@ -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: {},
}
diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts
index 6542087b8..3397c0c33 100644
--- a/frontend/projects/ui/src/app/services/api/api.types.ts
+++ b/frontend/projects/ui/src/app/services/api/api.types.ts
@@ -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 = {
diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
index baf06e005..775f38b18 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts
@@ -125,6 +125,10 @@ export abstract class ApiService {
abstract toggleZram(params: RR.ToggleZramReq): Promise
+ abstract setOsOutboundProxy(
+ params: RR.SetOsOutboundProxyReq,
+ ): Promise
+
// marketplace URLs
abstract marketplaceProxy(
@@ -150,15 +154,23 @@ export abstract class ApiService {
params: RR.DeleteAllNotificationsReq,
): Promise
+ // network
+
+ abstract addProxy(params: RR.AddProxyReq): Promise
+
+ abstract updateProxy(params: RR.UpdateProxyReq): Promise
+
+ abstract deleteProxy(params: RR.DeleteProxyReq): Promise
+
// domains
- abstract claimStart9MeDomain(
- params: RR.ClaimStart9MeReq,
- ): Promise
+ abstract claimStart9ToDomain(
+ params: RR.ClaimStart9ToReq,
+ ): Promise
- abstract deleteStart9MeDomain(
- params: RR.DeleteStart9MeReq,
- ): Promise
+ abstract deleteStart9ToDomain(
+ params: RR.DeleteStart9ToReq,
+ ): Promise
abstract addDomain(params: RR.AddDomainReq): Promise
@@ -322,4 +334,12 @@ export abstract class ApiService {
abstract getSetupStatus(): Promise
abstract followLogs(): Promise
+
+ abstract setInterfaceClearnetAddress(
+ params: RR.SetInterfaceClearnetAddressReq,
+ ): Promise
+
+ abstract setServiceOutboundProxy(
+ params: RR.SetServiceOutboundProxyReq,
+ ): Promise
}
diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
index 2a76dd1a6..3b1a91cfe 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts
@@ -233,6 +233,12 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'server.experimental.zram', params })
}
+ async setOsOutboundProxy(
+ params: RR.SetOsOutboundProxyReq,
+ ): Promise {
+ return this.rpcRequest({ method: 'server.proxy.set-outbound', params })
+ }
+
// marketplace URLs
async marketplaceProxy(
@@ -288,17 +294,31 @@ export class LiveApiService extends ApiService {
})
}
+ // network
+
+ async addProxy(params: RR.AddProxyReq): Promise {
+ return this.rpcRequest({ method: 'net.proxy.add', params })
+ }
+
+ async updateProxy(params: RR.UpdateProxyReq): Promise {
+ return this.rpcRequest({ method: 'net.proxy.update', params })
+ }
+
+ async deleteProxy(params: RR.DeleteProxyReq): Promise {
+ return this.rpcRequest({ method: 'net.proxy.delete', params })
+ }
+
// domains
- async claimStart9MeDomain(
- params: RR.ClaimStart9MeReq,
- ): Promise {
+ async claimStart9ToDomain(
+ params: RR.ClaimStart9ToReq,
+ ): Promise {
return this.rpcRequest({ method: 'net.domain.me.claim', params })
}
- async deleteStart9MeDomain(
- params: RR.DeleteStart9MeReq,
- ): Promise {
+ async deleteStart9ToDomain(
+ params: RR.DeleteStart9ToReq,
+ ): Promise {
return this.rpcRequest({ method: 'net.domain.me.delete', params })
}
@@ -544,6 +564,18 @@ export class LiveApiService extends ApiService {
})
}
+ async setInterfaceClearnetAddress(
+ params: RR.SetInterfaceClearnetAddressReq,
+ ): Promise {
+ return this.rpcRequest({ method: 'package.interface.set-clearnet', params })
+ }
+
+ async setServiceOutboundProxy(
+ params: RR.SetServiceOutboundProxyReq,
+ ): Promise {
+ return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
+ }
+
async getSetupStatus() {
return this.rpcRequest({
method: 'setup.status',
diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
index 662d104f6..48684093a 100644
--- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
+++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ async claimStart9ToDomain(
+ params: RR.ClaimStart9ToReq,
+ ): Promise {
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 {
+ async deleteStart9ToDomain(
+ params: RR.DeleteStart9ToReq,
+ ): Promise {
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 {
+ 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 {
+ 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 {
const progress = { ...PROGRESS }
const phases = [
diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts
index 240cf6ec9..e15257242 100644
--- a/frontend/projects/ui/src/app/services/api/mock-patch.ts
+++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts
@@ -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',
diff --git a/frontend/projects/ui/src/app/services/config.service.ts b/frontend/projects/ui/src/app/services/config.service.ts
index c6d25bf98..fb421da66 100644
--- a/frontend/projects/ui/src/app/services/config.service.ts
+++ b/frontend/projects/ui/src/app/services/config.service.ts
@@ -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 {
diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
index 71618f4bb..aeb60d79e 100644
--- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts
+++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts
@@ -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 | null
+ start9ToSubdomain: Omit | 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
'dependency-info': Record
- 'address-info': Record
+ interfaceInfo: Record
'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 {
diff --git a/frontend/projects/ui/src/app/services/proxy.service.ts b/frontend/projects/ui/src/app/services/proxy.service.ts
new file mode 100644
index 000000000..bffe20f82
--- /dev/null
+++ b/frontend/projects/ui/src/app/services/proxy.service.ts
@@ -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,
+ 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 }> = {}
+
+ 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: `
+ Use System Primary The primary inbound proxy will be used. If you do not have a primary inbound proxy, no proxy will be used
+ Mirror Primary Interface If you have an inbound proxy enabled for the primary interface, outbound traffic will flow through the same proxy
+ Other The specific proxy you select will be used, overriding the default
+ `,
+ disabled: serviceContext?.hasP2P ? [] : ['mirror'],
+ },
+ Variants.of(variants),
+ ),
+ })
+
+ const options: Partial<
+ TuiDialogOptions>
+ > = {
+ 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()
+ }
+ }
+}
diff --git a/frontend/projects/ui/src/app/util/configBuilderToSpec.ts b/frontend/projects/ui/src/app/util/configBuilderToSpec.ts
index fd4573e99..0174c65e5 100644
--- a/frontend/projects/ui/src/app/util/configBuilderToSpec.ts
+++ b/frontend/projects/ui/src/app/util/configBuilderToSpec.ts
@@ -2,8 +2,8 @@ import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
export async function configBuilderToSpec(
builder:
- | Config, unknown, unknown>
- | Config, never, never>,
+ | Config, unknown>
+ | Config, never>,
) {
return builder.build({} as any)
}
diff --git a/frontend/projects/ui/src/styles.scss b/frontend/projects/ui/src/styles.scss
index c98d52162..8180ca0cf 100644
--- a/frontend/projects/ui/src/styles.scss
+++ b/frontend/projects/ui/src/styles.scss
@@ -330,7 +330,7 @@ h2 {
scrollbar-width: none;
ion-grid {
- min-width: 840px;
+ min-width: 900px;
}
}