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,98 @@
|
||||
export abstract class ApiService {
|
||||
// unencrypted
|
||||
abstract getStatus (): Promise<GetStatusRes> // setup.status
|
||||
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 (guid: string): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupEmbassy (setupInfo: SetupEmbassyReq): Promise<SetupEmbassyRes> // setup.execute
|
||||
abstract setupComplete (): Promise<void> // setup.complete
|
||||
}
|
||||
|
||||
export interface GetStatusRes {
|
||||
'product-key': boolean
|
||||
migrating: boolean
|
||||
}
|
||||
|
||||
export interface SetupEmbassyReq {
|
||||
'embassy-logicalname': string
|
||||
'embassy-password': string
|
||||
'recovery-source': CifsRecoverySource | DiskRecoverySource | null
|
||||
'recovery-password': string | null
|
||||
}
|
||||
|
||||
export interface SetupEmbassyRes {
|
||||
'tor-address': string
|
||||
'lan-address': string
|
||||
'root-ca': string
|
||||
}
|
||||
|
||||
export interface EmbassyOSRecoveryInfo {
|
||||
version: string
|
||||
full: boolean
|
||||
'password-hash': string | null
|
||||
'wrapped-key': string | null
|
||||
}
|
||||
|
||||
export interface DiskListResponse {
|
||||
disks: DiskInfo[]
|
||||
reconnect: string[]
|
||||
}
|
||||
|
||||
export interface DiskBackupTarget {
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export interface CifsBackupTarget {
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null
|
||||
}
|
||||
|
||||
export interface DiskRecoverySource {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export interface CifsRecoverySource {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
|
||||
export interface DiskInfo {
|
||||
logicalname: string,
|
||||
vendor: string | null,
|
||||
model: string | null,
|
||||
partitions: PartitionInfo[],
|
||||
capacity: number,
|
||||
guid: string | null, // cant back up if guid exists
|
||||
}
|
||||
|
||||
export interface RecoveryStatusRes {
|
||||
'bytes-transferred': number
|
||||
'total-bytes': number
|
||||
complete: boolean
|
||||
}
|
||||
|
||||
export interface PartitionInfo {
|
||||
logicalname: string,
|
||||
label: string | null,
|
||||
capacity: number,
|
||||
used: number | null,
|
||||
'embassy-os': EmbassyOSRecoveryInfo | null,
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Observable } from 'rxjs'
|
||||
import * as aesjs from 'aes-js'
|
||||
import * as pbkdf2 from 'pbkdf2'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class HttpService {
|
||||
fullUrl: string
|
||||
productKey: string
|
||||
|
||||
constructor (
|
||||
private readonly http: HttpClient,
|
||||
) {
|
||||
const port = window.location.port
|
||||
this.fullUrl = `${window.location.protocol}//${window.location.hostname}:${port}/rpc/v1`
|
||||
}
|
||||
|
||||
async rpcRequest<T> (body: RPCOptions, encrypted = true): Promise<T> {
|
||||
|
||||
const httpOpts = {
|
||||
method: Method.POST,
|
||||
body,
|
||||
url: this.fullUrl,
|
||||
}
|
||||
|
||||
let res: RPCResponse<T>
|
||||
|
||||
if (encrypted) {
|
||||
res = await this.encryptedHttpRequest<RPCResponse<T>>(httpOpts)
|
||||
} else {
|
||||
res = await this.httpRequest<RPCResponse<T>>(httpOpts)
|
||||
}
|
||||
|
||||
if (isRpcError(res)) {
|
||||
console.error('RPC ERROR: ', res)
|
||||
throw new RpcError(res.error)
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) return res.result
|
||||
}
|
||||
|
||||
async encryptedHttpRequest<T> (httpOpts: {
|
||||
body: RPCOptions;
|
||||
url: string;
|
||||
}): Promise<T> {
|
||||
|
||||
const urlIsRelative = httpOpts.url.startsWith('/')
|
||||
const url = urlIsRelative ?
|
||||
this.fullUrl + httpOpts.url :
|
||||
httpOpts.url
|
||||
|
||||
const encryptedBody = await AES_CTR.encryptPbkdf2(this.productKey, encodeUtf8(JSON.stringify(httpOpts.body)))
|
||||
const options = {
|
||||
responseType: 'arraybuffer',
|
||||
body: encryptedBody.buffer,
|
||||
observe: 'events',
|
||||
reportProgress: false,
|
||||
|
||||
headers: {
|
||||
'Content-Encoding': 'aesctr256',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
} as any
|
||||
|
||||
const req = this.http.post(url, options.body, options)
|
||||
|
||||
return (req)
|
||||
.toPromise()
|
||||
.then(res => AES_CTR.decryptPbkdf2(this.productKey, (res as any).body as ArrayBuffer))
|
||||
.then(res => JSON.parse(res))
|
||||
.catch(e => {
|
||||
if (!e.status && !e.statusText) {
|
||||
throw new EncryptionError(e)
|
||||
} else {
|
||||
throw new HttpError(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async httpRequest<T> (httpOpts: {
|
||||
body: RPCOptions;
|
||||
url: string;
|
||||
}): Promise<T> {
|
||||
const urlIsRelative = httpOpts.url.startsWith('/')
|
||||
const url = urlIsRelative ?
|
||||
this.fullUrl + httpOpts.url :
|
||||
httpOpts.url
|
||||
|
||||
const options = {
|
||||
responseType: 'json',
|
||||
body: httpOpts.body,
|
||||
observe: 'events',
|
||||
reportProgress: false,
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
} as any
|
||||
|
||||
const req: Observable<{ body: T }> = this.http.post(url, httpOpts.body, options) as any
|
||||
|
||||
return (req)
|
||||
.toPromise()
|
||||
.then(res => res.body)
|
||||
.catch(e => { throw new HttpError(e) })
|
||||
}
|
||||
}
|
||||
|
||||
function RpcError (e: RPCError['error']): void {
|
||||
const { code, message, data } = e
|
||||
|
||||
this.code = code
|
||||
this.message = message
|
||||
|
||||
if (typeof data !== 'string') {
|
||||
this.details = data.details
|
||||
} else {
|
||||
this.details = data
|
||||
}
|
||||
}
|
||||
|
||||
function HttpError (e: HttpErrorResponse): void {
|
||||
const { status, statusText } = e
|
||||
|
||||
this.code = status
|
||||
this.message = statusText
|
||||
this.details = null
|
||||
}
|
||||
|
||||
function EncryptionError (e: HttpErrorResponse): void {
|
||||
this.code = null
|
||||
this.message = 'Invalid Key'
|
||||
this.details = null
|
||||
}
|
||||
|
||||
function isRpcError<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {
|
||||
return !!(arg as any).error
|
||||
}
|
||||
|
||||
function isRpcSuccess<Error, Result> (arg: { error: Error } | { result: Result }): arg is { result: Result } {
|
||||
return !!(arg as any).result
|
||||
}
|
||||
|
||||
export enum Method {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
PATCH = 'PATCH',
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
export interface RPCOptions {
|
||||
method: string
|
||||
params?: {
|
||||
[param: string]: string | number | boolean | object | string[] | number[];
|
||||
}
|
||||
}
|
||||
|
||||
interface RPCBase {
|
||||
jsonrpc: '2.0'
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface RPCRequest<T> extends RPCBase {
|
||||
method: string
|
||||
params?: T
|
||||
}
|
||||
|
||||
export interface RPCSuccess<T> extends RPCBase {
|
||||
result: T
|
||||
}
|
||||
|
||||
export interface RPCError extends RPCBase {
|
||||
error: {
|
||||
code: number,
|
||||
message: string
|
||||
data?: {
|
||||
details: string
|
||||
} | string
|
||||
}
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = RPCSuccess<T> | RPCError
|
||||
|
||||
type HttpError = HttpErrorResponse & { error: { code: string, message: string } }
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
headers?: HttpHeaders | {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: HttpParams | {
|
||||
[param: string]: string | string[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
withCredentials?: boolean
|
||||
body?: any
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type AES_CTR = {
|
||||
encryptPbkdf2: (secretKey: string, messageBuffer: Uint8Array) => Promise<Uint8Array>
|
||||
decryptPbkdf2: (secretKey, arr: ArrayBuffer) => Promise<string>
|
||||
}
|
||||
|
||||
export const AES_CTR: AES_CTR = {
|
||||
encryptPbkdf2: async (secretKey: string, messageBuffer: Uint8Array) => {
|
||||
const salt = window.crypto.getRandomValues(new Uint8Array(16))
|
||||
const counter = window.crypto.getRandomValues(new Uint8Array(16))
|
||||
|
||||
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
|
||||
|
||||
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter))
|
||||
const encryptedBytes = aesCtr.encrypt(messageBuffer)
|
||||
return new Uint8Array([...counter, ...salt, ...encryptedBytes])
|
||||
},
|
||||
decryptPbkdf2: async (secretKey: string, arr: ArrayBuffer) => {
|
||||
const buff = new Uint8Array(arr)
|
||||
const counter = buff.slice(0, 16)
|
||||
const salt = buff.slice(16, 32)
|
||||
|
||||
const cipher = buff.slice(32)
|
||||
const key = pbkdf2.pbkdf2Sync(secretKey, salt, 1000, 256 / 8, 'sha256')
|
||||
|
||||
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(counter))
|
||||
const decryptedBytes = aesCtr.decrypt(cipher)
|
||||
|
||||
return aesjs.utils.utf8.fromBytes(decryptedBytes)
|
||||
},
|
||||
}
|
||||
|
||||
export const encode16 = (buffer: Uint8Array) => buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
|
||||
export const decode16 = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)))
|
||||
|
||||
export function encodeUtf8 (str: string): Uint8Array {
|
||||
const encoder = new TextEncoder()
|
||||
return encoder.encode(str)
|
||||
}
|
||||
|
||||
export function decodeUtf8 (arr: Uint8Array): string {
|
||||
return new TextDecoder().decode(arr)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ApiService, CifsRecoverySource, DiskInfo, DiskListResponse, DiskRecoverySource, EmbassyOSRecoveryInfo, GetStatusRes, RecoveryStatusRes, SetupEmbassyReq, SetupEmbassyRes } from './api.service'
|
||||
import { HttpService } from './http.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService extends ApiService {
|
||||
|
||||
constructor (
|
||||
private readonly http: HttpService,
|
||||
) { super() }
|
||||
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus () {
|
||||
return this.http.rpcRequest<GetStatusRes>({
|
||||
method: 'setup.status',
|
||||
params: { },
|
||||
}, false)
|
||||
}
|
||||
|
||||
async getDrives () {
|
||||
return this.http.rpcRequest<DiskListResponse>({
|
||||
method: 'setup.disk.list',
|
||||
params: { },
|
||||
}, false)
|
||||
}
|
||||
|
||||
async set02XDrive (logicalname) {
|
||||
return this.http.rpcRequest<void>({
|
||||
method: 'setup.recovery.v2.set',
|
||||
params: { logicalname },
|
||||
}, false)
|
||||
}
|
||||
|
||||
async getRecoveryStatus () {
|
||||
return this.http.rpcRequest<RecoveryStatusRes>({
|
||||
method: 'setup.recovery.status',
|
||||
params: { },
|
||||
}, false)
|
||||
}
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs (source: CifsRecoverySource) {
|
||||
source.path = source.path.replace('/\\/g', '/')
|
||||
return this.http.rpcRequest<EmbassyOSRecoveryInfo>({
|
||||
method: 'setup.cifs.verify',
|
||||
params: source as any,
|
||||
})
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
return this.http.rpcRequest<void>({
|
||||
method: 'echo',
|
||||
params: { 'message': 'hello' },
|
||||
})
|
||||
}
|
||||
|
||||
async importDrive (guid: string) {
|
||||
const res = await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.attach',
|
||||
params: { guid },
|
||||
})
|
||||
|
||||
return {
|
||||
...res,
|
||||
'root-ca': btoa(res['root-ca']),
|
||||
}
|
||||
}
|
||||
|
||||
async setupEmbassy (setupInfo: SetupEmbassyReq) {
|
||||
if (isCifsSource(setupInfo['recovery-source'])) {
|
||||
setupInfo['recovery-source'].path = setupInfo['recovery-source'].path.replace('/\\/g', '/')
|
||||
}
|
||||
|
||||
const res = await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo as any,
|
||||
})
|
||||
|
||||
return {
|
||||
...res,
|
||||
'root-ca': btoa(res['root-ca']),
|
||||
}
|
||||
}
|
||||
|
||||
async setupComplete () {
|
||||
await this.http.rpcRequest<SetupEmbassyRes>({
|
||||
method: 'setup.complete',
|
||||
params: { },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function isCifsSource (source: CifsRecoverySource | DiskRecoverySource | undefined): source is CifsRecoverySource {
|
||||
return !!(source as CifsRecoverySource)?.hostname
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { ApiService, CifsRecoverySource, SetupEmbassyReq } from './api.service'
|
||||
|
||||
let tries = 0
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
}
|
||||
|
||||
// ** UNENCRYPTED **
|
||||
|
||||
async getStatus () {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
'product-key': true,
|
||||
migrating: false,
|
||||
}
|
||||
}
|
||||
|
||||
async getDrives () {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
disks: [
|
||||
{
|
||||
logicalname: 'abcd',
|
||||
vendor: 'Samsung',
|
||||
model: 'T5',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pabcd',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.2.17',
|
||||
full: true,
|
||||
'password-hash': null,
|
||||
'wrapped-key': null,
|
||||
},
|
||||
}
|
||||
],
|
||||
capacity: 123456789123,
|
||||
guid: 'uuid-uuid-uuid-uuid',
|
||||
}
|
||||
],
|
||||
reconnect: [],
|
||||
}
|
||||
}
|
||||
|
||||
async set02XDrive () {
|
||||
await pauseFor(1000)
|
||||
return
|
||||
}
|
||||
|
||||
async getRecoveryStatus () {
|
||||
tries = Math.min(tries + 1, 4)
|
||||
return {
|
||||
'bytes-transferred': tries,
|
||||
'total-bytes': 4,
|
||||
complete: tries === 4,
|
||||
}
|
||||
}
|
||||
|
||||
// ** ENCRYPTED **
|
||||
|
||||
async verifyCifs (params: CifsRecoverySource) {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': '',
|
||||
}
|
||||
}
|
||||
|
||||
async verifyProductKey () {
|
||||
await pauseFor(1000)
|
||||
return
|
||||
}
|
||||
|
||||
async importDrive (guid: string) {
|
||||
await pauseFor(3000)
|
||||
return setupRes
|
||||
}
|
||||
|
||||
async setupEmbassy (setupInfo: SetupEmbassyReq) {
|
||||
await pauseFor(3000)
|
||||
return setupRes
|
||||
}
|
||||
|
||||
async setupComplete () {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
|
||||
const rootCA =
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIRAIIuOarlQETlUQEOZJGZYdIwDQYJKoZIhvcNAQELBQAw
|
||||
bTELMAkGA1UEBhMCVVMxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEOMAwGA1UECwwF
|
||||
U2FsZXMxCzAJBgNVBAgMAldBMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20xEDAO
|
||||
BgNVBAcMB1NlYXR0bGUwHhcNMjEwMzA4MTU0NjI3WhcNMjIwMzA4MTY0NjI3WjBt
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQ4wDAYDVQQLDAVT
|
||||
YWxlczELMAkGA1UECAwCV0ExGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTEQMA4G
|
||||
A1UEBwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMP7
|
||||
t5AKFZQ7abqkeyUjsBVIWRa9tCh8oge9u/LvCbxU738G4jssT+Oud3WMajIjuNow
|
||||
cpc+0Q/e42ULO/6gTNrTs6OCOo9lV6G0Dprf/e91DWoKgPatem/pUjNyraifHZfu
|
||||
b5mLHCfahjWXUQtc/sjmDQaZRK3Kar6ljlUBE/Le9NEyOAIkSLPzDtW8LXm4iwcU
|
||||
BZrb828rKd1Aw9oI1+3bfzB6xXmzZxc5RLXveOCEhKGD32jKZ/RNFSC8AZAwJe+x
|
||||
bTsys/lUOYFTuT8Bn0TGxR8x7Y4H75+F9BavY3v+WkLj4M+olN9dMR7Et9FMt4u4
|
||||
YRokv5zp8zIb5iTne1kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
|
||||
FgQUaW3+r328uTLokog2TklmoBK+yt4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAXjd/7UZ8RDE+PLWSDNGQdLemOBTcawF+tK+PzA4Evlmn9VuNc
|
||||
g+x3oZvVZSDQBANUz0b9oPeo54aE38dW1zQm2qfTab8822aqeWMLyJ1dMsAgqYX2
|
||||
t9+u6w3NzRCw8Pvz18V69+dFE5AeXmNP0Z5/gdz8H/NSpctjlzopbScRZKCSlPid
|
||||
Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX
|
||||
2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W
|
||||
YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const setupRes = {
|
||||
'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
'lan-address': 'https://embassy-abcdefgh.local',
|
||||
'root-ca': btoa(rootCA),
|
||||
}
|
||||
|
||||
const disks = [
|
||||
{
|
||||
vendor: 'Samsung',
|
||||
model: 'SATA',
|
||||
logicalname: '/dev/sda',
|
||||
guid: 'theguid',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'sda1',
|
||||
label: 'label 1',
|
||||
capacity: 100000,
|
||||
used: 200.1255312,
|
||||
'embassy-os': null,
|
||||
},
|
||||
{
|
||||
logicalname: 'sda2',
|
||||
label: 'label 2',
|
||||
capacity: 50000,
|
||||
used: 200.1255312,
|
||||
'embassy-os': null,
|
||||
},
|
||||
],
|
||||
capacity: 150000,
|
||||
},
|
||||
{
|
||||
vendor: 'Samsung',
|
||||
model: null,
|
||||
logicalname: 'dev/sdb',
|
||||
partitions: [],
|
||||
capacity: 34359738369,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
vendor: 'Crucial',
|
||||
model: 'MX500',
|
||||
logicalname: 'dev/sdc',
|
||||
guid: null,
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'sdc1',
|
||||
label: 'label 1',
|
||||
capacity: 0,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.3.3',
|
||||
full: true,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: 'sdc1MOCKTESTER',
|
||||
label: 'label 1',
|
||||
capacity: 0,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.3.6',
|
||||
full: true,
|
||||
// password is 'asdfasdf'
|
||||
'password-hash': '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
logicalname: 'sdc1',
|
||||
label: 'label 1',
|
||||
capacity: 0,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.3.3',
|
||||
full: false,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
capacity: 100000,
|
||||
},
|
||||
{
|
||||
vendor: 'Sandisk',
|
||||
model: null,
|
||||
logicalname: '/dev/sdd',
|
||||
guid: null,
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'sdd1',
|
||||
label: null,
|
||||
capacity: 10000,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.2.7',
|
||||
full: true,
|
||||
'password-hash': 'asdfasdfasdf',
|
||||
'wrapped-key': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
capacity: 10000,
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ToastController } from '@ionic/angular'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorToastService {
|
||||
private toast: HTMLIonToastElement
|
||||
|
||||
constructor (
|
||||
private readonly toastCtrl: ToastController,
|
||||
) { }
|
||||
|
||||
async present (message: string): Promise<void> {
|
||||
if (this.toast) return
|
||||
|
||||
this.toast = await this.toastCtrl.create({
|
||||
header: 'Error',
|
||||
message,
|
||||
duration: 0,
|
||||
position: 'top',
|
||||
cssClass: 'error-toast',
|
||||
animated: true,
|
||||
buttons: [
|
||||
{
|
||||
side: 'end',
|
||||
icon: 'close',
|
||||
handler: () => {
|
||||
this.dismiss()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await this.toast.present()
|
||||
}
|
||||
|
||||
async dismiss (): Promise<void> {
|
||||
if (this.toast) {
|
||||
await this.toast.dismiss()
|
||||
this.toast = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ErrorHandler, Injectable } from '@angular/core'
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
|
||||
handleError (e: any): void {
|
||||
console.error(e)
|
||||
const chunkFailedMessage = /Loading chunk [\d]+ failed/
|
||||
|
||||
if (chunkFailedMessage.test(e.message)) {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService, CifsRecoverySource, DiskRecoverySource } from './api/api.service'
|
||||
import { ErrorToastService } from './error-toast.service'
|
||||
import { pauseFor } from '../util/misc.util'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StateService {
|
||||
hasProductKey: boolean
|
||||
isMigrating: boolean
|
||||
|
||||
polling = false
|
||||
embassyLoaded = false
|
||||
|
||||
recoverySource: CifsRecoverySource | DiskRecoverySource
|
||||
recoveryPassword: string
|
||||
|
||||
dataTransferProgress: { bytesTransferred: number, totalBytes: number, complete: boolean } | null
|
||||
dataProgress = 0
|
||||
dataCompletionSubject = new BehaviorSubject(false)
|
||||
|
||||
torAddress: string
|
||||
lanAddress: string
|
||||
cert: string
|
||||
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly errorToastService: ErrorToastService,
|
||||
) { }
|
||||
|
||||
async pollDataTransferProgress () {
|
||||
this.polling = true
|
||||
await pauseFor(500)
|
||||
|
||||
if (
|
||||
this.dataTransferProgress?.complete
|
||||
) {
|
||||
this.dataCompletionSubject.next(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let progress
|
||||
try {
|
||||
progress = await this.apiService.getRecoveryStatus()
|
||||
} catch (e) {
|
||||
this.errorToastService.present(`${e.message}: ${e.details}.\nRestart Embassy to try again.`)
|
||||
}
|
||||
if (progress) {
|
||||
this.dataTransferProgress = {
|
||||
bytesTransferred: progress['bytes-transferred'],
|
||||
totalBytes: progress['total-bytes'],
|
||||
complete: progress.complete,
|
||||
}
|
||||
if (this.dataTransferProgress.totalBytes) {
|
||||
this.dataProgress = this.dataTransferProgress.bytesTransferred / this.dataTransferProgress.totalBytes
|
||||
}
|
||||
}
|
||||
setTimeout(() => this.pollDataTransferProgress(), 0) // prevent call stack from growing
|
||||
}
|
||||
|
||||
async importDrive (guid: string): Promise<void> {
|
||||
const ret = await this.apiService.importDrive(guid)
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
this.cert = ret['root-ca']
|
||||
}
|
||||
|
||||
async setupEmbassy (storageLogicalname: string, password: string): Promise<void> {
|
||||
const ret = await this.apiService.setupEmbassy({
|
||||
'embassy-logicalname': storageLogicalname,
|
||||
'embassy-password': password,
|
||||
'recovery-source': this.recoverySource || null,
|
||||
'recovery-password': this.recoveryPassword || null,
|
||||
})
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address']
|
||||
this.cert = ret['root-ca']
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user