guid and new alert flow

This commit is contained in:
Drew Ansbacher
2021-11-01 15:07:43 -06:00
committed by Aiden McClelland
parent a5266c2e41
commit 7576b30a6b
15 changed files with 157 additions and 81 deletions

View File

@@ -25,6 +25,7 @@
[class]="pwError ? 'error-border' : password && !!storageDrive ? 'success-border' : ''"
>
<ion-input
#focusInput
[(ngModel)]="password"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked1 ? 'password' : 'text'"
@@ -45,6 +46,7 @@
</p>
<ion-item color="dark" [class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''">
<ion-input
#focusInput
[(ngModel)]="passwordVer"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked2 ? 'password' : 'text'"

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import { DiskInfo, PartitionInfo } from 'src/app/services/api/api.service'
import * as argon2 from '@start9labs/argon2'
@@ -9,6 +9,7 @@ import * as argon2 from '@start9labs/argon2'
styleUrls: ['password.page.scss'],
})
export class PasswordPage {
@ViewChild('focusInput', { static: false }) elem: IonInput
@Input() recoveryPartition: PartitionInfo
@Input() storageDrive: DiskInfo
@@ -24,6 +25,10 @@ export class PasswordPage {
private modalController: ModalController,
) { }
ngAfterViewInit () {
setTimeout(() => this.elem.setFocus(), 400)
}
async verifyPw () {
if (!this.recoveryPartition || !this.recoveryPartition['embassy-os']) this.pwError = 'No recovery drive' // unreachable

View File

@@ -17,6 +17,7 @@
[class]="error ? 'error-border' : ''"
>
<ion-input
#focusInput
[(ngModel)]="productKey"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked ? 'password' : 'text'"

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { LoadingController, ModalController } from '@ionic/angular'
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
import { ApiService, PartitionInfo } from 'src/app/services/api/api.service'
import { HttpService } from 'src/app/services/api/http.service'
@@ -9,6 +9,7 @@ import { HttpService } from 'src/app/services/api/http.service'
styleUrls: ['prod-key-modal.page.scss'],
})
export class ProdKeyModal {
@ViewChild('focusInput', { static: false }) elem: IonInput
@Input() recoveryPartition: PartitionInfo
error = ''

View File

@@ -18,7 +18,7 @@
<p class="input-label">Product Key</p>
<ion-item color="dark">
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
<ion-input color="medium" maxlength="12" name="productKey" [(ngModel)]="productKey" (ionChange)="error = ''"></ion-input>
<ion-input #focusInput color="medium" maxlength="12" name="productKey" [(ngModel)]="productKey" (ionChange)="error = ''"></ion-input>
</ion-item>
<div class="ion-text-left">
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core'
import { LoadingController, NavController } from '@ionic/angular'
import { Component, ViewChild } from '@angular/core'
import { IonInput, LoadingController, NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { HttpService } from 'src/app/services/api/http.service'
import { StateService } from 'src/app/services/state.service'
@@ -10,6 +10,7 @@ import { StateService } from 'src/app/services/state.service'
styleUrls: ['product-key.page.scss'],
})
export class ProductKeyPage {
@ViewChild('focusInput', { static: false }) elem: IonInput
productKey: string
error: string
@@ -21,6 +22,10 @@ export class ProductKeyPage {
private readonly httpService: HttpService,
) { }
ngAfterViewInit () {
setTimeout(() => this.elem.setFocus(), 400)
}
async submit () {
if (!this.productKey) return this.error = 'Must enter product key'

View File

@@ -15,7 +15,7 @@
<ion-card-content class="ion-margin">
<ng-container *ngIf="!loading && !recoveryPartitions.length">
<ng-container *ngIf="!loading && !drives.length">
<h2>No recovery drives found</h2>
<p>Please connect a recovery drive to your Embassy and refresh the page.</p>
<ion-button
@@ -29,43 +29,43 @@
<ion-item-group>
<ng-container *ngIf="loading">
<ion-item button color="light" lines="none">
<ion-avatar slot="start">
<ion-skeleton-text animated></ion-skeleton-text>
<ion-item-divider color="light">
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
</ion-item-divider>
<ion-item color="light">
<ion-avatar slot="start" style="margin-right: 24px;">
<ion-skeleton-text animated style="width: 30px; height: 30px; border-radius: 0;"></ion-skeleton-text>
</ion-avatar>
<ion-label class="ion-text-wrap">
<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: 30%; margin: 8px 0;" animated></ion-skeleton-text>
<ion-label>
<ion-skeleton-text animated style="width: 100px; height: 20px; margin-bottom: 12px;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 50px; height: 16px; margin-bottom: 16px;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 100px;"></ion-skeleton-text>
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="recoveryPartitions.length">
<ion-item (click)="choosePartition(p.partition)" class="ion-margin-bottom" button color="light" lines="none" *ngFor="let p of recoveryPartitions" [ngClass]="p.partition.logicalname === selectedPartition?.logicalname ? 'selected' : null">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label class="ion-text-wrap">
<h1>{{ p.partition.logicalname }} <span *ngIf="!!p.partition.label">-</span> {{ p.partition.label }}</h1>
<h2 *ngIf="p.vendor || p.model">
{{ p.vendor }}
<span *ngIf="p.vendor && p.model"> - </span>
{{ p.model }}
</h2>
<h2> Embassy version: {{p.partition['embassy-os'].version}}</h2>
</ion-label>
<ion-icon *ngIf="(p.partition['embassy-os'].version.startsWith('0.2') && stateService.hasProductKey) || passwords[p.partition.logicalname] || prodKeys[p.partition.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon>
<ion-icon *ngIf="(p.partition['embassy-os'].version.startsWith('0.2') && !stateService.hasProductKey && !prodKeys[p.partition.logicalname]) || (!p.partition['embassy-os'].version.startsWith('0.2') && !passwords[p.partition.logicalname])" color="danger" slot="end" name="lock-closed-outline"></ion-icon>
<!-- loaded -->
<div *ngFor="let drive of drives">
<ion-item-divider color="light">
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }} - {{ drive.capacity | convertBytes }}
</ion-item-divider>
<ion-item color="light" button *ngFor="let partition of drive.partitions" [disabled]="!partitionClickable(partition)" (click)="choosePartition(partition)">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label>
<h1>{{ partition.label || partition.logicalname }}</h1>
<h2>{{ drive.capacity | convertBytes }}</h2>
<p *ngIf="partition['embassy-os'] && partition['embassy-os'].full">
<ion-text color="success">
Embassy backups detected
</ion-text>
</p>
</ion-label>
<div *ngIf="partition['embassy-os'] && partition['embassy-os'].full">
<ion-icon *ngIf="partition['embassy-os'].version.startsWith('0.2')" color="success" slot="end" name="lock-open-outline" size="large"></ion-icon>
<ion-icon *ngIf="!partition['embassy-os'].version.startsWith('0.2')" color="danger" slot="end" name="lock-closed-outline" size="large"></ion-icon>
</div>
</ion-item>
</ng-container>
<ion-button
(click)="selectRecoveryPartition()"
color="light"
[disabled]="!selectedPartition || (!passwords[selectedPartition.logicalname] && !selectedPartition['embassy-os'].version.startsWith('0.2'))"
class="claim-button"
*ngIf="recoveryPartitions.length"
>
Next
</ion-button>
</div>
</ion-item-group>
</ion-card-content>
</ion-card>

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { ApiService, PartitionInfo } from 'src/app/services/api/api.service'
import { AlertController, LoadingController, ModalController, NavController } from '@ionic/angular'
import { ApiService, DiskInfo, PartitionInfo } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../password/password.page'
@@ -12,39 +12,60 @@ import { ProdKeyModal } from '../prod-key-modal/prod-key-modal.page'
styleUrls: ['recover.page.scss'],
})
export class RecoverPage {
passwords = { }
prodKeys = { }
recoveryPartitions: { partition: PartitionInfo, model: string, vendor: string }[] = []
selectedPartition: PartitionInfo = null
loading = true
drives: DiskInfo[] = []
hasShownGuidAlert = false
constructor (
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly loadingCtrl: LoadingController,
readonly stateService: StateService,
private readonly errorToastService: ErrorToastService,
) { }
async ngOnInit () {
await this.getPartitions()
await this.getDrives()
}
async refresh () {
this.recoveryPartitions = []
this.selectedPartition = null
this.loading = true
await this.getPartitions()
await this.getDrives()
}
async getPartitions () {
try {
let drives = await this.apiService.getDrives()
partitionClickable (partition: PartitionInfo) {
return partition['embassy-os']?.full && ((!this.stateService.hasProductKey && partition['embassy-os']?.version.startsWith('0.2') ) || this.stateService.hasProductKey)
}
this.recoveryPartitions = drives.map(d => d.partitions.map(p => ({ partition: p, vendor: d.vendor, model: d.model})).filter(p => p.partition['embassy-os']?.full)).flat()
// if theres no product key, only show 0.2s
if (!this.stateService.hasProductKey) {
this.recoveryPartitions = this.recoveryPartitions.filter(p => p.partition['embassy-os']?.version.startsWith('0.2'))
async getDrives () {
try {
this.drives = await this.apiService.getDrives()
const importableDrive = this.drives.filter(d => !!d.guid)[0]
if (!!importableDrive && !this.hasShownGuidAlert) {
const alert = await this.alertCtrl.create({
header: 'Warning',
subHeader: 'Drive contains data!',
message: 'All data stored on this drive will be permanently deleted.',
buttons: [
{
role: 'cancel',
text: 'Dismiss',
},
{
text: 'Use',
handler: async () => {
await this.importDrive(importableDrive.guid)
},
},
],
})
await alert.present()
this.hasShownGuidAlert = true
}
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.data}`)
@@ -53,15 +74,27 @@ export class RecoverPage {
}
}
async choosePartition (partition: PartitionInfo) {
if (this.selectedPartition?.logicalname === partition.logicalname) {
this.selectedPartition = null
return
} else {
this.selectedPartition = partition
async importDrive (guid: string) {
const loader = await this.loadingCtrl.create({
message: 'Importing Drive',
})
await loader.present()
try {
await this.stateService.importDrive(guid)
await this.navCtrl.navigateForward(`/success`)
} catch (e) {
this.errorToastService.present(`${e.message}: ${e.data}`)
} finally {
loader.dismiss()
}
}
if ((partition['embassy-os'].version.startsWith('0.2') && this.stateService.hasProductKey) || this.passwords[partition.logicalname] || this.prodKeys[partition.logicalname]) return
async choosePartition (partition: PartitionInfo) {
this.selectedPartition = partition
if (partition['embassy-os'].version.startsWith('0.2')) {
return this.selectRecoveryPartition()
}
if (this.stateService.hasProductKey) {
const modal = await this.modalController.create({
@@ -75,7 +108,7 @@ export class RecoverPage {
if (!ret.data) {
this.selectedPartition = null
} else if (ret.data.password) {
this.passwords[partition.logicalname] = ret.data.password
this.selectRecoveryPartition(ret.data.password)
}
})
@@ -92,7 +125,7 @@ export class RecoverPage {
if (!ret.data) {
this.selectedPartition = null
} else if (ret.data.productKey) {
this.prodKeys[partition.logicalname] = ret.data.productKey
this.selectRecoveryPartition()
}
})
@@ -100,11 +133,10 @@ export class RecoverPage {
}
}
async selectRecoveryPartition () {
async selectRecoveryPartition (password?: string) {
this.stateService.recoveryPartition = this.selectedPartition
const pw = this.passwords[this.selectedPartition.logicalname]
if (pw) {
this.stateService.recoveryPassword = pw
if (password) {
this.stateService.recoveryPassword = password
}
await this.navCtrl.navigateForward(`/embassy`)
}

View File

@@ -8,6 +8,7 @@ export abstract class ApiService {
// encrypted
abstract verifyProductKey (): Promise<void> // echo - throws error if invalid
abstract verify03XPassword (logicalname: string, password: string): Promise<boolean> // setup.recovery.test-password
abstract importDrive (guid: string): Promise<SetupEmbassyRes> // setup.execute
abstract setupEmbassy (setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
}
@@ -35,6 +36,7 @@ export interface DiskInfo {
model: string | null,
partitions: PartitionInfo[],
capacity: number,
guid: string | null, // cant back up if guid exists
}
export interface RecoveryStatusRes {

View File

@@ -57,6 +57,13 @@ export class LiveApiService extends ApiService {
})
}
async importDrive (guid: string) {
return this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.execute',
params: { guid },
})
}
async setupEmbassy (setupInfo: SetupEmbassyReq) {
return this.http.rpcRequest<SetupEmbassyRes>({
method: 'setup.execute',

View File

@@ -30,6 +30,7 @@ export class MockApiService extends ApiService {
vendor: 'Vendor',
model: 'Model',
logicalname: '/dev/sda',
guid: 'theguid',
partitions: [
{
logicalname: 'sda1',
@@ -54,11 +55,13 @@ export class MockApiService extends ApiService {
logicalname: 'dev/sdb',
partitions: [],
capacity: 1600.01234,
guid: null,
},
{
vendor: 'Vendor',
model: 'Model',
logicalname: 'dev/sdc',
guid: null,
partitions: [
{
logicalname: 'sdc1',
@@ -100,7 +103,8 @@ export class MockApiService extends ApiService {
vendor: 'Vendor',
model: 'Model',
logicalname: '/dev/sdd',
partitions: [
guid: null,
partitions: [
{
logicalname: 'sdd1',
label: null,
@@ -143,6 +147,15 @@ export class MockApiService extends ApiService {
return password.length > 8
}
async importDrive (guid: string) {
await pauseFor(3000)
return {
'tor-address': 'asdfasdfasdf.onion',
'lan-address': 'embassy-dfasdf.local',
'root-ca': rootCA,
}
}
async setupEmbassy (setupInfo: SetupEmbassyReq) {
await pauseFor(3000)
return {

View File

@@ -59,6 +59,14 @@ export class StateService {
this.pollDataTransferProgress()
}
async importDrive (guid: string) : Promise<void> {
const ret = await this.apiService.importDrive(guid)
this.torAddress = 'http://' + ret['tor-address']
this.lanAddress = 'https://' + ret['lan-address']
this.cert = ret['root-ca']
}
async setupEmbassy () : Promise<void> {
const ret = await this.apiService.setupEmbassy({
'embassy-logicalname': this.storageDrive.logicalname,