mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
UI/feature/enable disable lan (#192)
* ui: skip startup notifications in mocks * ui: enable-disable lan toggle in ui * ui: remove this.lanAddress for this.app.lanAddress
This commit is contained in:
committed by
Aiden McClelland
parent
a20970fa17
commit
5cf7d1ff88
@@ -3,6 +3,7 @@ set -e
|
|||||||
|
|
||||||
echo "turn off mocks"
|
echo "turn off mocks"
|
||||||
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
|
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
|
||||||
|
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
|
||||||
|
|
||||||
echo "FILTER: rm -rf www"
|
echo "FILTER: rm -rf www"
|
||||||
rm -rf www
|
rm -rf www
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export interface AppAvailableVersionSpecificInfo {
|
|||||||
// installed
|
// installed
|
||||||
|
|
||||||
export interface AppInstalledPreview extends BaseApp {
|
export interface AppInstalledPreview extends BaseApp {
|
||||||
|
lanAddress: string
|
||||||
|
lanEnabled: boolean
|
||||||
torAddress: string
|
torAddress: string
|
||||||
versionInstalled: string
|
versionInstalled: string
|
||||||
ui: boolean
|
ui: boolean
|
||||||
|
|||||||
@@ -20,10 +20,13 @@
|
|||||||
hasFetchedFull: app.hasFetchedFull | async,
|
hasFetchedFull: app.hasFetchedFull | async,
|
||||||
iconURL: app.iconURL | async,
|
iconURL: app.iconURL | async,
|
||||||
title: app.title | async,
|
title: app.title | async,
|
||||||
ui: app.ui | async
|
ui: app.ui | async,
|
||||||
|
lanAddress: app.lanAddress | async,
|
||||||
|
lanEnabled: app.lanEnabled | async,
|
||||||
|
launchDisabled: (app.status | async) !== 'RUNNING' || (!isTor && !($lanConnected$ | async)),
|
||||||
|
testingLanConnection: $testingLanConnection$ | async
|
||||||
} as vars" class="ion-padding-bottom">
|
} as vars" class="ion-padding-bottom">
|
||||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||||
|
|
||||||
<ng-container *ngIf="!($loading$ | async)">
|
<ng-container *ngIf="!($loading$ | async)">
|
||||||
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
<ion-icon color="medium" name="information-circle-outline">
|
<ion-icon color="medium" name="information-circle-outline">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-button [disabled]="vars.status !== 'RUNNING'" class="launch-button" [class.launch-button-off]="vars.status !== 'RUNNING'" (click)="launchUiTab()">
|
<ion-button [disabled]="vars.launchDisabled" class="launch-button" [class.launch-button-off]="vars.launchDisabled" (click)="launchUiTab()">
|
||||||
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
|
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
|
||||||
<ion-text>LAUNCH</ion-text>
|
<ion-text>LAUNCH</ion-text>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
@@ -97,20 +100,16 @@
|
|||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h2>Tor Address</h2>
|
<h2>Tor Address</h2>
|
||||||
<p>{{ vars.torAddress }}</p>
|
<p><a (click)="copyTor()">{{ vars.torAddress }} <ion-icon name="copy-outline" class="tiny-icon"></ion-icon></a></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
|
||||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="vars.ui" lines="none">
|
<ion-item *ngIf="vars.ui" lines="none">
|
||||||
<ion-label class="ion-text-wrap">
|
<ion-label class="ion-text-wrap">
|
||||||
<h2>LAN Address</h2>
|
<h2>LAN Address</h2>
|
||||||
<p>{{ lanAddress }}</p>
|
<p><a (click)="copyLan()">{{ vars.lanAddress }} <ion-icon name="copy-outline" class="tiny-icon"></ion-icon></a></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button slot="end" fill="clear" (click)="copyLan()">
|
<ion-toggle *ngIf="!vars.testingLanConnection" (ionChange)="$lanToggled$.next($event)" [checked]="vars.lanEnabled" slot="end" fill="clear" class="lan-toggle" [disabled]="vars.status !== 'RUNNING'"></ion-toggle>
|
||||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
<ion-spinner slot="end" *ngIf="vars.testingLanConnection" name="lines" color="medium"></ion-spinner>
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item-divider>Backups</ion-item-divider>
|
<ion-item-divider>Backups</ion-item-divider>
|
||||||
|
|||||||
@@ -59,3 +59,11 @@
|
|||||||
right: -2px;
|
right: -2px;
|
||||||
--border-radius: 100px;
|
--border-radius: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lan-toggle {
|
||||||
|
width: 2em;
|
||||||
|
height: 1em;
|
||||||
|
--handle-width: 0.9em;
|
||||||
|
--handle-height: 0.9em;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,21 +6,20 @@ import { copyToClipboard } from 'src/app/util/web.util'
|
|||||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||||
import { AppInstalledFull } from 'src/app/models/app-types'
|
import { AppInstalledFull } from 'src/app/models/app-types'
|
||||||
import { ModelPreload } from 'src/app/models/model-preload'
|
import { ModelPreload } from 'src/app/models/model-preload'
|
||||||
import { chill, pauseFor } from 'src/app/util/misc.util'
|
import { chill, modulateTime, pauseFor } from 'src/app/util/misc.util'
|
||||||
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
import { PropertySubject, peekProperties } from 'src/app/util/property-subject.util'
|
||||||
import { AppBackupPage } from 'src/app/modals/app-backup/app-backup.page'
|
import { AppBackupPage } from 'src/app/modals/app-backup/app-backup.page'
|
||||||
import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/app/services/loader.service'
|
import { LoaderService, markAsLoadingDuring$, markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs'
|
import { BehaviorSubject, combineLatest, from, merge, Observable, of, Subject } from 'rxjs'
|
||||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||||
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
import { catchError, concatMap, delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators'
|
||||||
import { Cleanup } from 'src/app/util/cleanup'
|
import { Cleanup } from 'src/app/util/cleanup'
|
||||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||||
import { Emver } from 'src/app/services/emver.service'
|
import { Emver } from 'src/app/services/emver.service'
|
||||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { ServerModel } from 'src/app/models/server-model'
|
import { squash } from 'src/app/util/rxjs.util'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-installed-show',
|
selector: 'app-installed-show',
|
||||||
templateUrl: './app-installed-show.page.html',
|
templateUrl: './app-installed-show.page.html',
|
||||||
@@ -32,17 +31,21 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
|
|
||||||
$error$ = new BehaviorSubject<string>('')
|
$error$ = new BehaviorSubject<string>('')
|
||||||
app: PropertySubject<AppInstalledFull> = { } as any
|
app: PropertySubject<AppInstalledFull> = { } as any
|
||||||
lanAddress = ''
|
|
||||||
appId: string
|
appId: string
|
||||||
AppStatus = AppStatus
|
AppStatus = AppStatus
|
||||||
showInstructions = false
|
showInstructions = false
|
||||||
isConsulate: boolean
|
isConsulate: boolean
|
||||||
isTor: boolean
|
isTor: boolean
|
||||||
|
|
||||||
|
// true iff service lan address has been tested and is accessible
|
||||||
|
$lanConnected$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||||
|
// true during service lan address testing
|
||||||
|
$testingLanConnection$: BehaviorSubject<boolean> = new BehaviorSubject(false)
|
||||||
|
|
||||||
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
||||||
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
|
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
|
||||||
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
|
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
|
||||||
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Visit your Embassy at its Tor address to launch this service!</p>`
|
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. To launch this service over LAN, enable the toggle below by your service's LAN Address.</p>`
|
||||||
|
|
||||||
@ViewChild(IonContent) content: IonContent
|
@ViewChild(IonContent) content: IonContent
|
||||||
|
|
||||||
@@ -59,7 +62,6 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
private readonly appModel: AppModel,
|
private readonly appModel: AppModel,
|
||||||
private readonly popoverController: PopoverController,
|
private readonly popoverController: PopoverController,
|
||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
private readonly serverModel: ServerModel,
|
|
||||||
config: ConfigService,
|
config: ConfigService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@@ -69,21 +71,72 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
|
|
||||||
async ngOnInit () {
|
async ngOnInit () {
|
||||||
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
this.appId = this.route.snapshot.paramMap.get('appId') as string
|
||||||
const server = this.serverModel.peek()
|
|
||||||
this.lanAddress = `https://${this.appId}.${server.serverId}.local`
|
|
||||||
|
|
||||||
this.cleanup(
|
this.cleanup(
|
||||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(app => this.app = app),
|
tap(app => this.app = app),
|
||||||
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
|
concatMap(app =>
|
||||||
catchError(e => of(this.setError(e))),
|
merge(
|
||||||
|
this.syncWhenDependencyInstalls(),
|
||||||
|
combineLatest([app.lanEnabled, this.$lanConnected$, app.status, this.$testingLanConnection$]).pipe(
|
||||||
|
filter(([_, __, s, alreadyConnecting]) => s === AppStatus.RUNNING && !alreadyConnecting),
|
||||||
|
concatMap(([enabled, connected]) => {
|
||||||
|
if (enabled && !connected) return markAsLoadingDuring$(this.$testingLanConnection$, this.testLanConnection())
|
||||||
|
if (!enabled && connected) return of(this.$lanConnected$.next(false))
|
||||||
|
return of()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), //must be final in stack
|
||||||
|
catchError(e => this.setError(e)),
|
||||||
).subscribe(),
|
).subscribe(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testLanConnection () : Observable<void> {
|
||||||
|
if (!this.app.lanAddress) return of()
|
||||||
|
|
||||||
|
return this.app.lanAddress.pipe(
|
||||||
|
switchMap(la => this.apiService.testConnection(la)),
|
||||||
|
retryWhen(errors => errors.pipe(delay(2500), take(20))),
|
||||||
|
catchError(() => of(false)),
|
||||||
|
take(1),
|
||||||
|
map(connected => this.$lanConnected$.next(connected)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableLan (): Observable<void> {
|
||||||
|
return from(this.apiService.toggleAppLAN(this.appId, 'enable')).pipe(squash)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableLan (): Observable<void> {
|
||||||
|
return from(this.apiService.toggleAppLAN(this.appId, 'disable')).pipe(
|
||||||
|
map(() => this.appModel.update({ id: this.appId, lanEnabled: false }), modulateTime(new Date(), 10, 'seconds')),
|
||||||
|
map(() => this.$lanConnected$.next(false)),
|
||||||
|
squash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$lanToggled$ = new Subject()
|
||||||
ionViewDidEnter () {
|
ionViewDidEnter () {
|
||||||
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
|
markAsLoadingDuringP(this.$loadingDependencies$, this.getApp())
|
||||||
|
this.cleanup(
|
||||||
|
combineLatest([this.$lanToggled$, this.app.lanEnabled, this.$testingLanConnection$]).pipe(
|
||||||
|
filter(([_, __, alreadyLoading]) => !alreadyLoading),
|
||||||
|
map(([e, _]) => [(e as any).detail.checked, _]),
|
||||||
|
// if the app is already in the desired state, we bail
|
||||||
|
// this can happen because ionChange triggers when the [checked] value changes
|
||||||
|
filter(([uiEnabled, appEnabled]) => (uiEnabled && !appEnabled) || (!uiEnabled && appEnabled)),
|
||||||
|
map(([enabled]) => enabled
|
||||||
|
? this.enableLan().pipe(concatMap(() => this.testLanConnection()))
|
||||||
|
: this.disableLan(),
|
||||||
|
),
|
||||||
|
concatMap(o => markAsLoadingDuring$(this.$testingLanConnection$, o).pipe(
|
||||||
|
catchError(e => this.setError(e)),
|
||||||
|
)),
|
||||||
|
).subscribe({ error: e => console.error(e) }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh (event: any) {
|
async doRefresh (event: any) {
|
||||||
@@ -113,7 +166,8 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
const torAddress = this.app.torAddress.getValue()
|
const torAddress = this.app.torAddress.getValue()
|
||||||
uiAddress = torAddress.startsWith('http') ? torAddress : `http://${torAddress}`
|
uiAddress = torAddress.startsWith('http') ? torAddress : `http://${torAddress}`
|
||||||
} else {
|
} else {
|
||||||
uiAddress = this.lanAddress
|
const lanAddress = this.app.lanAddress.getValue()
|
||||||
|
uiAddress = lanAddress.startsWith('http') ? lanAddress : `http://${lanAddress}`
|
||||||
}
|
}
|
||||||
return window.open(uiAddress, '_blank')
|
return window.open(uiAddress, '_blank')
|
||||||
}
|
}
|
||||||
@@ -183,8 +237,9 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async copyLan () {
|
async copyLan () {
|
||||||
|
const app = peekProperties(this.app)
|
||||||
let message = ''
|
let message = ''
|
||||||
await copyToClipboard(this.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
const toast = await this.toastCtrl.create({
|
||||||
header: message,
|
header: message,
|
||||||
@@ -324,8 +379,9 @@ export class AppInstalledShowPage extends Cleanup {
|
|||||||
return await popover.present()
|
return await popover.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
private setError (e: Error) {
|
private setError (e: Error): Observable<void> {
|
||||||
this.$error$.next(e.message)
|
this.$error$.next(e.message)
|
||||||
|
return of()
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearError () {
|
private clearError () {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export abstract class ApiService {
|
|||||||
this.$unauthorizedApiResponse$.next()
|
this.$unauthorizedApiResponse$.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract testConnection (url: string): Promise<true>
|
||||||
abstract getCheckAuth (): Promise<Unit> // Throws an error on failed auth.
|
abstract getCheckAuth (): Promise<Unit> // Throws an error on failed auth.
|
||||||
abstract postLogin (password: string): Promise<Unit> // Throws an error on failed auth.
|
abstract postLogin (password: string): Promise<Unit> // Throws an error on failed auth.
|
||||||
abstract postLogout (): Promise<Unit> // Throws an error on failed auth.
|
abstract postLogout (): Promise<Unit> // Throws an error on failed auth.
|
||||||
@@ -28,6 +29,7 @@ export abstract class ApiService {
|
|||||||
abstract getServerMetrics (): Promise<ReqRes.GetServerMetricsRes>
|
abstract getServerMetrics (): Promise<ReqRes.GetServerMetricsRes>
|
||||||
abstract getNotifications (page: number, perPage: number): Promise<S9Notification[]>
|
abstract getNotifications (page: number, perPage: number): Promise<S9Notification[]>
|
||||||
abstract deleteNotification (id: string): Promise<Unit>
|
abstract deleteNotification (id: string): Promise<Unit>
|
||||||
|
abstract toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit>
|
||||||
abstract updateAgent (version: any): Promise<Unit>
|
abstract updateAgent (version: any): Promise<Unit>
|
||||||
abstract acknowledgeOSWelcome (version: string): Promise<Unit>
|
abstract acknowledgeOSWelcome (version: string): Promise<Unit>
|
||||||
abstract getAvailableApps (): Promise<AppAvailablePreview[]>
|
abstract getAvailableApps (): Promise<AppAvailablePreview[]>
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPr
|
|||||||
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
||||||
import { ApiService, ReqRes } from './api.service'
|
import { ApiService, ReqRes } from './api.service'
|
||||||
import { ApiServer, Unit } from './api-types'
|
import { ApiServer, Unit } from './api-types'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http'
|
||||||
import { isUnauthorized } from 'src/app/util/web.util'
|
import { isUnauthorized } from 'src/app/util/web.util'
|
||||||
import { Replace } from 'src/app/util/types.util'
|
import { Replace } from 'src/app/util/types.util'
|
||||||
import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
import { AppMetrics, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
||||||
import { modulateTime } from 'src/app/util/misc.util'
|
import { modulateTime } from 'src/app/util/misc.util'
|
||||||
|
import { Observable, of, throwError } from 'rxjs'
|
||||||
|
import { catchError, mapTo } from 'rxjs/operators'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveApiService extends ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
@@ -20,6 +22,10 @@ export class LiveApiService extends ApiService {
|
|||||||
private readonly serverModel: ServerModel,
|
private readonly serverModel: ServerModel,
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
|
testConnection (url: string): Promise<true> {
|
||||||
|
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
|
||||||
|
}
|
||||||
|
|
||||||
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
||||||
async getCheckAuth (): Promise<Unit> {
|
async getCheckAuth (): Promise<Unit> {
|
||||||
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
||||||
@@ -214,6 +220,10 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||||
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
|
||||||
|
}
|
||||||
|
|
||||||
async addSSHKey (sshKey: string): Promise<Unit> {
|
async addSSHKey (sshKey: string): Promise<Unit> {
|
||||||
const data: ReqRes.PostAddSSHKeyReq = {
|
const data: ReqRes.PostAddSSHKeyReq = {
|
||||||
sshKey,
|
sshKey,
|
||||||
@@ -275,3 +285,11 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
|
|||||||
return first ? `?dryrun` : `&dryrun`
|
return first ? `?dryrun` : `&dryrun`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// A client-side or network error occurred. Handle it accordingly.
|
||||||
|
return throwError('Not Connected')
|
||||||
|
} else {
|
||||||
|
return of(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testCounter = 0
|
||||||
|
async testConnection (): Promise<true> {
|
||||||
|
console.log('testing connection')
|
||||||
|
this.testCounter ++
|
||||||
|
await pauseFor(1000)
|
||||||
|
if (this.testCounter > 5) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
throw new Error('Not Connected')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ejectExternalDisk (): Promise<Unit> {
|
async ejectExternalDisk (): Promise<Unit> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return { }
|
return { }
|
||||||
@@ -144,6 +156,10 @@ export class MockApiService extends ApiService {
|
|||||||
return mockAppDependentBreakages
|
return mockAppDependentBreakages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
|
||||||
async restartApp (appId: string): Promise<Unit> {
|
async restartApp (appId: string): Promise<Unit> {
|
||||||
return { }
|
return { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export function toInstalledPreview (f: AppInstalledFull): AppInstalledPreview {
|
|||||||
iconURL: f.iconURL,
|
iconURL: f.iconURL,
|
||||||
torAddress: f.torAddress,
|
torAddress: f.torAddress,
|
||||||
ui: f.ui,
|
ui: f.ui,
|
||||||
|
lanAddress: f.lanAddress,
|
||||||
|
lanEnabled: f.lanEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +49,10 @@ export function toServiceBreakage (f: BaseApp): DependentBreakage {
|
|||||||
export const bitcoinI: AppInstalledFull = {
|
export const bitcoinI: AppInstalledFull = {
|
||||||
id: 'bitcoind',
|
id: 'bitcoind',
|
||||||
versionInstalled: '0.18.1',
|
versionInstalled: '0.18.1',
|
||||||
|
lanAddress: 'bitcoinLan.local',
|
||||||
|
lanEnabled: true,
|
||||||
title: 'Bitcoin Core',
|
title: 'Bitcoin Core',
|
||||||
torAddress: 'sample-bitcoin-tor-address-and-some-more-tor-address.onion',
|
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
|
||||||
status: AppStatus.STOPPED,
|
status: AppStatus.STOPPED,
|
||||||
iconURL: 'assets/img/service-icons/bitcoind.png',
|
iconURL: 'assets/img/service-icons/bitcoind.png',
|
||||||
instructions: 'some instructions',
|
instructions: 'some instructions',
|
||||||
@@ -61,10 +65,12 @@ export const bitcoinI: AppInstalledFull = {
|
|||||||
|
|
||||||
export const lightningI: AppInstalledFull = {
|
export const lightningI: AppInstalledFull = {
|
||||||
id: 'c-lightning',
|
id: 'c-lightning',
|
||||||
|
lanAddress: 'lightningLan.local',
|
||||||
|
lanEnabled: true,
|
||||||
status: AppStatus.RUNNING,
|
status: AppStatus.RUNNING,
|
||||||
title: 'C Lightning',
|
title: 'C Lightning',
|
||||||
versionInstalled: '1.0.0',
|
versionInstalled: '1.0.0',
|
||||||
torAddress: 'sample-bitcoin-tor-address-and-some-more-tor-address.onion',
|
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
|
||||||
iconURL: 'assets/img/service-icons/bitwarden.png',
|
iconURL: 'assets/img/service-icons/bitwarden.png',
|
||||||
instructions: 'some instructions',
|
instructions: 'some instructions',
|
||||||
lastBackup: new Date().toISOString(),
|
lastBackup: new Date().toISOString(),
|
||||||
@@ -84,6 +90,8 @@ export const lightningI: AppInstalledFull = {
|
|||||||
|
|
||||||
export const cupsI: AppInstalledFull = {
|
export const cupsI: AppInstalledFull = {
|
||||||
id: 'cups',
|
id: 'cups',
|
||||||
|
lanAddress: 'cupsLan.local',
|
||||||
|
lanEnabled: false,
|
||||||
versionInstalled: '2.1.0',
|
versionInstalled: '2.1.0',
|
||||||
title: 'Cups Messenger',
|
title: 'Cups Messenger',
|
||||||
torAddress: 'sample-cups-tor-address.onion',
|
torAddress: 'sample-cups-tor-address.onion',
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
|
|
||||||
|
const { useMocks, mockOver, skipStartupAlerts } = require('../../../use-mocks.json') as UseMocks
|
||||||
|
|
||||||
|
type UseMocks = {
|
||||||
|
useMocks: boolean
|
||||||
|
mockOver: 'tor' | 'lan'
|
||||||
|
skipStartupAlerts: boolean
|
||||||
|
}
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -8,17 +15,18 @@ export class ConfigService {
|
|||||||
version = require('../../../package.json').version
|
version = require('../../../package.json').version
|
||||||
|
|
||||||
api = {
|
api = {
|
||||||
useMocks: require('../../../use-mocks.json').useMocks,
|
useMocks,
|
||||||
url: '/api',
|
url: '/api',
|
||||||
version: '/v0',
|
version: '/v0',
|
||||||
root: '', // empty will default to same origin
|
root: '', // empty will default to same origin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skipStartupAlerts = skipStartupAlerts
|
||||||
isConsulateIos = window['platform'] === 'ios'
|
isConsulateIos = window['platform'] === 'ios'
|
||||||
isConsulateAndroid = window['platform'] === 'android'
|
isConsulateAndroid = window['platform'] === 'android'
|
||||||
|
|
||||||
isTor () : boolean {
|
isTor () : boolean {
|
||||||
return this.api.useMocks || this.origin.endsWith('.onion')
|
return (this.api.useMocks && mockOver === 'tor') || this.origin.endsWith('.onion')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export class HttpService {
|
|||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
get raw () : HttpClient {
|
||||||
|
return this.http
|
||||||
|
}
|
||||||
|
|
||||||
async serverRequest<T> (options: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
async serverRequest<T> (options: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||||
options.url = leadingSlash(`${this.config.api.url}${exists(overrides.version) ? overrides.version : this.config.api.version}${options.url}`)
|
options.url = leadingSlash(`${this.config.api.url}${exists(overrides.version) ? overrides.version : this.config.api.version}${options.url}`)
|
||||||
if ( this.config.api.root && this.config.api.root !== '' ) {
|
if ( this.config.api.root && this.config.api.root !== '' ) {
|
||||||
|
|||||||
@@ -21,7 +21,30 @@ export class StartupAlertsNotifier {
|
|||||||
private readonly emver: Emver,
|
private readonly emver: Emver,
|
||||||
private readonly osUpdateService: OsUpdateService,
|
private readonly osUpdateService: OsUpdateService,
|
||||||
private readonly wizardBaker: WizardBaker,
|
private readonly wizardBaker: WizardBaker,
|
||||||
) { }
|
) {
|
||||||
|
const welcome: Check<S9Server> = {
|
||||||
|
name: 'welcome',
|
||||||
|
shouldRun: s => this.shouldRunOsWelcome(s),
|
||||||
|
check: async s => s,
|
||||||
|
display: s => this.displayOsWelcome(s),
|
||||||
|
hasRun: this.config.skipStartupAlerts,
|
||||||
|
}
|
||||||
|
const osUpdate: Check<ReqRes.GetVersionLatestRes | undefined> = {
|
||||||
|
name: 'osUpdate',
|
||||||
|
shouldRun: s => this.shouldRunOsUpdateCheck(s),
|
||||||
|
check: s => this.osUpdateCheck(s),
|
||||||
|
display: vl => this.displayOsUpdateCheck(vl),
|
||||||
|
hasRun: this.config.skipStartupAlerts,
|
||||||
|
}
|
||||||
|
const apps: Check<boolean> = {
|
||||||
|
name: 'apps',
|
||||||
|
shouldRun: s => this.shouldRunAppsCheck(s),
|
||||||
|
check: () => this.appsCheck(),
|
||||||
|
display: () => this.displayAppsCheck(),
|
||||||
|
hasRun: this.config.skipStartupAlerts,
|
||||||
|
}
|
||||||
|
this.checks = [welcome, osUpdate, apps]
|
||||||
|
}
|
||||||
|
|
||||||
// This takes our three checks and filters down to those that should run.
|
// This takes our three checks and filters down to those that should run.
|
||||||
// Then, the reduce fires, quickly iterating through yielding a promise (previousDisplay) to the next element
|
// Then, the reduce fires, quickly iterating through yielding a promise (previousDisplay) to the next element
|
||||||
@@ -48,29 +71,7 @@ export class StartupAlertsNotifier {
|
|||||||
}, Promise.resolve(true))
|
}, Promise.resolve(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
welcome: Check<S9Server> = {
|
checks: Check<any>[]
|
||||||
name: 'welcome',
|
|
||||||
shouldRun: s => this.shouldRunOsWelcome(s),
|
|
||||||
check: async s => s,
|
|
||||||
display: s => this.displayOsWelcome(s),
|
|
||||||
hasRun: false,
|
|
||||||
}
|
|
||||||
osUpdate: Check<ReqRes.GetVersionLatestRes | undefined> = {
|
|
||||||
name: 'osUpdate',
|
|
||||||
shouldRun: s => this.shouldRunOsUpdateCheck(s),
|
|
||||||
check: s => this.osUpdateCheck(s),
|
|
||||||
display: vl => this.displayOsUpdateCheck(vl),
|
|
||||||
hasRun: false,
|
|
||||||
}
|
|
||||||
apps: Check<boolean> = {
|
|
||||||
name: 'apps',
|
|
||||||
shouldRun: s => this.shouldRunAppsCheck(s),
|
|
||||||
check: () => this.appsCheck(),
|
|
||||||
display: () => this.displayAppsCheck(),
|
|
||||||
hasRun: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
checks: Check<any>[] = [this.welcome, this.osUpdate, this.apps]
|
|
||||||
|
|
||||||
private shouldRunOsWelcome (s: S9Server): boolean {
|
private shouldRunOsWelcome (s: S9Server): boolean {
|
||||||
return !s.welcomeAck && s.versionInstalled === this.config.version
|
return !s.welcomeAck && s.versionInstalled === this.config.version
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Observable, from, interval, race, OperatorFunction, Observer, BehaviorSubject } from 'rxjs'
|
import { Observable, from, interval, race, OperatorFunction, Observer, BehaviorSubject } from 'rxjs'
|
||||||
import { take, map, switchMap, delay, tap } from 'rxjs/operators'
|
import { take, map, switchMap, delay, tap, concatMap } from 'rxjs/operators'
|
||||||
|
|
||||||
export function fromAsync$<S, T> (async: (s: S) => Promise<T>, s: S): Observable<T>
|
export function fromAsync$<S, T> (async: (s: S) => Promise<T>, s: S): Observable<T>
|
||||||
export function fromAsync$<T> (async: () => Promise<T>): Observable<T>
|
export function fromAsync$<T> (async: () => Promise<T>): Observable<T>
|
||||||
@@ -51,3 +51,15 @@ export function onCooldown<T> (cooldown: number, o: () => Observable<T>): Observ
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function bindPipe<T, S1> (o: Observable<T>, then: (t: T) => Observable<S1>): Observable<S1>
|
||||||
|
export function bindPipe<T, S1, S2> (o: Observable<T>, then1: (t: T) => Observable<S1>, then2: (s: S1) => Observable<S2>): Observable<S2>
|
||||||
|
export function bindPipe<T, S1, S2, S3> (o: Observable<T>, then1: (t: T) => Observable<S1>, then2: (s: S1) => Observable<S2>, then3: (s: S2) => Observable<S3>): Observable<S3>
|
||||||
|
export function bindPipe<T, S1, S2, S3, S4> (o: Observable<T>, then1: (t: T) => Observable<S1>, then2: (s: S1) => Observable<S2>, then3: (s: S2) => Observable<S3>, then4: (s: S3) => Observable<S4>): Observable<S4>
|
||||||
|
export function bindPipe<T> (o: Observable<T>, ...thens: ((t: any) => Observable<any>)[]): Observable<any> {
|
||||||
|
const concatted = thens.map(m => concatMap(m))
|
||||||
|
return concatted.reduce( (acc, next) => {
|
||||||
|
return acc.pipe(next)
|
||||||
|
}, o)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"useMocks": false
|
"useMocks": true,
|
||||||
|
"mockOver": "lan",
|
||||||
|
"skipStartupAlerts": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user