Merge pull request #2668 from Start9Labs/fix/backup-create

solve infinite recursion and promise returning true
This commit is contained in:
Matt Hill
2024-07-12 11:16:00 -06:00
committed by GitHub
11 changed files with 174 additions and 116 deletions

94
web/package-lock.json generated
View File

@@ -28,12 +28,12 @@
"@start9labs/argon2": "^0.2.2",
"@start9labs/emver": "^0.1.5",
"@start9labs/start-sdk": "file:../sdk/dist",
"@taiga-ui/addon-charts": "3.84.0",
"@taiga-ui/cdk": "3.84.0",
"@taiga-ui/core": "3.84.0",
"@taiga-ui/experimental": "3.84.0",
"@taiga-ui/icons": "3.84.0",
"@taiga-ui/kit": "3.84.0",
"@taiga-ui/addon-charts": "3.86.0",
"@taiga-ui/cdk": "3.86.0",
"@taiga-ui/core": "3.86.0",
"@taiga-ui/experimental": "3.86.0",
"@taiga-ui/icons": "3.86.0",
"@taiga-ui/kit": "3.86.0",
"@tinkoff/ng-dompurify": "4.0.0",
"@tinkoff/ng-event-plugins": "3.2.0",
"angular-svg-round-progressbar": "^9.0.0",
@@ -4128,9 +4128,9 @@
}
},
"node_modules/@taiga-ui/addon-charts": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.84.0.tgz",
"integrity": "sha512-XR7UFywnrv4NRLHOCbba63gXDYYDL4Rt0MbjnF54p5U2EXnbt2of7VbjlB6cPx40XkQqfqa3CNayYxWZP82Ijg==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.86.0.tgz",
"integrity": "sha512-Du/85qqaj8hpFSI6hPuFeIhtE93Z6WSkYZLt0gvnsaCb2qSAg8D4oHSogrtF1rsWGGoM+fvXjD7UEUw9GzFIPg==",
"dependencies": {
"tslib": "^2.6.2"
},
@@ -4138,15 +4138,15 @@
"@angular/common": ">=12.0.0",
"@angular/core": ">=12.0.0",
"@ng-web-apis/common": "^3.0.6",
"@taiga-ui/cdk": "^3.84.0",
"@taiga-ui/core": "^3.84.0",
"@taiga-ui/cdk": "^3.86.0",
"@taiga-ui/core": "^3.86.0",
"@tinkoff/ng-polymorpheus": "^4.3.0"
}
},
"node_modules/@taiga-ui/addon-commerce": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.84.0.tgz",
"integrity": "sha512-1zqLwnZLAYYcHvjH89d7JmtV2+QeZ2YnSJ3YWEMNLjGPzpev4RvQXtDfglIyu0LCyTxqpXmuzes9v/cgq2P5TQ==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.86.0.tgz",
"integrity": "sha512-8QSB490ckI4jnU+1sQ3x8os2GVE162hbvzPVYIZ0TruoeXl076dAz6PT2WRaFwjcaCAIGsuaQgQ4Cv02NjkiYQ==",
"peer": true,
"dependencies": {
"tslib": "^2.6.2"
@@ -4159,18 +4159,18 @@
"@maskito/core": "^1.9.0",
"@maskito/kit": "^1.9.0",
"@ng-web-apis/common": "^3.0.6",
"@taiga-ui/cdk": "^3.84.0",
"@taiga-ui/core": "^3.84.0",
"@taiga-ui/i18n": "^3.84.0",
"@taiga-ui/kit": "^3.84.0",
"@taiga-ui/cdk": "^3.86.0",
"@taiga-ui/core": "^3.86.0",
"@taiga-ui/i18n": "^3.86.0",
"@taiga-ui/kit": "^3.86.0",
"@tinkoff/ng-polymorpheus": "^4.3.0",
"rxjs": ">=6.0.0"
}
},
"node_modules/@taiga-ui/cdk": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.84.0.tgz",
"integrity": "sha512-0umw/CUmYNEYOCUNQVTQS53zXzxZsH/6+lj1mFVzocvfJFJWAUT6ltCH9QvxYmxSDDGWwNGg16AaVo2K+aGL0w==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.86.0.tgz",
"integrity": "sha512-aVbnW01Oh0Er1sHKVGHP8W05mOSKxjSzFE3Qx4iF4T6KW7Rlz9HZoNx5ADMg0TATYChtWh9Kwjo8I4LSVj2ZUw==",
"dependencies": {
"@ng-web-apis/common": "3.0.6",
"@ng-web-apis/mutation-observer": "3.1.0",
@@ -4221,11 +4221,11 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@taiga-ui/core": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.84.0.tgz",
"integrity": "sha512-FZy77z0E4qjYcszVcp+qPFkPwJPl8qXZb7t2P+juUtJvSmSn2foQHHdyhbIYN808H26tqCdgkTMG1BWQxVuDSg==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.86.0.tgz",
"integrity": "sha512-diQKOnPtDDfxPOMk6wLRq8nyDVfNSPSNy+1TeyqzUgOvJ6XAjfaBXGsL3iuR7AN8+sz/b3rJmBce+vdw6FjMLQ==",
"dependencies": {
"@taiga-ui/i18n": "^3.84.0",
"@taiga-ui/i18n": "^3.86.0",
"tslib": "^2.6.2"
},
"peerDependencies": {
@@ -4237,35 +4237,35 @@
"@angular/router": ">=12.0.0",
"@ng-web-apis/common": "^3.0.6",
"@ng-web-apis/mutation-observer": "^3.1.0",
"@taiga-ui/cdk": "^3.84.0",
"@taiga-ui/i18n": "^3.84.0",
"@taiga-ui/cdk": "^3.86.0",
"@taiga-ui/i18n": "^3.86.0",
"@tinkoff/ng-event-plugins": "^3.2.0",
"@tinkoff/ng-polymorpheus": "^4.3.0",
"rxjs": ">=6.0.0"
}
},
"node_modules/@taiga-ui/experimental": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.84.0.tgz",
"integrity": "sha512-q0hNVy+EmywCG8hpZlg/+haKIFhnmxicQiSeV/D1P7CHO10safjGo0ptT6e1hYMFa5/cJZOM4OwDPen2xs17Wg==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.86.0.tgz",
"integrity": "sha512-ACjoRVeX5MgsNJsiu2ukliXLD2mfEWm8Vtmk78vqcnkyPUmy1ZWK4sG3p5ybFN8AdIMHkblVq0l+x2qAwr/+LQ==",
"dependencies": {
"tslib": "^2.6.2"
},
"peerDependencies": {
"@angular/common": ">=12.0.0",
"@angular/core": ">=12.0.0",
"@taiga-ui/addon-commerce": "^3.84.0",
"@taiga-ui/cdk": "^3.84.0",
"@taiga-ui/core": "^3.84.0",
"@taiga-ui/kit": "^3.84.0",
"@taiga-ui/addon-commerce": "^3.86.0",
"@taiga-ui/cdk": "^3.86.0",
"@taiga-ui/core": "^3.86.0",
"@taiga-ui/kit": "^3.86.0",
"@tinkoff/ng-polymorpheus": "^4.3.0",
"rxjs": ">=6.0.0"
}
},
"node_modules/@taiga-ui/i18n": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.85.0.tgz",
"integrity": "sha512-CGoxfq9WY+psX5ZOfWmuQZ6OA/0CAPYJTlbHkw5sRKAyhEQ3NM/Wbx3xcwrcYRRJDnt9yOlfibz+3a+WDF2bFA==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.86.0.tgz",
"integrity": "sha512-8zkNhMo/QtxZ2Zp6EP/nxo4SOLwaIrX+P3X/Wt+1cjFNZUYWWfdvfHLLdNviKFPVl4RAOxvkhDfza/wkrwv+iQ==",
"dependencies": {
"tslib": "^2.6.2"
},
@@ -4276,20 +4276,20 @@
}
},
"node_modules/@taiga-ui/icons": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.84.0.tgz",
"integrity": "sha512-KiH7BJRZ6wbkOHlJAS0XHq2gYnQTpRgdEogKW+GoD0da/4trCdM66vhDk2j0DwDFdBGq5U0inHJCjnskBI1nSQ==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.86.0.tgz",
"integrity": "sha512-jVBEbvE/r9JG+knmXMTn/l/js3JjYi8nSGbrLCryJZZoS2izRnQARN2txABieUJm8H463CoF0rcdXlHKRuA4Ew==",
"dependencies": {
"tslib": "^2.6.2"
},
"peerDependencies": {
"@taiga-ui/cdk": "^3.84.0"
"@taiga-ui/cdk": "^3.86.0"
}
},
"node_modules/@taiga-ui/kit": {
"version": "3.84.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.84.0.tgz",
"integrity": "sha512-lSUPDco5FeBYK3ESnXeEPLCdMCmNXwcdHNK/we+0ZoH4VPx/OGg2hpEP0Fej7jfGHwXFTzDbufQD0hT6WlfTAw==",
"version": "3.86.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.86.0.tgz",
"integrity": "sha512-naAy4pyhCaQ9+vWxqSMjbV+9KwnMxT5ybrw+MAJgMn2evzRq0FjqzyFZFog7oiRbRvgVdoWPQfBNKaaLhJcpsw==",
"dependencies": {
"@maskito/angular": "1.9.0",
"@maskito/core": "1.9.0",
@@ -4306,9 +4306,9 @@
"@ng-web-apis/common": "3.0.6",
"@ng-web-apis/mutation-observer": "^3.1.0",
"@ng-web-apis/resize-observer": "^3.0.6",
"@taiga-ui/cdk": "^3.84.0",
"@taiga-ui/core": "^3.84.0",
"@taiga-ui/i18n": "^3.84.0",
"@taiga-ui/cdk": "^3.86.0",
"@taiga-ui/core": "^3.86.0",
"@taiga-ui/i18n": "^3.86.0",
"@tinkoff/ng-polymorpheus": "^4.3.0",
"rxjs": ">=6.0.0"
}

View File

@@ -51,12 +51,12 @@
"@start9labs/argon2": "^0.2.2",
"@start9labs/emver": "^0.1.5",
"@start9labs/start-sdk": "file:../sdk/dist",
"@taiga-ui/addon-charts": "3.84.0",
"@taiga-ui/cdk": "3.84.0",
"@taiga-ui/core": "3.84.0",
"@taiga-ui/experimental": "3.84.0",
"@taiga-ui/icons": "3.84.0",
"@taiga-ui/kit": "3.84.0",
"@taiga-ui/addon-charts": "3.86.0",
"@taiga-ui/cdk": "3.86.0",
"@taiga-ui/core": "3.86.0",
"@taiga-ui/experimental": "3.86.0",
"@taiga-ui/icons": "3.86.0",
"@taiga-ui/kit": "3.86.0",
"@tinkoff/ng-dompurify": "4.0.0",
"@tinkoff/ng-event-plugins": "3.2.0",
"angular-svg-round-progressbar": "^9.0.0",

View File

@@ -15,7 +15,6 @@ export class ErrorService extends ErrorHandler {
this.alerts
.open(getErrorMessage(error, link), {
label: 'Error',
autoClose: false,
status: TuiNotification.Error,
})
.subscribe()

View File

@@ -61,7 +61,7 @@ export class BackupService {
)
}
async hasThisBackup(target: BackupTarget, id: string): Promise<boolean> {
hasThisBackup(target: BackupTarget, id: string): boolean {
return (
target.startOs[id] &&
this.emver.compare(target.startOs[id].version, '0.3.6') !== -1

View File

@@ -32,7 +32,7 @@ export class BadgeMenuComponent {
constructor(
private readonly splitPane: SplitPaneTracker,
private readonly patch: PatchDB<DataModel>,
private readonly dialog: TuiDialogService,
private readonly dialogs: TuiDialogService,
private readonly clientStorageService: ClientStorageService,
) {}
@@ -44,6 +44,6 @@ export class BadgeMenuComponent {
}
onWidgets() {
this.dialog.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe()
this.dialogs.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe()
}
}

View File

@@ -11,6 +11,7 @@
<span *ngIf="spec.required">*</span>
<select
tuiSelect
[placeholder]="spec.name"
[items]="items"
[disabledItemHandler]="disabledItemHandler"
></select>

View File

@@ -6,6 +6,10 @@ import {
LoadingService,
StartOSDiskInfo,
} from '@start9labs/shared'
import {
PasswordPromptComponent,
PromptOptions,
} from 'src/app/modals/password-prompt.component'
import {
BackupInfo,
CifsBackupTarget,
@@ -14,7 +18,6 @@ import {
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page'
import { PasswordPromptModal } from './password-prompt.modal'
@Component({
selector: 'backup-server-select',
@@ -38,23 +41,35 @@ export class BackupServerSelectModal {
async presentModalPassword(
serverId: string,
server: StartOSDiskInfo,
{ passwordHash }: StartOSDiskInfo,
): Promise<void> {
const options: PromptOptions = {
title: 'Password Required',
message:
'Enter the password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.',
label: 'Decrypt Backup',
placeholder: 'Enter password',
buttonText: 'Next',
}
const modal = await this.modalCtrl.create({
component: PasswordPromptModal,
component: PasswordPromptComponent,
componentProps: { options },
canDismiss: async password => {
if (password === null) {
return true
}
try {
argon2.verify(passwordHash!, password)
await this.restoreFromBackup(serverId, password)
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
}
},
})
modal.present()
const { data, role } = await modal.onWillDismiss()
if (role === 'confirm') {
try {
argon2.verify(server.passwordHash!, data)
await this.restoreFromBackup(serverId, data)
} catch (e: any) {
this.errorService.handleError(e)
}
}
}
private async restoreFromBackup(

View File

@@ -1,14 +1,29 @@
import { Component } from '@angular/core'
import {
AfterViewInit,
Component,
ElementRef,
Input,
ViewChild,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule, ModalController } from '@ionic/angular'
import { TuiTextfieldComponent } from '@taiga-ui/core'
import { TuiInputPasswordModule } from '@taiga-ui/kit'
export interface PromptOptions {
title: string
message: string
label: string
placeholder: string
buttonText: string
}
@Component({
standalone: true,
template: `
<ion-header>
<ion-toolbar>
<ion-title>Decrypt Backup</ion-title>
<ion-title>{{ options.title }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="cancel()">
<ion-icon slot="icon-only" name="close"></ion-icon>
@@ -18,13 +33,11 @@ import { TuiInputPasswordModule } from '@taiga-ui/kit'
</ion-header>
<ion-content class="ion-padding">
<p>{{ options.message }}</p>
<p>
Enter the password that was used to encrypt this backup. On the next
screen, you will select the individual services you want to restore.
</p>
<p>
<tui-input-password [(ngModel)]="password">
Enter password
<tui-input-password [(ngModel)]="password" (keydown.enter)="confirm()">
{{ options.label }}
<input tuiTextfield [placeholder]="options.placeholder" />
</tui-input-password>
</p>
</ion-content>
@@ -47,18 +60,30 @@ import { TuiInputPasswordModule } from '@taiga-ui/kit'
[disabled]="!password"
(click)="confirm()"
>
Next
{{ options.buttonText }}
</ion-button>
</ion-toolbar>
</ion-footer>
`,
imports: [IonicModule, FormsModule, TuiInputPasswordModule],
})
export class PasswordPromptModal {
export class PasswordPromptComponent implements AfterViewInit {
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
input?: ElementRef<HTMLInputElement>
@Input()
options!: PromptOptions
password = ''
constructor(private modalCtrl: ModalController) {}
ngAfterViewInit() {
setTimeout(() => {
this.input?.nativeElement.focus({ preventScroll: true })
}, 300)
}
cancel() {
return this.modalCtrl.dismiss(null, 'cancel')
}

View File

@@ -1,11 +1,13 @@
import { Component } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component'
import { ErrorService, LoadingService } from '@start9labs/shared'
import {
PasswordPromptComponent,
PromptOptions,
} from 'src/app/modals/password-prompt.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDB } from 'patch-db-client'
import { skip, take, takeUntil } from 'rxjs/operators'
import { skip, takeUntil } from 'rxjs/operators'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import * as argon2 from '@start9labs/argon2'
import { TuiDestroyService } from '@taiga-ui/cdk'
@@ -22,7 +24,6 @@ import { BackupService } from 'src/app/components/backup-drives/backup.service'
@Component({
selector: 'server-backup',
templateUrl: './server-backup.page.html',
styleUrls: ['./server-backup.page.scss'],
providers: [TuiDestroyService],
})
export class ServerBackupPage {
@@ -31,8 +32,8 @@ export class ServerBackupPage {
readonly backingUp$ = this.eosService.backingUp$
constructor(
private readonly errorService: ErrorService,
private readonly loader: LoadingService,
private readonly dialogs: TuiDialogService,
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
private readonly navCtrl: NavController,
@@ -60,7 +61,7 @@ export class ServerBackupPage {
component: BackupSelectPage,
})
modal.onWillDismiss().then(res => {
modal.onDidDismiss().then(res => {
if (res.data) {
this.serviceIds = res.data
this.presentModalPassword(target)
@@ -74,28 +75,35 @@ export class ServerBackupPage {
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
): Promise<void> {
const options: PromptOptions = {
title: 'Master Password Needed',
message: 'Enter your master password to encrypt this backup.',
label: 'Master Password',
placeholder: 'Enter master password',
useMask: true,
buttonText: 'Create Backup',
}
this.dialogs
.open<string>(PROMPT, {
label: 'Master Password Needed',
data: options,
})
.pipe(take(1))
.subscribe(async (password: string) => {
const modal = await this.modalCtrl.create({
component: PasswordPromptComponent,
componentProps: { options },
canDismiss: async password => {
if (password === null) {
return true
}
const { passwordHash, id } = await getServerInfo(this.patch)
// confirm password matches current master password
argon2.verify(passwordHash, password)
try {
argon2.verify(passwordHash, password)
} catch (e: any) {
this.errorService.handleError(e)
return false
}
// first time backup
if (!this.backupService.hasThisBackup(target.entry, id)) {
await this.createBackup(target, password)
this.createBackup(target, password)
return true
// existing backup
} else {
try {
@@ -103,41 +111,51 @@ export class ServerBackupPage {
} catch {
setTimeout(
() => this.presentModalOldPassword(target, password),
500,
250,
)
return
return true
}
await this.createBackup(target, password)
return true
}
})
},
})
modal.present()
}
private async presentModalOldPassword(
target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>,
password: string,
): Promise<void> {
const { id } = await getServerInfo(this.patch)
const options: PromptOptions = {
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: 'Create Backup',
}
const { id } = await getServerInfo(this.patch)
const modal = await this.modalCtrl.create({
component: PasswordPromptComponent,
componentProps: { options },
canDismiss: async oldPassword => {
if (oldPassword === null) {
return true
}
this.dialogs
.open<string>(PROMPT, {
label: 'Original Password Needed',
data: options,
})
.pipe(take(1))
.subscribe(async (oldPassword: string) => {
const passwordHash = target.entry.startOs[id].passwordHash!
argon2.verify(passwordHash, oldPassword)
await this.createBackup(target, password, oldPassword)
})
try {
argon2.verify(target.entry.startOs[id].passwordHash!, oldPassword)
await this.createBackup(target, password, oldPassword)
return true
} catch (e: any) {
this.errorService.handleError(e)
return false
}
},
})
modal.present()
}
private async createBackup(

View File

@@ -56,7 +56,7 @@ export class WidgetsPage {
@Optional()
@Inject(POLYMORPHEUS_CONTEXT)
readonly context: TuiDialogContext | null,
private readonly dialog: TuiDialogService,
private readonly dialogs: TuiDialogService,
private readonly patch: PatchDB<DataModel>,
private readonly cdr: ChangeDetectorRef,
private readonly api: ApiService,
@@ -83,7 +83,7 @@ export class WidgetsPage {
}
add() {
this.dialog.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => {
this.dialogs.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => {
this.addWidget(widget!)
})
}