0.2.5 initial commit

Makefile incomplete
This commit is contained in:
Aiden McClelland
2020-11-23 13:44:28 -07:00
commit 95d3845906
503 changed files with 53448 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { DevOptionsPage } from './dev-options.page'
import { Routes, RouterModule } from '@angular/router'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
const routes: Routes = [
{
path: '',
component: DevOptionsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
ObjectConfigComponentModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [DevOptionsPage],
})
export class DevOptionsPageModule { }

View File

@@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Developer Options</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item-group>
<ion-item button [routerLink]="['ssh-keys']">
<ion-label>SSH Keys</ion-label>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('alternativeRegistryUrl')">
<ion-label>Alt Marketplace</ion-label>
<ion-note slot="end">{{ server.alternativeRegistryUrl | async }}</ion-note>
</ion-item> -->
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,42 @@
import { Component } from '@angular/core'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { S9Server } from 'src/app/models/server-model'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { ApiService } from 'src/app/services/api/api.service'
import { pauseFor } from 'src/app/util/misc.util'
import { LoaderService } from 'src/app/services/loader.service'
import { ModelPreload } from 'src/app/models/model-preload'
@Component({
selector: 'dev-options',
templateUrl: './dev-options.page.html',
styleUrls: ['./dev-options.page.scss'],
})
export class DevOptionsPage {
server: PropertySubject<S9Server> = { } as any
constructor (
private readonly serverConfigService: ServerConfigService,
private readonly apiService: ApiService,
private readonly loader: LoaderService,
private readonly preload: ModelPreload,
) { }
ngOnInit () {
this.loader.displayDuring$(
this.preload.server(),
).subscribe(s => this.server = s)
}
async doRefresh (event: any) {
await Promise.all([
this.apiService.getServer(),
pauseFor(600),
])
event.target.complete()
}
async presentModalValueEdit (key: string): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key)
}
}

View File

@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevSSHKeysPage } from './dev-ssh-keys.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
path: '',
component: DevSSHKeysPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [DevSSHKeysPage],
})
export class DevSSHKeysPageModule { }

View File

@@ -0,0 +1,51 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>SSH Keys</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group>
<ion-item-divider style="margin-top: 0px;">Description</ion-item-divider>
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<h2><ion-text color="medium">Add SSH keys to your Embassy to gain root access from the command line.</ion-text></h2>
</ion-label>
</ion-item>
<ion-item-divider>Saved Keys</ion-item-divider>
<ion-item-sliding *ngFor="let fingerprint of server.ssh | async">
<ion-item-options side="end">
<ion-item-option color="danger" (click)="delete(fingerprint)">
<ion-icon slot="icon-only" name="trash-outline"></ion-icon>
</ion-item-option>
</ion-item-options>
<ion-item>
<ion-label class="ion-text-wrap">{{ fingerprint.alg }} {{ fingerprint.hash }} {{ fingerprint.hostname }}
</ion-label>
</ion-item>
</ion-item-sliding>
</ion-item-group>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button (click)="presentModalAdd()" class="fab-button">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@@ -0,0 +1,56 @@
import { Component } from '@angular/core'
import { SSHFingerprint, S9Server } from 'src/app/models/server-model'
import { ApiService } from 'src/app/services/api/api.service'
import { pauseFor } from 'src/app/util/misc.util'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ModelPreload } from 'src/app/models/model-preload'
@Component({
selector: 'dev-ssh-keys',
templateUrl: 'dev-ssh-keys.page.html',
styleUrls: ['dev-ssh-keys.page.scss'],
})
export class DevSSHKeysPage {
server: PropertySubject<S9Server> = { } as any
error = ''
constructor (
private readonly apiService: ApiService,
private readonly loader: LoaderService,
private readonly preload: ModelPreload,
private readonly serverConfigService: ServerConfigService,
) { }
ngOnInit () {
this.loader.displayDuring$(
this.preload.server(),
).subscribe(s => this.server = s)
}
async doRefresh (event: any) {
await Promise.all([
this.apiService.getServer(),
pauseFor(600),
])
event.target.complete()
}
async presentModalAdd () {
await this.serverConfigService.presentModalValueEdit('ssh', true)
}
async delete (fingerprint: SSHFingerprint) {
this.loader.of({
message: 'Deleting...',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringP(
this.apiService.deleteSSHKey(fingerprint).then(() => this.error = ''),
).catch(e => {
console.error(e)
this.error = e.message
})
}
}

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () => import('./dev-options/dev-options.module').then(m => m.DevOptionsPageModule),
},
{
path: 'ssh-keys',
loadChildren: () => import('./dev-ssh-keys/dev-ssh-keys.module').then(m => m.DevSSHKeysPageModule),
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DeveloperRoutingModule { }

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { LANPage } from './lan.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: LANPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
SharingModule,
],
declarations: [LANPage],
})
export class LANPageModule { }

View File

@@ -0,0 +1,67 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
<ion-title>Secure LAN Setup</ion-title>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group>
<ion-item lines="none" style="font-size: small; --background: var(--ion-background-color);">
<ion-label size="small" class="ion-text-wrap">
<ion-text color="medium">For a <ion-text style="font-style: italic;">faster</ion-text> experience, you can also securely communicate with your Embassy by visiting its Local Area Network (LAN) address.</ion-text>
</ion-label>
</ion-item>
<!-- info -->
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<h2><ion-text color="warning">Instructions</ion-text></h2>
<ng-container *ngIf="!lanDisabled">
<ul style="font-size: smaller">
<li>Download your Embassy's SSL Certificate Authority by clicking the download button below.</li>
<li>Install and trust the CA.</li>
<li>Connect this device to the same network as the Embassy. This should be your private home network.</li>
<li>Navigate to your Embassy LAN address, indicated below.</li>
</ul>
</ng-container>
<div *ngIf="lanDisabled" class="ion-padding-top ion-padding-bottom">
<p [innerHtml]="lanDisabledExplanation[lanDisabled]"></p>
</div>
<a *ngIf="!isConsulate" [href]="fullDocumentationLink" target="_blank">full documentation</a>
<ion-button *ngIf="isConsulate" fill="outline" (click)="copyDocumentation()">full documentation</ion-button>
</ion-label>
</ion-item>
<ion-item-divider style="margin-top: 0px"></ion-item-divider>
<!-- Certificate -->
<ion-item [disabled]="!!lanDisabled">
<ion-label class="ion-text-wrap">
<h2>SSL Certificate</h2>
<p>Embassy Local CA</p>
</ion-label>
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="installCert()">
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
</ion-button>
</ion-item>
<!-- URL -->
<ion-item [disabled]="!!lanDisabled" >
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<a [href]="lanAddress" target="_blank">{{ lanAddress }}</a>
</ion-label>
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ion-item-group>
<!-- hidden element for downloading cert -->
<a id="install-cert" href="/api/v0/certificate" download="Embassy Local CA.crt"></a>
</ion-content>

View File

@@ -0,0 +1,82 @@
import { Component } from '@angular/core'
import { isPlatform, ToastController } from '@ionic/angular'
import { ServerModel } from 'src/app/models/server-model'
import { copyToClipboard } from 'src/app/util/web.util'
import { ConfigService } from 'src/app/services/config.service'
@Component({
selector: 'lan',
templateUrl: './lan.page.html',
styleUrls: ['./lan.page.scss'],
})
export class LANPage {
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manuals/embassyos/general/secure-lan'
lanDocs = 'docs.start9labs.com/user-manuals/embassyos/general/secure-lan'
lanAddress: string
isTor: boolean
fullDocumentationLink: string
isConsulate: boolean
lanDisabled: LanSetupIssue = undefined
readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = {
NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`,
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection.<br /><br/>Navigate to your Embassy Tor Address and try again.`,
}
constructor (
private readonly serverModel: ServerModel,
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
) { }
ngOnInit () {
if (isPlatform('ios') || isPlatform('android')) {
this.lanDisabled = 'NotDesktop'
} else if (!this.config.isTor()) {
this.lanDisabled = 'NotTor'
}
this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid
if (this.config.isTor()) {
this.fullDocumentationLink = `http://${this.torDocs}`
} else {
this.fullDocumentationLink = `https://${this.lanDocs}`
}
const server = this.serverModel.peek()
this.lanAddress = `https://${server.serverId}.local`
}
async copyLAN (): Promise < void > {
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
cssClass: 'notification-toast',
})
await toast.present()
}
async copyDocumentation (): Promise < void > {
const message = await copyToClipboard(this.fullDocumentationLink).then(
success => success ? 'copied documentation link to clipboard!' : 'failed to copy',
)
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
cssClass: 'notification-toast',
})
await toast.present()
}
installCert (): void {
document.getElementById('install-cert').click()
}
}
type LanSetupIssue = 'NotTor' | 'NotDesktop'

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { ServerConfigPage } from './server-config.page'
import { Routes, RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
const routes: Routes = [
{
path: '',
component: ServerConfigPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
SharingModule,
ObjectConfigComponentModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [ServerConfigPage],
})
export class ServerConfigPageModule { }

View File

@@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Config</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item-group>
<ion-item button (click)="presentModalValueEdit('name')">
<ion-label>Device Name</ion-label>
<ion-note slot="end">{{ server.name | async }}</ion-note>
</ion-item>
<!-- <ion-item style="word-break: break-all;" button (click)="presentModalValueEdit('password', true)">
<ion-label>Change Password</ion-label>
<ion-note slot="end">********</ion-note>
</ion-item> -->
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,43 @@
import { Component } from '@angular/core'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { pauseFor } from 'src/app/util/misc.util'
import { NavController } from '@ionic/angular'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { S9Server, ServerModel } from 'src/app/models/server-model'
import { ApiService } from 'src/app/services/api/api.service'
@Component({
selector: 'server-config',
templateUrl: './server-config.page.html',
styleUrls: ['./server-config.page.scss'],
})
export class ServerConfigPage {
server: PropertySubject<S9Server>
constructor (
private readonly serverModel: ServerModel,
private readonly serverConfigService: ServerConfigService,
private readonly apiService: ApiService,
private readonly navController: NavController,
) { }
ngOnInit () {
this.server = this.serverModel.watch()
}
async doRefresh (event: any) {
await Promise.all([
this.apiService.getServer(),
pauseFor(600),
])
event.target.complete()
}
async presentModalValueEdit (key: string, add = false): Promise<void> {
await this.serverConfigService.presentModalValueEdit(key, add)
}
navigateBack () {
this.navController.back()
}
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { ServerMetricsPage } from './server-metrics.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
path: '',
component: ServerMetricsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [ServerMetricsPage],
})
export class ServerMetricsPageModule { }

View File

@@ -0,0 +1,32 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Metrics</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-spinner *ngIf="loading" class="center" name="lines" color="warning"></ion-spinner>
<ion-item-group *ngFor="let metricGroup of metrics | keyvalue : asIsOrder">
<ion-item-divider class="divider">{{ metricGroup.key }}</ion-item-divider>
<ion-item *ngFor="let metric of metricGroup.value | keyvalue : asIsOrder">
<ion-label>
<ion-text color="medium">{{ metric.key }}</ion-text>
</ion-label>
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
<ion-text style="color: white;">{{ metric.value.value }} {{ metric.value.unit }}</ion-text>
</ion-note>
</ion-item>
</ion-item-group>
</ion-content>

View File

@@ -0,0 +1,3 @@
.metric-note {
font-size: 16px;
}

View File

@@ -0,0 +1,70 @@
import { Component } from '@angular/core'
import { ServerMetrics } from 'src/app/models/server-model'
import { ApiService } from 'src/app/services/api/api.service'
import { pauseFor } from 'src/app/util/misc.util'
@Component({
selector: 'server-metrics',
templateUrl: './server-metrics.page.html',
styleUrls: ['./server-metrics.page.scss'],
})
export class ServerMetricsPage {
error = ''
loading = true
going = false
metrics: ServerMetrics = { }
constructor (
private readonly apiService: ApiService,
) { }
async ngOnInit () {
await Promise.all([
this.getMetrics(),
pauseFor(600),
])
this.loading = false
this.startDaemon()
}
ngOnDestroy () {
this.stopDaemon()
}
async startDaemon (): Promise<void> {
this.going = true
while (this.going) {
await pauseFor(250)
await this.getMetrics()
}
}
stopDaemon () {
this.going = false
}
async getMetrics (): Promise<void> {
try {
const metrics = await this.apiService.getServerMetrics()
Object.keys(metrics).forEach(outerKey => {
if (!this.metrics[outerKey]) {
this.metrics[outerKey] = metrics[outerKey]
} else {
Object.entries(metrics[outerKey]).forEach(([key, value]) => {
this.metrics[outerKey][key] = value
})
}
})
} catch (e) {
console.error(e)
this.error = e.message
this.stopDaemon()
}
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -0,0 +1,47 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { AuthGuard } from '../../guards/auth.guard'
const routes: Routes = [
{
path: '',
canActivate: [AuthGuard],
loadChildren: () => import('./server-show/server-show.module').then(m => m.ServerShowPageModule),
},
{
path: 'specs',
canActivate: [AuthGuard],
loadChildren: () => import('./server-specs/server-specs.module').then(m => m.ServerSpecsPageModule),
},
{
path: 'metrics',
canActivate: [AuthGuard],
loadChildren: () => import('./server-metrics/server-metrics.module').then(m => m.ServerMetricsPageModule),
},
{
path: 'config',
canActivate: [AuthGuard],
loadChildren: () => import('./server-config/server-config.module').then(m => m.ServerConfigPageModule),
},
{
path: 'wifi',
canActivate: [AuthGuard],
loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiListPageModule),
},
{
path: 'lan',
canActivate: [AuthGuard],
loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule),
},
{
path: 'developer',
canActivate: [AuthGuard],
loadChildren: () => import('./developer-routes/developer-routing.module').then( m => m.DeveloperRoutingModule),
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ServerRoutingModule { }

View File

@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { ServerShowPage } from './server-show.page'
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
import { FormsModule } from '@angular/forms'
import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
path: '',
component: ServerShowPage,
},
]
@NgModule({
imports: [
CommonModule,
FormsModule,
StatusComponentModule,
IonicModule,
RouterModule.forChild(routes),
SharingModule,
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [ServerShowPage],
})
export class ServerShowPageModule { }

View File

@@ -0,0 +1,86 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ server.name | async }}</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-bottom">
<ng-container *ngIf="updating">
<ion-item class="ion-text-center">
<div style="display: flex; justify-content: center; width: 100%;">
<ion-text class="ion-text-wrap" style="margin-right: 5px; margin-top: 5px" color="primary">Server Updating</ion-text>
<ion-spinner style="margin-left: 5px" name="lines"></ion-spinner>
</div>
</ion-item>
</ng-container>
<ng-container *ngIf="!updating">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item *ngIf="error">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group>
<ion-item-divider></ion-item-divider>
<ion-item [routerLink]="['specs']">
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">About</ion-text></ion-label>
</ion-item>
<ion-item [routerLink]="['metrics']">
<ion-icon slot="start" name="pulse" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Monitor</ion-text></ion-label>
</ion-item>
<ion-item lines="none" [routerLink]="['config']">
<ion-icon slot="start" name="cog-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Config</ion-text></ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<ion-item lines="none" button (click)="checkForUpdates()">
<ion-icon slot="start" name="refresh-outline" color="primary"></ion-icon>
<ion-label><ion-text style="font-weight: bold;" color="primary">Check for Updates</ion-text></ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<ion-item [routerLink]="['lan']">
<ion-icon slot="start" name="home-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Secure LAN Setup</ion-text></ion-label>
</ion-item>
<ion-item [routerLink]="['wifi']">
<ion-icon slot="start" name="wifi" color="primary"></ion-icon>
<ion-label><ion-text color="primary">WiFi</ion-text></ion-label>
</ion-item>
<ion-item lines="none" [routerLink]="['developer']">
<ion-icon slot="start" name="terminal-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Developer Options</ion-text></ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<ion-item button (click)="presentAlertRestart()">
<ion-icon slot="start" name="reload-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Restart</ion-text></ion-label>
</ion-item>
<ion-item button lines="none" (click)="presentAlertShutdown()">
<ion-icon slot="start" name="power" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Shutdown</ion-text></ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ion-content>

View File

@@ -0,0 +1,12 @@
.notification-button {
ion-badge {
position: absolute;
font-size: 8px;
bottom: .7rem;
left: .8rem;
}
}
ion-item-divider {
margin-top: 0px;
}

View File

@@ -0,0 +1,222 @@
import { Component } from '@angular/core'
import { LoadingOptions } from '@ionic/core'
import { ServerModel, ServerStatus } from 'src/app/models/server-model'
import { AlertController } from '@ionic/angular'
import { S9Server } from 'src/app/models/server-model'
import { ApiService } from 'src/app/services/api/api.service'
import { SyncDaemon } from 'src/app/services/sync.service'
import { Subscription, Observable } from 'rxjs'
import { PropertySubject, toObservable } from 'src/app/util/property-subject.util'
import { doForAtLeast } from 'src/app/util/misc.util'
import { LoaderService } from 'src/app/services/loader.service'
import { Emver } from 'src/app/services/emver.service'
@Component({
selector: 'server-show',
templateUrl: 'server-show.page.html',
styleUrls: ['server-show.page.scss'],
})
export class ServerShowPage {
error = ''
s9Host$: Observable<string>
server: PropertySubject<S9Server>
currentServer: S9Server
subsToTearDown: Subscription[] = []
updatingFreeze = false
updating = false
constructor (
private readonly serverModel: ServerModel,
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
private readonly apiService: ApiService,
private readonly syncDaemon: SyncDaemon,
private readonly emver: Emver,
) { }
async ngOnInit () {
this.server = this.serverModel.watch()
this.subsToTearDown.push(
// serverUpdateSubscription
this.server.status.subscribe(status => {
if (status === ServerStatus.UPDATING) {
this.updating = true
} else {
if (!this.updatingFreeze) { this.updating = false }
}
}),
// currentServerSubscription
toObservable(this.server).subscribe(currentServerProperties => {
this.currentServer = currentServerProperties
}),
)
}
ionViewDidEnter () {
this.error = ''
}
ngOnDestroy () {
this.subsToTearDown.forEach(s => s.unsubscribe())
}
async doRefresh (event: any) {
await doForAtLeast([this.getServerAndApps()], 600)
event.target.complete()
}
async getServerAndApps (): Promise<void> {
try {
this.syncDaemon.sync()
this.error = ''
} catch (e) {
console.error(e)
this.error = e.message
}
}
async checkForUpdates (): Promise<void> {
const loader = await this.loader.ctrl.create(LoadingSpinner('Checking for updates...'))
await loader.present()
try {
const { versionLatest } = await this.apiService.getVersionLatest()
if (this.emver.compare(this.server.versionInstalled.getValue(), versionLatest) === -1) {
this.presentAlertUpdate(versionLatest)
} else {
this.presentAlertUpToDate()
}
} catch (e) {
console.error(e)
this.error = e.message
} finally {
await loader.dismiss()
}
}
async presentAlertUpToDate () {
const alert = await this.alertCtrl.create({
header: 'Up To Date',
message: `You are running the latest version of EmbassyOS!`,
buttons: ['OK'],
})
await alert.present()
}
async presentAlertUpdate (versionLatest: string) {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Confirm',
message: `Update EmbassyOS to ${versionLatest}?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Update',
handler: () => {
this.updateEmbassyOS(versionLatest)
},
},
],
})
await alert.present()
}
async presentAlertRestart () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Confirm',
message: `Are you sure you want to restart your Embassy?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Restart',
cssClass: 'alert-danger',
handler: () => {
this.restart()
},
},
]},
)
await alert.present()
}
async presentAlertShutdown () {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Confirm',
message: `Are you sure you shut down your Embassy? To turn it back on, you will need to physically unplug the device and plug it back in.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Shutdown',
cssClass: 'alert-danger',
handler: () => {
this.shutdown()
},
},
],
})
await alert.present()
}
private async updateEmbassyOS (versionLatest: string) {
this.loader
.displayDuringAsync(async () => {
await this.apiService.updateAgent(versionLatest)
this.serverModel.update({ status: ServerStatus.UPDATING })
// hides the "Update Embassy to..." button for this intance of the component
this.updatingFreeze = true
this.updating = true
setTimeout(() => this.updatingFreeze = false, 8000)
})
.catch(e => this.setError(e))
}
private async restart () {
this.loader
.of(LoadingSpinner(`Restarting ${this.currentServer.name}...`))
.displayDuringAsync( async () => {
this.serverModel.markUnreachable()
await this.apiService.restartServer()
})
.catch(e => this.setError(e))
}
private async shutdown () {
this.loader
.of(LoadingSpinner(`Shutting down ${this.currentServer.name}...`))
.displayDuringAsync( async () => {
this.serverModel.markUnreachable()
await this.apiService.shutdownServer()
})
.catch(e => this.setError(e))
}
setError (e: Error) {
console.error(e)
this.error = e.message
}
}
const LoadingSpinner: (m?: string) => LoadingOptions = (m) => {
const toMergeIn = m ? { message: m } : { }
return {
spinner: 'lines',
cssClass: 'loader',
...toMergeIn,
} as LoadingOptions
}

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { ServerSpecsPage } from './server-specs.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: ServerSpecsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
SharingModule,
],
declarations: [ServerSpecsPage],
})
export class ServerSpecsPageModule { }

View File

@@ -0,0 +1,33 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>About</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
<ng-container *ngIf="!($loading$ | async)">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group>
<!-- TODO: Tor address needs a copy button. -->
<ion-item *ngFor="let spec of (server.specs | async) | keyvalue : asIsOrder" [class.break-all]="spec.key === 'Tor Address'">
<ion-label class="ion-text-wrap">
<h2>{{ spec.key }}</h2>
<p *ngIf="spec.value | isValidEmver">{{ spec.value | displayEmver }}</p>
<p *ngIf="!(spec.value | isValidEmver)">{{ spec.value }}</p>
</ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ion-content>

View File

@@ -0,0 +1,53 @@
import { Component } from '@angular/core'
import { S9Server } from 'src/app/models/server-model'
import { ToastController } from '@ionic/angular'
import { copyToClipboard } from 'src/app/util/web.util'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { ModelPreload } from 'src/app/models/model-preload'
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { BehaviorSubject } from 'rxjs'
@Component({
selector: 'server-specs',
templateUrl: './server-specs.page.html',
styleUrls: ['./server-specs.page.scss'],
})
export class ServerSpecsPage {
server: PropertySubject<S9Server> = { } as any
error = ''
$loading$ = new BehaviorSubject(true)
constructor (
private readonly toastCtrl: ToastController,
private readonly preload: ModelPreload,
) { }
async ngOnInit () {
markAsLoadingDuring$(this.$loading$, this.preload.server()).subscribe({
next: s => this.server = s,
error: e => {
console.error(e)
this.error = e.message
},
})
}
async copyTor () {
let message = ''
await copyToClipboard((this.server.specs.getValue()['Tor Address'] as string).trim() || '')
.then(success => { message = success ? 'copied to clipboard!' : 'failed to copy'})
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
cssClass: 'notification-toast',
})
await toast.present()
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { WifiAddPage } from './wifi-add.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
path: '',
component: WifiAddPage,
},
]
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [WifiAddPage],
})
export class WifiAddPageModule { }

View File

@@ -0,0 +1,52 @@
<ion-header>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-toolbar>
<ion-title>Add Network</ion-title>
</ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item *ngIf="error">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group>
<ion-item>
<ion-label>Select Country</ion-label>
<ion-select slot="end" placeholder="Select" [(ngModel)]="countryCode" [selectedText]="countryCode">
<ion-select-option *ngFor="let country of countries | keyvalue : asIsOrder" [value]="country.key">
{{ country.key }} - {{ country.value }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item-divider>Network and Password</ion-item-divider>
<ion-item>
<ion-input placeholder="Network Name (SSID)" [(ngModel)]="ssid"></ion-input>
</ion-item>
<ion-item>
<ion-input type="password" placeholder="Password" [(ngModel)]="password"></ion-input>
</ion-item>
</ion-item-group>
<ion-grid style="margin-top: 40px;">
<ion-row>
<ion-col size="6">
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="primary" (click)="add()">
Add
</ion-button>
</ion-col>
<ion-col size="6">
<ion-button [disabled]="!ssid" expand="block" fill="outline" color="success" (click)="addAndConnect()">
Add and Connect
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,67 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { WifiService } from '../wifi.service'
import { LoaderService } from 'src/app/services/loader.service'
@Component({
selector: 'wifi-add',
templateUrl: 'wifi-add.page.html',
styleUrls: ['wifi-add.page.scss'],
})
export class WifiAddPage {
countries = require('../../../../util/countries.json')
countryCode = 'US'
ssid = ''
password = ''
error = ''
constructor (
private readonly navCtrl: NavController,
private readonly apiService: ApiService,
private readonly loader: LoaderService,
private readonly wifiService: WifiService,
) { }
async add (): Promise<void> {
this.loader.of({
message: 'Saving...',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync( async () => {
await this.apiService.addWifi(this.ssid, this.password, this.countryCode, false)
this.wifiService.addWifi(this.ssid)
this.ssid = ''
this.password = ''
this.error = ''
this.navCtrl.back()
}).catch(e => {
console.error(e)
this.error = e.message
})
}
async addAndConnect (): Promise<void> {
this.loader.of({
message: 'Connecting. This could take while...',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync( async () => {
await this.apiService.addWifi(this.ssid, this.password, this.countryCode, true)
const success = await this.wifiService.confirmWifi(this.ssid)
if (!success) { return }
this.wifiService.addWifi(this.ssid)
this.ssid = ''
this.password = ''
this.error = ''
this.navCtrl.back()
}).catch (e => {
console.error(e)
this.error = e.message
})
}
asIsOrder (a: any, b: any) {
return 0
}
}

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { WifiListPage } from './wifi.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
path: '',
component: WifiListPage,
},
{
path: 'add',
loadChildren: () => import('./wifi-add/wifi-add.module').then(m => m.WifiAddPageModule),
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [WifiListPage],
})
export class WifiListPageModule { }

View File

@@ -0,0 +1,47 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Wifi</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
</ion-refresher>
<ion-item *ngIf="error">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-item-group>
<ion-item>
<ion-label class="ion-text-wrap">
<p>
Add WiFi credentials to your Embassy so it can connect to the Internet without an ethernet cable.
</p>
</ion-label>
</ion-item>
<ion-item-divider class="borderless"></ion-item-divider>
<ion-item-divider>Saved Networks</ion-item-divider>
<ion-item button detail="false" *ngFor="let ssid of (server.wifi | async)?.ssids" (click)="presentAction(ssid)">
<ion-label>{{ ssid }}</ion-label>
<ion-icon *ngIf="ssid === (server.wifi | async).current" name="wifi" color="success"></ion-icon>
</ion-item>
</ion-item-group>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button [routerLink]="['add']" class="fab-button">
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>

View File

@@ -0,0 +1,102 @@
import { Component } from '@angular/core'
import { ActionSheetController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { ActionSheetButton } from '@ionic/core'
import { pauseFor } from 'src/app/util/misc.util'
import { WifiService } from './wifi.service'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { S9Server } from 'src/app/models/server-model'
import { LoaderService } from 'src/app/services/loader.service'
import { ModelPreload } from 'src/app/models/model-preload'
@Component({
selector: 'wifi',
templateUrl: 'wifi.page.html',
styleUrls: ['wifi.page.scss'],
})
export class WifiListPage {
server: PropertySubject<S9Server> = { } as any
error: string
constructor (
private readonly apiService: ApiService,
private readonly loader: LoaderService,
private readonly actionCtrl: ActionSheetController,
private readonly wifiService: WifiService,
private readonly preload: ModelPreload,
) { }
async ngOnInit () {
this.loader.displayDuring$(
this.preload.server(),
).subscribe(s => this.server = s)
}
async doRefresh (event: any) {
await Promise.all([
this.apiService.getServer(),
pauseFor(600),
])
event.target.complete()
}
async presentAction (ssid: string) {
const buttons: ActionSheetButton[] = [
{
text: 'Forget',
cssClass: 'alert-danger',
handler: () => {
this.delete(ssid)
},
},
]
if (ssid !== this.server.wifi.getValue().current) {
buttons.unshift(
{
text: 'Connect',
handler: () => {
this.connect(ssid)
},
},
)
}
const action = await this.actionCtrl.create({
buttons,
})
await action.present()
}
// Let's add country code here.
async connect (ssid: string): Promise<void> {
this.loader.of({
message: 'Connecting. This could take while...',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync(async () => {
await this.apiService.connectWifi(ssid)
await this.wifiService.confirmWifi(ssid)
this.error = ''
}).catch(e => {
console.error(e)
this.error = e.message
})
}
async delete (ssid: string): Promise<void> {
this.loader.of({
message: 'Deleting...',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync( async () => {
await this.apiService.deleteWifi(ssid)
this.wifiService.removeWifi(ssid)
this.error = ''
}).catch(e => {
console.error(e)
this.error = e.message
})
}
}

View File

@@ -0,0 +1,80 @@
import { Injectable } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { pauseFor } from 'src/app/util/misc.util'
import { ServerModel } from 'src/app/models/server-model'
@Injectable({
providedIn: 'root',
})
export class WifiService {
constructor (
private readonly apiService: ApiService,
private readonly toastCtrl: ToastController,
private readonly serverModel: ServerModel,
) { }
addWifi (ssid: string): void {
const wifi = this.serverModel.peek().wifi
this.serverModel.update({ wifi: { ...wifi, ssids: [...new Set([ssid, ...wifi.ssids])] } })
}
removeWifi (ssid: string): void {
const wifi = this.serverModel.peek().wifi
this.serverModel.update({ wifi: { ...wifi, ssids: wifi.ssids.filter(s => s !== ssid) } })
}
async confirmWifi (ssid: string): Promise<boolean> {
const timeout = 4000
const maxAttempts = 5
let attempts = 0
while (attempts < maxAttempts) {
try {
const start = new Date().valueOf()
const { current, ssids } = (await this.apiService.getServer(timeout)).wifi
const end = new Date().valueOf()
if (current === ssid) {
this.serverModel.update({ wifi: { current, ssids } })
break
} else {
attempts++
const diff = end - start
await pauseFor(Math.max(0, timeout - diff))
if (attempts === maxAttempts) {
this.serverModel.update({ wifi: { current, ssids } })
}
}
} catch (e) {
attempts++
console.error(e)
}
}
if (this.serverModel.peek().wifi.current === ssid) {
return true
} else {
const toast = await this.toastCtrl.create({
header: 'Failed to connect:',
message: `Check credentials and try again`,
position: 'bottom',
duration: 4000,
buttons: [
{
side: 'start',
icon: 'close',
handler: () => {
return true
},
},
],
cssClass: 'notification-toast',
})
setTimeout(() => toast.present(), 300)
return false
}
}
}