better eager loading and better error messaging for backup flow (#1368)

* better eager loading and better error messaging for backup flow

* add arch qp to marketplace proxy requests

* better styling for eos release notes
This commit is contained in:
Matt Hill
2022-03-31 11:05:59 -06:00
committed by GitHub
parent e79b27e0bb
commit 01f14061ec
22 changed files with 162 additions and 140 deletions

View File

@@ -13,6 +13,7 @@
"mocks": {
"maskAs": "tor",
"skipStartupAlerts": true
}
},
"targetArch": "aarch64"
}
}

View File

@@ -3,7 +3,7 @@
New in {{ pkg.manifest.version | displayEmver }}
<ion-button routerLink="notes" class="all-notes" fill="clear" color="dark">
All Release Notes
<ion-icon slot="end" name="arrow-forward-outline"></ion-icon>
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item lines="none" color="transparent">

View File

@@ -9,14 +9,14 @@
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('license')">
<ion-label>
<h2>License</h2>
<p>{{ pkg.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
button
@@ -27,7 +27,7 @@
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward-outline"></ion-icon>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>

View File

@@ -1,4 +1,5 @@
export type WorkspaceConfig = {
targetArch: 'aarch64' | 'x86_64'
gitHash: string
useMocks: boolean
// each key corresponds to a project and values adjust settings for that project, eg: ui, setup-wizard, diagnostic-ui

View File

@@ -119,6 +119,7 @@
<ion-icon name="alert-circle-outline"></ion-icon>
<ion-icon name="aperture-outline"></ion-icon>
<ion-icon name="arrow-back"></ion-icon>
<ion-icon name="arrow-forward"></ion-icon>
<ion-icon name="arrow-up"></ion-icon>
<ion-icon name="briefcase-outline"></ion-icon>
<ion-icon name="bookmark-outline"></ion-icon>
@@ -165,7 +166,9 @@
<ion-icon name="medkit-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="open-outline"></ion-icon>
<ion-icon name="options-outline"></ion-icon>
<ion-icon name="pencil"></ion-icon>
<ion-icon name="phone-portrait-outline"></ion-icon>
<ion-icon name="play-circle-outline"></ion-icon>
<ion-icon name="power"></ion-icon>
@@ -177,12 +180,15 @@
<ion-icon name="remove"></ion-icon>
<ion-icon name="remove-circle-outline"></ion-icon>
<ion-icon name="remove-outline"></ion-icon>
<ion-icon name="reorder-three"></ion-icon>
<ion-icon name="rocket-outline"></ion-icon>
<ion-icon name="save-outline"></ion-icon>
<ion-icon name="shield-checkmark-outline"></ion-icon>
<ion-icon name="stop-outline"></ion-icon>
<ion-icon name="storefront-outline"></ion-icon>
<ion-icon name="swap-vertical"></ion-icon>
<ion-icon name="terminal-outline"></ion-icon>
<ion-icon name="trash"></ion-icon>
<ion-icon name="trash-outline"></ion-icon>
<ion-icon name="warning-outline"></ion-icon>
<ion-icon name="wifi"></ion-icon>
@@ -190,6 +196,7 @@
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>
<ion-alert></ion-alert>
<ion-back-button></ion-back-button>
<ion-badge></ion-badge>
<ion-button></ion-button>
<ion-buttons></ion-buttons>
@@ -220,7 +227,9 @@
<ion-modal></ion-modal>
<ion-note></ion-note>
<ion-radio></ion-radio>
<ion-reorder></ion-reorder>
<ion-row></ion-row>
<ion-searchbar></ion-searchbar>
<ion-segment></ion-segment>
<ion-segment-button></ion-segment-button>
<ion-select></ion-select>
@@ -234,6 +243,21 @@
<ion-toggle></ion-toggle>
<ion-toolbar></ion-toolbar>
<ion-menu-button></ion-menu-button>
<!-- fonts -->
<p style="font-family: Montserrat">a</p>
<p style="font-family: Montserrat; font-weight: bold">a</p>
<p style="font-family: Montserrat; font-weight: 100">a</p>
<p style="font-family: Open Sans">a</p>
<p style="font-family: Open Sans; font-weight: bold">a</p>
<p style="font-family: Open Sans; font-weight: 100">a</p>
<!-- images -->
<img src="assets/img/logo.png" />
<img src="assets/img/icons/snek.png" />
<img src="assets/img/icons/wifi-1.png" />
<img src="assets/img/icons/wifi-2.png" />
<img src="assets/img/icons/wifi-3.png" />
</section>
</ion-content>
<ion-footer

View File

@@ -56,6 +56,7 @@ export class BackupService {
}
hasValidBackup(target: BackupTarget): boolean {
return this.emver.compare(target['embassy-os']?.version, '0.3.0') !== -1
const backup = target['embassy-os']
return !!backup && this.emver.compare(backup.version, '0.3.0') !== -1
}
}

View File

@@ -4,7 +4,7 @@
}
ion-input {
font-weight: 500;
font-weight: bold;
--placeholder-font-weight: 400;
}

View File

@@ -35,6 +35,11 @@
min-height: 40vh
}
.notes-content {
text-align: left;
margin: 32px;
}
.status-label {
font-size: xx-large;
font-weight: bold;

View File

@@ -1,14 +1,17 @@
<div class="slide-content">
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="params.titleColor">
<h1>{{ params.title }}</h1>
<h2>{{ params.headline }}</h2>
</ion-label>
</div>
<div *ngFor="let note of params.notes | keyvalue : asIsOrder">
<h2>{{ note.key }}</h2>
<div class="long-message" [innerHTML]="note.value | markdown"></div>
<div class="notes-content">
<h1>{{ params.title }}</h1>
<br />
<div *ngFor="let v of params.versions; let i = index">
<h4>
<b>
{{ v.version }}
<span *ngIf="i === 0"> (Current Version)</span>
</b>
</h4>
<hr style="height: 0; border-width: 1px" />
<div [innerHTML]="v.notes | markdown"></div>
<br />
</div>
</div>
</div>

View File

@@ -8,17 +8,17 @@ import { BehaviorSubject, Subject } from 'rxjs'
})
export class NotesComponent {
@Input() params: {
notes: { [version: string]: string }
versions: { version: string; notes: string }[]
title: string
titleColor: string
headline: string
}
load () { }
load() {}
loading$ = new BehaviorSubject(false)
cancel$ = new Subject<void>()
asIsOrder () {
asIsOrder() {
return 0
}
}

View File

@@ -103,6 +103,16 @@ export class WizardBaker {
}): InstallWizardComponent['params'] {
const { version, releaseNotes, headline } = values
const versions = Object.keys(releaseNotes)
.sort()
.reverse()
.map(version => {
return {
version,
notes: releaseNotes[version],
}
})
const action = 'update'
const title = 'EmbassyOS'
const toolbar: TopbarParams = { action, title, version }
@@ -112,7 +122,7 @@ export class WizardBaker {
slide: {
selector: 'notes',
params: {
notes: releaseNotes,
versions,
title: 'Release Notes',
titleColor: 'dark',
headline,

View File

@@ -99,7 +99,7 @@ function loading(
return pipe(
// Show notification on error
catchError(e => from(errToast.present(e))),
// Map any result to false to stop loading inidicator
// Map any result to false to stop loading indicator
mapTo(false),
// Start operation with true
startWith(true),

View File

@@ -1,5 +1,9 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import {
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
GenericInputComponent,
@@ -12,7 +16,6 @@ import {
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { AppRecoverSelectPage } from 'src/app/modals/app-recover-select/app-recover-select.page'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import * as argon2 from '@start9labs/argon2'
@Component({
@@ -25,21 +28,24 @@ export class RestorePage {
private readonly modalCtrl: ModalController,
private readonly navCtrl: NavController,
private readonly embassyApi: ApiService,
private readonly patch: PatchDbService,
private readonly loadingCtrl: LoadingController,
) {}
async presentModalPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
): Promise<void> {
const options: GenericInputOptions = {
title: 'Master Password Required',
title: 'Password Required',
message:
'Enter your master password. On the next screen, you will select the individual services you want to restore.',
'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.',
label: 'Master Password',
placeholder: 'Enter master password',
useMask: true,
buttonText: 'Next',
submitFn: (password: string) => this.decryptDrive(target, password),
submitFn: async (password: string) => {
argon2.verify(target.entry['embassy-os']['password-hash'], password)
await this.restoreFromBackup(target, password)
},
}
const modal = await this.modalCtrl.create({
@@ -52,57 +58,27 @@ export class RestorePage {
await modal.present()
}
private async decryptDrive(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
): Promise<void> {
const passwordHash = this.patch.getData()['server-info']['password-hash']
argon2.verify(passwordHash, password)
try {
argon2.verify(target.entry['embassy-os']['password-hash'], password)
await this.restoreFromBackup(target, password)
} catch (e) {
setTimeout(() => this.presentModalOldPassword(target, password), 500)
}
}
private async presentModalOldPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
): Promise<void> {
const options: GenericInputOptions = {
title: 'Original Password Needed',
message:
'This backup was created with a different password. Enter the ORIGINAL password that was used to encrypt this backup.',
label: 'Original Password',
placeholder: 'Enter original password',
useMask: true,
buttonText: 'Restore From Backup',
submitFn: (oldPassword: string) =>
this.restoreFromBackup(target, password, oldPassword),
}
const m = await this.modalCtrl.create({
component: GenericInputComponent,
componentProps: { options },
presentingElement: await this.modalCtrl.getTop(),
cssClass: 'alertlike-modal',
})
await m.present()
}
private async restoreFromBackup(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
oldPassword?: string,
): Promise<void> {
const backupInfo = await this.embassyApi.getBackupInfo({
'target-id': target.id,
password,
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Decrypting drive...',
cssClass: 'loader',
})
this.presentModalSelect(target.id, backupInfo, password, oldPassword)
await loader.present()
try {
const backupInfo = await this.embassyApi.getBackupInfo({
'target-id': target.id,
password,
})
this.presentModalSelect(target.id, backupInfo, password, oldPassword)
} finally {
loader.dismiss()
}
}
private async presentModalSelect(

View File

@@ -86,7 +86,29 @@ export class ServerBackupPage {
placeholder: 'Enter master password',
useMask: true,
buttonText: 'Create Backup',
submitFn: (password: string) => this.test(target, password),
submitFn: async (password: string) => {
// confirm password matches current master password
const passwordHash =
this.patch.getData()['server-info']['password-hash']
argon2.verify(passwordHash, password)
// first time backup
if (!target.hasValidBackup) {
await this.createBackup(target.id, password)
// existing backup
} else {
try {
argon2.verify(target.entry['embassy-os']['password-hash'], password)
} catch {
setTimeout(
() => this.presentModalOldPassword(target, password),
500,
)
return
}
await this.createBackup(target.id, password)
}
},
}
const m = await this.modalCtrl.create({
@@ -98,33 +120,6 @@ export class ServerBackupPage {
await m.present()
}
private async test(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
oldPassword?: string,
): Promise<void> {
const passwordHash = this.patch.getData()['server-info']['password-hash']
argon2.verify(passwordHash, password)
if (!target.hasValidBackup) {
await this.createBackup(target.id, password)
} else {
try {
argon2.verify(
target.entry['embassy-os']['password-hash'],
oldPassword || password,
)
await this.createBackup(target.id, password)
} catch (e) {
if (oldPassword) {
throw e
} else {
setTimeout(() => this.presentModalOldPassword(target, password), 500)
}
}
}
}
private async presentModalOldPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
@@ -137,8 +132,10 @@ export class ServerBackupPage {
placeholder: 'Enter original password',
useMask: true,
buttonText: 'Create Backup',
submitFn: (oldPassword: string) =>
this.test(target, password, oldPassword),
submitFn: async (oldPassword: string) => {
argon2.verify(target.entry['embassy-os']['password-hash'], oldPassword)
await this.createBackup(target.id, password, oldPassword)
},
}
const m = await this.modalCtrl.create({

View File

@@ -1,5 +1,4 @@
import { PackageState } from 'src/app/types/package-state'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import {
DependencyErrorType,
DockerIoFormat,
@@ -1034,7 +1033,8 @@ export module Mock {
version: '0.3.0',
full: true,
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNK',
// password is asdfasdf
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': '',
},
},
@@ -1056,21 +1056,23 @@ export module Mock {
mountable: true,
'embassy-os': null,
},
// 'powjefhjbnwhdva': {
// type: 'disk',
// logicalname: 'sdba1',
// label: 'Another Drive',
// capacity: 2000000000000,
// used: 100000000000,
// model: null,
// vendor: 'SSK',
// 'embassy-os': {
// version: '0.3.0',
// full: true,
// 'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
// 'wrapped-key': '',
// },
// },
powjefhjbnwhdva: {
type: 'disk',
logicalname: 'sdba1',
label: 'Another Drive',
capacity: 2000000000000,
used: 100000000000,
model: null,
vendor: 'SSK',
'embassy-os': {
version: '0.3.0',
full: true,
// password is asdfasdf
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'wrapped-key': '',
},
},
}
export const BackupInfo: RR.GetBackupInfoRes = {

View File

@@ -245,7 +245,7 @@ export module RR {
export type GetMarketplaceDataRes = MarketplaceData
export type GetMarketplaceEOSReq = {
'eos-version-compat': string
'eos-version': string
}
export type GetMarketplaceEOSRes = MarketplaceEOS

View File

@@ -105,8 +105,9 @@ export class LiveApiService extends ApiService {
// marketplace URLs
async marketplaceProxy<T>(path: string, params: {}, url: string): Promise<T> {
const fullURL = `${url}${path}?${new URLSearchParams(params).toString()}`
async marketplaceProxy<T>(path: string, qp: {}, url: string): Promise<T> {
Object.assign(qp, { arch: this.config.targetArch })
const fullURL = `${url}${path}?${new URLSearchParams(qp).toString()}`
return this.http.rpcRequest({
method: 'marketplace.get',
params: { url: fullURL },

View File

@@ -374,7 +374,7 @@ export class MockApiService extends ApiService {
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes> {
await pauseFor(2000)
const path = '/server-info/status'
const path = '/server-info/status-info/backing-up'
const ids = ['bitcoind', 'lnd']
setTimeout(async () => {
@@ -402,7 +402,7 @@ export class MockApiService extends ApiService {
{
op: PatchOp.REPLACE,
path,
value: ServerStatus.Running,
value: false,
},
]
this.updateMock(lastPatch)
@@ -412,7 +412,7 @@ export class MockApiService extends ApiService {
{
op: PatchOp.REPLACE,
path,
value: ServerStatus.BackingUp,
value: true,
},
]
@@ -788,6 +788,10 @@ export class MockApiService extends ApiService {
op: PatchOp.REMOVE,
path: `/package-data/${id}/install-progress`,
},
{
op: PatchOp.REMOVE,
path: `/recovered-packages/${id}`,
},
]
this.updateMock(patch2)
}, 1000)

View File

@@ -25,10 +25,15 @@ export const mockPatchData: DataModel = {
'lan-address': 'https://embassy-abcdefgh.local',
'tor-address': 'http://myveryownspecialtoraddress.onion',
'unread-notification-count': 4,
// password is asdfasdf
'password-hash':
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
'eos-version-compat': '>=0.3.0 <=0.3.0.1',
'status-info': null,
'status-info': {
'backing-up': false,
updated: false,
'update-progress': null,
},
},
'recovered-packages': {
'btc-rpc-proxy': {

View File

@@ -8,6 +8,7 @@ import {
} from 'src/app/services/patch-db/data-model'
const {
targetArch,
gitHash,
useMocks,
ui: { patchDb, api, mocks, marketplace },
@@ -19,15 +20,13 @@ const {
export class ConfigService {
origin = removePort(removeProtocol(window.origin))
version = require('../../../../../package.json').version
useMocks = useMocks
mocks = mocks
targetArch = targetArch
gitHash = gitHash
patchDb = patchDb
api = api
marketplace = marketplace
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
isConsulate = window['platform'] === 'ios'
supportsWebSockets = !!window.WebSocket || this.isConsulate

View File

@@ -19,9 +19,9 @@ export class EOSService {
) {}
async getEOS(): Promise<boolean> {
const version = this.patch.getData()['server-info'].version
this.eos = await this.api.getEos({
'eos-version-compat':
this.patch.getData()['server-info']['eos-version-compat'],
'eos-version': version,
})
const updateAvailable =
this.emver.compare(

View File

@@ -33,13 +33,6 @@
src: url('/assets/fonts/Open_Sans/OpenSans-Bold.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 500;
src: url('/assets/fonts/Open_Sans/OpenSans-SemiBold.ttf');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
@@ -81,7 +74,7 @@ $subheader-height: 48px;
.input-label {
margin-bottom: 6px;
font-size: medium;
font-weight: 500;
font-weight: bold;
* {
display: inline-block;
vertical-align: middle;