mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Feature/cloud backups (#889)
* cifs for cloud backups on lan * password spelling fix * fix spelling and fix rpc method * fix other methods * remove old code and rename method * add support for cifs backup targets wip cifs api simplify idiom add doc comment wip wip should work™ * add password hash to server info * fix type * fix types for cifs * minor fixes for cifs feature * fix rpc structure * fix copy, address some TODOs * add subcommand * backup path and navigation * wizard edits * rebased success page * wiz conflicts resolved * current change actually * only unsub if done * no fileter if necessary * fix copy for cifs old password * setup complete (#913) * setup complete * minor fixes * setup.complete * complete bool * setup-wizard: complete boolean Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
13
ui/package-lock.json
generated
13
ui/package-lock.json
generated
@@ -16,7 +16,8 @@
|
||||
"@angular/router": "^12.2.4",
|
||||
"@ionic/angular": "^5.7.0",
|
||||
"@ionic/storage-angular": "^3.0.6",
|
||||
"@start9labs/emver": "0.1.5",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"ajv": "^6.12.6",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"core-js": "^3.17.2",
|
||||
@@ -3063,6 +3064,11 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@start9labs/argon2": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/argon2/-/argon2-0.1.0.tgz",
|
||||
"integrity": "sha512-Ng9Ibuj0p2drQRW013AkUz6TqWysXw/9OyoEoXQZL7kfac0LrxWIDj+xvg+orqQMxcvClWgzeQY/c+IgJtcevA=="
|
||||
},
|
||||
"node_modules/@start9labs/emver": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz",
|
||||
@@ -19016,6 +19022,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@start9labs/argon2": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/argon2/-/argon2-0.1.0.tgz",
|
||||
"integrity": "sha512-Ng9Ibuj0p2drQRW013AkUz6TqWysXw/9OyoEoXQZL7kfac0LrxWIDj+xvg+orqQMxcvClWgzeQY/c+IgJtcevA=="
|
||||
},
|
||||
"@start9labs/emver": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz",
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"@angular/router": "^12.2.4",
|
||||
"@ionic/angular": "^5.7.0",
|
||||
"@ionic/storage-angular": "^3.0.6",
|
||||
"@start9labs/emver": "0.1.5",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"ajv": "^6.12.6",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"core-js": "^3.17.2",
|
||||
|
||||
@@ -62,11 +62,17 @@
|
||||
<ion-icon name="arrow-up"></ion-icon>
|
||||
<ion-icon name="briefcase-outline"></ion-icon>
|
||||
<ion-icon name="bookmark-outline"></ion-icon>
|
||||
<ion-icon name="cellular-outline"></ion-icon>
|
||||
<ion-icon name="checkmark"></ion-icon>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
<ion-icon name="chevron-up"></ion-icon>
|
||||
<ion-icon name="chevron-forward"></ion-icon> <!-- needed for detail="true" on ion-item button -->
|
||||
<ion-icon name="close"></ion-icon>
|
||||
<ion-icon name="cloud-outline"></ion-icon>
|
||||
<ion-icon name="cloud-done-outline"></ion-icon>
|
||||
<ion-icon name="cloud-download-outline"></ion-icon>
|
||||
<ion-icon name="cloud-offline-outline"></ion-icon>
|
||||
<ion-icon name="cloud-upload-outline"></ion-icon>
|
||||
<ion-icon name="code-outline"></ion-icon>
|
||||
<ion-icon name="color-wand-outline"></ion-icon>
|
||||
<ion-icon name="construct-outline"></ion-icon>
|
||||
@@ -81,6 +87,7 @@
|
||||
<ion-icon name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-icon name="finger-print-outline"></ion-icon>
|
||||
<ion-icon name="flash-outline"></ion-icon>
|
||||
<ion-icon name="folder-open-outline"></ion-icon>
|
||||
<ion-icon name="grid-outline"></ion-icon>
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
<ion-icon name="home-outline"></ion-icon>
|
||||
|
||||
@@ -160,10 +160,9 @@ export class AppComponent {
|
||||
}
|
||||
|
||||
async presentAlertLogout () {
|
||||
// @TODO warn user no way to recover Embassy if logout and forget password. Maybe require password to logout?
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Caution',
|
||||
message: 'Are you sure you want to logout?',
|
||||
message: 'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="inline">
|
||||
<h2 *ngIf="type === 'create'; else restore">
|
||||
<ion-icon name="cloud-outline" color="success"></ion-icon>
|
||||
{{ hasValidBackup ? 'Available, contains existing backup' : 'Available for fresh backup' }}
|
||||
</h2>
|
||||
<ng-template #restore>
|
||||
<h2 *ngIf="hasValidBackup">
|
||||
<ion-icon name="cloud-done-outline" color="success"></ion-icon>
|
||||
Embassy backup detected
|
||||
</h2>
|
||||
<h2 *ngIf="!hasValidBackup">
|
||||
<ion-icon name="cloud-offline-outline" color="danger"></ion-icon>
|
||||
No Embassy backup
|
||||
</h2>
|
||||
</ng-template>
|
||||
</div>
|
||||
@@ -1,70 +1,82 @@
|
||||
<ion-item-group>
|
||||
<!-- always -->
|
||||
<ion-item class="ion-margin-bottom">
|
||||
<ion-label>{{ message }}</ion-label>
|
||||
<!-- loading -->
|
||||
<text-spinner *ngIf="backupService.loading; else loaded" [text]="loadingText"></text-spinner>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
<!-- error -->
|
||||
<ion-item *ngIf="backupService.loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ backupService.loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- loading -->
|
||||
<ng-container *ngIf="backupService.loading; else loaded">
|
||||
<ion-item-divider>
|
||||
<ion-skeleton-text animated style="width: 120px; height: 16px;"></ion-skeleton-text>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- loaded -->
|
||||
<ng-template #loaded>
|
||||
|
||||
<!-- error -->
|
||||
<ion-item *ngIf="backupService.loadingError; else noError">
|
||||
<ion-label>
|
||||
<ion-text color="danger">
|
||||
{{ backupService.loadingError }}
|
||||
</ion-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template #noError>
|
||||
|
||||
<ion-item *ngIf="!backupService.drives.length; else hasDrives">
|
||||
<ng-template #noError>
|
||||
<ion-item-group>
|
||||
<!-- ** cifs ** -->
|
||||
<ion-item-divider>Shared Network Folders</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<ion-text color="warning">
|
||||
No drives found. Insert a backup drive into your Embassy and click "Refresh" above.
|
||||
</ion-text>
|
||||
<h2>
|
||||
Shared folders are the recommended way to create Embassy backups.
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- add new cifs -->
|
||||
<ion-item button detail="false" (click)="presentModalAddCifs()">
|
||||
<ion-icon slot="start" name="add" size="large" color="dark"></ion-icon>
|
||||
<ion-label>New shared folder</ion-label>
|
||||
</ion-item>
|
||||
<!-- cifs list -->
|
||||
<ng-container *ngFor="let target of backupService.cifs; let i = index">
|
||||
<ion-item button *ngIf="target.entry as cifs" (click)="presentActionCifs(target, i)">
|
||||
<ion-icon slot="start" name="folder-open-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ cifs.path.split('/').pop() }}</h1>
|
||||
<ng-container *ngIf="cifs.mountable">
|
||||
<backup-drives-status [type]="type" [hasValidBackup]="target.hasValidBackup"></backup-drives-status>
|
||||
</ng-container>
|
||||
<h2 *ngIf="!cifs.mountable" class="inline">
|
||||
<ion-icon name="cellular-outline" color="danger"></ion-icon>
|
||||
Unable to connect
|
||||
</h2>
|
||||
<p>Hostname: {{ cifs.hostname }}</p>
|
||||
<p>Path: {{ cifs.path }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<!-- ** drives ** -->
|
||||
<ion-item-divider>Physical Drives</ion-item-divider>
|
||||
<!-- no drives -->
|
||||
<ion-item *ngIf="!backupService.drives.length; else hasDrives" class="ion-padding-bottom">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<ion-text color="warning">
|
||||
Warning! Plugging a 2nd physical drive directly into your Embassy can lead to data corruption.
|
||||
</ion-text>
|
||||
</h2>
|
||||
<br />
|
||||
<h2>
|
||||
To backup to a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- drives detected -->
|
||||
<ng-template #hasDrives>
|
||||
<ion-item-group>
|
||||
<div *ngFor="let drive of backupService.drives">
|
||||
<ion-item-divider>
|
||||
{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }} - {{ drive.capacity | convertBytes }}
|
||||
</ion-item-divider>
|
||||
<ion-item button *ngFor="let partition of drive.partitions" [disabled]="type === 'restore' && !partition.hasBackup" (click)="handleSelect(partition)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ion-label>
|
||||
<h1>{{ partition.label || partition.logicalname }}</h1>
|
||||
<h2>{{ partition.capacity | convertBytes }}</h2>
|
||||
<p *ngIf="partition.hasBackup">
|
||||
<ion-text color="success">
|
||||
Embassy backups detected
|
||||
</ion-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-icon *ngIf="partition.hasBackup" slot="end" color="danger" name="lock-closed-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
<ion-item button *ngFor="let target of backupService.drives" (click)="select(target)">
|
||||
<ion-icon slot="start" name="save-outline" size="large"></ion-icon>
|
||||
<ng-container *ngIf="target.entry as drive">
|
||||
<ion-label>
|
||||
<h1>{{ drive.label || drive.logicalname }}</h1>
|
||||
<backup-drives-status [type]="type" [hasValidBackup]="target.hasValidBackup"></backup-drives-status>
|
||||
<p>{{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || 'Unknown Model' }}</p>
|
||||
<p>Capacity: {{ drive.capacity | convertBytes }}</p>
|
||||
</ion-label>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { BackupDrivesComponent, BackupDrivesHeaderComponent } from './backup-drives.component'
|
||||
import { BackupDrivesComponent, BackupDrivesHeaderComponent, BackupDrivesStatusComponent } from './backup-drives.component'
|
||||
import { SharingModule } from '../../modules/sharing.module'
|
||||
import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BackupDrivesComponent,
|
||||
BackupDrivesHeaderComponent,
|
||||
BackupDrivesStatusComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
GenericFormPageModule,
|
||||
],
|
||||
exports: [
|
||||
BackupDrivesComponent,
|
||||
BackupDrivesHeaderComponent,
|
||||
BackupDrivesStatusComponent,
|
||||
],
|
||||
})
|
||||
export class BackupDrivesComponentModule { }
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { BackupService } from './backup.service'
|
||||
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { CifsBackupTarget, DiskBackupTarget, RR } from 'src/app/services/api/api.types'
|
||||
import { ActionSheetController, AlertController, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { MappedBackupTarget } from 'src/app/util/misc.util'
|
||||
|
||||
@Component({
|
||||
selector: 'backup-drives',
|
||||
@@ -8,25 +14,193 @@ import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
styleUrls: ['./backup-drives.component.scss'],
|
||||
})
|
||||
export class BackupDrivesComponent {
|
||||
@Input() type: 'backup' | 'restore'
|
||||
@Output() onSelect: EventEmitter<MappedPartitionInfo> = new EventEmitter()
|
||||
message: string
|
||||
@Input() type: 'create' | 'restore'
|
||||
@Output() onSelect: EventEmitter<MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>> = new EventEmitter()
|
||||
loadingText: string
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly actionCtrl: ActionSheetController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly errToast: ErrorToastService,
|
||||
public readonly backupService: BackupService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
if (this.type === 'backup') {
|
||||
this.message = 'Select the drive where you want to create a backup of your Embassy.'
|
||||
} else {
|
||||
this.message = 'Select the drive containing backups you would like to restore.'
|
||||
}
|
||||
this.backupService.getExternalDrives()
|
||||
this.loadingText = this.type === 'create' ? 'Fetching Backup Targets' : 'Fetching Backup Sources'
|
||||
this.backupService.getBackupTargets()
|
||||
}
|
||||
|
||||
handleSelect (partition: MappedPartitionInfo): void {
|
||||
this.onSelect.emit(partition)
|
||||
select (target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>): void {
|
||||
if (target.entry.type === 'cifs' && !target.entry.mountable) {
|
||||
const message = 'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.'
|
||||
this.presentAlertError(message)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.type === 'restore' && !target.hasValidBackup) {
|
||||
const message = `${target.entry.type === 'cifs' ? 'Shared folder' : 'Drive partition'} does not contain a valid Embassy backup.`
|
||||
this.presentAlertError(message)
|
||||
return
|
||||
}
|
||||
|
||||
this.onSelect.emit(target)
|
||||
}
|
||||
|
||||
async presentModalAddCifs (): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: 'New Shared Folder',
|
||||
spec: CifsSpec,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (value: RR.AddBackupTargetReq) => {
|
||||
return this.addCifs(value)
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentActionCifs (target: MappedBackupTarget<CifsBackupTarget>, index: number): Promise<void> {
|
||||
const entry = target.entry as CifsBackupTarget
|
||||
|
||||
const action = await this.actionCtrl.create({
|
||||
header: entry.hostname,
|
||||
subHeader: 'Shared Folder',
|
||||
mode: 'ios',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Forget',
|
||||
icon: 'trash',
|
||||
role: 'destructive',
|
||||
handler: () => {
|
||||
this.deleteCifs(target.id, index)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Edit',
|
||||
icon: 'pencil',
|
||||
handler: () => {
|
||||
this.presentModalEditCifs(target.id, entry, index)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: this.type === 'create' ? 'Create Backup' : 'Restore From Backup',
|
||||
icon: this.type === 'create' ? 'cloud-upload-outline' : 'cloud-download-outline',
|
||||
handler: () => {
|
||||
this.select(target)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await action.present()
|
||||
}
|
||||
|
||||
private async presentAlertError (message: string): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Error',
|
||||
message,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async addCifs (value: RR.AddBackupTargetReq): Promise<boolean> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Testing connectivity to shared folder...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const res = await this.embassyApi.addBackupTarget(value)
|
||||
const [id, entry] = Object.entries(res)[0]
|
||||
this.backupService.cifs.unshift({
|
||||
id,
|
||||
hasValidBackup: this.backupService.hasValidBackup(entry),
|
||||
entry,
|
||||
})
|
||||
return true
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalEditCifs (id: string, entry: CifsBackupTarget, index: number): Promise<void> {
|
||||
const { hostname, path, username } = entry
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: 'Update Shared Folder',
|
||||
spec: CifsSpec,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (value: RR.AddBackupTargetReq) => {
|
||||
return this.editCifs({ id, ...value }, index)
|
||||
},
|
||||
isSubmit: true,
|
||||
},
|
||||
],
|
||||
initialValue: {
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
},
|
||||
},
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async editCifs (value: RR.UpdateBackupTargetReq, index: number): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Testing connectivity to shared folder...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const res = await this.embassyApi.updateBackupTarget(value)
|
||||
const entry = Object.values(res)[0]
|
||||
this.backupService.cifs[index].entry = entry
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteCifs (id: string, index: number): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Removing...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.removeBackupTarget({ id })
|
||||
this.backupService.cifs.splice(index, 1)
|
||||
} catch (e) {
|
||||
this.errToast.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +219,54 @@ export class BackupDrivesHeaderComponent {
|
||||
) { }
|
||||
|
||||
refresh () {
|
||||
this.backupService.getExternalDrives()
|
||||
this.backupService.getBackupTargets()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'backup-drives-status',
|
||||
templateUrl: './backup-drives-status.component.html',
|
||||
styleUrls: ['./backup-drives.component.scss'],
|
||||
})
|
||||
export class BackupDrivesStatusComponent {
|
||||
@Input() type: string
|
||||
@Input() hasValidBackup: boolean
|
||||
}
|
||||
|
||||
const CifsSpec: ConfigSpec = {
|
||||
hostname: {
|
||||
type: 'string',
|
||||
name: 'Hostname',
|
||||
description: 'The local URL of the shared folder.',
|
||||
placeholder: `e.g. My Computer, Bob's Laptop`,
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
name: 'Path',
|
||||
description: 'The path to the shared folder on the target device.',
|
||||
placeholder: 'e.g. /Desktop/my-folder',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
name: 'Username',
|
||||
description: 'The username of the user account on your target device.',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
name: 'Password',
|
||||
description: 'The password of the user account on your target device.',
|
||||
nullable: true,
|
||||
masked: true,
|
||||
copyable: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import { Injectable } from '@angular/core'
|
||||
import { IonicSafeString } from '@ionic/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
import { MappedDriveInfo, MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { BackupTarget, CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.types'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { MappedBackupTarget } from 'src/app/util/misc.util'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BackupService {
|
||||
drives: MappedDriveInfo[]
|
||||
cifs: MappedBackupTarget<CifsBackupTarget>[]
|
||||
drives: MappedBackupTarget<DiskBackupTarget>[]
|
||||
loading = true
|
||||
loadingError: string | IonicSafeString
|
||||
|
||||
@@ -18,25 +20,31 @@ export class BackupService {
|
||||
private readonly emver: Emver,
|
||||
) { }
|
||||
|
||||
async getExternalDrives (): Promise<void> {
|
||||
async getBackupTargets (): Promise<void> {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const drives = await this.embassyApi.getDrives({ })
|
||||
this.drives = drives
|
||||
.filter(d => !d.guid)
|
||||
.map(d => {
|
||||
const partionInfo: MappedPartitionInfo[] = d.partitions.map(p => {
|
||||
return {
|
||||
...p,
|
||||
hasBackup: [0, 1].includes(this.emver.compare(p['embassy-os']?.version, '0.3.0')),
|
||||
}
|
||||
})
|
||||
return {
|
||||
...d,
|
||||
partitions: partionInfo,
|
||||
}
|
||||
})
|
||||
const targets = await this.embassyApi.getBackupTargets({ })
|
||||
// cifs
|
||||
this.cifs = Object.entries(targets)
|
||||
.filter(([_, target]) => target.type === 'cifs')
|
||||
.map(([id, cifs]) => {
|
||||
return {
|
||||
id,
|
||||
hasValidBackup: this.hasValidBackup(cifs),
|
||||
entry: cifs as CifsBackupTarget,
|
||||
}
|
||||
})
|
||||
// drives
|
||||
this.drives = Object.entries(targets)
|
||||
.filter(([_, target]) => target.type === 'disk')
|
||||
.map(([id, drive]) => {
|
||||
return {
|
||||
id,
|
||||
hasValidBackup: this.hasValidBackup(drive),
|
||||
entry: drive as DiskBackupTarget,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
this.loadingError = getErrorMessage(e)
|
||||
} finally {
|
||||
@@ -44,4 +52,7 @@ export class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
hasValidBackup (target: BackupTarget): boolean {
|
||||
return [0, 1].includes(this.emver.compare(target['embassy-os']?.version, '0.3.0'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<ion-input
|
||||
[type]="spec.type === 'string' && spec.masked && !unmasked[entry.key] ? 'password' : 'text'"
|
||||
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
|
||||
[placeholder]="'Enter ' + spec.name"
|
||||
[placeholder]="spec.placeholder || 'Enter ' + spec.name"
|
||||
[formControlName]="entry.key"
|
||||
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
|
||||
(ionChange)="handleInputChange()"
|
||||
@@ -188,7 +188,7 @@
|
||||
<ion-input
|
||||
[type]="$any(spec.spec).masked ? 'password' : 'text'"
|
||||
[inputmode]="spec.subtype === 'number' ? 'tel' : 'text'"
|
||||
[placeholder]="'Enter ' + spec.name"
|
||||
[placeholder]="$any(spec.spec).placeholder || 'Enter ' + spec.name"
|
||||
[formControlName]="i"
|
||||
>
|
||||
</ion-input>
|
||||
|
||||
@@ -100,9 +100,6 @@ export class FormObjectComponent {
|
||||
addListItem (key: string, markDirty = true, val?: string): void {
|
||||
const arr = this.formGroup.get(key) as FormArray
|
||||
if (markDirty) arr.markAsDirty()
|
||||
// @TODO why are these commented out?
|
||||
// const validators = this.formService.getListItemValidators(this.objectSpec[key] as ValueSpecList, key, arr.length)
|
||||
// arr.push(new FormControl(value, validators))
|
||||
const listSpec = this.objectSpec[key] as ValueSpecList
|
||||
const newItem = this.formService.getListItem(listSpec, val)
|
||||
newItem.markAllAsTouched()
|
||||
|
||||
@@ -28,14 +28,11 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
}
|
||||
|
||||
private runAuthCheck (): boolean {
|
||||
switch (this.authState){
|
||||
case AuthState.VERIFIED:
|
||||
return true
|
||||
case AuthState.UNVERIFIED:
|
||||
// @TODO could initializing cause a loop?
|
||||
case AuthState.INITIALIZING:
|
||||
this.router.navigate(['/auth'], { replaceUrl: true })
|
||||
return false
|
||||
if (this.authState === AuthState.VERIFIED) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigate(['/login'], { replaceUrl: true })
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,11 @@ export class UnauthGuard implements CanActivate {
|
||||
}
|
||||
|
||||
canActivate (): boolean {
|
||||
|
||||
switch (this.authState){
|
||||
case AuthState.VERIFIED: {
|
||||
this.router.navigateByUrl('')
|
||||
return false
|
||||
}
|
||||
case AuthState.UNVERIFIED:
|
||||
case AuthState.INITIALIZING:
|
||||
return true
|
||||
if (this.authState === AuthState.VERIFIED) {
|
||||
this.router.navigateByUrl('')
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
styleUrls: ['./app-recover-select.page.scss'],
|
||||
})
|
||||
export class AppRecoverSelectPage {
|
||||
@Input() logicalname: string
|
||||
@Input() password: string
|
||||
@Input() id: string
|
||||
@Input() backupInfo: BackupInfo
|
||||
@Input() password: string
|
||||
@Input() oldPassword: string
|
||||
options: (PackageBackupInfo & {
|
||||
id: string
|
||||
checked: boolean
|
||||
@@ -69,7 +70,8 @@ export class AppRecoverSelectPage {
|
||||
try {
|
||||
await this.embassyApi.restorePackages({
|
||||
ids,
|
||||
logicalname: this.logicalname,
|
||||
'target-id': this.id,
|
||||
'old-password': this.oldPassword,
|
||||
password: this.password,
|
||||
})
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
|
||||
@@ -19,6 +19,7 @@ export class GenericFormPage {
|
||||
@Input() title: string
|
||||
@Input() spec: ConfigSpec
|
||||
@Input() buttons: ActionButton[]
|
||||
@Input() initialValue: object = { }
|
||||
submitBtn: ActionButton
|
||||
formGroup: FormGroup
|
||||
|
||||
@@ -28,7 +29,7 @@ export class GenericFormPage {
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.formGroup = this.formService.createForm(this.spec)
|
||||
this.formGroup = this.formService.createForm(this.spec, this.initialValue)
|
||||
this.submitBtn = this.buttons.find(btn => btn.isSubmit) || {
|
||||
text: '',
|
||||
handler: () => Promise.resolve(true),
|
||||
@@ -48,6 +49,7 @@ export class GenericFormPage {
|
||||
return
|
||||
}
|
||||
|
||||
// @TODO make this more like generic input component dismissal
|
||||
const success = await handler(this.formGroup.value)
|
||||
if (success !== false) this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
<ion-item style="padding-bottom: 8px;">
|
||||
<ion-label>
|
||||
<h1>{{ title }}</h1>
|
||||
<h1>{{ options.title }}</h1>
|
||||
<br />
|
||||
<p>{{ message }}</p>
|
||||
<ng-container *ngIf="warning">
|
||||
<p>{{ options.message }}</p>
|
||||
<ng-container *ngIf="options.warning">
|
||||
<br />
|
||||
<p>
|
||||
<ion-text color="warning">{{ warning }}</ion-text>
|
||||
<ion-text color="warning">{{ options.warning }}</ion-text>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
<form (ngSubmit)="submit()">
|
||||
<div style="margin: 0 0 24px 16px;">
|
||||
<p class="input-label">{{ label }}</p>
|
||||
<p class="input-label">{{ options.label }}</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-input #mainInput [type]="useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="placeholder" (ionChange)="error = ''"></ion-input>
|
||||
<ion-button slot="end" *ngIf="useMask" fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-input #mainInput [type]="options.useMask && !unmasked ? 'password' : 'text'" [(ngModel)]="value" name="value" [placeholder]="options.placeholder" (ionChange)="error = ''"></ion-input>
|
||||
<ion-button slot="end" *ngIf="options.useMask" fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-icon slot="icon-only" [name]="unmasked ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
@@ -32,8 +32,8 @@
|
||||
<ion-button fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button fill="clear" type="submit" [disabled]="!value && !nullable">
|
||||
{{ buttonText }}
|
||||
<ion-button fill="clear" type="submit" [disabled]="!value && !options.nullable">
|
||||
{{ options.buttonText }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { ModalController, IonicSafeString, LoadingController, IonInput } from '@ionic/angular'
|
||||
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
||||
import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
|
||||
@Component({
|
||||
@@ -9,25 +9,31 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
|
||||
})
|
||||
export class GenericInputComponent {
|
||||
@ViewChild('mainInput') elem: IonInput
|
||||
@Input() title: string
|
||||
@Input() message: string
|
||||
@Input() warning: string
|
||||
@Input() label: string
|
||||
@Input() buttonText = 'Submit'
|
||||
@Input() placeholder = 'Enter Value'
|
||||
@Input() nullable = false
|
||||
@Input() useMask = false
|
||||
@Input() value = ''
|
||||
@Input() loadingText = ''
|
||||
@Input() submitFn: (value: string) => Promise<any>
|
||||
@Input() options: GenericInputOptions
|
||||
value: string
|
||||
unmasked = false
|
||||
error: string | IonicSafeString
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
const defaultOptions: Partial<GenericInputOptions> = {
|
||||
buttonText: 'Submit',
|
||||
placeholder: 'Enter value',
|
||||
nullable: false,
|
||||
useMask: false,
|
||||
initialValue: '',
|
||||
}
|
||||
this.options = {
|
||||
...defaultOptions,
|
||||
...this.options,
|
||||
}
|
||||
|
||||
this.value = this.options.initialValue
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
@@ -43,25 +49,28 @@ export class GenericInputComponent {
|
||||
async submit () {
|
||||
const value = this.value.trim()
|
||||
|
||||
if (!value && !this.nullable) {
|
||||
return
|
||||
}
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
message: this.loadingText,
|
||||
})
|
||||
await loader.present()
|
||||
if (!value && !this.options.nullable) return
|
||||
|
||||
try {
|
||||
await this.submitFn(value)
|
||||
await this.options.submitFn(value)
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
} catch (e) {
|
||||
this.error = getErrorMessage(e)
|
||||
}
|
||||
finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenericInputOptions {
|
||||
// required
|
||||
title: string
|
||||
message: string
|
||||
label: string
|
||||
submitFn: (value: string) => Promise<any>
|
||||
// optional
|
||||
warning?: string
|
||||
buttonText?: string
|
||||
placeholder?: string
|
||||
nullable?: boolean
|
||||
useMask?: boolean
|
||||
initialValue?: string
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { MarketplaceData, MarketplaceEOS, MarketplacePkg } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { IonContent, ModalController } from '@ionic/angular'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { IonContent, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { ConfigSpec } from 'src/app/pkg-config/config-types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ServerConfigService } from 'src/app/services/server-config.service'
|
||||
@@ -17,6 +17,7 @@ export class PreferencesPage {
|
||||
defaultName: string
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly api: ApiService,
|
||||
public readonly serverConfig: ServerConfigService,
|
||||
@@ -32,19 +33,20 @@ export class PreferencesPage {
|
||||
}
|
||||
|
||||
async presentModalName (): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: this.defaultName,
|
||||
nullable: true,
|
||||
initialValue: this.patch.getData().ui.name,
|
||||
buttonText: 'Save',
|
||||
submitFn: (value: string) => this.setDbValue('name', value || this.defaultName),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Edit Device Name',
|
||||
message: 'This is for your reference only.',
|
||||
label: 'Device Name',
|
||||
useMask: false,
|
||||
placeholder: this.defaultName,
|
||||
nullable: true,
|
||||
value: this.patch.getData().ui.name,
|
||||
buttonText: 'Save',
|
||||
loadingText: 'Saving',
|
||||
submitFn: (value: string) => this.setDbValue('name', value || this.defaultName),
|
||||
},
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
@@ -54,7 +56,20 @@ export class PreferencesPage {
|
||||
}
|
||||
|
||||
private async setDbValue (key: string, value: string): Promise<void> {
|
||||
await this.api.setDbValue({ pointer: `/${key}`, value })
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Saving...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.api.setDbValue({ pointer: `/${key}`, value })
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<backup-drives-header title="Restore From Backup"></backup-drives-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-content class="ion-padding">
|
||||
<backup-drives type="restore" (onSelect)="presentModalPassword($event)"></backup-drives>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ModalController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { BackupInfo } from 'src/app/services/api/api.types'
|
||||
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { MappedBackupTarget } from 'src/app/util/misc.util'
|
||||
import { BackupInfo, CifsBackupTarget, 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({
|
||||
selector: 'restore',
|
||||
@@ -17,20 +19,22 @@ export class RestorePage {
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
async presentModalPassword (partition: MappedPartitionInfo): Promise<void> {
|
||||
async presentModalPassword (target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>): Promise<void> {
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Master Password Required',
|
||||
message: 'Enter your master password. 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),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Decryption Required',
|
||||
message: 'Enter the password that was originally used to encrypt this backup. After decrypting the drive, you will select the services you want to restore.',
|
||||
label: 'Password',
|
||||
placeholder: 'Enter password',
|
||||
useMask: true,
|
||||
buttonText: 'Restore',
|
||||
loadingText: 'Decrypting drive...',
|
||||
submitFn: (password: string) => this.decryptDrive(partition.logicalname, password),
|
||||
},
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: GenericInputComponent,
|
||||
@@ -39,20 +43,54 @@ export class RestorePage {
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async decryptDrive (logicalname: string, password: string): Promise<void> {
|
||||
const backupInfo = await this.embassyApi.getBackupInfo({
|
||||
logicalname,
|
||||
password,
|
||||
})
|
||||
this.presentModalSelect(logicalname, password, backupInfo)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalSelect (logicalname: string, password: string, backupInfo: BackupInfo): Promise<void> {
|
||||
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,
|
||||
})
|
||||
this.presentModalSelect(target.id, backupInfo, password, oldPassword)
|
||||
}
|
||||
|
||||
private async presentModalSelect (id: string, backupInfo: BackupInfo, password: string, oldPassword?: string): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
logicalname,
|
||||
password,
|
||||
id,
|
||||
backupInfo,
|
||||
password,
|
||||
oldPassword,
|
||||
},
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: AppRecoverSelectPage,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!-- not backing up -->
|
||||
<ng-container *ngIf="!backingUp">
|
||||
<backup-drives-header title="Create Backup"></backup-drives-header>
|
||||
<ion-content class="ion-padding-top">
|
||||
<backup-drives type="backup" (onSelect)="presentModalPassword($event)"></backup-drives>
|
||||
<ion-content class="ion-padding">
|
||||
<backup-drives type="create" (onSelect)="presentModalPassword($event)"></backup-drives>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
@@ -38,9 +38,8 @@
|
||||
</ion-note>
|
||||
<!-- active -->
|
||||
<ion-note *ngIf="pkg.active" class="inline" slot="end">
|
||||
<ion-spinner color="dark" style="height: 12px; width: 12px; margin-right: 6px;"></ion-spinner>
|
||||
<ion-text color="dark">Backing up</ion-text>
|
||||
|
||||
<ion-spinner color="dark" style="height: 12px; width: 12px;"></ion-spinner>
|
||||
</ion-note>
|
||||
<!-- queued -->
|
||||
<ion-note *ngIf="!pkg.complete && !pkg.active" slot="end">
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { LoadingController, ModalController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { MappedPartitionInfo } from 'src/app/util/misc.util'
|
||||
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { PackageDataEntry, PackageMainStatus, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { take } from 'rxjs/operators'
|
||||
import { MappedBackupTarget } from 'src/app/util/misc.util'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.types'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup',
|
||||
@@ -19,22 +21,31 @@ export class ServerBackupPage {
|
||||
subs: Subscription[]
|
||||
|
||||
constructor (
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patch: PatchDbService,
|
||||
private readonly navCtrl: NavController,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.subs = [
|
||||
this.patch.watch$('server-info', 'status').subscribe(status => {
|
||||
if (status === ServerStatus.BackingUp) {
|
||||
this.backingUp = true
|
||||
this.subscribeToBackup()
|
||||
} else {
|
||||
this.backingUp = false
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
}
|
||||
}),
|
||||
this.patch.watch$('server-info', 'status')
|
||||
.pipe()
|
||||
.subscribe(status => {
|
||||
if (status === ServerStatus.BackingUp) {
|
||||
if (!this.backingUp) {
|
||||
this.backingUp = true
|
||||
this.subscribeToBackup()
|
||||
}
|
||||
} else {
|
||||
if (this.backingUp) {
|
||||
this.backingUp = false
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
this.navCtrl.navigateRoot('/embassy')
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,34 +54,90 @@ export class ServerBackupPage {
|
||||
this.pkgs.forEach(pkg => pkg.sub.unsubscribe())
|
||||
}
|
||||
|
||||
async presentModalPassword (partition: MappedPartitionInfo): Promise<void> {
|
||||
let message: string
|
||||
if (partition.hasBackup) {
|
||||
message = 'Enter your master password to decrypt this drive and update its backup. Depending on how much data was added or changed, this could be very fast, or it could take a while.'
|
||||
} else {
|
||||
message = 'Enter your master password to create an encrypted backup of your Embassy and all its installed services. Since this is a fresh backup, it could take a while. Future backups will likely be much faster.'
|
||||
async presentModalPassword (target: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>): Promise<void> {
|
||||
let message = 'Enter your master password to create an encrypted backup of your Embassy and all its services.'
|
||||
if (!target.hasValidBackup) {
|
||||
message = message + ' Since this is a fresh backup, it could take a while. Future backups will likely be much faster.'
|
||||
}
|
||||
|
||||
const options: GenericInputOptions = {
|
||||
title: 'Master Password Needed',
|
||||
message,
|
||||
label: 'Master Password',
|
||||
placeholder: 'Enter master password',
|
||||
useMask: true,
|
||||
buttonText: 'Create Backup',
|
||||
submitFn: (password: string) => this.test(target, password),
|
||||
}
|
||||
|
||||
const m = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
title: 'Create Backup',
|
||||
message,
|
||||
label: 'Password',
|
||||
placeholder: 'Enter password',
|
||||
useMask: true,
|
||||
buttonText: 'Create Backup',
|
||||
loadingText: 'Beginning backup...',
|
||||
submitFn: (password: string) => this.create(partition.logicalname, password),
|
||||
},
|
||||
cssClass: 'alertlike-modal',
|
||||
component: GenericInputComponent,
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
|
||||
await m.present()
|
||||
}
|
||||
|
||||
private async create (logicalname: string, password: string): Promise<void> {
|
||||
await this.embassyApi.createBackup({ logicalname, password })
|
||||
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 new Error(e)
|
||||
} else {
|
||||
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: 'Create Backup',
|
||||
submitFn: (oldPassword: string) => this.test(target, password, oldPassword),
|
||||
}
|
||||
|
||||
const m = await this.modalCtrl.create({
|
||||
component: GenericInputComponent,
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
|
||||
await m.present()
|
||||
}
|
||||
|
||||
private async createBackup (id: string, password: string, oldPassword?: string): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Beginning backup...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.embassyApi.createBackup({
|
||||
'target-id': id,
|
||||
'old-password': oldPassword || null,
|
||||
password,
|
||||
})
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToBackup () {
|
||||
|
||||
@@ -11,15 +11,23 @@
|
||||
<ion-item-group>
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider><ion-text color="dark">{{ cat.key }}</ion-text></ion-item-divider>
|
||||
<ion-item [detail]="button.detail" button *ngFor="let button of cat.value" (click)="button.action()">
|
||||
<ion-item button *ngFor="let button of cat.value" [detail]="button.detail" [disabled]="button.disabled | async" (click)="button.action()">
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ion-text color="warning">
|
||||
Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }}
|
||||
</ion-text>
|
||||
<ng-container *ngIf="patch.data['server-info'].status as status">
|
||||
<ion-text color="warning" *ngIf="status === ServerStatus.Running">
|
||||
Last Backup: {{ patch.data['server-info']['last-backup'] ? (patch.data['server-info']['last-backup'] | date: 'short') : 'never' }}
|
||||
</ion-text>
|
||||
<span *ngIf="status === ServerStatus.BackingUp" class="inline">
|
||||
<ion-spinner color="success" style="height: 12px; width: 12px; margin-right: 6px;"></ion-spinner>
|
||||
<ion-text color="success">
|
||||
Backing up
|
||||
</ion-text>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
import { ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { Observable, of } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'server-show',
|
||||
@@ -11,7 +14,7 @@ import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
|
||||
styleUrls: ['server-show.page.scss'],
|
||||
})
|
||||
export class ServerShowPage {
|
||||
settings: ServerSettings = { }
|
||||
ServerStatus = ServerStatus
|
||||
|
||||
constructor (
|
||||
private readonly alertCtrl: AlertController,
|
||||
@@ -23,10 +26,6 @@ export class ServerShowPage {
|
||||
public readonly patch: PatchDbService,
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.setButtons()
|
||||
}
|
||||
|
||||
async presentAlertRestart () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
@@ -49,6 +48,7 @@ export class ServerShowPage {
|
||||
}
|
||||
|
||||
async presentAlertShutdown () {
|
||||
const sts = this.patch.data['server-info'].status
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: 'Are you sure you want to power down your Embassy? This can take several minutes, and your Embassy will not come back online automatically. To power on again, You will need to physically unplug your Embassy and plug it back in.',
|
||||
@@ -103,101 +103,111 @@ export class ServerShowPage {
|
||||
}
|
||||
}
|
||||
|
||||
private setButtons (): void {
|
||||
this.settings = {
|
||||
'Backups': [
|
||||
{
|
||||
title: 'Create Backup',
|
||||
description: 'Back up your Embassy and all its services',
|
||||
icon: 'save-outline',
|
||||
action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'Restore From Backup',
|
||||
description: 'Restore one or more services from a prior backup',
|
||||
icon: 'color-wand-outline',
|
||||
action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
],
|
||||
'Insights': [
|
||||
{
|
||||
title: 'About',
|
||||
description: 'Basic information about your Embassy',
|
||||
icon: 'information-circle-outline',
|
||||
action: () => this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'Monitor',
|
||||
description: 'CPU, disk, memory, and other useful metrics',
|
||||
icon: 'pulse',
|
||||
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
description: 'Raw, unfiltered device logs',
|
||||
icon: 'newspaper-outline',
|
||||
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
],
|
||||
'Settings': [
|
||||
{
|
||||
title: 'Preferences',
|
||||
description: 'Device name, background tasks',
|
||||
icon: 'options-outline',
|
||||
action: () => this.navCtrl.navigateForward(['preferences'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'LAN',
|
||||
description: 'Access your Embassy on the Local Area Network',
|
||||
icon: 'home-outline',
|
||||
action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
description: 'Access your Embassy from the command line',
|
||||
icon: 'terminal-outline',
|
||||
action: () => this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
description: 'Add or remove WiFi networks',
|
||||
icon: 'wifi',
|
||||
action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
{
|
||||
title: 'Active Sessions',
|
||||
description: 'View and manage device access',
|
||||
icon: 'desktop-outline',
|
||||
action: () => this.navCtrl.navigateForward(['sessions'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
},
|
||||
],
|
||||
'Power': [
|
||||
{
|
||||
title: 'Restart',
|
||||
description: '',
|
||||
icon: 'reload',
|
||||
action: () => this.presentAlertRestart(),
|
||||
detail: false,
|
||||
},
|
||||
{
|
||||
title: 'Shutdown',
|
||||
description: '',
|
||||
icon: 'power',
|
||||
action: () => this.presentAlertShutdown(),
|
||||
detail: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
settings: ServerSettings = {
|
||||
'Backups': [
|
||||
{
|
||||
title: 'Create Backup',
|
||||
description: 'Back up your Embassy and all its services',
|
||||
icon: 'save-outline',
|
||||
action: () => this.navCtrl.navigateForward(['backup'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Restore From Backup',
|
||||
description: 'Restore one or more services from a prior backup',
|
||||
icon: 'color-wand-outline',
|
||||
action: () => this.navCtrl.navigateForward(['restore'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: this.patch.watch$('server-info', 'status').pipe(map(status => [ServerStatus.Updated, ServerStatus.BackingUp].includes(status))),
|
||||
},
|
||||
],
|
||||
'Insights': [
|
||||
{
|
||||
title: 'About',
|
||||
description: 'Basic information about your Embassy',
|
||||
icon: 'information-circle-outline',
|
||||
action: () => this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Monitor',
|
||||
description: 'CPU, disk, memory, and other useful metrics',
|
||||
icon: 'pulse',
|
||||
action: () => this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
description: 'Raw, unfiltered device logs',
|
||||
icon: 'newspaper-outline',
|
||||
action: () => this.navCtrl.navigateForward(['logs'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
],
|
||||
'Settings': [
|
||||
{
|
||||
title: 'Preferences',
|
||||
description: 'Device name, background tasks',
|
||||
icon: 'options-outline',
|
||||
action: () => this.navCtrl.navigateForward(['preferences'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'LAN',
|
||||
description: 'Access your Embassy on the Local Area Network',
|
||||
icon: 'home-outline',
|
||||
action: () => this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
description: 'Access your Embassy from the command line',
|
||||
icon: 'terminal-outline',
|
||||
action: () => this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
description: 'Add or remove WiFi networks',
|
||||
icon: 'wifi',
|
||||
action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Active Sessions',
|
||||
description: 'View and manage device access',
|
||||
icon: 'desktop-outline',
|
||||
action: () => this.navCtrl.navigateForward(['sessions'], { relativeTo: this.route }),
|
||||
detail: true,
|
||||
disabled: of(false),
|
||||
},
|
||||
],
|
||||
'Power': [
|
||||
{
|
||||
title: 'Restart',
|
||||
description: '',
|
||||
icon: 'reload',
|
||||
action: () => this.presentAlertRestart(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Shutdown',
|
||||
description: '',
|
||||
icon: 'power',
|
||||
action: () => this.presentAlertShutdown(),
|
||||
detail: false,
|
||||
disabled: of(false),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
asIsOrder () {
|
||||
@@ -212,5 +222,6 @@ interface ServerSettings {
|
||||
icon: string
|
||||
action: Function
|
||||
detail: boolean
|
||||
disabled: Observable<boolean>
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AlertController, LoadingController, ModalController } from '@ionic/angu
|
||||
import { SSHKey } from 'src/app/services/api/api.types'
|
||||
import { ErrorToastService } from 'src/app/services/error-toast.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { GenericInputComponent } from 'src/app/modals/generic-input/generic-input.component'
|
||||
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
|
||||
|
||||
@Component({
|
||||
selector: 'ssh-keys',
|
||||
@@ -40,23 +40,37 @@ export class SSHKeysPage {
|
||||
async presentModalAdd () {
|
||||
const { name, description } = sshSpec
|
||||
|
||||
const options: GenericInputOptions = {
|
||||
title: name,
|
||||
message: description,
|
||||
label: name,
|
||||
submitFn: (pk: string) => this.add(pk),
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericInputComponent,
|
||||
componentProps: {
|
||||
title: name,
|
||||
message: description,
|
||||
label: name,
|
||||
loadingText: 'Saving',
|
||||
submitFn: (pk: string) => this.add(pk),
|
||||
},
|
||||
componentProps: { options },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async add (pubkey: string): Promise<void> {
|
||||
const key = await this.embassyApi.addSshKey({ key: pubkey })
|
||||
this.sshKeys.push(key)
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Saving...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const key = await this.embassyApi.addSshKey({ key: pubkey })
|
||||
this.sshKeys.push(key)
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
async presentAlertDelete (i: number) {
|
||||
|
||||
@@ -109,6 +109,7 @@ export class WifiPage {
|
||||
{
|
||||
text: 'Forget',
|
||||
icon: 'trash',
|
||||
role: 'destructive',
|
||||
handler: () => {
|
||||
this.delete(ssid, i)
|
||||
},
|
||||
|
||||
@@ -93,12 +93,14 @@ export interface ListValueSpecString {
|
||||
'pattern-description'?: string
|
||||
masked: boolean
|
||||
copyable: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export interface ListValueSpecNumber {
|
||||
range: string
|
||||
integral: boolean
|
||||
units?: string
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export interface ListValueSpecEnum {
|
||||
|
||||
@@ -975,50 +975,54 @@ export module Mock {
|
||||
'signal-strength': 50,
|
||||
}
|
||||
|
||||
export const Drives: RR.GetDrivesRes = [
|
||||
{
|
||||
logicalname: '/dev/sda',
|
||||
model: null,
|
||||
vendor: 'SSK',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'sdba1',
|
||||
label: 'Matt Stuff',
|
||||
capacity: 1000000000000,
|
||||
used: 0,
|
||||
'embassy-os': null,
|
||||
},
|
||||
],
|
||||
capacity: 1000000000000,
|
||||
guid: 'asdfasdf',
|
||||
export const BackupTargets: RR.GetBackupTargetsRes = {
|
||||
'hsbdjhasbasda': {
|
||||
type: 'cifs',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/embassy-backups',
|
||||
username: 'TestUser',
|
||||
mountable: false,
|
||||
'embassy-os': {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNK',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: '/dev/sdb',
|
||||
model: 'JMS567 SATA 6Gb/s bridge',
|
||||
vendor: 'Samsung',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'sdba1',
|
||||
label: 'Partition 1',
|
||||
capacity: 1000000000,
|
||||
used: 1000000000,
|
||||
'embassy-os': {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: 'sdba2',
|
||||
label: 'Partition 2',
|
||||
capacity: 900000000,
|
||||
used: 300000000,
|
||||
'embassy-os': null,
|
||||
},
|
||||
],
|
||||
capacity: 10000000000,
|
||||
guid: null,
|
||||
// 'ftcvewdnkemfksdm': {
|
||||
// type: 'disk',
|
||||
// logicalname: 'sdba1',
|
||||
// label: 'Matt Stuff',
|
||||
// capacity: 1000000000000,
|
||||
// used: 0,
|
||||
// model: 'Evo SATA 2.5',
|
||||
// vendor: 'Samsung',
|
||||
// 'embassy-os': null,
|
||||
// },
|
||||
'csgashbdjkasnd': {
|
||||
type: 'cifs',
|
||||
hostname: 'smb://192.169.10.0',
|
||||
path: '/Desktop/embassy-backups-2',
|
||||
username: 'TestUser',
|
||||
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': '',
|
||||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
export const BackupInfo: RR.GetBackupInfoRes = {
|
||||
version: '0.3.0',
|
||||
|
||||
@@ -118,17 +118,33 @@ export module RR {
|
||||
|
||||
// backup
|
||||
|
||||
export type CreateBackupReq = WithExpire<{ logicalname: string, password: string }> // backup.create
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
export type GetBackupTargetsReq = { } // backup.target.list
|
||||
export type GetBackupTargetsRes = { [id: string]: BackupTarget }
|
||||
|
||||
// drive
|
||||
export type AddBackupTargetReq = { // backup.target.cifs.add
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
export type AddBackupTargetRes = { [id: string]: CifsBackupTarget }
|
||||
|
||||
export type GetDrivesReq = { } // disk.list
|
||||
export type GetDrivesRes = DriveInfo[]
|
||||
export type UpdateBackupTargetReq = AddBackupTargetReq & { id: string } // backup.target.cifs.update
|
||||
export type UpdateBackupTargetRes = AddBackupTargetRes
|
||||
|
||||
export type GetBackupInfoReq = { logicalname: string, password: string } // disk.backup-info
|
||||
export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove
|
||||
export type RemoveBackupTargetRes = null
|
||||
|
||||
export type GetBackupInfoReq = { 'target-id': string, password: string } // backup.target.info
|
||||
export type GetBackupInfoRes = BackupInfo
|
||||
|
||||
export type CreateBackupReq = WithExpire<{ // backup.create
|
||||
'target-id': string
|
||||
'old-password': string | null
|
||||
password: string
|
||||
}>
|
||||
export type CreateBackupRes = WithRevision<null>
|
||||
|
||||
// package
|
||||
|
||||
export type GetPackagePropertiesReq = { id: string } // package.properties
|
||||
@@ -157,7 +173,12 @@ export module RR {
|
||||
export type SetPackageConfigReq = WithExpire<DrySetPackageConfigReq> // package.config.set
|
||||
export type SetPackageConfigRes = WithRevision<null>
|
||||
|
||||
export type RestorePackagesReq = WithExpire<{ ids: string[], logicalname: string, password: string }> // package.backup.restore
|
||||
export type RestorePackagesReq = WithExpire<{ // package.backup.restore
|
||||
ids: string[]
|
||||
'target-id': string
|
||||
'old-password': string | null,
|
||||
password: string
|
||||
}>
|
||||
export type RestorePackagesRes = WithRevision<null>
|
||||
|
||||
export type ExecutePackageActionReq = { id: string, 'action-id': string, input?: object } // package.action
|
||||
@@ -200,8 +221,8 @@ export module RR {
|
||||
|
||||
export type GetMarketplacePackagesReq = {
|
||||
ids?: { id: string, version: string }[]
|
||||
// iff !id
|
||||
'eos-version-compat': string
|
||||
// iff !ids
|
||||
category?: string
|
||||
query?: string
|
||||
page?: string
|
||||
@@ -294,7 +315,51 @@ export interface SessionMetadata {
|
||||
|
||||
export type PlatformType = 'cli' | 'ios' | 'ipad' | 'iphone' | 'android' | 'phablet' | 'tablet' | 'cordova' | 'capacitor' | 'electron' | 'pwa' | 'mobile' | 'mobileweb' | 'desktop' | 'hybrid'
|
||||
|
||||
export interface DriveInfo {
|
||||
export type BackupTarget = DiskBackupTarget | CifsBackupTarget
|
||||
|
||||
export interface EmbassyOSRecoveryInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
'password-hash': string | null
|
||||
'wrapped-key': string | null
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget {
|
||||
type: 'disk'
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export type RecoverySource = DiskRecoverySource | CifsRecoverySource
|
||||
|
||||
export interface DiskRecoverySource {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export interface CifsRecoverySource {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
logicalname: string
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
@@ -308,10 +373,10 @@ export interface PartitionInfo {
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOsDriveInfo | null
|
||||
'embassy-os': EmbassyOsDiskInfo | null
|
||||
}
|
||||
|
||||
export interface EmbassyOsDriveInfo {
|
||||
export interface EmbassyOsDiskInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
}
|
||||
|
||||
@@ -121,17 +121,21 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
|
||||
|
||||
// backup
|
||||
|
||||
abstract getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes>
|
||||
|
||||
abstract addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes>
|
||||
|
||||
abstract updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes>
|
||||
|
||||
abstract removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes>
|
||||
|
||||
abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
protected abstract createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes>
|
||||
createBackup = (params: RR.CreateBackupReq) => this.syncResponse(
|
||||
() => this.createBackupRaw(params),
|
||||
)()
|
||||
|
||||
// drive
|
||||
|
||||
abstract getDrives (params: RR.GetDrivesReq): Promise<RR.GetDrivesRes>
|
||||
|
||||
abstract getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes>
|
||||
|
||||
// package
|
||||
|
||||
abstract getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<any>['data']>
|
||||
|
||||
@@ -90,7 +90,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise <RR.GetMarketplaceDataRes> {
|
||||
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/package/data',
|
||||
@@ -98,7 +98,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise <RR.GetMarketplacePackagesRes> {
|
||||
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise<RR.GetMarketplacePackagesRes> {
|
||||
if (params.query) params.category = undefined
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
@@ -110,7 +110,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise <RR.GetReleaseNotesRes> {
|
||||
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/package/release-notes',
|
||||
@@ -118,7 +118,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getLatestVersion (params: RR.GetLatestVersionReq): Promise <RR.GetLatestVersionRes> {
|
||||
async getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes> {
|
||||
return this.http.httpRequest({
|
||||
method: Method.GET,
|
||||
url: '/marketplace/latest-version',
|
||||
@@ -137,138 +137,149 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// notification
|
||||
|
||||
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise <RR.GetNotificationsRes> {
|
||||
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.list', params })
|
||||
}
|
||||
|
||||
async deleteNotification (params: RR.DeleteNotificationReq): Promise <RR.DeleteNotificationRes> {
|
||||
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.delete', params })
|
||||
}
|
||||
|
||||
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise <RR.DeleteAllNotificationsRes> {
|
||||
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
|
||||
return this.http.rpcRequest({ method: 'notification.delete-before', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async getWifi (params: RR.GetWifiReq, timeout?: number): Promise <RR.GetWifiRes> {
|
||||
async getWifi (params: RR.GetWifiReq, timeout?: number): Promise<RR.GetWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.get', params, timeout })
|
||||
}
|
||||
|
||||
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> {
|
||||
async setWifiCountry (params: RR.SetWifiCountryReq): Promise<RR.SetWifiCountryRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.country.set', params })
|
||||
}
|
||||
|
||||
async addWifi (params: RR.AddWifiReq): Promise <RR.AddWifiRes> {
|
||||
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.add', params })
|
||||
}
|
||||
|
||||
async connectWifi (params: RR.ConnectWifiReq): Promise <RR.ConnectWifiRes> {
|
||||
async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.connect', params })
|
||||
}
|
||||
|
||||
async deleteWifi (params: RR.DeleteWifiReq): Promise <RR.DeleteWifiRes> {
|
||||
async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
|
||||
return this.http.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys (params: RR.GetSSHKeysReq): Promise <RR.GetSSHKeysRes> {
|
||||
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.list', params })
|
||||
}
|
||||
|
||||
async addSshKey (params: RR.AddSSHKeyReq): Promise <RR.AddSSHKeyRes> {
|
||||
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.add', params })
|
||||
}
|
||||
|
||||
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise <RR.DeleteSSHKeyRes> {
|
||||
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
|
||||
return this.http.rpcRequest({ method: 'ssh.delete', params })
|
||||
}
|
||||
|
||||
// backup
|
||||
|
||||
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.list', params })
|
||||
}
|
||||
|
||||
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> {
|
||||
params.path = params.path.replace('/\\/g', '/')
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.add', params })
|
||||
}
|
||||
|
||||
async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.update', params })
|
||||
}
|
||||
|
||||
async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.cifs.remove', params })
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.target.info', params })
|
||||
}
|
||||
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise <RR.CreateBackupRes> {
|
||||
return this.http.rpcRequest({ method: 'backup.create', params })
|
||||
}
|
||||
|
||||
// drives
|
||||
|
||||
getDrives (params: RR.GetDrivesReq): Promise <RR.GetDrivesRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.list', params })
|
||||
}
|
||||
|
||||
getBackupInfo (params: RR.GetBackupInfoReq): Promise <RR.GetBackupInfoRes> {
|
||||
return this.http.rpcRequest({ method: 'disk.backup-info', params })
|
||||
}
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise <RR.GetPackagePropertiesRes < any > ['data'] > {
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes < any > ['data'] > {
|
||||
return this.http.rpcRequest({ method: 'package.properties', params })
|
||||
.then(parsePropertiesPermissive)
|
||||
}
|
||||
|
||||
async getPackageLogs (params: RR.GetPackageLogsReq): Promise <RR.GetPackageLogsRes> {
|
||||
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> {
|
||||
return this.http.rpcRequest( { method: 'package.logs', params })
|
||||
}
|
||||
|
||||
async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise <RR.GetPackageMetricsRes> {
|
||||
async getPkgMetrics (params: RR.GetPackageMetricsReq): Promise<RR.GetPackageMetricsRes> {
|
||||
return this.http.rpcRequest({ method: 'package.metrics', params })
|
||||
}
|
||||
|
||||
async installPackageRaw (params: RR.InstallPackageReq): Promise <RR.InstallPackageRes> {
|
||||
async installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise <RR.DryUpdatePackageRes> {
|
||||
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise <RR.GetPackageConfigRes> {
|
||||
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.get', params })
|
||||
}
|
||||
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise <RR.DrySetPackageConfigRes> {
|
||||
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set.dry', params })
|
||||
}
|
||||
|
||||
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise <RR.SetPackageConfigRes> {
|
||||
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> {
|
||||
return this.http.rpcRequest({ method: 'package.config.set', params })
|
||||
}
|
||||
|
||||
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise <RR.RestorePackagesRes> {
|
||||
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> {
|
||||
return this.http.rpcRequest({ method: 'package.backup.restore', params })
|
||||
}
|
||||
|
||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise <RR.ExecutePackageActionRes> {
|
||||
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
|
||||
return this.http.rpcRequest({ method: 'package.action', params })
|
||||
}
|
||||
|
||||
async startPackageRaw (params: RR.StartPackageReq): Promise <RR.StartPackageRes> {
|
||||
async startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.start', params })
|
||||
}
|
||||
|
||||
async dryStopPackage (params: RR.DryStopPackageReq): Promise <RR.DryStopPackageRes> {
|
||||
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop.dry', params })
|
||||
}
|
||||
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise <RR.StopPackageRes> {
|
||||
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.stop', params })
|
||||
}
|
||||
|
||||
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise <RR.DryUninstallPackageRes> {
|
||||
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.uninstall.dry', params })
|
||||
}
|
||||
|
||||
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise <RR.DeleteRecoveredPackageRes> {
|
||||
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise<RR.DeleteRecoveredPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.delete-recovered', params })
|
||||
}
|
||||
|
||||
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise <RR.UninstallPackageRes> {
|
||||
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> {
|
||||
return this.http.rpcRequest({ method: 'package.uninstall', params })
|
||||
}
|
||||
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise <RR.DryConfigureDependencyRes> {
|
||||
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
|
||||
return this.http.rpcRequest({ method: 'package.dependency.configure.dry', params })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pauseFor } from '../../util/misc.util'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
|
||||
import { DataModel, DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, RR, WithRevision } from './api.types'
|
||||
import { CifsBackupTarget, Log, RR, WithRevision } from './api.types'
|
||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!src/assets/markdown/md-sample.md'
|
||||
@@ -277,6 +277,49 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// backup
|
||||
|
||||
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupTargets
|
||||
}
|
||||
|
||||
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
const { hostname, path, username } = params
|
||||
return {
|
||||
'latfgvwdbhjsndmk': {
|
||||
type: 'cifs',
|
||||
hostname,
|
||||
path: path.replace(/\\/g, '/'),
|
||||
username,
|
||||
mountable: true,
|
||||
'embassy-os': null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
const { id, hostname, path, username } = params
|
||||
return {
|
||||
[id]: {
|
||||
...Mock.BackupTargets[id] as CifsBackupTarget,
|
||||
hostname,
|
||||
path,
|
||||
username,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupInfo
|
||||
}
|
||||
|
||||
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||
await pauseFor(2000)
|
||||
const path = '/server-info/status'
|
||||
@@ -324,18 +367,6 @@ export class MockApiService extends ApiService {
|
||||
return this.withRevision(originalPatch)
|
||||
}
|
||||
|
||||
// drives
|
||||
|
||||
async getDrives (params: RR.GetDrivesReq): Promise<RR.GetDrivesRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.Drives
|
||||
}
|
||||
|
||||
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
|
||||
await pauseFor(2000)
|
||||
return Mock.BackupInfo
|
||||
}
|
||||
|
||||
// package
|
||||
|
||||
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<any>['data']> {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const mockPatchData: DataModel = {
|
||||
'package-marketplace': null,
|
||||
'share-stats': false,
|
||||
'unread-notification-count': 4,
|
||||
// 'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'eos-version-compat': '>=0.3.0',
|
||||
},
|
||||
'recovered-packages': {
|
||||
|
||||
@@ -6,14 +6,13 @@ import { Storage } from '@ionic/storage-angular'
|
||||
export enum AuthState {
|
||||
UNVERIFIED,
|
||||
VERIFIED,
|
||||
INITIALIZING,
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly LOGGED_IN_KEY = 'loggedInKey'
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(AuthState.INITIALIZING)
|
||||
private readonly authState$: BehaviorSubject<AuthState> = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (
|
||||
private readonly storage: Storage,
|
||||
|
||||
@@ -133,10 +133,7 @@ export enum Method {
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
// @TODO what are valid params? object, bool?
|
||||
params?: {
|
||||
[param: string]: string | number | boolean | object | string[] | number[];
|
||||
}
|
||||
params?: object
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface ServerInfo {
|
||||
downloaded: number
|
||||
}
|
||||
'eos-version-compat': string
|
||||
'password-hash': string
|
||||
}
|
||||
|
||||
export enum ServerStatus {
|
||||
@@ -129,7 +130,7 @@ export interface Manifest {
|
||||
backup: BackupActions
|
||||
migrations: Migrations
|
||||
actions: { [id: string]: Action }
|
||||
permissions: any // @TODO
|
||||
permissions: any // @TODO 0.3.1
|
||||
dependencies: DependencyInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export const serverConfig: ConfigSpec = {
|
||||
'share-stats': {
|
||||
type: 'boolean',
|
||||
name: 'Report Bugs',
|
||||
description: new IonicSafeString(`If enabled, generic error codes will be anonymously transmitted over Tor to the Start9 team. This helps us identify and fix bugs quickly. <a href="https://docs.start9.com" target="_blank" rel="noreferrer">Read more</a> `) as any, // @TODO get actual link
|
||||
description: new IonicSafeString(`If enabled, generic error codes will be anonymously transmitted over Tor to the Start9 team. This helps us identify and fix bugs quickly. <a href="https://docs.start9.com/user-manual/general/user-preferences/report-bugs.html" target="_blank" rel="noreferrer">Read more</a> `) as any,
|
||||
default: false,
|
||||
},
|
||||
// password: {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { OperatorFunction } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { DriveInfo, PartitionInfo } from '../services/api/api.types'
|
||||
|
||||
export type Omit<ObjectType, KeysType extends keyof ObjectType> = Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>
|
||||
export type PromiseRes<T> = { result: 'resolve', value: T } | { result: 'reject', value: Error }
|
||||
|
||||
export interface MappedBackupTarget<T> {
|
||||
id: string
|
||||
hasValidBackup: boolean
|
||||
entry: T
|
||||
}
|
||||
|
||||
export interface DependentInfo {
|
||||
id: string
|
||||
title: string
|
||||
@@ -190,11 +195,3 @@ export function debounce (delay: number = 300): MethodDecorator {
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
export interface MappedDriveInfo extends DriveInfo {
|
||||
partitions: MappedPartitionInfo[]
|
||||
}
|
||||
|
||||
export interface MappedPartitionInfo extends PartitionInfo {
|
||||
hasBackup: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user