Wizard refactor 2 (#615)

* new flow and endpoints

* functional

* prod build errors addressed

* little more cleanup

* transfer progress fixed

* tor address fix

* remove eslint cause sucks

* fix skeleton text color and wording

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
This commit is contained in:
Drew Ansbacher
2021-10-07 16:51:33 -06:00
committed by Aiden McClelland
parent e58df7ec4a
commit ed395699b3
51 changed files with 18273 additions and 3137 deletions

View File

@@ -1,21 +0,0 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"rules": {
"semi": [1, "never"]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}

View File

@@ -16,7 +16,7 @@ setup.disk.list
model : string | null, model : string | null,
partitions : PartitionInfo[], partitions : PartitionInfo[],
capacity : number, capacity : number,
embassy_os : EmbassyOsDiskInfo | null, embassy-os : EmbassyOsDiskInfo | null,
}[] }[]
setup.recovery.status setup.recovery.status

File diff suppressed because it is too large Load Diff

View File

@@ -29,23 +29,14 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^12.2.1", "@angular-devkit/build-angular": "^12.2.1",
"@angular-eslint/builder": "^12.3.1",
"@angular-eslint/eslint-plugin": "^12.3.1",
"@angular-eslint/eslint-plugin-template": "^12.3.1",
"@angular-eslint/template-parser": "^12.3.1",
"@angular/cli": "^12.2.1", "@angular/cli": "^12.2.1",
"@angular/compiler": "^12.2.1", "@angular/compiler": "^12.2.1",
"@angular/compiler-cli": "^12.2.1", "@angular/compiler-cli": "^12.2.1",
"@angular/language-service": "^12.2.1", "@angular/language-service": "^12.2.1",
"@ionic/angular-toolkit": "^4.0.0", "@ionic/angular-toolkit": "^4.0.0",
"@types/node": "^16.9.1", "@types/node": "^16.9.1",
"@typescript-eslint/eslint-plugin": "^4.29.1",
"@typescript-eslint/parser": "^4.29.1",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-jsdoc": "^36.0.7",
"eslint-plugin-prefer-arrow": "^1.2.3",
"ts-node": "^10.2.0", "ts-node": "^10.2.0",
"tslint": "^6.1.3",
"typescript": "4.3.5" "typescript": "4.3.5"
} }
} }

View File

@@ -1,6 +1,19 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
const routes: Routes = [ const routes: Routes = [
{
path: 'product-key',
loadChildren: () => import('./pages/product-key/product-key.module').then( m => m.ProductKeyPageModule),
},
{
path: 'home',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule),
},
{
path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule),
},
{ {
path: 'embassy', path: 'embassy',
loadChildren: () => import('./pages/embassy/embassy.module').then( m => m.EmbassyPageModule), loadChildren: () => import('./pages/embassy/embassy.module').then( m => m.EmbassyPageModule),
@@ -9,23 +22,11 @@ const routes: Routes = [
path: 'loading', path: 'loading',
loadChildren: () => import('./pages/loading/loading.module').then( m => m.LoadingPageModule), loadChildren: () => import('./pages/loading/loading.module').then( m => m.LoadingPageModule),
}, },
{
path: 'product-key',
loadChildren: () => import('./pages/product-key/product-key.module').then( m => m.ProductKeyPageModule),
},
{
path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule),
},
{
path: 'home',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule),
},
{ {
path: 'success', path: 'success',
loadChildren: () => import('./pages/success/success.module').then( m => m.SuccessPageModule), loadChildren: () => import('./pages/success/success.module').then( m => m.SuccessPageModule),
}, },
]; ]
@NgModule({ @NgModule({
imports: [ imports: [
@@ -33,8 +34,8 @@ const routes: Routes = [
scrollPositionRestoration: 'enabled', scrollPositionRestoration: 'enabled',
preloadingStrategy: PreloadAllModules, preloadingStrategy: PreloadAllModules,
useHash: true, useHash: true,
}) }),
], ],
exports: [RouterModule] exports: [RouterModule],
}) })
export class AppRoutingModule { } export class AppRoutingModule { }

View File

@@ -1,5 +1,3 @@
<ion-app> <ion-app>
<ion-content class="has-header"> <ion-router-outlet></ion-router-outlet>
<ion-router-outlet></ion-router-outlet>
</ion-content>
</ion-app> </ion-app>

View File

@@ -1,18 +1,36 @@
import { Component, OnInit } from '@angular/core' import { Component } from '@angular/core'
import { NavController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from './services/error-toast.service'
import { StateService } from './services/state.service'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: 'app.component.html', templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'], styleUrls: ['app.component.scss'],
}) })
export class AppComponent implements OnInit { export class AppComponent {
constructor (
constructor( private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
) {} private readonly stateService: StateService,
) { }
async ngOnInit() { async ngOnInit () {
await this.navCtrl.navigateForward(`/product-key`) try {
const status = await this.apiService.getStatus()
if (status.migrating || status['product-key']) {
this.stateService.hasProductKey = true
this.stateService.isMigrating = status.migrating
await this.navCtrl.navigateForward(`/product-key`)
} else {
this.stateService.hasProductKey = false
this.stateService.isMigrating = false
await this.navCtrl.navigateForward(`/recover`)
}
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.details}`)
}
} }
} }

View File

@@ -16,16 +16,15 @@ const useMocks = require('../../config.json').useMocks as boolean
entryComponents: [], entryComponents: [],
imports: [ imports: [
BrowserModule, BrowserModule,
IonicModule.forRoot(), IonicModule.forRoot({
navAnimation: iosTransitionAnimation,
}),
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
IonicModule.forRoot({
navAnimation: iosTransitionAnimation,
}),
], ],
providers: [ providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ {
provide: ApiService , provide: ApiService ,
useFactory: (http: HttpService) => { useFactory: (http: HttpService) => {
if (useMocks) { if (useMocks) {
@@ -34,9 +33,9 @@ const useMocks = require('../../config.json').useMocks as boolean
return new LiveApiService(http) return new LiveApiService(http)
} }
}, },
deps: [HttpService] deps: [HttpService],
}, },
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule { }

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { EmbassyPage } from './embassy.page'; import { EmbassyPage } from './embassy.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: EmbassyPage, component: EmbassyPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class EmbassyPageRoutingModule {} export class EmbassyPageRoutingModule { }

View File

@@ -1,10 +1,10 @@
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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { EmbassyPage } from './embassy.page'; import { EmbassyPage } from './embassy.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { EmbassyPageRoutingModule } from './embassy-routing.module'; import { EmbassyPageRoutingModule } from './embassy-routing.module'
import { PipesModule } from 'src/app/pipes/pipe.module' import { PipesModule } from 'src/app/pipes/pipe.module'
@NgModule({ @NgModule({
@@ -16,6 +16,6 @@ import { PipesModule } from 'src/app/pipes/pipe.module'
PasswordPageModule, PasswordPageModule,
PipesModule, PipesModule,
], ],
declarations: [EmbassyPage] declarations: [EmbassyPage],
}) })
export class EmbassyPageModule {} export class EmbassyPageModule { }

View File

@@ -9,8 +9,8 @@
<ion-card color="dark"> <ion-card color="dark">
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;"> <ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
<ion-card-title>{{ loading ? 'Loading Drives' : 'Select Storage Drive'}}</ion-card-title> <ion-card-title>Select Storage Drive</ion-card-title>
<ion-card-subtitle>Select the drive where all your Embassy data will be stored.</ion-card-subtitle> <ion-card-subtitle>Select the drive where your Embassy data will be stored.</ion-card-subtitle>
</ion-card-header> </ion-card-header>
<ion-card-content class="ion-margin"> <ion-card-content class="ion-margin">
@@ -31,7 +31,6 @@
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
<ion-skeleton-text style="width: 80%; margin: 13px 0;" animated></ion-skeleton-text> <ion-skeleton-text style="width: 80%; margin: 13px 0;" animated></ion-skeleton-text>
<ion-skeleton-text style="width: 60%; margin: 10px 0;" animated></ion-skeleton-text> <ion-skeleton-text style="width: 60%; margin: 10px 0;" animated></ion-skeleton-text>
<ion-skeleton-text style="width: 30%; margin: 8px 0;" animated></ion-skeleton-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { AlertController, iosTransitionAnimation, LoadingController, ModalController, NavController } from '@ionic/angular' import { AlertController, LoadingController, ModalController, NavController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service' import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@@ -15,7 +15,7 @@ export class EmbassyPage {
selectedDrive: DiskInfo = null selectedDrive: DiskInfo = null
loading = true loading = true
constructor( constructor (
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly modalController: ModalController, private readonly modalController: ModalController,
@@ -61,9 +61,9 @@ export class EmbassyPage {
text: 'Continue', text: 'Continue',
handler: () => { handler: () => {
this.presentModalPassword(drive) this.presentModalPassword(drive)
} },
} },
] ],
}) })
await alert.present() await alert.present()
} else { } else {
@@ -75,21 +75,21 @@ export class EmbassyPage {
const modal = await this.modalController.create({ const modal = await this.modalController.create({
component: PasswordPage, component: PasswordPage,
componentProps: { componentProps: {
storageDrive: drive storageDrive: drive,
}, },
}) })
modal.onDidDismiss().then(async ret => { modal.onDidDismiss().then(async ret => {
if (!ret.data || !ret.data.password) return if (!ret.data || !ret.data.password) return
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Setting up your Embassy!' message: 'Setting up your Embassy!',
}) })
await loader.present() await loader.present()
this.stateService.storageDrive = drive this.stateService.storageDrive = drive
this.stateService.embassyPassword = ret.data.password this.stateService.embassyPassword = ret.data.password
try { try {
this.stateService.torAddress = (await this.stateService.setupEmbassy()).torAddress this.stateService.torAddress = (await this.stateService.setupEmbassy()).torAddress
} catch (e) { } catch (e) {
@@ -98,10 +98,10 @@ export class EmbassyPage {
console.error(e.details) console.error(e.details)
} finally { } finally {
loader.dismiss() loader.dismiss()
if(!!this.stateService.recoveryDrive) { if (!!this.stateService.recoveryDrive) {
await this.navCtrl.navigateForward(`/loading`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/loading`, { animationDirection: 'forward' })
} else { } else {
await this.navCtrl.navigateForward(`/success`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/success`, { animationDirection: 'forward' })
} }
} }
}) })

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { HomePage } from './home.page'; import { HomePage } from './home.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: HomePage, component: HomePage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class HomePageRoutingModule {} export class HomePageRoutingModule { }

View File

@@ -1,11 +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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { HomePage } from './home.page'; import { HomePage } from './home.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { HomePageRoutingModule } from './home-routing.module'; import { HomePageRoutingModule } from './home-routing.module'
@NgModule({ @NgModule({
@@ -16,6 +16,6 @@ import { HomePageRoutingModule } from './home-routing.module';
HomePageRoutingModule, HomePageRoutingModule,
PasswordPageModule, PasswordPageModule,
], ],
declarations: [HomePage] declarations: [HomePage],
}) })
export class HomePageModule {} export class HomePageModule { }

View File

@@ -11,11 +11,10 @@
<ion-card-content class="ion-margin"> <ion-card-content class="ion-margin">
<!-- fresh --> <!-- fresh -->
<ion-card <ion-card
(click)="embassyNav()" routerLink="/embassy"
button="true" button="true"
color="light" color="light"
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light); style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
"
> >
<ion-card-header> <ion-card-header>
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title> <ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
@@ -25,11 +24,10 @@
<!-- recover --> <!-- recover -->
</ion-card> </ion-card>
<ion-card <ion-card
(click)="recoverNav()" routerLink="/recover"
button="true" button="true"
color="light" color="light"
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light); style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
"
> >
<ion-card-header> <ion-card-header>
<ion-card-title style="font-size: 40px;">Recover</ion-card-title> <ion-card-title style="font-size: 40px;">Recover</ion-card-title>

View File

@@ -1,22 +1,9 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { iosTransitionAnimation, NavController } from '@ionic/angular'
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: 'home.page.html', templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'], styleUrls: ['home.page.scss'],
}) })
export class HomePage { export class HomePage { }
constructor(
private readonly navCtrl: NavController,
) {}
async recoverNav () {
await this.navCtrl.navigateForward(`/recover`, { animationDirection: 'forward', animation: iosTransitionAnimation })
}
async embassyNav () {
await this.navCtrl.navigateForward(`/embassy`, { animationDirection: 'forward', animation: iosTransitionAnimation })
}
}

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { LoadingPage } from './loading.page'; import { LoadingPage } from './loading.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: LoadingPage, component: LoadingPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class LoadingPageRoutingModule {} export class LoadingPageRoutingModule { }

View File

@@ -1,10 +1,10 @@
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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { LoadingPage } from './loading.page'; import { LoadingPage } from './loading.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { LoadingPageRoutingModule } from './loading-routing.module'; import { LoadingPageRoutingModule } from './loading-routing.module'
@NgModule({ @NgModule({
imports: [ imports: [
@@ -14,6 +14,6 @@ import { LoadingPageRoutingModule } from './loading-routing.module';
LoadingPageRoutingModule, LoadingPageRoutingModule,
PasswordPageModule, PasswordPageModule,
], ],
declarations: [LoadingPage] declarations: [LoadingPage],
}) })
export class LoadingPageModule {} export class LoadingPageModule { }

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { iosTransitionAnimation, NavController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@Component({ @Component({
@@ -8,20 +8,19 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['loading.page.scss'], styleUrls: ['loading.page.scss'],
}) })
export class LoadingPage { export class LoadingPage {
constructor( constructor (
public stateService: StateService, public stateService: StateService,
private navCtrl: NavController private navCtrl: NavController,
) {} ) { }
ngOnInit () { ngOnInit () {
this.stateService.pollDataTransferProgress() this.stateService.pollDataTransferProgress()
const progSub = this.stateService.dataProgSubject.subscribe(async progress => { const progSub = this.stateService.dataProgSubject.subscribe(async progress => {
if(progress === 1) { if (progress === 1) {
progSub.unsubscribe() progSub.unsubscribe()
await this.navCtrl.navigateForward(`/success`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/success`)
} }
}) })
} }
} }

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { PasswordPage } from './password.page'; import { PasswordPage } from './password.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: PasswordPage, component: PasswordPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class PasswordPageRoutingModule {} export class PasswordPageRoutingModule { }

View File

@@ -12,8 +12,8 @@ import { PasswordPageRoutingModule } from './password-routing.module'
CommonModule, CommonModule,
FormsModule, FormsModule,
IonicModule, IonicModule,
PasswordPageRoutingModule PasswordPageRoutingModule,
], ],
declarations: [PasswordPage] declarations: [PasswordPage],
}) })
export class PasswordPageModule {} export class PasswordPageModule { }

View File

@@ -30,7 +30,6 @@
[ngModelOptions]="{'standalone': true}" [ngModelOptions]="{'standalone': true}"
[type]="!unmasked1 ? 'password' : 'text'" [type]="!unmasked1 ? 'password' : 'text'"
placeholder="Enter Password" placeholder="Enter Password"
debounce="500"
(ionChange)="validate()" (ionChange)="validate()"
maxlength="64" maxlength="64"
></ion-input> ></ion-input>
@@ -49,7 +48,6 @@
[(ngModel)]="passwordVer" [(ngModel)]="passwordVer"
[ngModelOptions]="{'standalone': true}" [ngModelOptions]="{'standalone': true}"
[type]="!unmasked2 ? 'password' : 'text'" [type]="!unmasked2 ? 'password' : 'text'"
debounce="500"
(ionChange)="checkVer()" (ionChange)="checkVer()"
maxlength="64" maxlength="64"
placeholder="Retype Password" placeholder="Retype Password"

View File

@@ -21,13 +21,13 @@ export class PasswordPage {
hasData: boolean hasData: boolean
constructor( constructor (
private modalController: ModalController, private modalController: ModalController,
private apiService: ApiService, private apiService: ApiService,
private loadingCtrl: LoadingController, private loadingCtrl: LoadingController,
) {} ) { }
ngOnInit() { ngOnInit () {
if (this.storageDrive && this.storageDrive.partitions.find(p => p.used)) { if (this.storageDrive && this.storageDrive.partitions.find(p => p.used)) {
this.hasData = true this.hasData = true
} }
@@ -36,16 +36,16 @@ export class PasswordPage {
async verifyPw () { async verifyPw () {
if (!this.recoveryDrive) this.pwError = 'No recovery drive' // unreachable if (!this.recoveryDrive) this.pwError = 'No recovery drive' // unreachable
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Verifying Password' message: 'Verifying Password',
}) })
await loader.present() await loader.present()
try { try {
const isCorrectPassword = await this.apiService.verifyRecoveryPassword(this.recoveryDrive.logicalname, this.password) const isCorrectPassword = await this.apiService.verify03XPassword(this.recoveryDrive.logicalname, this.password)
if(isCorrectPassword) { if (isCorrectPassword) {
this.modalController.dismiss({ password: this.password }) this.modalController.dismiss({ password: this.password })
} else { } else {
this.pwError = "Incorrect password provided" this.pwError = 'Incorrect password provided'
} }
} catch (e) { } catch (e) {
this.pwError = 'Error connecting to Embassy' this.pwError = 'Error connecting to Embassy'
@@ -57,29 +57,29 @@ export class PasswordPage {
async submitPw () { async submitPw () {
this.validate() this.validate()
if (this.password !== this.passwordVer) { if (this.password !== this.passwordVer) {
this.verError="*passwords do not match" this.verError = '*passwords do not match'
} }
if(this.pwError || this.verError) return if (this.pwError || this.verError) return
this.modalController.dismiss({ password: this.password }) this.modalController.dismiss({ password: this.password })
} }
validate () { validate () {
if(!!this.recoveryDrive) return this.pwError = '' if (!!this.recoveryDrive) return this.pwError = ''
if (this.passwordVer) { if (this.passwordVer) {
this.checkVer() this.checkVer()
} }
if (this.password.length < 12) { if (this.password.length < 12) {
this.pwError="*password must be 12 characters or greater" this.pwError = '*password must be 12 characters or greater'
} else { } else {
this.pwError = '' this.pwError = ''
} }
} }
checkVer () { checkVer () {
this.verError = this.password !== this.passwordVer ? "*passwords do not match" : '' this.verError = this.password !== this.passwordVer ? '*passwords do not match' : ''
} }
cancel () { cancel () {

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ProdKeyModal } from './prod-key-modal.page'
const routes: Routes = [
{
path: '',
component: ProdKeyModal,
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProdKeyModalRoutingModule { }

View File

@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { ProdKeyModal } from './prod-key-modal.page'
import { ProdKeyModalRoutingModule } from './prod-key-modal-routing.module'
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ProdKeyModalRoutingModule,
],
declarations: [ProdKeyModal],
})
export class ProdKeyModalModule { }

View File

@@ -0,0 +1,47 @@
<ion-header>
<ion-toolbar color="light">
<ion-title>
Verify Recovery Product Key
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content color="light">
<form (ngSubmit)="verifyProductKey()">
<div style="padding: 8px 24px;">
<div style="padding-bottom: 16px;">
<p>Verify the product key for the chosen recovery drive.</p>
</div>
<ion-item
color="dark"
[class]="error ? 'error-border' : ''"
>
<ion-input
[(ngModel)]="productKey"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked ? 'password' : 'text'"
placeholder="Enter Product Key"
maxlength="64"
maxlength="12"
></ion-input>
<ion-button fill="clear" color="light" (click)="unmasked = !unmasked">
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
</ion-button>
</ion-item>
<p style="color: var(--ion-color-danger);">{{ error }}</p>
</div>
<input type="submit" style="display: none" />
</form>
</ion-content>
<ion-footer>
<ion-toolbar color="light">
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
Cancel
</ion-button>
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
Verify
</ion-button>
</ion-toolbar>
</ion-footer>

View File

@@ -0,0 +1,26 @@
.password-input {
color: var(--ion-color-dark);
margin-bottom: 6px;
font-size: medium;
font-weight: 500;
* {
display: inline-block;
vertical-align: middle;
}
}
.error-border {
border: 2px solid var(--ion-color-danger);
}
.success-border {
border: 2px solid var(--ion-color-success);
}
ion-input {
font-weight: 500;
--placeholder-font-weight: 400;
width: 100%;
background: var(--ion-color-dark);
border-radius: 3px;
}

View File

@@ -0,0 +1,49 @@
import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { HttpService } from 'src/app/services/api/http.service'
@Component({
selector: 'prod-key-modal',
templateUrl: 'prod-key-modal.page.html',
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@Input() recoveryDrive: DiskInfo
error = ''
productKey = ''
unmasked = false
constructor (
private readonly modalController: ModalController,
private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly httpService: HttpService,
) { }
async verifyProductKey () {
if (!this.productKey) return
const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key',
})
await loader.present()
try {
await this.apiService.set02XDrive(this.recoveryDrive.logicalname)
this.httpService.productKey = this.productKey
await this.apiService.verifyProductKey()
this.modalController.dismiss({ productKey: this.productKey })
} catch (e) {
this.httpService.productKey = undefined
this.error = 'Invalid Product Key'
} finally {
loader.dismiss()
}
}
cancel () {
this.modalController.dismiss()
}
}

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { ProductKeyPage } from './product-key.page'; import { ProductKeyPage } from './product-key.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: ProductKeyPage, component: ProductKeyPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class ProductKeyPageRoutingModule {} export class ProductKeyPageRoutingModule { }

View File

@@ -1,10 +1,10 @@
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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { ProductKeyPage } from './product-key.page'; import { ProductKeyPage } from './product-key.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { ProductKeyPageRoutingModule } from './product-key-routing.module'; import { ProductKeyPageRoutingModule } from './product-key-routing.module'
@NgModule({ @NgModule({
imports: [ imports: [
@@ -14,6 +14,6 @@ import { ProductKeyPageRoutingModule } from './product-key-routing.module';
ProductKeyPageRoutingModule, ProductKeyPageRoutingModule,
PasswordPageModule, PasswordPageModule,
], ],
declarations: [ProductKeyPage] declarations: [ProductKeyPage],
}) })
export class ProductKeyPageModule {} export class ProductKeyPageModule { }

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { iosTransitionAnimation, LoadingController, NavController } from '@ionic/angular' import { LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service' import { ApiService } from 'src/app/services/api/api.service'
import { HttpService } from 'src/app/services/api/http.service' import { HttpService } from 'src/app/services/api/http.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
@@ -13,35 +13,32 @@ export class ProductKeyPage {
productKey: string productKey: string
error: string error: string
constructor( constructor (
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly stateService: StateService, private readonly stateService: StateService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly httpService: HttpService private readonly httpService: HttpService,
) {} ) { }
async submit () { async submit () {
if(!this.productKey) return this.error = "Must enter product key" if (!this.productKey) return this.error = 'Must enter product key'
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Verifying Product Key' message: 'Verifying Product Key',
}) })
await loader.present() await loader.present()
try { try {
this.httpService.productKey = this.productKey this.httpService.productKey = this.productKey
const state = await this.apiService.verifyProductKey() await this.apiService.verifyProductKey()
if(state['is-recovering']) { if (this.stateService.isMigrating) {
await this.navCtrl.navigateForward(`/loading`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/loading`)
} else if (!!state['tor-address']) {
this.stateService.torAddress = state['tor-address']
await this.navCtrl.navigateForward(`/success`, { animationDirection: 'forward', animation: iosTransitionAnimation })
} else { } else {
await this.navCtrl.navigateForward(`/home`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/home`)
} }
} catch (e) { } catch (e) {
this.error = e.message this.error = 'Invalid Product Key'
this.httpService.productKey = undefined this.httpService.productKey = undefined
} finally { } finally {
loader.dismiss() loader.dismiss()

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { RecoverPage } from './recover.page'; import { RecoverPage } from './recover.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: RecoverPage, component: RecoverPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class RecoverPageRoutingModule {} export class RecoverPageRoutingModule { }

View File

@@ -1,11 +1,12 @@
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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { RecoverPage } from './recover.page'; import { RecoverPage } from './recover.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { RecoverPageRoutingModule } from './recover-routing.module'; import { ProdKeyModalModule } from '../prod-key-modal/prod-key-modal.module'
import { PipesModule } from 'src/app/pipes/pipe.module'; import { RecoverPageRoutingModule } from './recover-routing.module'
import { PipesModule } from 'src/app/pipes/pipe.module'
@NgModule({ @NgModule({
@@ -15,8 +16,9 @@ import { PipesModule } from 'src/app/pipes/pipe.module';
IonicModule, IonicModule,
RecoverPageRoutingModule, RecoverPageRoutingModule,
PasswordPageModule, PasswordPageModule,
ProdKeyModalModule,
PipesModule, PipesModule,
], ],
declarations: [RecoverPage] declarations: [RecoverPage],
}) })
export class RecoverPageModule {} export class RecoverPageModule { }

View File

@@ -9,7 +9,7 @@
<ion-card color="dark"> <ion-card color="dark">
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;"> <ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
<ion-card-title>{{ loading ? 'Loading Recovery Drives' : 'Select Recovery Drive'}}</ion-card-title> <ion-card-title>Select Recovery Drive</ion-card-title>
<ion-card-subtitle>Select the drive containing the Embassy you want to recover.</ion-card-subtitle> <ion-card-subtitle>Select the drive containing the Embassy you want to recover.</ion-card-subtitle>
</ion-card-header> </ion-card-header>
@@ -50,17 +50,17 @@
<span *ngIf="drive.vendor && drive.model"> - </span> <span *ngIf="drive.vendor && drive.model"> - </span>
{{ drive.model }} {{ drive.model }}
</h2> </h2>
<h2> Embassy version: {{drive['embassy_os'].version}}</h2> <h2> Embassy version: {{drive['embassy-os'].version}}</h2>
</ion-label> </ion-label>
<ion-icon *ngIf="drive['embassy_os'].version.startsWith('0.2') || passwords[drive.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon> <ion-icon *ngIf="(drive['embassy-os'].version.startsWith('0.2') && stateService.hasProductKey) || passwords[drive.logicalname] || prodKeys[drive.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon>
<ion-icon *ngIf="!drive['embassy_os'].version.startsWith('0.2') && !passwords[drive.logicalname]" color="danger" slot="end" name="lock-closed-outline"></ion-icon> <ion-icon *ngIf="(drive['embassy-os'].version.startsWith('0.2') && !stateService.hasProductKey && !prodKeys[drive.logicalname]) || (!drive['embassy-os'].version.startsWith('0.2') && !passwords[drive.logicalname])" color="danger" slot="end" name="lock-closed-outline"></ion-icon>
</ion-item> </ion-item>
</ng-container> </ng-container>
<ion-button <ion-button
(click)="selectRecoveryDrive()" (click)="selectRecoveryDrive()"
color="light" color="light"
[disabled]="!selectedDrive || (!passwords[selectedDrive.logicalname] && !selectedDrive['embassy_os'].version.startsWith('0.2'))" [disabled]="!selectedDrive || (!passwords[selectedDrive.logicalname] && !selectedDrive['embassy-os'].version.startsWith('0.2'))"
class="claim-button" class="claim-button"
*ngIf="recoveryDrives.length" *ngIf="recoveryDrives.length"
> >

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { iosTransitionAnimation, ModalController, NavController } from '@ionic/angular' import { ModalController, NavController } from '@ionic/angular'
import { ApiService, DiskInfo } from 'src/app/services/api/api.service' import { ApiService, DiskInfo } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service' import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service' import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../password/password.page' import { PasswordPage } from '../password/password.page'
import { ProdKeyModal } from '../prod-key-modal/prod-key-modal.page'
@Component({ @Component({
selector: 'app-recover', selector: 'app-recover',
@@ -11,18 +12,19 @@ import { PasswordPage } from '../password/password.page'
styleUrls: ['recover.page.scss'], styleUrls: ['recover.page.scss'],
}) })
export class RecoverPage { export class RecoverPage {
passwords = {} passwords = { }
prodKeys = { }
recoveryDrives = [] recoveryDrives = []
selectedDrive: DiskInfo = null selectedDrive: DiskInfo = null
loading = true loading = true
constructor( constructor (
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly modalController: ModalController, private readonly modalController: ModalController,
private readonly stateService: StateService, readonly stateService: StateService,
private readonly errorToastService: ErrorToastService, private readonly errorToastService: ErrorToastService,
) {} ) { }
async ngOnInit () { async ngOnInit () {
await this.getDrives() await this.getDrives()
@@ -37,7 +39,14 @@ export class RecoverPage {
async getDrives () { async getDrives () {
try { try {
this.recoveryDrives = (await this.apiService.getDrives()).filter(d => !!d['embassy_os']) let drives = (await this.apiService.getDrives()).filter(d => !!d['embassy-os'])
if (!this.stateService.hasProductKey) {
drives = drives.filter(d => d['embassy-os'].version.startsWith('0.2'))
}
this.recoveryDrives = drives
} catch (e) { } catch (e) {
this.errorToastService.present(`${e.message}: ${e.data}`) this.errorToastService.present(`${e.message}: ${e.data}`)
} finally { } finally {
@@ -45,7 +54,7 @@ export class RecoverPage {
} }
} }
async chooseDrive(drive: DiskInfo) { async chooseDrive (drive: DiskInfo) {
if (this.selectedDrive?.logicalname === drive.logicalname) { if (this.selectedDrive?.logicalname === drive.logicalname) {
this.selectedDrive = null this.selectedDrive = null
@@ -54,32 +63,51 @@ export class RecoverPage {
this.selectedDrive = drive this.selectedDrive = drive
} }
if (drive['embassy_os'].version.startsWith('0.2') || this.passwords[drive.logicalname]) return if ((drive['embassy-os'].version.startsWith('0.2') && this.stateService.hasProductKey) || this.passwords[drive.logicalname] || this.prodKeys[drive.logicalname]) return
const modal = await this.modalController.create({ if (this.stateService.hasProductKey) {
component: PasswordPage, const modal = await this.modalController.create({
componentProps: { component: PasswordPage,
recoveryDrive: this.selectedDrive componentProps: {
}, recoveryDrive: this.selectedDrive,
cssClass: 'alertlike-modal', },
}) cssClass: 'alertlike-modal',
modal.onDidDismiss().then(async ret => { })
if (!ret.data) { modal.onDidDismiss().then(async ret => {
this.selectedDrive = null if (!ret.data) {
} else if(ret.data.password) { this.selectedDrive = null
this.passwords[drive.logicalname] = ret.data.password } else if (ret.data.password) {
} this.passwords[drive.logicalname] = ret.data.password
}
})
await modal.present(); })
await modal.present()
} else {
const modal = await this.modalController.create({
component: ProdKeyModal,
componentProps: {
recoveryDrive: this.selectedDrive,
},
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(async ret => {
if (!ret.data) {
this.selectedDrive = null
} else if (ret.data.productKey) {
this.prodKeys[drive.logicalname] = ret.data.productKey
}
})
await modal.present()
}
} }
async selectRecoveryDrive() { async selectRecoveryDrive () {
this.stateService.recoveryDrive = this.selectedDrive this.stateService.recoveryDrive = this.selectedDrive
const pw = this.passwords[this.selectedDrive.logicalname] const pw = this.passwords[this.selectedDrive.logicalname]
if(pw) { if (pw) {
this.stateService.recoveryPassword = pw this.stateService.recoveryPassword = pw
} }
await this.navCtrl.navigateForward(`/embassy`, { animationDirection: 'forward', animation: iosTransitionAnimation }) await this.navCtrl.navigateForward(`/embassy`)
} }
} }

View File

@@ -1,16 +1,16 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router'
import { SuccessPage } from './success.page'; import { SuccessPage } from './success.page'
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: SuccessPage, component: SuccessPage,
} },
]; ]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class SuccessPageRoutingModule {} export class SuccessPageRoutingModule { }

View File

@@ -1,11 +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 { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms'
import { SuccessPage } from './success.page'; import { SuccessPage } from './success.page'
import { PasswordPageModule } from '../password/password.module'; import { PasswordPageModule } from '../password/password.module'
import { SuccessPageRoutingModule } from './success-routing.module'; import { SuccessPageRoutingModule } from './success-routing.module'
@NgModule({ @NgModule({
@@ -16,6 +16,6 @@ import { SuccessPageRoutingModule } from './success-routing.module';
SuccessPageRoutingModule, SuccessPageRoutingModule,
PasswordPageModule, PasswordPageModule,
], ],
declarations: [SuccessPage] declarations: [SuccessPage],
}) })
export class SuccessPageModule {} export class SuccessPageModule { }

View File

@@ -8,9 +8,9 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['success.page.scss'], styleUrls: ['success.page.scss'],
}) })
export class SuccessPage { export class SuccessPage {
constructor( constructor (
public stateService: StateService, private readonly toastCtrl: ToastController,
private toastCtrl: ToastController public readonly stateService: StateService,
) { } ) { }
window = window window = window

View File

@@ -1,5 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core'
import { ConvertBytesPipe } from './convert-bytes.pipe'; import { ConvertBytesPipe } from './convert-bytes.pipe'
@NgModule({ @NgModule({
declarations: [ConvertBytesPipe], declarations: [ConvertBytesPipe],
@@ -7,4 +7,4 @@ imports: [],
exports: [ConvertBytesPipe], exports: [ConvertBytesPipe],
}) })
export class PipesModule {} export class PipesModule { }

View File

@@ -1,30 +1,26 @@
import { Subject } from 'rxjs'
export abstract class ApiService { export abstract class ApiService {
abstract verifyProductKey (): Promise<VerifyProductKeyRes>; // unencrypted
abstract getDrives (): Promise<DiskInfo[]>; abstract getStatus (): Promise<GetStatusRes> // setup.status
abstract getDataTransferProgress (): Promise<TransferProgressRes>; abstract getDrives (): Promise<DiskInfo[]> // setup.disk.list
abstract verifyRecoveryPassword (logicalname: string, password: string): Promise<boolean>; abstract set02XDrive (logicalname: string): Promise<void> // setup.recovery.v2.set
abstract setupEmbassy (setupInfo: { abstract getRecoveryStatus (): Promise<RecoveryStatusRes> // setup.recovery.status
'embassy-logicalname': string,
'embassy-password': string // encrypted
'recovery-logicalname'?: string, abstract verifyProductKey (): Promise<void> // echo - throws error if invalid
'recovery-password'?: string abstract verify03XPassword (logicalname: string, password: string): Promise<boolean> // setup.recovery.test-password
}): Promise<SetupEmbassyRes> abstract setupEmbassy (setupInfo: SetupEmbassyReq): Promise<string> // setup.execute
} }
export interface VerifyProductKeyRes { export interface GetStatusRes {
"is-recovering": boolean 'product-key': boolean
"tor-address": string migrating: boolean
} }
export interface TransferProgressRes { export interface SetupEmbassyReq {
'bytes-transfered': number; 'embassy-logicalname': string
'total-bytes': number; 'embassy-password': string
} 'recovery-logicalname'?: string
'recovery-password'?: string
export interface SetupEmbassyRes {
"tor-address": string
} }
export interface DiskInfo { export interface DiskInfo {
@@ -33,7 +29,12 @@ export interface DiskInfo {
model: string | null, model: string | null,
partitions: PartitionInfo[], partitions: PartitionInfo[],
capacity: number, capacity: number,
embassy_os: EmbassyOsDiskInfo | null, 'embassy-os': EmbassyOsDiskInfo | null,
}
export interface RecoveryStatusRes {
'bytes-transferred': number
'total-bytes': number
} }
interface PartitionInfo { interface PartitionInfo {

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
import { Observable, from, interval, race } from 'rxjs' import { Observable } from 'rxjs'
import { map, take } from 'rxjs/operators'
import * as aesjs from 'aes-js' import * as aesjs from 'aes-js'
import * as pbkdf2 from 'pbkdf2' import * as pbkdf2 from 'pbkdf2'
@@ -19,7 +18,7 @@ export class HttpService {
this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/rpc/v1` this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/rpc/v1`
} }
async rpcRequest<T> (body: RPCOptions): Promise<T> { async rpcRequest<T> (body: RPCOptions, encrypted = true): Promise<T> {
const httpOpts = { const httpOpts = {
method: Method.POST, method: Method.POST,
@@ -27,14 +26,20 @@ export class HttpService {
url: this.fullUrl, url: this.fullUrl,
} }
const res = await this.httpRequest<RPCResponse<T>>(httpOpts) let res: RPCResponse<T>
if (encrypted) {
res = await this.encryptedHttpRequest<RPCResponse<T>>(httpOpts)
} else {
res = await this.httpRequest<RPCResponse<T>>(httpOpts)
}
if (isRpcError(res)) throw new RpcError(res.error) if (isRpcError(res)) throw new RpcError(res.error)
if (isRpcSuccess(res)) return res.result if (isRpcSuccess(res)) return res.result
} }
async httpRequest<T> (httpOpts: { async encryptedHttpRequest<T> (httpOpts: {
body: RPCOptions; body: RPCOptions;
url: string; url: string;
}): Promise<T> { }): Promise<T> {
@@ -53,7 +58,7 @@ export class HttpService {
headers: { headers: {
'Content-Encoding': 'aesctr256', 'Content-Encoding': 'aesctr256',
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
} as any } as any
@@ -71,6 +76,31 @@ export class HttpService {
} }
}) })
} }
async httpRequest<T> (httpOpts: {
body: RPCOptions;
url: string;
}): Promise<T> {
const urlIsRelative = httpOpts.url.startsWith('/')
const url = urlIsRelative ?
this.fullUrl + httpOpts.url :
httpOpts.url
const options = {
responseType: 'json',
body: httpOpts.body,
observe: 'events',
reportProgress: false,
headers: { 'content-type': 'application/json', accept: 'application/json' },
} as any
const req: Observable<{ body: T }> = this.http.post(url, httpOpts.body, options) as any
return (req)
.toPromise()
.then(res => res.body)
.catch(e => { throw new HttpError(e) })
}
} }
function RpcError (e: RPCError['error']): void { function RpcError (e: RPCError['error']): void {
@@ -167,13 +197,6 @@ export interface HttpOptions {
timeout?: number timeout?: number
} }
function withTimeout<U> (req: Observable<U>, timeout: number): Observable<U> {
return race(
from(req.toPromise()), // this guarantees it only emits on completion, intermediary emissions are suppressed.
interval(timeout).pipe(take(1), map(() => { throw new Error('timeout') })),
)
}
type AES_CTR = { type AES_CTR = {
encryptPbkdf2: (secretKey: string, messageBuffer: Uint8Array) => Promise<Uint8Array> encryptPbkdf2: (secretKey: string, messageBuffer: Uint8Array) => Promise<Uint8Array>
decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise<string> decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise<string>
@@ -184,10 +207,10 @@ export const AES_CTR: AES_CTR = {
const salt = window.crypto.getRandomValues(new Uint8Array(16)) const salt = window.crypto.getRandomValues(new Uint8Array(16))
const counter = window.crypto.getRandomValues(new Uint8Array(16)) const counter = window.crypto.getRandomValues(new Uint8Array(16))
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256'); const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)); const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter))
const encryptedBytes = aesCtr.encrypt(messageBuffer); const encryptedBytes = aesCtr.encrypt(messageBuffer)
return new Uint8Array([...counter, ...salt, ...encryptedBytes]) return new Uint8Array([...counter, ...salt, ...encryptedBytes])
}, },
decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => { decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => {
@@ -196,12 +219,12 @@ export const AES_CTR: AES_CTR = {
const salt = buff.slice(16, 32) const salt = buff.slice(16, 32)
const cipher = buff.slice(32) const cipher = buff.slice(32)
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256'); const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter)); const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter))
const decryptedBytes = aesCtr.decrypt(cipher); const decryptedBytes = aesCtr.decrypt(cipher)
return aesjs.utils.utf8.fromBytes(decryptedBytes); return aesjs.utils.utf8.fromBytes(decryptedBytes)
}, },
} }
@@ -214,5 +237,5 @@ export function encodeUtf8 (str: string): Uint8Array {
} }
export function decodeUtf8 (arr: Uint8Array): string { export function decodeUtf8 (arr: Uint8Array): string {
return new TextDecoder().decode(arr); return new TextDecoder().decode(arr)
} }

View File

@@ -1,53 +1,66 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ApiService, DiskInfo, SetupEmbassyRes, TransferProgressRes, VerifyProductKeyRes } from './api.service' import { ApiService, DiskInfo, GetStatusRes, RecoveryStatusRes, SetupEmbassyReq } from './api.service'
import { HttpService } from './http.service' import { HttpService } from './http.service'
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class LiveApiService extends ApiService { export class LiveApiService extends ApiService {
constructor( constructor (
private readonly http: HttpService private readonly http: HttpService,
) { super() } ) { super() }
async verifyProductKey () { // ** UNENCRYPTED **
return this.http.rpcRequest<VerifyProductKeyRes>({
method: 'setup.status',
params: {}
})
}
async getDataTransferProgress () { async getStatus () {
return this.http.rpcRequest<TransferProgressRes>({ return this.http.rpcRequest<GetStatusRes>({
method: 'setup.recovery.status', method: 'setup.status',
params: {} params: { },
}) }, false)
} }
async getDrives () { async getDrives () {
return this.http.rpcRequest<DiskInfo[]>({ return this.http.rpcRequest<DiskInfo[]>({
method: 'setup.disk.list', method: 'setup.disk.list',
params: {} params: { },
}, false)
}
async set02XDrive (logicalname) {
return this.http.rpcRequest<void>({
method: 'setup.recovery.v2.set',
params: { logicalname },
}, false)
}
async getRecoveryStatus () {
return this.http.rpcRequest<RecoveryStatusRes>({
method: 'setup.recovery.status',
params: { },
}, false)
}
// ** ENCRYPTED **
async verifyProductKey () {
return this.http.rpcRequest<void>({
method: 'echo',
params: { },
}) })
} }
async verifyRecoveryPassword (logicalname: string, password: string) { async verify03XPassword (logicalname: string, password: string) {
return this.http.rpcRequest<boolean>({ return this.http.rpcRequest<boolean>({
method: 'setup.recovery.test-password', method: 'setup.recovery.test-password',
params: {logicalname, password} params: { logicalname, password },
}) })
} }
async setupEmbassy (setupInfo: { async setupEmbassy (setupInfo: SetupEmbassyReq) {
'embassy-logicalname': string, return this.http.rpcRequest<string>({
'embassy-password': string
'recovery-logicalname'?: string,
'recovery-password'?: string
}) {
return this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.execute', method: 'setup.execute',
params: setupInfo params: setupInfo as any,
}) })
} }
} }

View File

@@ -1,33 +1,30 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { pauseFor } from '../state.service' import { pauseFor } from '../state.service'
import { ApiService } from './api.service' import { ApiService, SetupEmbassyReq } from './api.service'
let tries = 0
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class MockApiService extends ApiService { export class MockApiService extends ApiService {
constructor() { constructor () {
super() super()
} }
async verifyProductKey () { // ** UNENCRYPTED **
await pauseFor(2000)
return {
"is-recovering": false,
"tor-address": null
}
}
async getDataTransferProgress () { async getStatus () {
tries = Math.min(tries + 1, 4) await pauseFor(1000)
return { return {
'bytes-transfered': tries, 'product-key': true,
'total-bytes': 4 migrating: false,
} }
} }
async getDrives () { async getDrives () {
await pauseFor(1000)
return [ return [
{ {
vendor: 'Vendor', vendor: 'Vendor',
@@ -38,17 +35,17 @@ export class MockApiService extends ApiService {
logicalname: 'sda1', logicalname: 'sda1',
label: 'label 1', label: 'label 1',
capacity: 100000, capacity: 100000,
used: 200.1255312 used: 200.1255312,
}, },
{ {
logicalname: 'sda2', logicalname: 'sda2',
label: 'label 2', label: 'label 2',
capacity: 50000, capacity: 50000,
used: 200.1255312 used: 200.1255312,
} },
], ],
capacity: 150000, capacity: 150000,
'embassy_os': null 'embassy-os': null,
}, },
{ {
vendor: 'Vendor', vendor: 'Vendor',
@@ -63,7 +60,7 @@ export class MockApiService extends ApiService {
// } // }
], ],
capacity: 1600.01234, capacity: 1600.01234,
'embassy_os': null 'embassy-os': null,
}, },
{ {
vendor: 'Vendor', vendor: 'Vendor',
@@ -74,64 +71,77 @@ export class MockApiService extends ApiService {
logicalname: 'sdc1', logicalname: 'sdc1',
label: 'label 1', label: 'label 1',
capacity: null, capacity: null,
used: null used: null,
} },
], ],
capacity: 100000, capacity: 100000,
'embassy_os': { 'embassy-os': {
version: '0.3.3', version: '0.3.3',
} },
}, },
{ {
vendor: 'Vendor', vendor: 'Vendor',
model: 'Model', model: 'Model',
logicalname: '/dev/sdd', logicalname: '/dev/sdd',
partitions: [ partitions: [
{ {
logicalname: 'sdd1', logicalname: 'sdd1',
label: null, label: null,
capacity: 10000, capacity: 10000,
used: null used: null,
} },
], ],
capacity: 10000, capacity: 10000,
'embassy_os': { 'embassy-os': {
version: '0.2.7', version: '0.2.7',
} },
} },
] ]
} }
async set02XDrive () {
await pauseFor(1000)
return
}
async getRecoveryStatus () {
tries = Math.min(tries + 1, 4)
return {
'bytes-transferred': tries,
'total-bytes': 4,
}
}
// ** ENCRYPTED **
async verifyProductKey () {
await pauseFor(1000)
return
}
async verify03XPassword (logicalname: string, password: string) {
await pauseFor(2000)
return password.length > 8
}
async setupEmbassy (setupInfo: SetupEmbassyReq) {
await pauseFor(3000)
return 'asdfasdfasdf.onion'
}
async getRecoveryDrives () { async getRecoveryDrives () {
await pauseFor(2000) await pauseFor(2000)
return [ return [
{ {
logicalname: 'Name1', logicalname: 'Name1',
version: '0.3.3', version: '0.3.3',
name: 'My Embassy' name: 'My Embassy',
}, },
{ {
logicalname: 'Name2', logicalname: 'Name2',
version: '0.2.7', version: '0.2.7',
name: 'My Embassy' name: 'My Embassy',
} },
] ]
} }
async verifyRecoveryPassword (logicalname: string, password: string) {
await pauseFor(2000)
return password.length > 8
}
async setupEmbassy (setupInfo: {
'embassy-logicalname': string,
'embassy-password': string
'recovery-logicalname'?: string,
'recovery-password'?: string
}) {
await pauseFor(2000)
return { "tor-address": 'asdfasdfasdf.onion' }
}
} }
let tries = 0

View File

@@ -1,52 +1,55 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs'
import { ApiService, DiskInfo } from './api/api.service' import { ApiService, DiskInfo } from './api/api.service'
import { ErrorToastService } from './error-toast.service'; import { ErrorToastService } from './error-toast.service'
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class StateService { export class StateService {
hasProductKey: boolean
isMigrating: boolean
polling = false polling = false
storageDrive: DiskInfo; storageDrive: DiskInfo
embassyPassword: string embassyPassword: string
recoveryDrive: DiskInfo; recoveryDrive: DiskInfo
recoveryPassword: string recoveryPassword: string
dataTransferProgress: { bytesTransfered: number; totalBytes: number } | null; dataTransferProgress: { bytesTransferred: number; totalBytes: number } | null
dataProgress = 0; dataProgress = 0
dataProgSubject = new BehaviorSubject(this.dataProgress) dataProgSubject = new BehaviorSubject(this.dataProgress)
torAddress: string torAddress: string
constructor( constructor (
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService private readonly errorToastService: ErrorToastService,
) {} ) { }
async pollDataTransferProgress(callback?: () => void) { async pollDataTransferProgress (callback?: () => void) {
this.polling = true this.polling = true
await pauseFor(1000) await pauseFor(1000)
if ( if (
this.dataTransferProgress?.totalBytes && this.dataTransferProgress?.totalBytes &&
this.dataTransferProgress.bytesTransfered === this.dataTransferProgress.totalBytes this.dataTransferProgress.bytesTransferred === this.dataTransferProgress.totalBytes
) {return } ) return
let progress
let progress
try { try {
progress =await this.apiService.getDataTransferProgress() progress = await this.apiService.getRecoveryStatus()
} catch (e) { } catch (e) {
this.errorToastService.present(`${e.message}: ${e.details}`) this.errorToastService.present(`${e.message}: ${e.details}`)
} }
if (progress) { if (progress) {
this.dataTransferProgress = { this.dataTransferProgress = {
bytesTransfered: progress['bytes-transfered'], bytesTransferred: progress['bytes-transferred'],
totalBytes: progress['total-bytes'] totalBytes: progress['total-bytes'],
} }
if (this.dataTransferProgress.totalBytes) { if (this.dataTransferProgress.totalBytes) {
this.dataProgress = this.dataTransferProgress.bytesTransfered / this.dataTransferProgress.totalBytes this.dataProgress = this.dataTransferProgress.bytesTransferred / this.dataTransferProgress.totalBytes
this.dataProgSubject.next(this.dataProgress) this.dataProgSubject.next(this.dataProgress)
} }
} }
@@ -58,13 +61,13 @@ export class StateService {
'embassy-logicalname': this.storageDrive.logicalname, 'embassy-logicalname': this.storageDrive.logicalname,
'embassy-password': this.embassyPassword, 'embassy-password': this.embassyPassword,
'recovery-logicalname': this.recoveryDrive?.logicalname, 'recovery-logicalname': this.recoveryDrive?.logicalname,
'recovery-password': this.recoveryPassword 'recovery-password': this.recoveryPassword,
}) })
return { torAddress: ret['tor-address'] } return { torAddress: ret }
} }
} }
export const pauseFor = (ms: number) => { export const pauseFor = (ms: number) => {
const promise = new Promise(resolve => setTimeout(resolve, ms)) const promise = new Promise(resolve => setTimeout(resolve, ms))
return promise return promise
}; }

View File

@@ -1,3 +1,3 @@
export const environment = { export const environment = {
production: true production: true,
}; }

View File

@@ -3,8 +3,8 @@
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
export const environment = { export const environment = {
production: false production: false,
}; }
/* /*
* For easier debugging in development mode, you can import the following file * For easier debugging in development mode, you can import the following file

View File

@@ -1,12 +1,12 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module'
import { environment } from './environments/environment'; import { environment } from './environments/environment'
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode()
} }
platformBrowserDynamic().bootstrapModule(AppModule) platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err)); .catch(err => console.log(err))

View File

@@ -52,12 +52,12 @@
* *
*/ */
import './zone-flags'; import './zone-flags'
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * Zone JS is required by default for Angular itself.
*/ */
import 'zone.js/dist/zone'; // Included with Angular CLI. import 'zone.js/dist/zone' // Included with Angular CLI.
/*************************************************************************************************** /***************************************************************************************************

View File

@@ -4,6 +4,8 @@
/** Ionic CSS Variables **/ /** Ionic CSS Variables **/
:root { :root {
--ion-font-family: 'Benton Sans'; --ion-font-family: 'Benton Sans';
--ion-text-color: var(--ion-color-dark);
--ion-text-color-rgb: var(--ion-color-dark-rgb);
/** primary **/ /** primary **/
--ion-color-primary: #428cff; --ion-color-primary: #428cff;
--ion-color-primary-rgb: 66,140,255; --ion-color-primary-rgb: 66,140,255;

View File

@@ -3,4 +3,4 @@
* running with certain Web Component callbacks * running with certain Web Component callbacks
*/ */
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
(window as any).__Zone_disable_customElements = true; (window as any).__Zone_disable_customElements = true

47
setup-wizard/tslint.json Normal file
View File

@@ -0,0 +1,47 @@
{
"rules": {
"no-unused-variable": true,
"no-unused-expression": true,
"space-before-function-paren": true,
"semicolon": [
true,
"never"
],
"no-trailing-whitespace": true,
"indent": [
true,
"spaces",
2
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-module",
"check-operator",
"check-separator",
"check-rest-spread",
"check-type",
"check-typecast",
"check-type-operator",
"check-preblock",
"check-postbrace"
],
"trailing-comma": [
true,
{
"multiline": {
"objects": "always",
"arrays": "always",
"functions": "always",
"typeLiterals": "never"
},
"singleline": "never"
}
],
"quotemark": [
true,
"single"
]
}
}