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