Feat/domains

update FE types and unify sideload page with marketplace show

begin popover for UI launch select

update node version for github workflows

fix type errors

eager load more components

fix mocks for types

recalculate updates bad on pkg uninstall

chore: break form-object file structure

files for config

finish file upload API and implement for config

chore: break down form-object by type, part 1

remove NEW from config

comment entire setTimeout for new

generic form options

chore: break down form-object by type, part 2

headers for enums and unions

implement select and multiselect for config

update union types and camel case for specs

implement textarea config value

inputspec and required instead of nullable

remove subtype from list spec

update start-sdk

bump start-sdk

feat: use Taiga UI for config modal (#2250)

* feat: use Taiga UI for config modal

* chore: finish remaining changes

* chore: address comments

* bump sdk version

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update package lock

update to sdk 20 and fix types

chore: update Taiga UI and migrate some more forms (#2252)

update form to latest sdk

validate length for textarea too

chore: accommodate new changes to the specs (#2254)

* chore: accommodate new changes to the specs

* chore: fix error

* chore: fix error

feat: add input color (#2257)

* feat: add input color

* patterns will always be there

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: properly type pattern error

update to latest sdk

Add sans-serif font fallback (#2263)

* Add sans-serif font fallback

* Update frontend readme start scripts

feat: add datetime spec support (#2264)

Wifi optional (#2249)

* begin work

* allow enable and disable wifi

* nice styling

* done except for popover not dismissing

* update wifi.ts

* address comments

Feat/automated backups (#2142)

* initial restructuring

* very cool

* new structure in place

* delete unnecessary T

* down the rabbit hole

* getting better

* dont like it

* nice

* very nice

* sessions select all

* nice

* backup runs

* fix targets and more

* small improvements

* mostly working

* address PR comments

* fix error

* delete issue with merge

* fix checkboxes and add API for deleting backup runs

* better styling for checkboxes

* small button in ssh kpage too

* complete multiple UI launcher

* fix actions

* present error toast too

* fix target forms

Add logs window to setup wizard loading screen (#2076)

* add logs window to setup wizard loading screen

* fix type error

* Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

---------

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

statically type server metrics and use websocket (#2124)

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

Feat/external-smtp (#1791)

* UI for EOS smtp, missing API layer

* implement api

* fix errors

* switch to external smtp creds

* fix things up

* fix types

* update types for new forms

* feat: add new form to emails and marketplace (#2268)

* import tuilet module

* feat: get rid of old form completely (#2270)

* move to builder spec and delete developer menu

* update sdk

* tiny

* getting better

* working

* done

* feat: add step to number config

* chore: small fixes

* update SDK and step for numbers

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>

latest sdk, fix build

update SDK for better disabled props

feat: implement `disabled`, `immutable` and `generate` (#2280)

* feat: implement `disabled`, `immutable` and `generate`

* chore: remove unnecessary code

* chore: add generate to textarea and implement immutable

* no generate for textarea

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update lockfile

refactor: extract loading status to shared library (#2282)

* refactor: extract loading status to shared library

* chore: remove inline style

refactor: break routing down to apps level (#2285)

closes #2212 and closes #2214

Feat/credentials (#2290)

add credentials and remove properties

refactor: break ui up further down (#2292)

* refactor: break ui up further down

* permit loading even when authed

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update patchdb for package compatability fixes

fix file structure

WIP

finish rebase

mvp complete

port forwards mvp

looking good

cleaner system page

move experimental features

manual port overrides

better info headers for jobs pages

refactor: move diagnostic-ui app under ui route (#2306)

* refactor: move diagnostic-ui app under ui route

* chore: hide navigation

* chore: remove ionic from diagnostic

* fix navbar showing on login

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: partially remove ionic modals and loaders (#2308)

* chore: partially remove ionic modals and loaders

* change to snake

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

better session data fetching

abstract store icon component to shared marketplace project (#2311)

* abstract store icon component to shared marketplace project

* better than using a pipe

* minor cleanup

* chore: fix missing node types in libraries

* typo

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>

refactor: continue to get rid of ionic infrastructure (#2325)

refactor: finish removing ionic entities: (#2333)

* refactor: finish removing ionic entities:

ToastController
ErrorToastService
ModalController
AlertController
LoadingController

* chore: rollback testing code

* chore: fix comments

* minor form change

* chore: fix comments

* update clearnet address parts

* move around patchDB

* chore: fix comments

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

fixup after rebase
This commit is contained in:
Matt Hill
2023-03-07 14:37:14 -07:00
committed by Aiden McClelland
parent c03778ec8b
commit 38c2c47789
268 changed files with 4746 additions and 4784 deletions

View File

@@ -1,4 +1,5 @@
<tui-root>
<tui-theme-night></tui-theme-night>
<tui-root tuiMode="onDark">
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import { ErrorService } from '@start9labs/shared'
@Component({
selector: 'app-root',
@@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared'
export class AppComponent {
constructor(
private readonly apiService: ApiService,
private readonly errorToastService: ErrorToastService,
private readonly errorService: ErrorService,
private readonly navCtrl: NavController,
) {}
@@ -26,7 +26,7 @@ export class AppComponent {
await this.navCtrl.navigateForward(route)
} catch (e: any) {
this.errorToastService.present(e)
this.errorService.handleError(e)
}
}
}

View File

@@ -2,7 +2,14 @@ import { NgModule } from '@angular/core'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { RouteReuseStrategy } from '@angular/router'
import { HttpClientModule } from '@angular/common/http'
import { TuiRootModule } from '@taiga-ui/core'
import {
TuiAlertModule,
tuiButtonOptionsProvider,
TuiDialogModule,
TuiModeModule,
TuiRootModule,
TuiThemeNightModule,
} from '@taiga-ui/core'
import { ApiService } from './services/api/api.service'
import { MockApiService } from './services/api/mock-api.service'
import { LiveApiService } from './services/api/live-api.service'
@@ -19,6 +26,7 @@ import { LoadingPageModule } from './pages/loading/loading.module'
import { RecoverPageModule } from './pages/recover/recover.module'
import { TransferPageModule } from './pages/transfer/transfer.module'
import {
LoadingModule,
provideSetupLogsService,
provideSetupService,
RELATIVE_URL,
@@ -46,10 +54,16 @@ const {
RecoverPageModule,
TransferPageModule,
TuiRootModule,
TuiDialogModule,
TuiAlertModule,
LoadingModule,
TuiModeModule,
TuiThemeNightModule,
],
providers: [
provideSetupService(ApiService),
provideSetupLogsService(ApiService),
tuiButtonOptionsProvider({ size: 'm' }),
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService,

View File

@@ -1,20 +1,26 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
import {
TuiFieldErrorPipeModule,
TuiInputModule,
TuiInputPasswordModule,
} from '@taiga-ui/kit'
import { CifsModal } from './cifs-modal.page'
@NgModule({
declarations: [
CifsModal,
],
declarations: [CifsModal],
imports: [
CommonModule,
FormsModule,
IonicModule,
],
exports: [
CifsModal,
TuiButtonModule,
TuiInputModule,
TuiErrorModule,
ReactiveFormsModule,
TuiFieldErrorPipeModule,
TuiInputPasswordModule,
],
exports: [CifsModal],
})
export class CifsModalModule { }
export class CifsModalModule {}

View File

@@ -1,94 +1,39 @@
<ion-header>
<ion-toolbar>
<ion-title> Connect Network Folder </ion-title>
</ion-toolbar>
</ion-header>
<form [formGroup]="form" (ngSubmit)="submit()">
<tui-input formControlName="hostname">
Hostname
<input tuiTextfield placeholder="'My Computer' OR 'my-computer.local'" />
</tui-input>
<tui-error
formControlName="hostname"
[error]="['required'] | tuiFieldError | async"
></tui-error>
<ion-content class="ion-padding">
<form (ngSubmit)="submit()" #cifsForm="ngForm">
<p>Hostname *</p>
<ion-item>
<ion-input
id="hostname"
required
[(ngModel)]="cifs.hostname"
name="hostname"
#hostname="ngModel"
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
pattern="^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$"
></ion-input>
</ion-item>
<p [hidden]="hostname.valid || hostname.pristine">
<ion-text color="danger"
>Hostname is required. e.g. 'My Computer' OR
'my-computer.local'</ion-text
>
</p>
<tui-input formControlName="path" class="input">
Path
<input tuiTextfield placeholder="/Desktop/my-folder'" />
</tui-input>
<tui-error
formControlName="path"
[error]="[] | tuiFieldError | async"
></tui-error>
<p>Path *</p>
<ion-item>
<ion-input
id="path"
required
[(ngModel)]="cifs.path"
name="path"
#path="ngModel"
placeholder="ex. /Desktop/my-folder'"
></ion-input>
</ion-item>
<p [hidden]="path.valid || path.pristine">
<ion-text color="danger">Path is required</ion-text>
</p>
<tui-input formControlName="username" class="input">
Username
<input tuiTextfield placeholder="Enter username" />
</tui-input>
<tui-error
formControlName="username"
[error]="[] | tuiFieldError | async"
></tui-error>
<p>Username *</p>
<ion-item>
<ion-input
id="username"
required
[(ngModel)]="cifs.username"
name="username"
#username="ngModel"
placeholder="Enter username"
></ion-input>
</ion-item>
<p [hidden]="username.valid || username.pristine">
<ion-text color="danger">Username is required</ion-text>
</p>
<tui-input-password formControlName="password" class="input">
Password
</tui-input-password>
<p>Password</p>
<ion-item>
<ion-input
id="password"
type="password"
[(ngModel)]="cifs.password"
name="password"
#password="ngModel"
></ion-input>
</ion-item>
<button hidden type="submit"></button>
</form>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button
class="ion-padding-end"
slot="end"
color="warning"
(click)="cancel()"
>
<footer class="modal-buttons">
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
Cancel
</ion-button>
<ion-button
class="ion-padding-end"
slot="end"
color="primary"
strong="true"
[disabled]="!cifsForm.form.valid"
(click)="submit()"
>
Verify
</ion-button>
</ion-toolbar>
</ion-footer>
</button>
<button tuiButton [disabled]="form.invalid">Verify</button>
</footer>
</form>

View File

@@ -1,16 +1,3 @@
.item-interactive {
--highlight-background: var(--ion-color-dark) !important;
.input {
margin-top: 16px;
}
ion-item {
&:hover {
transition-property: transform;
transform: none;
}
}
.item-has-focus {
--background: var(--ion-color-dark-tint) !important;
}

View File

@@ -1,94 +1,117 @@
import { Component } from '@angular/core'
import { Component, Inject } from '@angular/core'
import { FormControl, FormGroup, Validators } from '@angular/forms'
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
import { LoadingService, StartOSDiskInfo } from '@start9labs/shared'
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import {
AlertController,
LoadingController,
ModalController,
} from '@ionic/angular'
import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service'
import { StartOSDiskInfo } from '@start9labs/shared'
import { PasswordPage } from '../password/password.page'
ApiService,
CifsBackupTarget,
CifsRecoverySource,
} from 'src/app/services/api/api.service'
import { PASSWORD } from '../password/password.page'
@Component({
selector: 'cifs-modal',
templateUrl: 'cifs-modal.page.html',
styleUrls: ['cifs-modal.page.scss'],
providers: [
{
provide: TUI_VALIDATION_ERRORS,
useValue: {
required: 'This field is required',
},
},
],
})
export class CifsModal {
cifs = {
type: 'cifs' as 'cifs',
hostname: '',
path: '',
username: '',
password: '',
}
readonly form = new FormGroup({
hostname: new FormControl('', {
validators: [
Validators.required,
Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/),
],
nonNullable: true,
}),
path: new FormControl('', {
validators: [Validators.required],
nonNullable: true,
}),
username: new FormControl('', {
validators: [Validators.required],
nonNullable: true,
}),
password: new FormControl(),
})
constructor(
private readonly modalController: ModalController,
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<{
cifs: CifsRecoverySource
recoveryPassword: string
}>,
private readonly dialogs: TuiDialogService,
private readonly api: ApiService,
private readonly loadingCtrl: LoadingController,
private readonly alertCtrl: AlertController,
private readonly loader: LoadingService,
) {}
cancel() {
this.modalController.dismiss()
this.context.$implicit.complete()
}
async submit(): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Connecting to shared folder...',
cssClass: 'loader',
})
await loader.present()
const loader = this.loader
.open('Connecting to shared folder...')
.subscribe()
try {
const diskInfo = await this.api.verifyCifs({
...this.cifs,
password: this.cifs.password
? await this.api.encrypt(this.cifs.password)
...this.form.getRawValue(),
type: 'cifs',
password: this.form.value.password
? await this.api.encrypt(String(this.form.value.password))
: null,
})
await loader.dismiss()
loader.unsubscribe()
this.presentModalPassword(diskInfo)
} catch (e) {
await loader.dismiss()
loader.unsubscribe()
this.presentAlertFailed()
}
}
private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise<void> {
private presentModalPassword(diskInfo: StartOSDiskInfo) {
const target: CifsBackupTarget = {
...this.cifs,
...this.form.getRawValue(),
mountable: true,
'embassy-os': diskInfo,
}
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
this.modalController.dismiss(
{
cifs: this.cifs,
recoveryPassword: res.data.password,
},
'success',
)
}
})
await modal.present()
this.dialogs
.open<string>(PASSWORD, {
label: 'Unlock Drive',
size: 's',
data: { target },
})
.subscribe(recoveryPassword => {
this.context.completeWith({
cifs: { ...this.form.getRawValue(), type: 'cifs' },
recoveryPassword,
})
})
}
private async presentAlertFailed(): Promise<void> {
const alert = await this.alertCtrl.create({
header: 'Connection Failed',
message:
private presentAlertFailed() {
this.dialogs
.open(
'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.',
buttons: ['OK'],
})
alert.present()
{
label: 'Connection Failed',
size: 's',
},
)
.subscribe()
}
}

View File

@@ -1,20 +1,20 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { FormsModule } from '@angular/forms'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core'
import { TuiInputPasswordModule } from '@taiga-ui/kit'
import { PasswordPage } from './password.page'
@NgModule({
declarations: [
PasswordPage,
],
declarations: [PasswordPage],
imports: [
CommonModule,
FormsModule,
IonicModule,
],
exports: [
PasswordPage,
TuiButtonModule,
TuiInputPasswordModule,
TuiErrorModule,
ReactiveFormsModule,
],
exports: [PasswordPage],
})
export class PasswordPageModule { }
export class PasswordPageModule {}

View File

@@ -1,91 +1,35 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ storageDrive ? 'Set Password' : 'Unlock Drive' }}</ion-title>
</ion-toolbar>
</ion-header>
<p *ngIf="!storageDrive else choose">
Enter the password that was used to encrypt this drive.
</p>
<ng-template #choose>
<p>
Choose a password for your server.
<i>Make it good. Write it down.</i>
</p>
</ng-template>
<ion-content>
<div style="padding: 8px 24px">
<p *ngIf="!storageDrive else choose">
Enter the password that was used to encrypt this drive.
</p>
<ng-template #choose>
<p>
Choose a password for your server.
<i>Make it good. Write it down.</i>
</p>
</ng-template>
<form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
<ion-item
[class]="pwError ? 'error-border' : password && storageDrive ? 'success-border' : ''"
>
<ion-input
#focusInput
[(ngModel)]="password"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked1 ? 'password' : 'text'"
placeholder="Enter Password"
(ionChange)="validate()"
maxlength="64"
></ion-input>
<ion-button fill="clear" color="light" (click)="unmasked1 = !unmasked1">
<ion-icon
slot="icon-only"
[name]="unmasked1 ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button>
</ion-item>
<p *ngIf="pwError" class="error-message">{{ pwError }}</p>
<ng-container *ngIf="storageDrive">
<ion-item
[class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''"
>
<ion-input
[(ngModel)]="passwordVer"
[ngModelOptions]="{'standalone': true}"
[type]="!unmasked2 ? 'password' : 'text'"
placeholder="Retype Password"
(ionChange)="checkVer()"
maxlength="64"
></ion-input>
<ion-button
fill="clear"
color="light"
(click)="unmasked2 = !unmasked2"
>
<ion-icon
slot="icon-only"
[name]="unmasked2 ? 'eye-off-outline' : 'eye-outline'"
size="small"
></ion-icon>
</ion-button>
</ion-item>
<p *ngIf="verError" class="error-message">{{ verError }}</p>
</ng-container>
<input type="submit" style="display: none" />
</form>
</div>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-button
class="ion-padding-end"
slot="end"
color="warning"
(click)="cancel()"
>
<form (ngSubmit)="storageDrive ? submitPw() : verifyPw()">
<tui-input-password [formControl]="password">
Enter Password
<input tuiTextfield maxlength="64" />
</tui-input-password>
<tui-error [error]="passwordError"></tui-error>
<ng-container *ngIf="storageDrive">
<tui-input-password style="margin-top: 16px" [formControl]="confirm">
Retype Password
<input tuiTextfield maxlength="64" />
</tui-input-password>
<tui-error [error]="confirmError"></tui-error>
</ng-container>
<footer class="modal-buttons">
<button tuiButton appearance="secondary" type="button" (click)="cancel()">
Cancel
</ion-button>
<ion-button
class="ion-padding-end"
slot="end"
strong="true"
(click)="storageDrive ? submitPw() : verifyPw()"
</button>
<button
tuiButton
[disabled]="!password.value || !!confirmError || !!passwordError"
>
{{ storageDrive ? 'Finish' : 'Unlock' }}
</ion-button>
</ion-toolbar>
</ion-footer>
</button>
</footer>
</form>

View File

@@ -1,21 +0,0 @@
.item-interactive {
--highlight-background: var(--ion-color-dark) !important;
}
ion-item {
&:hover {
transition-property: transform;
transform: none;
}
}
.item-has-focus {
--background: var(--ion-color-dark-tint) !important;
}
.error-message {
color: var(--ion-color-danger) !important;
font-size: .9rem !important;
margin-left: 36px;
margin-top: -16px;
}

View File

@@ -1,81 +1,77 @@
import { Component, Input, ViewChild } from '@angular/core'
import { IonInput, ModalController } from '@ionic/angular'
import { Component, Inject } from '@angular/core'
import { FormControl } from '@angular/forms'
import * as argon2 from '@start9labs/argon2'
import { ErrorService } from '@start9labs/shared'
import { TuiDialogContext } from '@taiga-ui/core'
import {
PolymorpheusComponent,
POLYMORPHEUS_CONTEXT,
} from '@tinkoff/ng-polymorpheus'
import {
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
import * as argon2 from '@start9labs/argon2'
interface DialogData {
target?: CifsBackupTarget | DiskBackupTarget
storageDrive?: boolean
}
@Component({
selector: 'app-password',
templateUrl: 'password.page.html',
styleUrls: ['password.page.scss'],
})
export class PasswordPage {
@ViewChild('focusInput') elem?: IonInput
@Input() target?: CifsBackupTarget | DiskBackupTarget
@Input() storageDrive = false
readonly target = this.context.data.target
readonly storageDrive = this.context.data.storageDrive
readonly password = new FormControl('', { nonNullable: true })
readonly confirm = new FormControl('', { nonNullable: true })
pwError = ''
password = ''
unmasked1 = false
constructor(
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<string, DialogData>,
private readonly errorService: ErrorService,
) {}
verError = ''
passwordVer = ''
unmasked2 = false
get passwordError(): string | null {
if (!this.password.touched || this.target) return null
constructor(private modalController: ModalController) {}
if (!this.storageDrive && !this.target?.['embassy-os'])
return 'No recovery target' // unreachable
ngAfterViewInit() {
setTimeout(() => this.elem?.setFocus(), 400)
if (this.password.value.length < 12)
return 'Must be 12 characters or greater'
if (this.password.value.length > 64)
return 'Must be less than 65 characters'
return null
}
async verifyPw() {
if (!this.target || !this.target['embassy-os'])
this.pwError = 'No recovery target' // unreachable
get confirmError(): string | null {
return this.confirm.touched && this.password.value !== this.confirm.value
? 'Passwords do not match'
: null
}
verifyPw() {
try {
const passwordHash = this.target!['embassy-os']?.['password-hash'] || ''
argon2.verify(passwordHash, this.password)
this.modalController.dismiss({ password: this.password }, 'success')
argon2.verify(passwordHash, this.password.value)
this.context.completeWith(this.password.value)
} catch (e) {
this.pwError = 'Incorrect password provided'
this.errorService.handleError('Incorrect password provided')
}
}
async submitPw() {
this.validate()
if (this.password !== this.passwordVer) {
this.verError = '*passwords do not match'
}
if (this.pwError || this.verError) return
this.modalController.dismiss({ password: this.password }, 'success')
}
validate() {
if (!!this.target) return (this.pwError = '')
if (this.passwordVer) {
this.checkVer()
}
if (this.password.length < 12) {
this.pwError = 'Must be 12 characters or greater'
} else if (this.password.length > 64) {
this.pwError = 'Must be less than 65 characters'
} else {
this.pwError = ''
}
}
checkVer() {
this.verError =
this.password !== this.passwordVer ? 'Passwords do not match' : ''
submitPw() {
this.context.completeWith(this.password.value)
}
cancel() {
this.modalController.dismiss()
this.context.$implicit.complete()
}
}
export const PASSWORD = new PolymorpheusComponent(PasswordPage)

View File

@@ -1,13 +1,10 @@
import { Component } from '@angular/core'
import {
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
import { NavController } from '@ionic/angular'
import { DiskInfo, ErrorService, LoadingService } from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import { ApiService } from 'src/app/services/api/api.service'
import { DiskInfo, ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from 'src/app/modals/password/password.page'
import { PASSWORD, PasswordPage } from 'src/app/modals/password/password.page'
@Component({
selector: 'app-attach',
@@ -21,10 +18,10 @@ export class AttachPage {
constructor(
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly errToastService: ErrorToastService,
private readonly errorService: ErrorService,
private readonly stateService: StateService,
private readonly modalCtrl: ModalController,
private readonly loadingCtrl: LoadingController,
private readonly dialogs: TuiDialogService,
private readonly loader: LoadingService,
) {}
async ngOnInit() {
@@ -41,38 +38,34 @@ export class AttachPage {
try {
this.drives = await this.apiService.getDrives()
} catch (e: any) {
this.errToastService.present(e)
this.errorService.handleError(e)
} finally {
this.loading = false
}
}
async select(guid: string) {
const modal = await this.modalCtrl.create({
component: PasswordPage,
componentProps: { storageDrive: true },
})
modal.onDidDismiss().then(res => {
if (res.data && res.data.password) {
this.attachDrive(guid, res.data.password)
}
})
await modal.present()
select(guid: string) {
this.dialogs
.open<string>(PASSWORD, {
label: 'Set Password',
size: 's',
data: { storageDrive: true },
})
.subscribe(password => {
this.attachDrive(guid, password)
})
}
private async attachDrive(guid: string, password: string) {
const loader = await this.loadingCtrl.create({
message: 'Connecting to drive...',
cssClass: 'loader',
})
await loader.present()
const loader = this.loader.open('Connecting to drive...').subscribe()
try {
await this.stateService.importDrive(guid, password)
await this.navCtrl.navigateForward(`/loading`)
} catch (e: any) {
this.errToastService.present(e)
this.errorService.handleError(e)
} finally {
loader.dismiss()
loader.unsubscribe()
}
}
}

View File

@@ -1,19 +1,22 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import {
AlertController,
LoadingController,
ModalController,
NavController,
} from '@ionic/angular'
DiskInfo,
ErrorService,
GuidPipe,
LoadingService,
} from '@start9labs/shared'
import { TuiDialogService } from '@taiga-ui/core'
import {
ApiService,
BackupRecoverySource,
DiskRecoverySource,
DiskMigrateSource,
} from 'src/app/services/api/api.service'
import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { PASSWORD, PasswordPage } from '../../modals/password/password.page'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { filter, of, switchMap } from 'rxjs'
@Component({
selector: 'app-embassy',
@@ -28,11 +31,10 @@ export class EmbassyPage {
constructor(
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly modalController: ModalController,
private readonly alertCtrl: AlertController,
private readonly dialogs: TuiDialogService,
private readonly stateService: StateService,
private readonly loadingCtrl: LoadingController,
private readonly errorToastService: ErrorToastService,
private readonly loader: LoadingService,
private readonly errorService: ErrorService,
private readonly guidPipe: GuidPipe,
) {}
@@ -77,87 +79,71 @@ export class EmbassyPage {
})
}
} catch (e: any) {
this.errorToastService.present(e)
this.errorService.handleError(e)
} finally {
this.loading = false
}
}
async chooseDrive(drive: DiskInfo) {
if (
this.guidPipe.transform(drive) ||
!!drive.partitions.find(p => p.used)
) {
const alert = await this.alertCtrl.create({
header: 'Warning',
subHeader: 'Drive contains data!',
message: 'All data stored on this drive will be permanently deleted.',
buttons: [
{
role: 'cancel',
text: 'Cancel',
},
{
text: 'Continue',
handler: () => {
// for backup recoveries
if (this.stateService.recoveryPassword) {
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
)
} else {
// for migrations and fresh setups
this.presentModalPassword(drive.logicalname)
}
},
},
],
chooseDrive(drive: DiskInfo) {
of(!this.guidPipe.transform(drive) && !drive.partitions.some(p => p.used))
.pipe(
switchMap(unused =>
unused
? of(true)
: this.dialogs.open(TUI_PROMPT, {
label: 'Warning',
size: 's',
data: {
content:
'<strong>Drive contains data!</strong><p>All data stored on this drive will be permanently deleted.</p>',
yes: 'Continue',
no: 'Cancel',
},
}),
),
)
.pipe(filter(Boolean))
.subscribe(() => {
// for backup recoveries
if (this.stateService.recoveryPassword) {
this.setupEmbassy(
drive.logicalname,
this.stateService.recoveryPassword,
)
} else {
// for migrations and fresh setups
this.presentModalPassword(drive.logicalname)
}
})
await alert.present()
} else {
// for backup recoveries
if (this.stateService.recoveryPassword) {
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
} else {
// for migrations and fresh setups
this.presentModalPassword(drive.logicalname)
}
}
}
private async presentModalPassword(logicalname: string): Promise<void> {
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: {
storageDrive: true,
},
})
modal.onDidDismiss().then(async ret => {
if (!ret.data || !ret.data.password) return
this.setupEmbassy(logicalname, ret.data.password)
})
await modal.present()
private presentModalPassword(logicalname: string) {
this.dialogs
.open<string>(PASSWORD, {
label: 'Set Password',
size: 's',
data: { storageDrive: true },
})
.subscribe(password => {
this.setupEmbassy(logicalname, password)
})
}
private async setupEmbassy(
logicalname: string,
password: string,
): Promise<void> {
const loader = await this.loadingCtrl.create({
message: 'Connecting to drive...',
cssClass: 'loader',
})
await loader.present()
const loader = this.loader.open('Connecting to drive...').subscribe()
try {
await this.stateService.setupEmbassy(logicalname, password)
await this.navCtrl.navigateForward(`/loading`)
} catch (e: any) {
this.errorToastService.present(e)
this.errorService.handleError(e)
console.error(e)
} finally {
loader.dismiss()
loader.unsubscribe()
}
}
}

View File

@@ -2,7 +2,7 @@ import { Component } from '@angular/core'
import { IonicSlides } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import SwiperCore, { Swiper } from 'swiper'
import { ErrorToastService } from '@start9labs/shared'
import { ErrorService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
SwiperCore.use([IonicSlides])
@@ -19,7 +19,7 @@ export class HomePage {
constructor(
private readonly api: ApiService,
private readonly errToastService: ErrorToastService,
private readonly errorService: ErrorService,
private readonly stateService: StateService,
) {}
@@ -33,7 +33,7 @@ export class HomePage {
await this.api.getPubKey()
} catch (e: any) {
this.error = true
this.errToastService.present(e)
this.errorService.handleError(e)
} finally {
this.loading = false
}

View File

@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { LoadingModule } from '@start9labs/shared'
import { InitializingModule } from '@start9labs/shared'
import { LoadingPage } from './loading.page'
const routes: Routes = [
@@ -11,7 +11,7 @@ const routes: Routes = [
]
@NgModule({
imports: [LoadingModule, RouterModule.forChild(routes)],
imports: [InitializingModule, RouterModule.forChild(routes)],
declarations: [LoadingPage],
})
export class LoadingPageModule {}

View File

@@ -1,5 +1,5 @@
<app-loading
<app-initializing
class="ion-page"
[setupType]="stateService.setupType"
(finished)="navCtrl.navigateForward('/success')"
></app-loading>
></app-initializing>

View File

@@ -1,10 +1,17 @@
import { Component, Input } from '@angular/core'
import { ModalController, NavController } from '@ionic/angular'
import { NavController } from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from '@start9labs/shared'
import {
ApiService,
CifsRecoverySource,
DiskBackupTarget,
} from 'src/app/services/api/api.service'
import { ErrorService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { PASSWORD } from '../../modals/password/password.page'
import { TuiDialogService } from '@taiga-ui/core'
import { filter } from 'rxjs'
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
@Component({
selector: 'app-recover',
@@ -18,9 +25,8 @@ export class RecoverPage {
constructor(
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly modalCtrl: ModalController,
private readonly modalController: ModalController,
private readonly errToastService: ErrorToastService,
private readonly dialogs: TuiDialogService,
private readonly errorService: ErrorService,
private readonly stateService: StateService,
) {}
@@ -62,34 +68,28 @@ export class RecoverPage {
})
})
} catch (e: any) {
this.errToastService.present(e)
this.errorService.handleError(e)
} finally {
this.loading = false
}
}
async presentModalCifs(): Promise<void> {
const modal = await this.modalCtrl.create({
component: CifsModal,
})
modal.onDidDismiss().then(res => {
if (res.role === 'success') {
const { hostname, path, username, password } = res.data.cifs
presentModalCifs() {
this.dialogs
.open<{ cifs: CifsRecoverySource; recoveryPassword: string }>(
new PolymorpheusComponent(CifsModal),
{
label: 'Connect Network Folder',
},
)
.subscribe(({ cifs, recoveryPassword }) => {
this.stateService.recoverySource = {
type: 'backup',
target: {
type: 'cifs',
hostname,
path,
username,
password,
},
target: cifs,
}
this.stateService.recoveryPassword = res.data.recoveryPassword
this.stateService.recoveryPassword = recoveryPassword
this.navCtrl.navigateForward('/storage')
}
})
await modal.present()
})
}
async select(target: DiskBackupTarget) {
@@ -97,17 +97,16 @@ export class RecoverPage {
if (!logicalname) return
const modal = await this.modalController.create({
component: PasswordPage,
componentProps: { target },
cssClass: 'alertlike-modal',
})
modal.onDidDismiss().then(res => {
if (res.data?.password) {
this.selectRecoverySource(logicalname, res.data.password)
}
})
await modal.present()
this.dialogs
.open<string>(PASSWORD, {
label: 'Unlock Drive',
size: 's',
data: { target },
})
.pipe(filter(Boolean))
.subscribe(password => {
this.selectRecoverySource(logicalname, password)
})
}
private async selectRecoverySource(logicalname: string, password?: string) {

View File

@@ -1,6 +1,6 @@
import { DOCUMENT } from '@angular/common'
import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core'
import { DownloadHTMLService, ErrorToastService } from '@start9labs/shared'
import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
import { ApiService } from 'src/app/services/api/api.service'
import { StateService } from 'src/app/services/state.service'
@@ -12,7 +12,8 @@ import { StateService } from 'src/app/services/state.service'
})
export class SuccessPage {
@ViewChild('canvas', { static: true })
private canvas: ElementRef<HTMLCanvasElement> = {} as ElementRef<HTMLCanvasElement>
private canvas: ElementRef<HTMLCanvasElement> =
{} as ElementRef<HTMLCanvasElement>
private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D
torAddress?: string
@@ -28,7 +29,7 @@ export class SuccessPage {
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly errCtrl: ErrorToastService,
private readonly errorService: ErrorService,
private readonly stateService: StateService,
private readonly api: ApiService,
private readonly downloadHtml: DownloadHTMLService,
@@ -55,7 +56,7 @@ export class SuccessPage {
await this.api.exit()
}
} catch (e: any) {
await this.errCtrl.present(e)
await this.errorService.handleError(e)
}
}

View File

@@ -1,8 +1,11 @@
import { Component } from '@angular/core'
import { AlertController, NavController } from '@ionic/angular'
import { NavController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
import { DiskInfo, ErrorToastService } from '@start9labs/shared'
import { DiskInfo, ErrorService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { TuiDialogService } from '@taiga-ui/core'
import { TUI_PROMPT } from '@taiga-ui/kit'
import { filter } from 'rxjs'
@Component({
selector: 'app-transfer',
@@ -16,8 +19,8 @@ export class TransferPage {
constructor(
private readonly apiService: ApiService,
private readonly navCtrl: NavController,
private readonly alertCtrl: AlertController,
private readonly errToastService: ErrorToastService,
private readonly dialogs: TuiDialogService,
private readonly errorService: ErrorService,
private readonly stateService: StateService,
) {}
@@ -35,34 +38,31 @@ export class TransferPage {
try {
this.drives = await this.apiService.getDrives()
} catch (e: any) {
this.errToastService.present(e)
this.errorService.handleError(e)
} finally {
this.loading = false
}
}
async select(guid: string) {
const alert = await this.alertCtrl.create({
header: 'Warning',
message:
'After transferring data from this drive, <b>do not</b> attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.',
buttons: [
{
role: 'cancel',
text: 'Cancel',
select(guid: string) {
this.dialogs
.open(TUI_PROMPT, {
label: 'Warning',
size: 's',
data: {
content:
'After transferring data from this drive, <b>do not</b> attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.',
yes: 'Continue',
no: 'Cancel',
},
{
text: 'Continue',
handler: () => {
this.stateService.recoverySource = {
type: 'migrate',
guid,
}
this.navCtrl.navigateForward(`/storage`)
},
},
],
})
await alert.present()
})
.pipe(filter(Boolean))
.subscribe(() => {
this.stateService.recoverySource = {
type: 'migrate',
guid,
}
this.navCtrl.navigateForward(`/storage`)
})
}
}