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 @@ > - - - - + + + + + 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:
  1. ISP IPv6 support
  2. 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: +
      +
    1. + Proxying + outbound + traffic to mask your home/business IP from other servers accessed by + your server/services +
    2. +
    3. + Proxying + inbound + traffic to mask your home/business IP from anyone accessing your + server/services over clearnet +
    4. +
    5. + Creating a Virtual Local Area Network (VLAN) to enable private, remote + VPN access to your server/services +
    6. +
    + 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 + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    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; } }