mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
feat: move all frontend projects under the same Angular workspace (#1141)
* feat: move all frontend projects under the same Angular workspace * Refactor/angular workspace (#1154) * update frontend build steps Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { CifsModal } from './cifs-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CifsModal,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
CifsModal,
|
||||
],
|
||||
})
|
||||
export class CifsModalModule { }
|
||||
@@ -0,0 +1,82 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Connect Shared Folder
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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="dark" fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" [disabled]="!cifsForm.form.valid" (click)="submit()">
|
||||
Verify
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService, CifsBackupTarget, EmbassyOSRecoveryInfo } from 'src/app/services/api/api.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'cifs-modal',
|
||||
templateUrl: 'cifs-modal.page.html',
|
||||
styleUrls: ['cifs-modal.page.scss'],
|
||||
})
|
||||
export class CifsModal {
|
||||
cifs = {
|
||||
type: 'cifs' as 'cifs',
|
||||
hostname: '',
|
||||
path: '',
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
constructor (
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
) { }
|
||||
|
||||
cancel () {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
|
||||
async submit (): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
spinner: 'lines',
|
||||
message: 'Connecting to shared folder...',
|
||||
cssClass: 'loader',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const embassyOS = await this.apiService.verifyCifs(this.cifs)
|
||||
const is02x = embassyOS.version.startsWith('0.2')
|
||||
|
||||
if (is02x) {
|
||||
this.modalController.dismiss({
|
||||
cifs: this.cifs,
|
||||
}, 'success')
|
||||
} else {
|
||||
this.presentModalPassword(embassyOS)
|
||||
}
|
||||
} catch (e) {
|
||||
this.presentAlertFailed()
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword (embassyOS: EmbassyOSRecoveryInfo): Promise<void> {
|
||||
const target: CifsBackupTarget = {
|
||||
...this.cifs,
|
||||
mountable: true,
|
||||
'embassy-os': embassyOS,
|
||||
}
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalController.dismiss({
|
||||
cifs: this.cifs,
|
||||
recoveryPassword: res.data.password,
|
||||
}, 'success')
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async presentAlertFailed (): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Connection Failed',
|
||||
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.',
|
||||
buttons: ['OK'],
|
||||
})
|
||||
alert.present()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { PasswordPage } from './password.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
PasswordPage,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
PasswordPage,
|
||||
],
|
||||
})
|
||||
export class PasswordPageModule { }
|
||||
@@ -0,0 +1,72 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
{{ !!storageDrive ? 'Set Password' : 'Unlock Drive' }}
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<div style="padding: 8px 24px;">
|
||||
<div style="padding-bottom: 16px;">
|
||||
<ng-container *ngIf="!!storageDrive">
|
||||
<p>Choose a password for your Embassy. <i>Make it good. Write it down.</i></p>
|
||||
<p style="color: var(--ion-color-warning);">Losing your password can result in total loss of data.</p>
|
||||
</ng-container>
|
||||
<p *ngIf="!storageDrive">Enter the password that was used to encrypt this drive.</p>
|
||||
</div>
|
||||
|
||||
<form (ngSubmit)="!!storageDrive ? submitPw() : verifyPw()">
|
||||
<p>Password</p>
|
||||
<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>
|
||||
<div style="height: 16px;">
|
||||
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ pwError }}</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!!storageDrive">
|
||||
<p>Confirm Password</p>
|
||||
<ion-item [class]="verError ? 'error-border' : passwordVer ? 'success-border' : ''">
|
||||
<ion-input
|
||||
[(ngModel)]="passwordVer"
|
||||
[ngModelOptions]="{'standalone': true}"
|
||||
[type]="!unmasked2 ? 'password' : 'text'"
|
||||
(ionChange)="checkVer()"
|
||||
maxlength="64"
|
||||
placeholder="Retype Password"
|
||||
></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>
|
||||
<div style="height: 16px;">
|
||||
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ verError }}</p>
|
||||
</div>
|
||||
</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="dark" fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="!!storageDrive ? submitPw() : verifyPw()">
|
||||
{{ !!storageDrive ? 'Finish' : 'Unlock' }}
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import { DiskInfo, CifsBackupTarget, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
|
||||
@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: DiskInfo
|
||||
|
||||
pwError = ''
|
||||
password = ''
|
||||
unmasked1 = false
|
||||
|
||||
verError = ''
|
||||
passwordVer = ''
|
||||
unmasked2 = false
|
||||
|
||||
constructor (
|
||||
private modalController: ModalController,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyPw () {
|
||||
if (!this.target || !this.target['embassy-os']) this.pwError = 'No recovery target' // unreachable
|
||||
|
||||
try {
|
||||
argon2.verify(this.target['embassy-os']['password-hash'], this.password)
|
||||
this.modalController.dismiss({ password: this.password }, 'success')
|
||||
} catch (e) {
|
||||
this.pwError = '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 {
|
||||
this.pwError = ''
|
||||
}
|
||||
}
|
||||
|
||||
checkVer () {
|
||||
this.verError = this.password !== this.passwordVer ? 'Passwords do not match' : ''
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProdKeyModal } from './prod-key-modal.page'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
],
|
||||
exports: [
|
||||
ProdKeyModal,
|
||||
],
|
||||
})
|
||||
export class ProdKeyModalModule { }
|
||||
@@ -0,0 +1,41 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Enter Product Key
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<form (ngSubmit)="verifyProductKey()">
|
||||
<div style="padding: 8px 24px;">
|
||||
<div style="padding-bottom: 16px;">
|
||||
<p>Enter your 0.2.x Product Key to establish an encrypted connection with your new Embassy.</p>
|
||||
</div>
|
||||
<ion-item>
|
||||
<ion-input
|
||||
#focusInput
|
||||
[(ngModel)]="productKey"
|
||||
placeholder="Enter Product Key"
|
||||
maxlength="12"
|
||||
></ion-input>
|
||||
</ion-item>
|
||||
<div style="height: 16px;">
|
||||
<p style="color: var(--ion-color-danger); font-size: x-small;">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" style="display: none" />
|
||||
</form>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" (click)="cancel()">
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button class="ion-padding-end" slot="end" color="dark" fill="clear" strong="true" (click)="verifyProductKey()">
|
||||
Submit
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, LoadingController, ModalController } from '@ionic/angular'
|
||||
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
|
||||
import { HttpService } from 'src/app/services/api/http.service'
|
||||
|
||||
@Component({
|
||||
selector: 'prod-key-modal',
|
||||
templateUrl: 'prod-key-modal.page.html',
|
||||
styleUrls: ['prod-key-modal.page.scss'],
|
||||
})
|
||||
export class ProdKeyModal {
|
||||
@ViewChild('focusInput') elem: IonInput
|
||||
@Input() target: DiskBackupTarget
|
||||
|
||||
error = ''
|
||||
productKey = ''
|
||||
unmasked = false
|
||||
|
||||
constructor (
|
||||
private readonly modalController: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly httpService: HttpService,
|
||||
) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
setTimeout(() => this.elem.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
if (!this.productKey) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.set02XDrive(this.target.logicalname)
|
||||
this.httpService.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
this.modalController.dismiss({ productKey: this.productKey }, 'success')
|
||||
} catch (e) {
|
||||
this.httpService.productKey = undefined
|
||||
this.error = 'Invalid Product Key'
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
cancel () {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user