mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
guid and new alert flow
This commit is contained in:
committed by
Aiden McClelland
parent
a5266c2e41
commit
7576b30a6b
@@ -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'"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[class]="error ? 'error-border' : ''"
|
||||
>
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="productKey"
|
||||
[ngModelOptions]="{'standalone': true}"
|
||||
[type]="!unmasked ? 'password' : 'text'"
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user