mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Feature/consolidate setup (#3092)
* start consolidating * add start-cli flash-os * combine install and setup and refactor all * use http * undo mock * fix translation * translations * use dialogservice wrapper * better ST messaging on setup * only warn on update if breakages (#3097) * finish setup wizard and ui language-keyboard feature * fix typo * wip: localization * remove start-tunnel readme * switch to posix strings for language internal * revert mock * translate backend strings * fix missing about text * help text for args * feat: add "Add new gateway" option (#3098) * feat: add "Add new gateway" option * Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add translation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Matt Hill <mattnine@protonmail.com> * fix dns selection * keyboard keymap also * ability to shutdown after install * revert mock * working setup flow + manifest localization * (mostly) redundant localization on frontend * version bump * omit live medium from disk list and better space management * ignore missing package archive on 035 migration * fix device migration * add i18n helper to sdk * fix install over 0.3.5.1 * fix grub config --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,50 +1,65 @@
|
||||
import * as jose from 'node-jose'
|
||||
import {
|
||||
DiskInfo,
|
||||
DiskListResponse,
|
||||
FollowLogsRes,
|
||||
PartitionInfo,
|
||||
FullKeyboard,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
import {
|
||||
SetupStatusRes,
|
||||
InstallOsParams,
|
||||
InstallOsRes,
|
||||
AttachParams,
|
||||
SetupExecuteParams,
|
||||
SetupCompleteRes,
|
||||
EchoReq,
|
||||
} from '../types'
|
||||
|
||||
export abstract class ApiService {
|
||||
pubkey?: jose.JWK.Key
|
||||
|
||||
abstract getStatus(): Promise<T.SetupStatusRes | null> // setup.status
|
||||
// echo
|
||||
abstract echo(params: EchoReq, url: string): Promise<string>
|
||||
|
||||
// Status & Setup
|
||||
abstract getStatus(): Promise<SetupStatusRes> // setup.status
|
||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract setKeyboard(params: FullKeyboard): Promise<null> // setup.set-keyboard
|
||||
abstract setLanguage(params: SetLanguageParams): Promise<null> // setup.set-language
|
||||
|
||||
// Install
|
||||
abstract getDisks(): Promise<DiskInfo[]> // setup.disk.list
|
||||
abstract installOs(params: InstallOsParams): Promise<InstallOsRes> // setup.install-os
|
||||
|
||||
// Setup execution
|
||||
abstract attach(params: AttachParams): Promise<T.SetupProgress> // setup.attach
|
||||
abstract execute(params: SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
|
||||
// Recovery helpers
|
||||
abstract verifyCifs(
|
||||
cifs: T.VerifyCifsParams,
|
||||
): Promise<Record<string, StartOSDiskInfo>> // setup.cifs.verify
|
||||
abstract attach(importInfo: T.AttachParams): Promise<T.SetupProgress> // setup.attach
|
||||
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
|
||||
// Completion
|
||||
abstract complete(): Promise<SetupCompleteRes> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract shutdown(): Promise<void> // setup.shutdown
|
||||
|
||||
// Logs & Progress
|
||||
abstract initFollowLogs(): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract restart(): Promise<void> // setup.restart
|
||||
abstract openWebsocket$<T>(guid: string): Observable<T>
|
||||
|
||||
// Restart (for error recovery)
|
||||
abstract restart(): Promise<void> // setup.restart
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
|
||||
if (!this.pubkey) throw new Error('No pubkey found!')
|
||||
const encrypted = await jose.JWE.createEncrypt(this.pubkey!)
|
||||
.update(toEncrypt)
|
||||
.final()
|
||||
return {
|
||||
encrypted,
|
||||
}
|
||||
return { encrypted }
|
||||
}
|
||||
}
|
||||
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
export type StartOSDiskInfoWithId = StartOSDiskInfo & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type StartOSDiskInfoFull = StartOSDiskInfoWithId & {
|
||||
partition: PartitionInfo
|
||||
drive: DiskInfo
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Inject, Injectable, DOCUMENT } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
DiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsRes,
|
||||
FullKeyboard,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
RpcError,
|
||||
RPCOptions,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -14,6 +16,15 @@ import * as jose from 'node-jose'
|
||||
import { Observable } from 'rxjs'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { ApiService } from './api.service'
|
||||
import {
|
||||
SetupStatusRes,
|
||||
InstallOsParams,
|
||||
InstallOsRes,
|
||||
AttachParams,
|
||||
SetupExecuteParams,
|
||||
SetupCompleteRes,
|
||||
EchoReq,
|
||||
} from '../types'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -36,39 +47,54 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
return this.rpcRequest<T.SetupStatusRes | null>({
|
||||
async echo(params: EchoReq, url: string): Promise<string> {
|
||||
return this.rpcRequest({ method: 'echo', params }, url)
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
return this.rpcRequest<SetupStatusRes>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to update the pubkey, which means that we will call in clearnet the
|
||||
* getPubKey, 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 getPubKey(): Promise<void> {
|
||||
async getPubKey() {
|
||||
const response: jose.JWK.Key = await this.rpcRequest({
|
||||
method: 'setup.get-pubkey',
|
||||
params: {},
|
||||
})
|
||||
|
||||
this.pubkey = response
|
||||
}
|
||||
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
return this.rpcRequest<DiskListResponse>({
|
||||
async setKeyboard(params: FullKeyboard): Promise<null> {
|
||||
return this.rpcRequest({
|
||||
method: 'setup.set-keyboard',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||
return this.rpcRequest({
|
||||
method: 'setup.set-language',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async getDisks() {
|
||||
return this.rpcRequest<DiskInfo[]>({
|
||||
method: 'setup.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async verifyCifs(
|
||||
source: T.VerifyCifsParams,
|
||||
): Promise<Record<string, StartOSDiskInfo>> {
|
||||
async installOs(params: InstallOsParams) {
|
||||
return this.rpcRequest<InstallOsRes>({
|
||||
method: 'setup.install-os',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async verifyCifs(source: T.VerifyCifsParams) {
|
||||
source.path = source.path.replace('/\\/g', '/')
|
||||
return this.rpcRequest<Record<string, StartOSDiskInfo>>({
|
||||
method: 'setup.cifs.verify',
|
||||
@@ -76,33 +102,36 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
async attach(params: AttachParams) {
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.attach',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
if (setupInfo.recoverySource?.type === 'backup') {
|
||||
if (isCifsSource(setupInfo.recoverySource.target)) {
|
||||
setupInfo.recoverySource.target.path =
|
||||
setupInfo.recoverySource.target.path.replace('/\\/g', '/')
|
||||
async execute(params: SetupExecuteParams) {
|
||||
if (params.recoverySource?.type === 'backup') {
|
||||
const target = params.recoverySource.target
|
||||
if (target.type === 'cifs') {
|
||||
target.path = target.path.replace('/\\/g', '/')
|
||||
}
|
||||
}
|
||||
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async initFollowLogs(): Promise<FollowLogsRes> {
|
||||
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
|
||||
async initFollowLogs() {
|
||||
return this.rpcRequest<FollowLogsRes>({
|
||||
method: 'setup.logs.follow',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
const res = await this.rpcRequest<T.SetupResult>({
|
||||
async complete() {
|
||||
const res = await this.rpcRequest<SetupCompleteRes>({
|
||||
method: 'setup.complete',
|
||||
params: {},
|
||||
})
|
||||
@@ -113,23 +142,29 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async exit(): Promise<void> {
|
||||
async exit() {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.exit',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
async shutdown() {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.shutdown',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async restart() {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.restart',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts)
|
||||
|
||||
private async rpcRequest<T>(opts: RPCOptions, url?: string): Promise<T> {
|
||||
const res = await this.http.rpcRequest<T>(opts, url)
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
@@ -139,9 +174,3 @@ export class LiveApiService extends ApiService {
|
||||
return rpcRes.result
|
||||
}
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
source: T.BackupTargetFS | null,
|
||||
): source is T.Cifs & { type: 'cifs' } {
|
||||
return !!(source as T.Cifs)?.hostname
|
||||
}
|
||||
|
||||
@@ -1,111 +1,33 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
DiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsRes,
|
||||
FullKeyboard,
|
||||
pauseFor,
|
||||
SetLanguageParams,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import * as jose from 'node-jose'
|
||||
import { first, interval, map, Observable } from 'rxjs'
|
||||
import { interval, map, Observable } from 'rxjs'
|
||||
import { ApiService } from './api.service'
|
||||
import {
|
||||
SetupStatusRes,
|
||||
InstallOsParams,
|
||||
InstallOsRes,
|
||||
AttachParams,
|
||||
SetupExecuteParams,
|
||||
SetupCompleteRes,
|
||||
EchoReq,
|
||||
} from '../types'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
// fullProgress$(): Observable<T.FullProgress> {
|
||||
// const phases = [
|
||||
// {
|
||||
// name: 'Preparing Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Transferring Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Finalizing Setup',
|
||||
// progress: null,
|
||||
// },
|
||||
// ]
|
||||
|
||||
// return from(phases).pipe(
|
||||
// switchScan((acc, val, i) => {}, { overall: null, phases }),
|
||||
// )
|
||||
// }
|
||||
|
||||
// namedProgress$(namedProgress: T.NamedProgress): Observable<T.NamedProgress> {
|
||||
// return of(namedProgress).pipe(startWith(namedProgress))
|
||||
// }
|
||||
|
||||
// progress$(progress: T.Progress): Observable<T.Progress> {}
|
||||
|
||||
// websocket
|
||||
|
||||
// oldMockProgress$(): Promise<T.FullProgress> {
|
||||
// const numPhases = PROGRESS.phases.length
|
||||
|
||||
// return of(PROGRESS).pipe(
|
||||
// switchMap(full =>
|
||||
// from(PROGRESS.phases).pipe(
|
||||
// mergeScan((full, phase, i) => {
|
||||
// if (
|
||||
// !phase.progress ||
|
||||
// typeof phase.progress !== 'object' ||
|
||||
// !phase.progress.total
|
||||
// ) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// return of(full).pipe(delay(2000))
|
||||
// } else {
|
||||
// const total = phase.progress.total
|
||||
// const step = total / 4
|
||||
// let done = phase.progress.done
|
||||
|
||||
// return interval(1000).pipe(
|
||||
// takeWhile(() => done < total),
|
||||
// map(() => {
|
||||
// done += step
|
||||
|
||||
// console.error(done)
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases / 4
|
||||
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// if (done === total) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (i === numPhases - 1) {
|
||||
// full.overall = true
|
||||
// }
|
||||
// }
|
||||
// return full
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
// }, full),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
private statusIndex = 0
|
||||
private installCompleted = false
|
||||
|
||||
openWebsocket$<T>(guid: string): Observable<T> {
|
||||
if (guid === 'logs-guid') {
|
||||
@@ -117,24 +39,13 @@ export class MockApiService extends ApiService {
|
||||
})),
|
||||
) as Observable<T>
|
||||
} else if (guid === 'progress-guid') {
|
||||
// @TODO Matt mock progress
|
||||
return interval(1000).pipe(
|
||||
first(),
|
||||
map(() => ({
|
||||
overall: true,
|
||||
phases: [
|
||||
{
|
||||
name: 'Preparing Data',
|
||||
progress: true,
|
||||
},
|
||||
{
|
||||
name: 'Transferring Data',
|
||||
progress: true,
|
||||
},
|
||||
{
|
||||
name: 'Finalizing Setup',
|
||||
progress: true,
|
||||
},
|
||||
{ name: 'Preparing Data', progress: true },
|
||||
{ name: 'Transferring Data', progress: true },
|
||||
{ name: 'Finalizing Setup', progress: true },
|
||||
],
|
||||
})),
|
||||
) as Observable<T>
|
||||
@@ -143,40 +54,44 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
private statusIndex = 0
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
await pauseFor(1000)
|
||||
async echo(params: EchoReq, url: string): Promise<string> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
}
|
||||
await pauseFor(500)
|
||||
return params.message
|
||||
}
|
||||
|
||||
async getStatus(): Promise<SetupStatusRes> {
|
||||
await pauseFor(500)
|
||||
|
||||
this.statusIndex++
|
||||
|
||||
switch (this.statusIndex) {
|
||||
case 2:
|
||||
return {
|
||||
status: 'running',
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
status: 'complete',
|
||||
torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'],
|
||||
hostname: 'adjective-noun',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
}
|
||||
default:
|
||||
return null
|
||||
if (this.statusIndex === 1) {
|
||||
return { status: 'needs-install', keyboard: null }
|
||||
// return {
|
||||
// status: 'incomplete',
|
||||
// attach: false,
|
||||
// guid: 'mock-data-guid',
|
||||
// keyboard: null,
|
||||
// }
|
||||
}
|
||||
|
||||
if (this.statusIndex > 3) {
|
||||
return { status: 'complete' }
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'running',
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async getPubKey(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
|
||||
// randomly generated
|
||||
// const keystore = jose.JWK.createKeyStore()
|
||||
// this.pubkey = await keystore.generate('EC', 'P-256')
|
||||
|
||||
// generated from backend
|
||||
await pauseFor(300)
|
||||
this.pubkey = await jose.JWK.asKey({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
@@ -185,88 +100,28 @@ export class MockApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
await pauseFor(1000)
|
||||
return [
|
||||
{
|
||||
logicalname: '/dev/nvme0n1p3',
|
||||
vendor: 'Unknown Vendor',
|
||||
model: 'Samsung SSD - 970 EVO Plus 2TB',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pabcd',
|
||||
label: null,
|
||||
capacity: 1979120929996,
|
||||
used: null,
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 1979120929996,
|
||||
guid: 'uuid-uuid-uuid-uuid',
|
||||
},
|
||||
{
|
||||
logicalname: 'dcba',
|
||||
vendor: 'CT1000MX',
|
||||
model: '500SSD1',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 1000190509056,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
logicalname: '/dev/sda',
|
||||
vendor: 'ASMT',
|
||||
model: '2115',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: 'guid-guid-guid-guid',
|
||||
},
|
||||
],
|
||||
capacity: 1000190509,
|
||||
guid: null,
|
||||
},
|
||||
]
|
||||
async setKeyboard(_params: FullKeyboard): Promise<null> {
|
||||
await pauseFor(300)
|
||||
return null
|
||||
}
|
||||
|
||||
async setLanguage(params: SetLanguageParams): Promise<null> {
|
||||
await pauseFor(300)
|
||||
return null
|
||||
}
|
||||
|
||||
async getDisks(): Promise<DiskInfo[]> {
|
||||
await pauseFor(500)
|
||||
return MOCK_DISKS
|
||||
}
|
||||
|
||||
async installOs(params: InstallOsParams): Promise<InstallOsRes> {
|
||||
await pauseFor(2000)
|
||||
this.installCompleted = true
|
||||
return {
|
||||
guid: 'mock-data-guid',
|
||||
attach: !params.dataDrive.wipe,
|
||||
}
|
||||
}
|
||||
|
||||
async verifyCifs(
|
||||
@@ -282,21 +137,29 @@ export class MockApiService extends ApiService {
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
'9876-5432-1234-5671': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
async attach(params: AttachParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.statusIndex = 1 // Jump to running state
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
async execute(params: SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.statusIndex = 1 // Jump to running state
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
@@ -304,33 +167,113 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
|
||||
async initFollowLogs(): Promise<FollowLogsRes> {
|
||||
await pauseFor(1000)
|
||||
await pauseFor(500)
|
||||
return {
|
||||
startCursor: 'fakestartcursor',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
await pauseFor(1000)
|
||||
async complete(): Promise<SetupCompleteRes> {
|
||||
await pauseFor(500)
|
||||
return {
|
||||
torAddresses: ['https://asdafsadasdasasdasdfasdfasdf.onion'],
|
||||
hostname: 'adjective-noun',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
rootCa: encodeBase64(ROOT_CA),
|
||||
needsRestart: this.installCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
async exit(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
await pauseFor(500)
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
await pauseFor(500)
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
await pauseFor(500)
|
||||
}
|
||||
}
|
||||
|
||||
const rootCA = `-----BEGIN CERTIFICATE-----
|
||||
const MOCK_DISKS: DiskInfo[] = [
|
||||
{
|
||||
logicalname: '/dev/sda',
|
||||
vendor: 'Samsung',
|
||||
model: 'SSD 970 EVO Plus',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sda1',
|
||||
label: null,
|
||||
capacity: 500000000000,
|
||||
used: null,
|
||||
startOs: {},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 500000000000,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
logicalname: '/dev/sdb',
|
||||
vendor: 'Crucial',
|
||||
model: 'MX500',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdb1',
|
||||
label: null,
|
||||
capacity: 1000000000000,
|
||||
used: null,
|
||||
startOs: {
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'existing-server',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: 'existing-guid',
|
||||
},
|
||||
],
|
||||
capacity: 1000000000000,
|
||||
guid: 'existing-guid',
|
||||
},
|
||||
{
|
||||
logicalname: '/dev/sdc',
|
||||
vendor: 'WD',
|
||||
model: 'Blue SN570',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: '/dev/sdc1',
|
||||
label: 'Backup',
|
||||
capacity: 2000000000000,
|
||||
used: 500000000000,
|
||||
startOs: {
|
||||
'backup-server-id': {
|
||||
hostname: 'backup-server',
|
||||
version: '0.3.5',
|
||||
timestamp: new Date(Date.now() - 86400000).toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 2000000000000,
|
||||
guid: null,
|
||||
},
|
||||
]
|
||||
|
||||
const PROGRESS: T.FullProgress = {
|
||||
overall: null,
|
||||
phases: [],
|
||||
}
|
||||
|
||||
const ROOT_CA = `-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIRAIIuOarlQETlUQEOZJGZYdIwDQYJKoZIhvcNAQELBQAw
|
||||
bTELMAkGA1UEBhMCVVMxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDEOMAwGA1UECwwF
|
||||
U2FsZXMxCzAJBgNVBAgMAldBMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20xEDAO
|
||||
@@ -352,8 +295,3 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX
|
||||
2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W
|
||||
YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const PROGRESS = {
|
||||
overall: null,
|
||||
phases: [],
|
||||
}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ApiService } from './api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ApiService } from './api.service'
|
||||
|
||||
export type SetupType = 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
|
||||
export type RecoverySource =
|
||||
| {
|
||||
type: 'migrate'
|
||||
guid: string
|
||||
}
|
||||
| {
|
||||
type: 'backup'
|
||||
target:
|
||||
| { type: 'disk'; logicalname: string }
|
||||
| {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: string | null
|
||||
}
|
||||
serverId: string
|
||||
password: string // plaintext, will be encrypted before sending
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -8,34 +30,68 @@ import { T } from '@start9labs/start-sdk'
|
||||
export class StateService {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
kiosk?: boolean
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
recoverySource?: T.RecoverySource<string>
|
||||
// Determined at app init
|
||||
kiosk = false
|
||||
|
||||
async importDrive(guid: string, password: string): Promise<void> {
|
||||
// Set during install flow, or loaded from status response
|
||||
language = ''
|
||||
keyboard = ''
|
||||
|
||||
// From install response or status response (incomplete)
|
||||
dataDriveGuid = ''
|
||||
attach = false
|
||||
|
||||
// Set during setup flow
|
||||
setupType?: SetupType
|
||||
recoverySource?: RecoverySource
|
||||
|
||||
/**
|
||||
* Called for attach flow (existing data drive)
|
||||
*/
|
||||
async attachDrive(password: string | null): Promise<void> {
|
||||
await this.api.attach({
|
||||
guid,
|
||||
startOsPassword: await this.api.encrypt(password),
|
||||
kiosk: this.kiosk,
|
||||
guid: this.dataDriveGuid,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
})
|
||||
}
|
||||
|
||||
async setupEmbassy(
|
||||
storageLogicalname: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
/**
|
||||
* Called for fresh, restore, and transfer flows
|
||||
* password is required for fresh, optional for restore/transfer
|
||||
*/
|
||||
async executeSetup(password: string | null): Promise<void> {
|
||||
let recoverySource: T.RecoverySource<T.EncryptedWire> | null = null
|
||||
|
||||
if (this.recoverySource) {
|
||||
if (this.recoverySource.type === 'migrate') {
|
||||
recoverySource = this.recoverySource
|
||||
} else {
|
||||
// backup type - need to encrypt the backup password
|
||||
recoverySource = {
|
||||
type: 'backup',
|
||||
target: this.recoverySource.target,
|
||||
serverId: this.recoverySource.serverId,
|
||||
password: await this.api.encrypt(this.recoverySource.password),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.api.execute({
|
||||
startOsLogicalname: storageLogicalname,
|
||||
startOsPassword: await this.api.encrypt(password),
|
||||
recoverySource: this.recoverySource
|
||||
? this.recoverySource.type === 'migrate'
|
||||
? this.recoverySource
|
||||
: {
|
||||
...this.recoverySource,
|
||||
password: await this.api.encrypt(this.recoverySource.password),
|
||||
}
|
||||
: null,
|
||||
kiosk: this.kiosk,
|
||||
guid: this.dataDriveGuid,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
recoverySource,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state for a fresh start
|
||||
*/
|
||||
reset(): void {
|
||||
this.language = ''
|
||||
this.keyboard = ''
|
||||
this.dataDriveGuid = ''
|
||||
this.attach = false
|
||||
this.setupType = undefined
|
||||
this.recoverySource = undefined
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user