mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
remove product key from setup flow (#1750)
* remove product key flow from setup * feat: backend turned off encryption + new Id + no package id * implement new encryption scheme in FE * decode response string * crypto not working * update setup wizard closes #1762 * feat: Get the encryption key * fix: Get to recovery * remove old code * fix build * fix: Install works for now * fix bug in config for adding new list items * dismiss action modal on success * clear button in config * wip: Currently broken in avahi mdns * include headers with req/res and refactor patchDB init and usage * fix: Can now run in the main * flatline on failed init * update patch DB * add last-wifi-region to data model even though not used by FE * chore: Fix the start. * wip: Fix wrong order for getting hostname before sql has been created * fix edge case where union keys displayed as new when not new * fix: Can start * last backup color, markdown links always new tab, fix bug with login * refactor to remove WithRevision * resolve circular dep issue * update submodule * fix patch-db * update patchDB * update patch again * escape error * decodeuricomponent * increase proxy buffer size * increase proxy buffer size * fix nginx Co-authored-by: BluJ <mogulslayer@gmail.com> Co-authored-by: BluJ <dragondef@gmail.com> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
@@ -1,45 +1,32 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'
|
||||
import { NavGuard, RecoveryNavGuard } from './guards/nav-guard'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/product-key', pathMatch: 'full' },
|
||||
{
|
||||
path: 'product-key',
|
||||
loadChildren: () =>
|
||||
import('./pages/product-key/product-key.module').then(
|
||||
m => m.ProductKeyPageModule,
|
||||
),
|
||||
},
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home.module').then(m => m.HomePageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'recover',
|
||||
loadChildren: () =>
|
||||
import('./pages/recover/recover.module').then(m => m.RecoverPageModule),
|
||||
canActivate: [RecoveryNavGuard],
|
||||
},
|
||||
{
|
||||
path: 'embassy',
|
||||
loadChildren: () =>
|
||||
import('./pages/embassy/embassy.module').then(m => m.EmbassyPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'loading',
|
||||
loadChildren: () =>
|
||||
import('./pages/loading/loading.module').then(m => m.LoadingPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
{
|
||||
path: 'success',
|
||||
loadChildren: () =>
|
||||
import('./pages/success/success.module').then(m => m.SuccessPageModule),
|
||||
canActivate: [NavGuard],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { ApiService } from './services/api/api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import { StateService } from './services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -14,21 +13,12 @@ export class AppComponent {
|
||||
private readonly apiService: ApiService,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const status = await this.apiService.getStatus()
|
||||
if (status.migrating || status['product-key']) {
|
||||
this.stateService.hasProductKey = true
|
||||
this.stateService.isMigrating = status.migrating
|
||||
await this.navCtrl.navigateForward(`/product-key`)
|
||||
} else {
|
||||
this.stateService.hasProductKey = false
|
||||
this.stateService.isMigrating = false
|
||||
await this.navCtrl.navigateForward(`/recover`)
|
||||
}
|
||||
const { migrating } = await this.apiService.getStatus()
|
||||
await this.navCtrl.navigateForward(migrating ? '/loading' : '/home')
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorHandler, NgModule } from '@angular/core'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouteReuseStrategy } from '@angular/router'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
@@ -15,8 +15,6 @@ import { AppRoutingModule } from './app-routing.module'
|
||||
import { SuccessPageModule } from './pages/success/success.module'
|
||||
import { HomePageModule } from './pages/home/home.module'
|
||||
import { LoadingPageModule } from './pages/loading/loading.module'
|
||||
import { ProdKeyModalModule } from './modals/prod-key-modal/prod-key-modal.module'
|
||||
import { ProductKeyPageModule } from './pages/product-key/product-key.module'
|
||||
import { RecoverPageModule } from './pages/recover/recover.module'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
|
||||
@@ -35,8 +33,6 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
|
||||
SuccessPageModule,
|
||||
HomePageModule,
|
||||
LoadingPageModule,
|
||||
ProdKeyModalModule,
|
||||
ProductKeyPageModule,
|
||||
RecoverPageModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CanActivate, Router } from '@angular/router'
|
||||
import { RPCEncryptedService } from '../services/rpc-encrypted.service'
|
||||
import { StateService } from '../services/state.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NavGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.encrypted.productKey) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigateByUrl('product-key')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RecoveryNavGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.encrypted.productKey || !this.stateService.hasProductKey) {
|
||||
return true
|
||||
} else {
|
||||
this.router.navigateByUrl('product-key')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { IonInput, ModalController } from '@ionic/angular'
|
||||
import {
|
||||
DiskInfo,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.service'
|
||||
@@ -15,7 +14,7 @@ import * as argon2 from '@start9labs/argon2'
|
||||
export class PasswordPage {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
@Input() target?: CifsBackupTarget | DiskBackupTarget
|
||||
@Input() storageDrive?: DiskInfo
|
||||
@Input() storageDrive = false
|
||||
|
||||
pwError = ''
|
||||
password = ''
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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 { }
|
||||
@@ -1,41 +0,0 @@
|
||||
<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>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ion-content {
|
||||
--ion-text-color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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 { RPCEncryptedService } from 'src/app/services/rpc-encrypted.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 encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.elem?.setFocus(), 400)
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
if (!this.productKey || !this.target.logicalname) return
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.apiService.set02XDrive(this.target.logicalname)
|
||||
this.encrypted.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
this.modalController.dismiss({ productKey: this.productKey }, 'success')
|
||||
} catch (e) {
|
||||
this.encrypted.productKey = undefined
|
||||
this.error = 'Invalid Product Key'
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,7 @@ export class EmbassyPage {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: `One or more devices you connected had to be reconfigured to support the current hardware platform. Please unplug and replug the following device(s), then refresh the page:<br> ${list}`,
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'OK',
|
||||
},
|
||||
],
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
@@ -95,40 +90,45 @@ export class EmbassyPage {
|
||||
text: 'Continue',
|
||||
handler: () => {
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
this.setupEmbassy(
|
||||
drive.logicalname,
|
||||
this.stateService.recoveryPassword,
|
||||
)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
if (this.stateService.recoveryPassword) {
|
||||
this.setupEmbassy(drive, this.stateService.recoveryPassword)
|
||||
this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword)
|
||||
} else {
|
||||
this.presentModalPassword(drive)
|
||||
this.presentModalPassword(drive.logicalname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalPassword(drive: DiskInfo): Promise<void> {
|
||||
private async presentModalPassword(logicalname: string): Promise<void> {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: {
|
||||
storageDrive: drive,
|
||||
storageDrive: true,
|
||||
},
|
||||
})
|
||||
modal.onDidDismiss().then(async ret => {
|
||||
if (!ret.data || !ret.data.password) return
|
||||
this.setupEmbassy(drive, ret.data.password)
|
||||
this.setupEmbassy(logicalname, ret.data.password)
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private async setupEmbassy(drive: DiskInfo, password: string): Promise<void> {
|
||||
private async setupEmbassy(
|
||||
logicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Initializing data drive. This could take a while...',
|
||||
})
|
||||
@@ -136,7 +136,7 @@ export class EmbassyPage {
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
await this.stateService.setupEmbassy(drive.logicalname, password)
|
||||
await this.stateService.setupEmbassy(logicalname, password)
|
||||
if (!!this.stateService.recoverySource) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { HomePage } from './home.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
|
||||
import { HomePageRoutingModule } from './home-routing.module'
|
||||
|
||||
import { SwiperModule } from 'swiper/angular'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,7 +14,8 @@ import { HomePageRoutingModule } from './home-routing.module'
|
||||
IonicModule,
|
||||
HomePageRoutingModule,
|
||||
PasswordPageModule,
|
||||
SwiperModule,
|
||||
],
|
||||
declarations: [HomePage],
|
||||
})
|
||||
export class HomePageModule { }
|
||||
export class HomePageModule {}
|
||||
|
||||
@@ -2,36 +2,78 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px;" />
|
||||
<div style="padding-bottom: 32px">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-content class="ion-margin">
|
||||
<!-- fresh -->
|
||||
<ion-card
|
||||
routerLink="/embassy"
|
||||
<ion-card-header>
|
||||
<ion-button
|
||||
*ngIf="swiper?.activeIndex === 1"
|
||||
class="back-button"
|
||||
fill="clear"
|
||||
color="light"
|
||||
style="text-align: center; background-color: #00919b !important; height: 160px; margin-bottom: 20px; box-shadow: 4px 4px 16px var(--ion-color-light);"
|
||||
(click)="previous()"
|
||||
>
|
||||
<ion-card-header>
|
||||
<ion-card-title style="font-size: 40px;">Start Fresh</ion-card-title>
|
||||
<ion-card-subtitle>Get started with a brand new Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
|
||||
<!-- recover -->
|
||||
</ion-card>
|
||||
<ion-card
|
||||
routerLink="/recover"
|
||||
color="light"
|
||||
style="text-align: center; background-color: #bf5900 !important; height: 160px; box-shadow: 4px 4px 16px var(--ion-color-light);"
|
||||
>
|
||||
<ion-card-header>
|
||||
<ion-card-title style="font-size: 40px;">Recover</ion-card-title>
|
||||
<ion-card-subtitle>Restore from backup or recover an old Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-card-title>
|
||||
Embassy Setup
|
||||
<span *ngIf="swiper?.activeIndex === 1"> (recover)</span>
|
||||
</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content class="ion-margin-bottom">
|
||||
<swiper (swiper)="setSwiperInstance($event)">
|
||||
<ng-template swiperSlide>
|
||||
<ion-item
|
||||
button
|
||||
[disabled]="error"
|
||||
detail="true"
|
||||
routerLink="/embassy"
|
||||
>
|
||||
<ion-icon color="dark" slot="start" name="add"></ion-icon>
|
||||
<ion-label>
|
||||
<h2><ion-text color="success">Start Fresh</ion-text></h2>
|
||||
<p>Get started with a brand new Embassy</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
button
|
||||
[disabled]="error"
|
||||
detail="true"
|
||||
lines="none"
|
||||
(click)="next()"
|
||||
>
|
||||
<ion-icon color="dark" slot="start" name="reload"></ion-icon>
|
||||
<ion-label>
|
||||
<h2><ion-text color="danger">Recover</ion-text></h2>
|
||||
<p>
|
||||
Restore from backup or use an existing Embassy data drive
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
<ng-template swiperSlide>
|
||||
<ion-item button detail="true" routerLink="/recover">
|
||||
<ion-icon color="dark" slot="start" name="save"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<ion-text color="warning">Restore From Backup</ion-text>
|
||||
</h2>
|
||||
<p>Recover an Embassy from encrypted backup</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button detail="true" lines="none" (click)="import()">
|
||||
<ion-icon color="dark" slot="start" name="cube"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<ion-text color="primary">Use Existing Drive</ion-text>
|
||||
</h2>
|
||||
<p>Attach and use a valid Embassy data drive</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</swiper>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 24px;
|
||||
z-index: 1000000;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: var(--ion-color-medium);
|
||||
--color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
@@ -1,9 +1,112 @@
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSlides,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { PasswordPage } from 'src/app/modals/password/password.page'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
import SwiperCore, { Swiper } from 'swiper'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
|
||||
SwiperCore.use([IonicSlides])
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage { }
|
||||
export class HomePage {
|
||||
swiper?: Swiper
|
||||
guid?: string | null
|
||||
error = false
|
||||
|
||||
constructor(
|
||||
private readonly unencrypted: ApiService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly stateService: StateService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.encrypted.secret = await this.unencrypted.getSecret()
|
||||
const { disks } = await this.unencrypted.getDrives()
|
||||
this.guid = disks.find(d => !!d.guid)?.guid
|
||||
} catch (e: any) {
|
||||
this.error = true
|
||||
this.errToastService.present(e)
|
||||
}
|
||||
}
|
||||
|
||||
async ionViewDidEnter() {
|
||||
if (this.swiper) {
|
||||
this.swiper.allowTouchMove = false
|
||||
}
|
||||
}
|
||||
|
||||
setSwiperInstance(swiper: any) {
|
||||
this.swiper = swiper
|
||||
}
|
||||
|
||||
next() {
|
||||
this.swiper?.slideNext(500)
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.swiper?.slidePrev(500)
|
||||
}
|
||||
|
||||
async import() {
|
||||
if (this.guid) {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { storageDrive: true },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.importDrive(res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
} else {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Drive Not Found',
|
||||
message:
|
||||
'Please make sure the drive is a valid Embassy data drive (not a backup) and is firmly connected, then refresh the page.',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async importDrive(password: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Importing Drive',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
await this.stateService.importDrive(this.guid!, password)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e: any) {
|
||||
this.errToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeHex(hex: string) {
|
||||
let str = ''
|
||||
for (let n = 0; n < hex.length; n += 2) {
|
||||
str += String.fromCharCode(parseInt(hex.substring(n, 2), 16))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { ProductKeyPage } from './product-key.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProductKeyPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProductKeyPageRoutingModule { }
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ProductKeyPage } from './product-key.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProductKeyPageRoutingModule } from './product-key-routing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ProductKeyPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
],
|
||||
declarations: [ProductKeyPage],
|
||||
})
|
||||
export class ProductKeyPageModule { }
|
||||
@@ -1,43 +0,0 @@
|
||||
<ion-content>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
|
||||
<div style="padding-bottom: 32px;">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px;" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
<ion-card-header style="padding-bottom: 8px;">
|
||||
<ion-card-title>Product Key</ion-card-title>
|
||||
<ion-card-subtitle>Enter your product key to establish an encrypted connection with your Embassy</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form (submit)="submit()" style="margin-bottom: 12px;">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<ion-item color="dark">
|
||||
<ion-icon slot="start" name="key-outline" style="margin-right: 16px;"></ion-icon>
|
||||
<ion-input
|
||||
#focusInput
|
||||
name="productKey"
|
||||
[(ngModel)]="productKey"
|
||||
(ionChange)="error = ''"
|
||||
maxlength="12"
|
||||
>
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<div class="ion-text-left">
|
||||
<p *ngIf="error" style="padding-top: 4px"><ion-text color="danger">{{ error }}</ion-text></p>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
<ion-button type="submit" color="light" class="claim-button">
|
||||
Submit
|
||||
</ion-button>
|
||||
</form>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-content>
|
||||
@@ -1,5 +0,0 @@
|
||||
ion-item {
|
||||
--border-style: solid;
|
||||
--border-width: 1px;
|
||||
--border-color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { IonInput, LoadingController, NavController } from '@ionic/angular'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { RPCEncryptedService } from 'src/app/services/rpc-encrypted.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-key',
|
||||
templateUrl: 'product-key.page.html',
|
||||
styleUrls: ['product-key.page.scss'],
|
||||
})
|
||||
export class ProductKeyPage {
|
||||
@ViewChild('focusInput') elem?: IonInput
|
||||
productKey = ''
|
||||
error = ''
|
||||
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly stateService: StateService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {}
|
||||
|
||||
ionViewDidEnter() {
|
||||
setTimeout(() => this.elem?.setFocus(), 400)
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.productKey) return (this.error = 'Must enter product key')
|
||||
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Verifying Product Key',
|
||||
})
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
this.encrypted.productKey = this.productKey
|
||||
await this.apiService.verifyProductKey()
|
||||
if (this.stateService.isMigrating) {
|
||||
await this.navCtrl.navigateForward(`/loading`)
|
||||
} else {
|
||||
await this.navCtrl.navigateForward(`/home`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = 'Invalid Product Key'
|
||||
this.encrypted.productKey = undefined
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { FormsModule } from '@angular/forms'
|
||||
import { UnitConversionPipesModule } from '@start9labs/shared'
|
||||
import { DriveStatusComponent, RecoverPage } from './recover.page'
|
||||
import { PasswordPageModule } from '../../modals/password/password.module'
|
||||
import { ProdKeyModalModule } from '../../modals/prod-key-modal/prod-key-modal.module'
|
||||
import { RecoverPageRoutingModule } from './recover-routing.module'
|
||||
import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
|
||||
|
||||
@@ -17,7 +16,6 @@ import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module'
|
||||
IonicModule,
|
||||
RecoverPageRoutingModule,
|
||||
PasswordPageModule,
|
||||
ProdKeyModalModule,
|
||||
UnitConversionPipesModule,
|
||||
CifsModalModule,
|
||||
],
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import {
|
||||
AlertController,
|
||||
IonicSafeString,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
NavController,
|
||||
} from '@ionic/angular'
|
||||
import { AlertController, ModalController, 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 { StateService } from 'src/app/services/state.service'
|
||||
import { PasswordPage } from '../../modals/password/password.page'
|
||||
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-recover',
|
||||
@@ -21,7 +14,6 @@ import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
|
||||
export class RecoverPage {
|
||||
loading = true
|
||||
mappedDrives: MappedDisk[] = []
|
||||
hasShownGuidAlert = false
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService,
|
||||
@@ -29,8 +21,7 @@ export class RecoverPage {
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly modalController: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
private readonly errToastService: ErrorToastService,
|
||||
private readonly stateService: StateService,
|
||||
) {}
|
||||
|
||||
@@ -44,10 +35,7 @@ export class RecoverPage {
|
||||
}
|
||||
|
||||
driveClickable(mapped: MappedDisk) {
|
||||
return (
|
||||
mapped.drive['embassy-os']?.full &&
|
||||
(this.stateService.hasProductKey || mapped.is02x)
|
||||
)
|
||||
return mapped.drive['embassy-os']?.full
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
@@ -89,50 +77,8 @@ export class RecoverPage {
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
const importableDrive = disks.find(d => !!d.guid)
|
||||
if (
|
||||
!!importableDrive &&
|
||||
this.stateService.hasProductKey &&
|
||||
!this.hasShownGuidAlert
|
||||
) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Embassy Data Drive Detected',
|
||||
message: new IonicSafeString(
|
||||
`<strong>${importableDrive.vendor || 'Unknown Vendor'} - ${
|
||||
importableDrive.model || 'Unknown Model'
|
||||
}</strong> contains Embassy data.
|
||||
<p>To use this drive and its data, select <strong>"USE DRIVE"</strong>. This will complete the setup process.
|
||||
<p><strong style="color:red">Important!</strong><br><br>
|
||||
If you are trying to restore from a backup or update from 0.2.x, <strong>DO NOT</strong> select "USE DRIVE". Instead, select <strong>"CANCEL"</strong> and follow instructions.`,
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Use Drive',
|
||||
handler: async () => {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { storageDrive: importableDrive },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data && res.data.password) {
|
||||
this.importDrive(importableDrive.guid!, res.data.password)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
this.hasShownGuidAlert = true
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
this.errToastService.present(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -165,65 +111,20 @@ export class RecoverPage {
|
||||
|
||||
if (!logicalname) return
|
||||
|
||||
if (this.stateService.hasProductKey) {
|
||||
if (is02x) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
// if no product key, it means they are an upgrade kit user
|
||||
if (is02x) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
} else {
|
||||
if (!is02x) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Error',
|
||||
message:
|
||||
'In order to use this image, you must select a drive containing a valid 0.2.x Embassy.',
|
||||
buttons: [
|
||||
{
|
||||
role: 'cancel',
|
||||
text: 'OK',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
const modal = await this.modalController.create({
|
||||
component: ProdKeyModal,
|
||||
componentProps: { target },
|
||||
cssClass: 'alertlike-modal',
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.data?.productKey) {
|
||||
this.selectRecoverySource(logicalname)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async importDrive(guid: string, password: string) {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Importing Drive',
|
||||
})
|
||||
await loader.present()
|
||||
try {
|
||||
await this.stateService.importDrive(guid, password)
|
||||
await this.navCtrl.navigateForward(`/success`)
|
||||
} catch (e: any) {
|
||||
this.errorToastService.present(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,174 +9,143 @@
|
||||
name="checkmark-circle-outline"
|
||||
></ion-icon>
|
||||
<ion-card-title>Setup Complete</ion-card-title>
|
||||
<ion-card-subtitle
|
||||
><b
|
||||
>You have successully claimed your Embassy!</b
|
||||
></ion-card-subtitle
|
||||
>
|
||||
<br />
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<br />
|
||||
<ng-template
|
||||
[ngIf]="recoverySource && recoverySource.type === 'disk'"
|
||||
<br />
|
||||
<h2
|
||||
*ngIf="recoverySource && recoverySource.type === 'disk'"
|
||||
class="ion-padding-bottom"
|
||||
>
|
||||
<h2>You can now safely unplug your backup drive.</h2>
|
||||
</ng-template>
|
||||
<h2>
|
||||
You have successully claimed your Embassy! You can now access your
|
||||
device using the methods below.
|
||||
You can now safely unplug your backup drive.
|
||||
</h2>
|
||||
<h2 style="font-weight: bold">
|
||||
Access your Embassy using the methods below. You should
|
||||
<a (click)="download()" class="inline">
|
||||
download this page <ion-icon name="download-outline"></ion-icon>
|
||||
</a>
|
||||
for your records.
|
||||
</h2>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<b>Note:</b> embassy.local was for setup purposes only, it will no
|
||||
longer work.
|
||||
</p>
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- LAN Instructions -->
|
||||
<div (click)="toggleLan()" class="toggle-label">
|
||||
<h2>From Home (LAN)</h2>
|
||||
<ion-icon
|
||||
name="chevron-down-outline"
|
||||
[ngStyle]="{
|
||||
'transform': lanOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
'transition': 'transform 0.4s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<h1><b>From Home (LAN)</b></h1>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'overflow' : 'hidden',
|
||||
'max-height': lanOpen ? '500px' : '0px',
|
||||
'transition': 'max-height 0.4s ease-out'
|
||||
}"
|
||||
>
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>
|
||||
Visit the address below when you are conncted to the same WiFi
|
||||
or Local Area Network (LAN) as your Embassy:
|
||||
</p>
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>
|
||||
Visit the address below when you are conncted to the same WiFi
|
||||
or Local Area Network (LAN) as your Embassy:
|
||||
</p>
|
||||
|
||||
<ion-item
|
||||
lines="none"
|
||||
color="dark"
|
||||
class="ion-padding-top ion-padding-bottom"
|
||||
>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code
|
||||
><ion-text color="light"
|
||||
><b>{{ lanAddress }}</b></ion-text
|
||||
></code
|
||||
>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
(click)="copy(lanAddress)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<p>
|
||||
<b>Important!</b>
|
||||
Your browser will warn you that the website is untrusted. You
|
||||
can bypass this warning on most browsers. The warning will go
|
||||
away after you
|
||||
<a
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<b>download and trust</b>
|
||||
</a>
|
||||
your Embassy's Root Certificate Authority.
|
||||
</p>
|
||||
|
||||
<ion-button
|
||||
style="margin-top: 24px; margin-bottom: 24px"
|
||||
color="light"
|
||||
(click)="installCert()"
|
||||
>
|
||||
Download Root CA
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<!-- Tor Instructions -->
|
||||
<div (click)="toggleTor()" class="toggle-label">
|
||||
<h2>On The Go (Tor)</h2>
|
||||
<ion-icon
|
||||
name="chevron-down-outline"
|
||||
[ngStyle]="{
|
||||
'transform': torOpen ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
'transition': 'transform 0.4s ease-out'
|
||||
}"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<p>
|
||||
<b>Note:</b> embassy.local was for setup purposes only, it will
|
||||
no longer work.
|
||||
</p>
|
||||
|
||||
<div
|
||||
[ngStyle]="{
|
||||
'overflow' : 'hidden',
|
||||
'max-height': torOpen ? '500px' : '0px',
|
||||
'transition': 'max-height 0.4s ease-out'
|
||||
}"
|
||||
>
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>Visit the address below when you are away from home:</p>
|
||||
|
||||
<ion-item
|
||||
lines="none"
|
||||
color="dark"
|
||||
class="ion-padding-top ion-padding-bottom"
|
||||
>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code
|
||||
><ion-text color="light"
|
||||
><b>{{ torAddress }}</b></ion-text
|
||||
></code
|
||||
>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
(click)="copy(torAddress)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<p>
|
||||
<b>Important!</b>
|
||||
This address will only work from a
|
||||
<a
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<b>Tor-enabled browser</b> </a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="padding-bottom: 24px; border-bottom: solid 1px"></div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<div class="ion-text-center ion-padding-top">
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
color="primary"
|
||||
strong
|
||||
(click)="download()"
|
||||
<ion-item
|
||||
lines="none"
|
||||
color="dark"
|
||||
class="ion-padding-top ion-padding-bottom"
|
||||
>
|
||||
Download this page
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code
|
||||
><ion-text color="light"
|
||||
><b>{{ lanAddress }}</b></ion-text
|
||||
></code
|
||||
>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
[href]="lanAddress"
|
||||
target="_blank"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
(click)="copy(lanAddress)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<p>
|
||||
<b>Important!</b>
|
||||
Your browser will warn you that the website is untrusted. You
|
||||
can bypass this warning on most browsers. The warning will go
|
||||
away after you
|
||||
<a
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="inline"
|
||||
>
|
||||
follow the instructions
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
</a>
|
||||
to downlaod and trust your Embassy's Root Certificate Authority.
|
||||
</p>
|
||||
|
||||
<ion-button style="margin-top: 24px" (click)="installCert()">
|
||||
Download Root CA
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<!-- Tor Instructions -->
|
||||
<h1><b>On The Go (Tor)</b></h1>
|
||||
|
||||
<div class="ion-padding ion-text-start">
|
||||
<p>Visit the address below when you are away from home:</p>
|
||||
|
||||
<ion-item
|
||||
lines="none"
|
||||
color="dark"
|
||||
class="ion-padding-top ion-padding-bottom"
|
||||
>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<code
|
||||
><ion-text color="light"
|
||||
><b>{{ torAddress }}</b></ion-text
|
||||
></code
|
||||
>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
color="light"
|
||||
fill="clear"
|
||||
(click)="copy(torAddress)"
|
||||
>
|
||||
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<p>
|
||||
<b>Important!</b>
|
||||
This address will only work from a
|
||||
<a
|
||||
href="https://start9.com/latest/user-manual/connecting/connecting-tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="inline"
|
||||
>
|
||||
Tor-enabled browser
|
||||
<ion-icon name="open-outline"></ion-icon> </a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
|
||||
|
||||
@@ -4,26 +4,12 @@ p {
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
padding: 24px 0 8px 0;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
text-align: right;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
margin-bottom: 48px;
|
||||
padding-bottom: 48px;
|
||||
border-bottom: solid 1px;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { StateService } from 'src/app/services/state.service'
|
||||
})
|
||||
export class SuccessPage {
|
||||
@Output() onDownload = new EventEmitter()
|
||||
torOpen = false
|
||||
lanOpen = false
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@@ -69,14 +67,6 @@ export class SuccessPage {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
toggleTor() {
|
||||
this.torOpen = !this.torOpen
|
||||
}
|
||||
|
||||
toggleLan() {
|
||||
this.lanOpen = !this.lanOpen
|
||||
}
|
||||
|
||||
installCert() {
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
export abstract class ApiService {
|
||||
// unencrypted
|
||||
abstract getStatus(): Promise<GetStatusRes> // setup.status
|
||||
abstract getSecret(): Promise<string> // setup.get-secret
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract set02XDrive(logicalname: string): Promise<void> // setup.recovery.v2.set
|
||||
abstract getRecoveryStatus(): Promise<RecoveryStatusRes> // setup.recovery.status
|
||||
|
||||
// encrypted
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<EmbassyOSRecoveryInfo> // setup.cifs.verify
|
||||
abstract verifyProductKey(): Promise<void> // echo - throws error if invalid
|
||||
abstract importDrive(importInfo: ImportDriveReq): Promise<SetupEmbassyRes> // setup.attach
|
||||
abstract setupEmbassy(setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupComplete(): Promise<SetupEmbassyRes> // setup.complete
|
||||
}
|
||||
|
||||
export type GetStatusRes = {
|
||||
'product-key': boolean
|
||||
migrating: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpService } from '@start9labs/shared'
|
||||
import {
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
@@ -13,43 +18,71 @@ import {
|
||||
SetupEmbassyRes,
|
||||
} from './api.service'
|
||||
import { RPCEncryptedService } from '../rpc-encrypted.service'
|
||||
import * as jose from 'node-jose'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService extends ApiService {
|
||||
export class LiveApiService implements ApiService {
|
||||
constructor(
|
||||
private readonly unencrypted: HttpService,
|
||||
private readonly encrypted: RPCEncryptedService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
) {}
|
||||
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus() {
|
||||
return this.unencrypted.rpcRequest<GetStatusRes>({
|
||||
return this.rpcRequest<GetStatusRes>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to update the secret, which means that we will call in clearnet the
|
||||
* getSecret, and all the information is never in the clear, and only public
|
||||
* information is sent across the network. We don't want to expose that we do
|
||||
* this wil all public/private key, which means that there is no information loss
|
||||
* through the network.
|
||||
*/
|
||||
async getSecret() {
|
||||
const keystore = jose.JWK.createKeyStore()
|
||||
const key = await keystore.generate('EC', 'P-256')
|
||||
// const { privateKey, publicKey } =
|
||||
|
||||
// jose.generateKeyPair('ECDH-ES', {
|
||||
// extractable: true,
|
||||
// })
|
||||
console.log({ publicKey: key.toJSON() })
|
||||
const response: string = await this.rpcRequest({
|
||||
method: 'setup.get-secret',
|
||||
params: { pubkey: key.toJSON() },
|
||||
})
|
||||
|
||||
// const { plaintext } = await jose.compactDecrypt(response, privateKey)
|
||||
const decrypted = await jose.JWE.createDecrypt(key).decrypt(response)
|
||||
const decoded = new TextDecoder().decode(decrypted.plaintext)
|
||||
console.log({ decoded })
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
return this.unencrypted.rpcRequest<DiskListResponse>({
|
||||
return this.rpcRequest<DiskListResponse>({
|
||||
method: 'setup.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async set02XDrive(logicalname: string) {
|
||||
return this.unencrypted.rpcRequest<void>({
|
||||
return this.rpcRequest<void>({
|
||||
method: 'setup.recovery.v2.set',
|
||||
params: { logicalname },
|
||||
})
|
||||
}
|
||||
|
||||
async getRecoveryStatus() {
|
||||
return this.unencrypted.rpcRequest<RecoveryStatusRes>({
|
||||
return this.rpcRequest<RecoveryStatusRes>({
|
||||
method: 'setup.recovery.status',
|
||||
params: {},
|
||||
})
|
||||
@@ -65,13 +98,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
return this.encrypted.rpcRequest<void>({
|
||||
method: 'echo',
|
||||
params: { message: 'hello' },
|
||||
})
|
||||
}
|
||||
|
||||
async importDrive(params: ImportDriveReq) {
|
||||
const res = await this.encrypted.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.attach',
|
||||
@@ -113,6 +139,18 @@ export class LiveApiService extends ApiService {
|
||||
'root-ca': btoa(res['root-ca']),
|
||||
}
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.unencrypted.rpcRequest<T>(opts)
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
throw new RpcError(rpcRes.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
|
||||
@@ -12,21 +12,29 @@ let tries = 0
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
export class MockApiService implements ApiService {
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus() {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
'product-key': true,
|
||||
migrating: false,
|
||||
}
|
||||
}
|
||||
|
||||
async getSecret() {
|
||||
await pauseFor(1000)
|
||||
|
||||
const ascii = 'thisisasecret'
|
||||
|
||||
const arr1 = []
|
||||
for (let n = 0, l = ascii.length; n < l; n++) {
|
||||
var hex = Number(ascii.charCodeAt(n)).toString(16)
|
||||
arr1.push(hex)
|
||||
}
|
||||
return arr1.join('')
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
@@ -84,11 +92,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyProductKey() {
|
||||
await pauseFor(1000)
|
||||
return
|
||||
}
|
||||
|
||||
async importDrive(params: ImportDriveReq) {
|
||||
await pauseFor(3000)
|
||||
return setupRes
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RPCEncryptedService {
|
||||
productKey?: string
|
||||
secret?: string
|
||||
|
||||
constructor(private readonly http: HttpService) {}
|
||||
|
||||
async rpcRequest<T>(opts: Omit<RPCOptions, 'timeout'>): Promise<T> {
|
||||
const encryptedBody = await AES_CTR.encryptPbkdf2(
|
||||
this.productKey || '',
|
||||
this.secret || '',
|
||||
encodeUtf8(JSON.stringify(opts)),
|
||||
)
|
||||
|
||||
@@ -36,7 +36,11 @@ export class RPCEncryptedService {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(body => AES_CTR.decryptPbkdf2(this.productKey || '', body))
|
||||
.then(res => AES_CTR.decryptPbkdf2(this.secret || '', res.body))
|
||||
.then(x => {
|
||||
console.log(`Network: ${x}`)
|
||||
return x
|
||||
})
|
||||
.then(res => JSON.parse(res))
|
||||
.catch(e => {
|
||||
if (!e.status && !e.statusText) {
|
||||
|
||||
@@ -11,9 +11,6 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService {
|
||||
hasProductKey = false
|
||||
isMigrating = false
|
||||
|
||||
polling = false
|
||||
embassyLoaded = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user