checkpoiint

This commit is contained in:
Drew Ansbacher
2021-08-09 16:08:12 -06:00
committed by Matt Hill
parent 1cc7cc439f
commit bce87cc819
21 changed files with 440 additions and 485 deletions

View File

@@ -1,17 +1,17 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { CanActivateHome, CanActivateRecover } from './guards/guards'
const routes: Routes = [ const routes: Routes = [
{ {
path: 'wizard', path: 'wizard',
loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule), loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule),
canActivate: [CanActivateHome],
}, },
{ {
path: 'recover', path: 'recover',
loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule), loadChildren: () => import('./pages/recover/recover.module').then( m => m.RecoverPageModule),
canActivate: [CanActivateRecover] },
{
path: 'embassy',
loadChildren: () => import('./pages/embassy/embassy.module').then( m => m.EmbassyPageModule),
}, },
]; ];

View File

@@ -1,7 +1,5 @@
<ion-app> <ion-app>
<body> <ion-content class="has-header">
<ion-content *ngIf="!stateService.loading" class="has-header"> <ion-router-outlet></ion-router-outlet>
<ion-router-outlet></ion-router-outlet> </ion-content>
</ion-content>
</body>
</ion-app> </ion-app>

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { LoadingController, NavController, ToastController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { StateService } from './services/state.service' import { StateService } from './services/state.service'
@Component({ @Component({
@@ -11,51 +10,13 @@ import { StateService } from './services/state.service'
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor( constructor(
private readonly apiService: ApiService,
private readonly stateService: StateService,
private readonly navCtrl: NavController, private readonly navCtrl: NavController,
private readonly toastController: ToastController, private stateService: StateService
private readonly loadingCtrl: LoadingController,
) { ) {}
this.apiService.watchError$.subscribe(error => {
if(error) {
this.presentToast(error)
}
})
}
async ngOnInit() { async ngOnInit() {
const loader = await this.loadingCtrl.create({ this.stateService.reset()
message: 'Connecting to your Embassy' await this.navCtrl.navigateForward(`/wizard`)
})
await loader.present()
try {
await this.stateService.getState()
if (this.stateService.recoveryDrive) {
await this.navCtrl.navigateForward(`/recover`)
} else {
await this.navCtrl.navigateForward(`/wizard`)
}
} catch (e) {} finally {
loader.dismiss()
}
}
async presentToast(error: string) {
const toast = await this.toastController.create({
header: 'Error',
message: error,
position: 'bottom',
buttons: [
{
text: 'X',
role: 'cancel',
}
]
})
await toast.present()
await toast.onDidDismiss()
} }
} }

View File

@@ -1,32 +0,0 @@
import { Injectable } from '@angular/core'
import { CanActivate } from '@angular/router'
import { StateService } from '../services/state.service'
@Injectable({
providedIn: 'root',
})
export class CanActivateHome implements CanActivate {
constructor (
private readonly stateService: StateService
) {}
canActivate (): boolean {
console.log(!!this.stateService.recoveryDrive)
return !!this.stateService.recoveryDrive ? false : true
}
}
@Injectable({
providedIn: 'root',
})
export class CanActivateRecover implements CanActivate {
constructor (
private readonly stateService: StateService
) {}
canActivate (): boolean {
return this.stateService.dataDrive ? true : false
}
}

View File

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

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { EmbassyPage } from './embassy.page';
import { PasswordPageModule } from '../password/password.module';
import { EmbassyPageRoutingModule } from './embassy-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
EmbassyPageRoutingModule,
PasswordPageModule,
],
declarations: [EmbassyPage]
})
export class EmbassyPageModule {}

View File

@@ -0,0 +1,57 @@
<ion-content color="light">
<ion-grid style="padding-top: 32px; height: 100%; max-width: 540px;">
<ion-row class="ion-align-items-center" style="height: 100%;">
<ion-col class="ion-text-center">
<div style="padding-bottom: 32px;">
<img src="assets/png/logo.png" style="max-width: 240px;" />
</div>
<ion-card color="dark">
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
<ion-card-title>{{ loading ? 'Loading Embassy Drives' : 'Select Embassy Drive'}}</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin">
<ng-container *ngIf="!loading && !embassyDrives.length">
<h2 color="light">No Embassy drives found</h2>
<p color="light">Please connect an Embassy drive to your embassy and refresh the page.</p>
<ion-button
(click)="window.location.reload()"
style="text-align:center"
class="claim-button"
>
Refresh
</ion-button>
</ng-container>
<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-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-item>
</ng-container>
<ng-container *ngIf="embassyDrives.length">
<ion-item (click)="chooseDrive(drive)" class="ion-margin-bottom" button color="light" lines="none" *ngFor="let drive of embassyDrives">
<ion-icon slot="start" name="save-outline"></ion-icon>
<ion-label class="ion-text-wrap">
<h1>{{ drive.logicalname }}</h1>
<h2 style="min-height: 19px;">{{ drive.labels.length ? drive.labels.join(' / ') : 'unnamed' }}</h2>
<p> Using {{drive.used.toFixed(2)}} of {{drive.capacity.toFixed(2)}} GiB</p>
</ion-label>
</ion-item>
</ng-container>
</ion-item-group>
</ion-card-content>
</ion-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,44 @@
.selected {
border: 4px solid gray;
}
ion-card-title {
margin: 24px 0;
font-family: 'Montserrat';
font-size: x-large;
--color: var(--ion-color-light);
}
// ion-item {
// --border-radius: 4px;
// --border-style: solid;
// --border-width: 1px;
// --border-color: var(--ion-color-light);
// }
.input-label {
text-align: left;
padding-bottom: 2px;
font-size: small;
color: var(--ion-color-light);
font-weight: bold;
}
.claim-button {
margin-inline-start: 0;
margin-inline-end: 0;
margin-top: 24px;
height: 48px;
--background: linear-gradient(45deg, var(--ion-color-light) 16%, var(--ion-color-medium) 150%);
}
.card-footer {
text-align: left;
--background: rgb(222, 222, 222);
border-top: solid;
border-width: 1px;
border-color: var(--ion-color-medium);
ion-item {
--border-color: var(--ion-color-medium);
}
}

View File

@@ -0,0 +1,42 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { ApiService, EmbassyDrive } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../password/password.page'
@Component({
selector: 'embassy',
templateUrl: 'embassy.page.html',
styleUrls: ['embassy.page.scss'],
})
export class EmbassyPage {
embassyDrives = []
selectedDrive: EmbassyDrive = null
loading = true
constructor(
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private modalController: ModalController,
private stateService: StateService
) {}
async ngOnInit() {
this.embassyDrives = await this.apiService.getEmbassyDrives()
this.loading = false
}
async chooseDrive(drive: EmbassyDrive) {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
embassyDrive: drive,
verify: false
}
})
modal.onDidDismiss().then(async ret => {
if (!ret.data) return
})
await modal.present();
}
}

View File

@@ -7,107 +7,37 @@
<img src="assets/png/logo.png" style="max-width: 240px;" /> <img src="assets/png/logo.png" style="max-width: 240px;" />
</div> </div>
<ion-card> <ion-card color="dark">
<ion-card-header class="ion-text-center" style="padding-bottom: 8px;">
<ion-card-title>{{ stateService.dataDrive ? 'Startup Options' : 'Select Data Drive'}}</ion-card-title>
</ion-card-header>
<ion-card-content class="ion-margin"> <ion-card-content class="ion-margin">
<div *ngIf="!stateService.loading && !stateService.dataDrive && dataDrives"> <!-- fresh -->
<div *ngIf="!dataDrives.length"> <ion-card
<h2 color="light">No data drives found</h2> (click)="embassyNav()"
<p color="light">Please connect a data drive to your embassy and refresh the page.</p> button="true"
<ion-button color="light"
(click)="window.location.reload()" 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" "
class="claim-button" >
> <ion-card-header>
Refresh <ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
</ion-button> <ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</div> </ion-card-header>
<div *ngIf="dataDrives.length">
<ion-card <!-- recover -->
color="light" </ion-card>
*ngFor="let drive of dataDrives" <ion-card
(click)="selectDrive(drive)" (click)="recoverNav()"
button="true" button="true"
style="border-radius: 11px;" color="light"
[class.selected]="selectedDrive?.logicalname === drive.logicalname" style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);
> "
<ion-card-header> >
<ion-card-title>{{drive.logicalname}}</ion-card-title> <ion-card-header>
<ion-card-subtitle>{{drive.labels}}</ion-card-subtitle> <ion-card-title style="font-size: 40px;">Recover</ion-card-title>
</ion-card-header> <ion-card-subtitle>Recover the data from an old embassy</ion-card-subtitle>
</ion-card-header>
<ion-card-content> </ion-card>
Using {{drive.used}} out of {{drive.capacity}} bytes.
</ion-card-content>
</ion-card>
<div style="width: 100%; text-align: center;">
<ion-button
(click)="warn()"
[disabled]="!selectedDrive"
class="claim-button"
>
Next
</ion-button>
</div>
</div>
</div>
<div *ngIf="stateService.dataDrive">
<ion-card
(click)="presentPasswordModal()"
button="true"
color="light"
class="wiz-card"
style="text-align: center; background-color: #00919b !important; height: 160px;"
>
<ion-card-header>
<ion-card-title style="font-size: 40px">Start Fresh</ion-card-title>
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-card
[routerLink]="['/recover']"
button="true"
color="light"
class="wiz-card"
style="text-align: center; background-color: #bf5900 !important; height: 160px;"
>
<ion-card-header>
<ion-card-title style="font-size: 40px">Recover</ion-card-title>
<ion-card-subtitle>Recover the data from an old embassy</ion-card-subtitle>
</ion-card-header>
</ion-card>
</div>
</ion-card-content> </ion-card-content>
<ion-card-header class="card-footer">
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<p *ngIf="!stateService.dataDrive">
Choose drive to save all Embassy data to.
</p>
<p *ngIf="stateService.dataDrive">
Spin up a brand new Embassy, or recover data from an old device.
</p>
</ion-label>
</ion-item>
</ion-card-header>
</ion-card> </ion-card>
<div class="divider"></div>
<div class="footer">
<p style="margin-bottom: 30px;">Contact/Community</p>
<ion-icon name="logo-youtube"></ion-icon>
<ion-icon name="paper-plane"></ion-icon>
<ion-icon name="mail"></ion-icon>
<ion-icon name="logo-github"></ion-icon>
<ion-icon name="logo-twitter"></ion-icon>
<ion-icon name="logo-mastodon"></ion-icon>
<ion-icon name="logo-medium"></ion-icon>
</div>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>

View File

@@ -1,8 +1,5 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { AlertController, ModalController, LoadingController } from '@ionic/angular' import { NavController } from '@ionic/angular'
import { ApiService, DataDrive } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../password/password.page'
@Component({ @Component({
selector: 'home', selector: 'home',
@@ -10,109 +7,16 @@ import { PasswordPage } from '../password/password.page'
styleUrls: ['home.page.scss'], styleUrls: ['home.page.scss'],
}) })
export class HomePage { export class HomePage {
dataDrives = []
selectedDrive: DataDrive = null
console = console
constructor( constructor(
private readonly apiService: ApiService, private readonly navCtrl: NavController,
private readonly stateService: StateService,
private readonly alertController: AlertController,
private readonly modalController: ModalController,
private readonly loadingCtrl: LoadingController,
) {} ) {}
async ngOnInit() { async recoverNav () {
if(!this.stateService.dataDrive) { await this.navCtrl.navigateForward(`/recover`, { animationDirection: 'forward' })
const loader = await this.loadingCtrl.create({
message: 'Fetching data drives'
})
await loader.present()
this.dataDrives = await this.apiService.getDataDrives()
loader.dismiss()
}
} }
selectDrive(drive: DataDrive) { async embassyNav () {
if (drive.logicalname === this.selectedDrive?.logicalname) { await this.navCtrl.navigateForward(`/embassy`, { animationDirection: 'forward' })
this.selectedDrive = null
} else {
this.selectedDrive = drive
}
} }
async warn() {
const alert = await this.alertController.create({
cssClass: 'my-custom-class',
header: 'Warning!',
message: 'This drive will be entirely wiped of all existing memory.',
backdropDismiss: false,
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'cancel-button',
handler: () => {
this.selectedDrive = null
}
}, {
text: 'Okay',
cssClass: 'okay-button',
handler: async () => {
await this.chooseDrive()
}
}
]
});
await alert.present();
}
async chooseDrive() {
const loader = await this.loadingCtrl.create({
message: 'Selecting data drive'
})
await loader.present()
try {
await this.apiService.selectDataDrive(this.selectedDrive.logicalname)
this.stateService.dataDrive = this.selectedDrive
} catch (e) {
} finally {
loader.dismiss()
}
}
async presentPasswordModal() {
const modal = await this.modalController.create({
component: PasswordPage,
backdropDismiss: false,
cssClass: 'pw-modal',
})
modal.onDidDismiss().then(ret => {
if(ret.data) {
const pass = ret.data.password
if (pass) {
this.submitPassword(pass)
}
}
})
await modal.present();
}
async submitPassword (pw: string) {
const loader = await this.loadingCtrl.create({
message: 'Setting up your Embassy'
})
await loader.present()
try {
await this.apiService.submitPassword(pw)
console.log('reloading')
location.reload()
} catch (e) {
} finally {
loader.dismiss()
}
}
} }

View File

@@ -1,26 +1,56 @@
<ion-header> <ion-header>
<ion-toolbar color="light"> <ion-toolbar color="light">
<ion-title> <ion-title>
<span *ngIf="needsVer">Enter Password</span> <span *ngIf="verify">Verify Recovery Drive Password</span>
<span *ngIf="!needsVer">Create Password</span> <span *ngIf="!verify">Set Password</span>
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content color="light">
<div style="padding: 8px 24px; height: 100%;">
<div *ngIf="needsVer">
<ion-input style="color: #e6f4f1 !important;" type="password" [(ngModel)]="password" placeholder="_________">Password:</ion-input>
</div>
<div *ngIf="!needsVer"> <ion-content color="light">
<ion-input (click)="error = ''" (focusout)="validate()" style="color: #e6f4f1 !important;" (keyup)="error = ''" type="password" [(ngModel)]="password" placeholder="_________">Password:</ion-input> <div style="padding: 8px 24px;">
<ion-input (focusout)="checkMatch()" style="color: #e6f4f1 !important;" (keyup)="error = ''" type="password" [(ngModel)]="passwordVer" placeholder="_________">Verify Password:</ion-input> <ng-container *ngIf="verify">
<p style="padding: 3px 0">Password:</p>
<ion-input (click)="error = ''" color="light" type="password" [(ngModel)]="password" placeholder="*********"></ion-input>
</ng-container>
<div *ngIf="!verify">
<p *ngIf="embassyDrive.used > 0" style="padding-bottom: 15px;color: var(--ion-color-warning);"><b>Warning:</b> After submit, any data currently stored on <b>{{ embassyDrive.labels.length ? embassyDrive.labels.join(' / ') : embassyDrive.logicalname }}</b> will be wiped.</p>
<p style="padding: 3px 0">Password:</p>
<ion-input
(click)="error = ''"
color="light"
(focusout)="validate()"
(keyup)="error = ''"
type="password"
[(ngModel)]="password"
placeholder="*********"
></ion-input>
<p style="padding: 3px 0">Verify Password:</p>
<ion-input
(focusout)="validate()"
color="light"
(keyup)="error = ''"
type="password"
[(ngModel)]="passwordVer"
placeholder="*********"
></ion-input>
</div> </div>
<p style="color: #FF4961;">{{error}}</p> <p style="color: #FF4961;">{{error}}</p>
<div style="text-align: right; position: absolute; bottom: 10px; right: 24px;">
<ion-button class="claim-button" item-end (click)="cancel()" style="margin-right: 10px;">Cancel</ion-button>
<ion-button class="claim-button" item-end (click)="submitPassword()" [disabled]="!password">Submit</ion-button>
</div>
</div> </div>
</ion-content> </ion-content>
<ion-footer>
<ion-toolbar color="light">
<ion-buttons slot="end" class="ion-padding-end">
<ion-button class="claim-button" (click)="cancel()">
Cancel
</ion-button>
<ion-button class="claim-button" (click)="verify ? verifyPw() : submitPw()">
{{ verify ? 'Verify Password' : 'Submit' }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -1,7 +1,15 @@
.claim-button { .claim-button {
margin-inline-start: 0; margin-inline-start: 0;
margin-inline-end: 0; margin-inline-end: 0;
margin-top: 24px; margin: 6px;
height: 48px; height: 48px;
--background: linear-gradient(45deg, var(--ion-color-light) 16%, var(--ion-color-medium) 150%); --background: linear-gradient(45deg, var(--ion-color-light) 16%, var(--ion-color-medium) 150%);
}
ion-input {
font-weight: 500;
--placeholder-font-weight: 400;
width: 100%;
background: var(--ion-color-dark);
border-radius: 3px;
} }

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular' import { LoadingController, ModalController } from '@ionic/angular'
import { RecoveryDrive } from 'src/app/services/api/api.service' import { ApiService, EmbassyDrive, RecoveryDrive } from 'src/app/services/api/api.service'
@Component({ @Component({
selector: 'password', selector: 'password',
@@ -9,51 +9,61 @@ import { RecoveryDrive } from 'src/app/services/api/api.service'
}) })
export class PasswordPage { export class PasswordPage {
@Input() recoveryDrive: RecoveryDrive @Input() recoveryDrive: RecoveryDrive
@Input() embassyDrive: EmbassyDrive
needsVer: boolean @Input() verify: boolean
error = '' error = ''
password = '' password = ''
passwordVer = '' passwordVer = ''
constructor( constructor(
private modalController: ModalController private modalController: ModalController,
private apiService: ApiService,
private loadingCtrl: LoadingController
) {} ) {}
ngOnInit() { ngOnInit() { }
this.needsVer = !!this.recoveryDrive && !this.recoveryDrive.version.startsWith('0.2')
async verifyPw () {
if(!this.recoveryDrive) this.error = 'No recovery drive' // unreachable
const loader = await this.loadingCtrl.create({
message: 'Verifying Password'
})
await loader.present()
try {
const isCorrectPassword = await this.apiService.verifyRecoveryPassword(this.recoveryDrive.logicalname, this.password)
if(isCorrectPassword) {
this.modalController.dismiss({ password: this.password })
} else {
this.error = "Incorrect password provided"
}
} catch (e) {
this.error = 'Error connecting to Embassy'
} finally {
loader.dismiss()
}
} }
async submitPassword () { async submitPw () {
if(!this.needsVer) { this.validate()
this.validate() if(this.error) return
if(!this.error) {
this.checkMatch()
}
this.modalController.dismiss({
password: this.password,
})
} else {
this.modalController.dismiss({
password: this.password,
})
}
} }
validate () { validate () {
if (this.password.length < 12) { if (this.password.length < 12) {
this.error="*passwords must be 12 characters or greater" this.error="*passwords must be 12 characters or greater"
} } else if (this.password !== this.passwordVer) {
}
checkMatch () {
if (this.password !== this.passwordVer) {
this.error="*passwords dont match" this.error="*passwords dont match"
} else { } else {
this.error = '' this.error = ''
} }
} }
cancel () { cancel () {
this.modalController.dismiss() this.modalController.dismiss()
} }

View File

@@ -7,50 +7,61 @@
<img src="assets/png/logo.png" style="max-width: 240px;" /> <img src="assets/png/logo.png" style="max-width: 240px;" />
</div> </div>
<ion-card> <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>{{ stateService.recoveryDrive && stateService.polling ? 'Recovery Progress: ' + (100 * stateService.dataProgress) + '%' : 'Select Recovery Drive'}}</ion-card-title> <ion-card-title>{{ loading ? 'Loading Recovery Drives' : 'Select Recovery Drive'}}</ion-card-title>
</ion-card-header> </ion-card-header>
<ion-card-content class="ion-margin"> <ion-card-content class="ion-margin">
<div *ngIf="!loading && recoveryDrives && !recoveryDrives.length"> <ng-container *ngIf="!loading && !recoveryDrives.length">
<h2 color="light">No recovery drives found</h2> <h2 color="light">No recovery drives found</h2>
<p color="light">Please connect a recovery drive to your embassy and refresh the page.</p> <p color="light">Please connect a recovery drive to your embassy and refresh the page.</p>
</div> <ion-button
<div *ngIf="!stateService.polling && recoveryDrives?.length"> (click)="window.location.reload()"
<div> style="text-align:center"
<ion-card class="claim-button"
class="wiz-card" >
*ngFor="let drive of recoveryDrives" Refresh
(click)="selectDrive(drive)" </ion-button>
button="true" </ng-container>
[class.selected]="selectedDrive?.logicalname === drive.logicalname"
color="light" <ion-item-group>
> <ng-container *ngIf="loading">
<ion-card-header> <ion-item button color="light" lines="none">
<ion-card-title>{{drive.logicalname}}</ion-card-title> <ion-avatar slot="start">
<ion-card-subtitle>{{drive.name}}</ion-card-subtitle> <ion-skeleton-text animated></ion-skeleton-text>
</ion-card-header> </ion-avatar>
<ion-label class="ion-text-wrap">
<ion-card-content> <ion-skeleton-text style="width: 80%; margin: 13px 0;" animated></ion-skeleton-text>
Currently running {{drive.version}} <ion-skeleton-text style="width: 60%; margin: 10px 0;" animated></ion-skeleton-text>
</ion-card-content> <ion-skeleton-text style="width: 30%; margin: 8px 0;" animated></ion-skeleton-text>
</ion-card> </ion-label>
<div style="width: 100%; text-align: center;;"> </ion-item>
<ion-button </ng-container>
(click)="presentPasswordModal()" <ng-container *ngIf="recoveryDrives.length">
[disabled]="!selectedDrive" <ion-item (click)="chooseDrive(drive)" class="ion-margin-bottom" button color="light" lines="none" *ngFor="let drive of recoveryDrives" [ngClass]="drive.logicalname === selectedDrive?.logicalname ? 'selected' : null">
style="text-align:center" <ion-icon slot="start" name="save-outline"></ion-icon>
size="large" <ion-label class="ion-text-wrap">
<h1>{{ drive.logicalname }}</h1>
<h2>{{ drive.name }}</h2>
<p> Embassy version: {{drive.version}}</p>
</ion-label>
<ion-icon *ngIf="drive.version.startsWith('0.2') || passwords[drive.logicalname]" color="success" slot="end" name="lock-open-outline"></ion-icon>
<ion-icon *ngIf="!drive.version.startsWith('0.2') && !passwords[drive.logicalname]" color="danger" slot="end" name="lock-closed-outline"></ion-icon>
</ion-item>
</ng-container>
<ion-button
(click)="selectRecoveryDrive()"
[disabled]="!selectedDrive || (!passwords[selectedDrive.logicalname] && !selectedDrive.version.startsWith('0.2'))"
class="claim-button" class="claim-button"
> >
Next Next
</ion-button> </ion-button>
</div> </ion-item-group>
</div>
<!-- <div *ngIf="stateService.polling" style="width: 100%; text-align: center;">
</div>
<div *ngIf="stateService.polling" style="width: 100%; text-align: center;">
<ion-progress-bar color="primary" style="max-width: 700px; margin: auto; padding-bottom: 20px; margin-bottom: 40px;" value="{{stateService.dataProgress}}"></ion-progress-bar> <ion-progress-bar color="primary" style="max-width: 700px; margin: auto; padding-bottom: 20px; margin-bottom: 40px;" value="{{stateService.dataProgress}}"></ion-progress-bar>
<ion-button <ion-button
(click)="navToEmbassy()" (click)="navToEmbassy()"
@@ -60,35 +71,9 @@
> >
Go To Embassy Go To Embassy
</ion-button> </ion-button>
</div> </div> -->
</ion-card-content> </ion-card-content>
<ion-card-header class="card-footer">
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<p *ngIf="!stateService.recoveryDrive">
Choose drive recover Embassy data from.
</p>
<p *ngIf="stateService.recoveryDrive && stateService.polling">
Recovering old Embassy data.
</p>
</ion-label>
</ion-item>
</ion-card-header>
</ion-card> </ion-card>
<div class="divider"></div>
<div class="footer">
<p style="margin-bottom: 30px;">Contact/Community</p>
<ion-icon name="logo-youtube"></ion-icon>
<ion-icon name="paper-plane"></ion-icon>
<ion-icon name="mail"></ion-icon>
<ion-icon name="logo-github"></ion-icon>
<ion-icon name="logo-twitter"></ion-icon>
<ion-icon name="logo-mastodon"></ion-icon>
<ion-icon name="logo-medium"></ion-icon>
</div>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>

View File

@@ -1,5 +1,7 @@
.selected { .selected {
border: 4px solid gray; border-radius: 8px;
border: 4px solid var(--ion-color-secondary);
box-shadow: 4px 4px 16px var(--ion-color-light);
} }
ion-card-title { ion-card-title {

View File

@@ -1,5 +1,5 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { AlertController, ModalController, LoadingController } from '@ionic/angular' import { ModalController, NavController } from '@ionic/angular'
import { ApiService, RecoveryDrive } from 'src/app/services/api/api.service' import { ApiService, RecoveryDrive } from 'src/app/services/api/api.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'
@@ -10,86 +10,58 @@ import { PasswordPage } from '../password/password.page'
styleUrls: ['recover.page.scss'], styleUrls: ['recover.page.scss'],
}) })
export class RecoverPage { export class RecoverPage {
passwords = {}
recoveryDrives = [] recoveryDrives = []
selectedDrive: RecoveryDrive = null selectedDrive: RecoveryDrive = null
loading = true loading = true
constructor( constructor(
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly stateService: StateService, private readonly navCtrl: NavController,
public alertController: AlertController,
private modalController: ModalController, private modalController: ModalController,
private readonly loadingCtrl: LoadingController, private stateService: StateService
) {} ) {}
async ngOnInit() { async ngOnInit() {
if(!this.stateService.recoveryDrive) { this.recoveryDrives = await this.apiService.getRecoveryDrives()
const loader = await this.loadingCtrl.create({ this.loading = false
message: 'Fetching recovery drives'
})
await loader.present()
this.recoveryDrives = await this.apiService.getRecoveryDrives()
loader.dismiss()
this.loading = false
} else {
this.loading = false
this.stateService.pollDataTransferProgress()
}
} }
selectDrive(drive: RecoveryDrive) { async chooseDrive(drive: RecoveryDrive) {
if (drive.logicalname === this.selectedDrive?.logicalname) {
if (this.selectedDrive?.logicalname === drive.logicalname) {
this.selectedDrive = null this.selectedDrive = null
return
} else { } else {
this.selectedDrive = drive this.selectedDrive = drive
} }
}
async chooseDrive() { if (drive.version.startsWith('0.2') || this.passwords[drive.logicalname]) return
this.presentPasswordModal()
}
async presentPasswordModal() {
const modal = await this.modalController.create({ const modal = await this.modalController.create({
component: PasswordPage, component: PasswordPage,
backdropDismiss: false,
cssClass: 'pw-modal',
componentProps: { componentProps: {
recoveryDrive: this.selectedDrive, recoveryDrive: this.selectedDrive,
verify: true
} }
}) })
modal.onDidDismiss().then(ret => { modal.onDidDismiss().then(async ret => {
if(ret.data) { if (!ret.data) {
const pass = ret.data.password this.selectedDrive = null
if(pass) { } else if(ret.data.password) {
this.submitPWAndDrive(pass) this.passwords[drive.logicalname] = ret.data.password
}
} }
}) })
await modal.present(); await modal.present();
} }
async submitPWAndDrive(pw: string) { async selectRecoveryDrive() {
const loader = await this.loadingCtrl.create({ this.stateService.recoveryDrive = this.selectedDrive
message: 'Validating password' const pw = this.passwords[this.selectedDrive.logicalname]
}) if(pw) {
await loader.present() this.stateService.recoveryPassword = pw
}
try { await this.navCtrl.navigateForward(`/embassy`, { animationDirection: 'forward' })
this.stateService.recoveryDrive = this.selectedDrive
await this.apiService.selectRecoveryDrive(this.selectedDrive.logicalname, pw)
this.stateService.pollDataTransferProgress()
} catch (e) {
} finally {
loader.dismiss()
}
} }
async navToEmbassy() {
location.reload()
}
} }

View File

@@ -3,18 +3,13 @@ import { Subject } from 'rxjs'
export abstract class ApiService { export abstract class ApiService {
protected error$: Subject<string> = new Subject(); protected error$: Subject<string> = new Subject();
watchError$ = this.error$.asObservable(); watchError$ = this.error$.asObservable();
abstract getState (): Promise<State>; abstract getEmbassyDrives (): Promise<EmbassyDrive[]>;
abstract getDataDrives (): Promise<DataDrive[]>; abstract selectEmbassyDrive (logicalName: string): Promise<void>;
abstract selectDataDrive (logicalName: string): Promise<void>;
abstract getRecoveryDrives (): Promise<RecoveryDrive[]>; abstract getRecoveryDrives (): Promise<RecoveryDrive[]>;
abstract selectRecoveryDrive (logicalName: string, password: string): Promise<void>; abstract selectRecoveryDrive (logicalName: string, password: string): Promise<void>;
abstract getDataTransferProgress (): Promise<TransferProgress>; abstract getDataTransferProgress (): Promise<TransferProgress>;
abstract submitPassword (password: string): Promise<void>; abstract submitPassword (password: string): Promise<void>;
} abstract verifyRecoveryPassword (logicalname: string, password: string): Promise<boolean>;
export interface State {
'data-drive': DataDrive | null;
'recovery-drive': RecoveryDrive | null;
} }
export interface TransferProgress { export interface TransferProgress {
@@ -22,7 +17,7 @@ export interface TransferProgress {
'total-bytes': number; 'total-bytes': number;
} }
export interface DataDrive { export interface EmbassyDrive {
logicalname: string; logicalname: string;
labels: string[]; labels: string[];
capacity: number; capacity: number;

View File

@@ -14,14 +14,14 @@ export class MockApiService extends ApiService {
async getState() { async getState() {
await pauseFor(2000) await pauseFor(2000)
return { return {
'data-drive': 'embassy-drive':
// null, null,
{ // {
logicalname: 'name1', // logicalname: 'name1',
labels: ['label 1', 'label 2'], // labels: ['label 1', 'label 2'],
capacity: 1600, // capacity: 1600,
used: 200, // used: 200,
}, // },
'recovery-drive': 'recovery-drive':
null, null,
// { // {
@@ -40,25 +40,25 @@ export class MockApiService extends ApiService {
} }
} }
async getDataDrives() { async getEmbassyDrives() {
await pauseFor(2000) await pauseFor(2000)
return [ return [
{ {
logicalname: 'Name1', logicalname: 'Name1',
labels: ['label 1', 'label 2'], labels: ['label 1', 'label 2'],
capacity: 1600, capacity: 1600.66666,
used: 200, used: 200.1255312,
}, },
{ {
logicalname: 'Name2', logicalname: 'Name2',
labels: [], labels: [],
capacity: 1600, capacity: 1600.01234,
used: 0, used: 0.00,
} }
] ]
} }
async selectDataDrive(drive) { async selectEmbassyDrive(drive) {
await pauseFor(2000) await pauseFor(2000)
return return
} }
@@ -67,9 +67,14 @@ export class MockApiService extends ApiService {
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',
version: '0.2.7',
name: 'My Embassy'
} }
] ]
} }
@@ -83,6 +88,11 @@ export class MockApiService extends ApiService {
await pauseFor(2000) await pauseFor(2000)
return return
} }
async verifyRecoveryPassword(logicalname, password) {
await pauseFor(2000)
return password.length > 8
}
} }
let tries = 0 let tries = 0

View File

@@ -1,15 +1,15 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ApiService, DataDrive, RecoveryDrive } from './api/api.service' import { ApiService, EmbassyDrive, RecoveryDrive } from './api/api.service'
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class StateService { export class StateService {
loading = true
polling = false polling = false
dataDrive: DataDrive; embassyDrive: EmbassyDrive;
recoveryDrive: RecoveryDrive; recoveryDrive: RecoveryDrive;
recoveryPassword: string
dataTransferProgress: { bytesTransfered: number; totalBytes: number } | null; dataTransferProgress: { bytesTransfered: number; totalBytes: number } | null;
dataProgress = 0; dataProgress = 0;
@@ -17,15 +17,14 @@ export class StateService {
private readonly apiService: ApiService private readonly apiService: ApiService
) {} ) {}
async getState() { reset() {
this.loading = true this.polling = false
const state = await this.apiService.getState()
if(state) {
this.dataDrive = state['data-drive']
this.recoveryDrive = state['recovery-drive']
this.loading = false this.embassyDrive = null
} this.recoveryDrive = null
this.recoveryPassword = null
this.dataTransferProgress = null
this.dataProgress = 0
} }
async pollDataTransferProgress() { async pollDataTransferProgress() {

View File

@@ -3,6 +3,9 @@
/** Ionic CSS Variables **/ /** Ionic CSS Variables **/
:root { :root {
--ion-text-color: var(--ion-color-dark);
--ion-text-color-rgb: var(--ion-color-dark-rgb);
--ion-font-family: 'Benton Sans'; --ion-font-family: 'Benton Sans';
/** primary **/ /** primary **/
--ion-color-primary: #428cff; --ion-color-primary: #428cff;
@@ -53,12 +56,12 @@
--ion-color-danger-tint: #ff5b71; --ion-color-danger-tint: #ff5b71;
/** dark **/ /** dark **/
--ion-color-dark: #f4f5f8; --ion-color-dark: #e0e0e0;
--ion-color-dark-rgb: 244,245,248; --ion-color-dark-rgb: 224,224,224;
--ion-color-dark-contrast: #000000; --ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0; --ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da; --ion-color-dark-shade: #bfbfbf;
--ion-color-dark-tint: #f5f6f9; --ion-color-dark-tint: #d8d8d8;
/** medium **/ /** medium **/
--ion-color-medium: #989aa2; --ion-color-medium: #989aa2;