mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
Feat/external-smtp (#1791)
* UI for EOS smtp, missing API layer * implement api * fix errors * switch to external smtp creds * fix things up * fix types * update types for new forms * feat: add new form to emails and marketplace (#2268) * import tuilet module * feat: get rid of old form completely (#2270) * move to builder spec and delete developer menu * update sdk * tiny * getting better * working * done * feat: add step to number config * chore: small fixes * update SDK and step for numbers --------- Co-authored-by: Alex Inkin <alexander@inkin.ru>
This commit is contained in:
committed by
Aiden McClelland
parent
4c465850a2
commit
010be05920
@@ -2,26 +2,44 @@ import { Component } from '@angular/core'
|
||||
import {
|
||||
BackupTarget,
|
||||
BackupTargetType,
|
||||
DiskBackupTarget,
|
||||
RR,
|
||||
UnknownDisk,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import {
|
||||
AlertController,
|
||||
LoadingController,
|
||||
ModalController,
|
||||
} from '@ionic/angular'
|
||||
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ErrorToastService } from '@start9labs/shared'
|
||||
import {
|
||||
CifsSpec,
|
||||
DropboxSpec,
|
||||
GoogleDriveSpec,
|
||||
DiskBackupTargetSpec,
|
||||
RemoteBackupTargetSpec,
|
||||
cifsSpec,
|
||||
diskBackupTargetSpec,
|
||||
dropboxSpec,
|
||||
googleDriveSpec,
|
||||
remoteBackupTargetSpec,
|
||||
} from '../../types/target-types'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { BehaviorSubject, filter } from 'rxjs'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { FormDialogService } from '../../../../services/form-dialog.service'
|
||||
import { FormPage } from '../../../../modals/form/form.page'
|
||||
import { LoadingService } from '../../../../modals/loading/loading.service'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||
import {
|
||||
InputSpec,
|
||||
unionSelectKey,
|
||||
unionValueKey,
|
||||
} from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
|
||||
type BackupConfig =
|
||||
| {
|
||||
type: {
|
||||
[unionSelectKey]: 'dropbox' | 'google-drive'
|
||||
[unionValueKey]: RR.AddCloudBackupTargetReq
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: {
|
||||
[unionSelectKey]: 'cifs'
|
||||
[unionValueKey]: RR.AddCifsBackupTargetReq
|
||||
}
|
||||
}
|
||||
|
||||
export type BackupType = 'create' | 'restore'
|
||||
|
||||
@@ -41,27 +59,23 @@ export class BackupTargetsPage {
|
||||
loading$ = new BehaviorSubject(true)
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loadingCtrl: LoadingController,
|
||||
private readonly errToast: ErrorToastService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly api: ApiService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getTargets()
|
||||
}
|
||||
|
||||
async presentModalAddPhysical(
|
||||
disk: UnknownDisk,
|
||||
index: number,
|
||||
): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: 'New Physical Target',
|
||||
spec: DiskBackupTargetSpec,
|
||||
initialValue: {
|
||||
async presentModalAddPhysical(disk: UnknownDisk, index: number) {
|
||||
this.formDialog.open(FormPage, {
|
||||
label: 'New Physical Target',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(diskBackupTargetSpec),
|
||||
value: {
|
||||
name: disk.label || disk.logicalname,
|
||||
},
|
||||
buttons: [
|
||||
@@ -74,60 +88,56 @@ export class BackupTargetsPage {
|
||||
}).then(disk => {
|
||||
this.targets['unknown-disks'].splice(index, 1)
|
||||
this.targets.saved.push(disk)
|
||||
|
||||
return true
|
||||
}),
|
||||
isSubmit: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentModalAddRemote(): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: 'New Remote Target',
|
||||
spec: RemoteBackupTargetSpec,
|
||||
async presentModalAddRemote() {
|
||||
this.formDialog.open(FormPage, {
|
||||
label: 'New Remote Target',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(remoteBackupTargetSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (
|
||||
value:
|
||||
| (RR.AddCifsBackupTargetReq & { type: BackupTargetType })
|
||||
| (RR.AddCloudBackupTargetReq & { type: BackupTargetType }),
|
||||
) => this.add(value.type, value),
|
||||
isSubmit: true,
|
||||
handler: ({ type }: BackupConfig) =>
|
||||
this.add(
|
||||
type[unionSelectKey] === 'cifs' ? 'cifs' : 'cloud',
|
||||
type[unionValueKey],
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentModalUpdate(target: BackupTarget): Promise<void> {
|
||||
let spec: typeof RemoteBackupTargetSpec = {}
|
||||
async presentModalUpdate(target: BackupTarget) {
|
||||
let spec: InputSpec
|
||||
|
||||
switch (target.type) {
|
||||
case 'cifs':
|
||||
spec = CifsSpec
|
||||
spec = await configBuilderToSpec(cifsSpec)
|
||||
break
|
||||
case 'cloud':
|
||||
spec = target.provider === 'dropbox' ? DropboxSpec : GoogleDriveSpec
|
||||
spec = await configBuilderToSpec(
|
||||
target.provider === 'dropbox' ? dropboxSpec : googleDriveSpec,
|
||||
)
|
||||
break
|
||||
case 'disk':
|
||||
spec = DiskBackupTargetSpec
|
||||
spec = await configBuilderToSpec(diskBackupTargetSpec)
|
||||
break
|
||||
}
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: GenericFormPage,
|
||||
componentProps: {
|
||||
title: 'Update Remote Target',
|
||||
this.formDialog.open(FormPage, {
|
||||
label: 'Update Target',
|
||||
data: {
|
||||
spec,
|
||||
initialValue: target,
|
||||
value: target,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
@@ -136,49 +146,38 @@ export class BackupTargetsPage {
|
||||
| RR.UpdateCifsBackupTargetReq
|
||||
| RR.UpdateCloudBackupTargetReq
|
||||
| RR.UpdateDiskBackupTargetReq,
|
||||
) => this.update(target.type, value),
|
||||
isSubmit: true,
|
||||
) => this.update(target.type, { ...value, id: target.id }),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
async presentAlertDelete(id: string, index: number) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: 'Forget backup target? This actions cannot be undone.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
presentAlertDelete(id: string, index: number) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Forget backup target? This actions cannot be undone.',
|
||||
no: 'Cancel',
|
||||
yes: 'Delete',
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
handler: () => {
|
||||
this.delete(id, index)
|
||||
},
|
||||
cssClass: 'enter-click',
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.delete(id, index))
|
||||
}
|
||||
|
||||
async delete(id: string, index: number): Promise<void> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Removing...',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Removing...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeBackupTarget({ id })
|
||||
this.targets.saved.splice(index, 1)
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +201,7 @@ export class BackupTargetsPage {
|
||||
try {
|
||||
this.targets = await this.api.getBackupTargets({})
|
||||
} catch (e: any) {
|
||||
this.errToast.present(e)
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
this.loading$.next(false)
|
||||
}
|
||||
@@ -215,16 +214,12 @@ export class BackupTargetsPage {
|
||||
| RR.AddCloudBackupTargetReq
|
||||
| RR.AddDiskBackupTargetReq,
|
||||
): Promise<BackupTarget> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving target...',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Saving target...').subscribe()
|
||||
|
||||
try {
|
||||
const res = await this.api.addBackupTarget(type, value)
|
||||
return res
|
||||
return await this.api.addBackupTarget(type, value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,16 +230,12 @@ export class BackupTargetsPage {
|
||||
| RR.UpdateCloudBackupTargetReq
|
||||
| RR.UpdateDiskBackupTargetReq,
|
||||
): Promise<BackupTarget> {
|
||||
const loader = await this.loadingCtrl.create({
|
||||
message: 'Saving target...',
|
||||
})
|
||||
await loader.present()
|
||||
const loader = this.loader.open('Saving target...').subscribe()
|
||||
|
||||
try {
|
||||
const res = await this.api.updateBackupTarget(type, value)
|
||||
return res
|
||||
return await this.api.updateBackupTarget(type, value)
|
||||
} finally {
|
||||
loader.dismiss()
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,221 +1,121 @@
|
||||
import { InputSpec } from 'start-sdk/lib/config/configTypes'
|
||||
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
|
||||
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
|
||||
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
|
||||
|
||||
export const DropboxSpec: InputSpec = {
|
||||
name: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
export const dropboxSpec = Config.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'A friendly name for this Dropbox target',
|
||||
placeholder: 'My Dropbox',
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
token: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
required: { default: null },
|
||||
}),
|
||||
token: Value.text({
|
||||
name: 'Access Token',
|
||||
description: 'The secret access token for your custom Dropbox app',
|
||||
warning: null,
|
||||
placeholder: null,
|
||||
required: true,
|
||||
required: { default: null },
|
||||
masked: true,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
}),
|
||||
path: Value.text({
|
||||
name: 'Path',
|
||||
description: 'The fully qualified path to the backup directory',
|
||||
warning: null,
|
||||
placeholder: 'e.g. /Desktop/my-folder',
|
||||
required: true,
|
||||
masked: false,
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
required: { default: null },
|
||||
}),
|
||||
})
|
||||
|
||||
export const GoogleDriveSpec: InputSpec = {
|
||||
name: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
export const googleDriveSpec = Config.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'A friendly name for this Google Drive target',
|
||||
warning: null,
|
||||
placeholder: 'My Google Drive',
|
||||
required: true,
|
||||
masked: false,
|
||||
default: null,
|
||||
},
|
||||
key: {
|
||||
type: 'file',
|
||||
required: { default: null },
|
||||
}),
|
||||
path: Value.text({
|
||||
name: 'Path',
|
||||
description: 'The fully qualified path to the backup directory',
|
||||
placeholder: 'e.g. /Desktop/my-folder',
|
||||
required: { default: null },
|
||||
}),
|
||||
key: Value.file({
|
||||
name: 'Private Key File',
|
||||
description:
|
||||
'Your Google Drive service account private key file (.json file)',
|
||||
warning: null,
|
||||
required: true,
|
||||
extensions: ['json'],
|
||||
},
|
||||
path: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
name: 'Path',
|
||||
description: 'The fully qualified path to the backup directory',
|
||||
placeholder: 'e.g. /Desktop/my-folder',
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const CifsSpec: InputSpec = {
|
||||
name: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
export const cifsSpec = Config.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'A friendly name for this Network Folder',
|
||||
warning: null,
|
||||
placeholder: 'My Network Folder',
|
||||
required: true,
|
||||
masked: false,
|
||||
default: null,
|
||||
},
|
||||
hostname: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [
|
||||
{
|
||||
regex: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
|
||||
description: `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
|
||||
},
|
||||
],
|
||||
required: { default: null },
|
||||
}),
|
||||
hostname: Value.text({
|
||||
name: 'Hostname',
|
||||
description:
|
||||
'The hostname of your target device on the Local Area Network.',
|
||||
warning: null,
|
||||
required: true,
|
||||
masked: false,
|
||||
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
required: { default: null },
|
||||
patterns: [],
|
||||
}),
|
||||
path: Value.text({
|
||||
name: 'Path',
|
||||
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
|
||||
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
username: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
required: { default: null },
|
||||
}),
|
||||
username: Value.text({
|
||||
name: 'Username',
|
||||
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
required: { default: null },
|
||||
placeholder: 'My Network Folder',
|
||||
default: null,
|
||||
},
|
||||
password: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
|
||||
required: false,
|
||||
masked: true,
|
||||
warning: null,
|
||||
placeholder: 'My Network Folder',
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const RemoteBackupTargetSpec: InputSpec = {
|
||||
type: {
|
||||
type: 'union',
|
||||
name: 'Target Type',
|
||||
description: null,
|
||||
warning: null,
|
||||
required: true,
|
||||
variants: {
|
||||
export const remoteBackupTargetSpec = Config.of({
|
||||
type: Value.union(
|
||||
{
|
||||
name: 'Target Type',
|
||||
required: { default: 'dropbox' },
|
||||
},
|
||||
Variants.of({
|
||||
dropbox: {
|
||||
name: 'Dropbox',
|
||||
spec: DropboxSpec,
|
||||
spec: dropboxSpec,
|
||||
},
|
||||
'google-drive': {
|
||||
name: 'Google Drive',
|
||||
spec: GoogleDriveSpec,
|
||||
spec: googleDriveSpec,
|
||||
},
|
||||
cifs: {
|
||||
name: 'Network Folder',
|
||||
spec: CifsSpec,
|
||||
spec: cifsSpec,
|
||||
},
|
||||
},
|
||||
default: 'dropbox',
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const DiskBackupTargetSpec: InputSpec = {
|
||||
name: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
export const diskBackupTargetSpec = Config.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description: 'A friendly name for this physical target',
|
||||
placeholder: 'My Physical Target',
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: 'text',
|
||||
inputmode: 'text',
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
required: { default: null },
|
||||
}),
|
||||
path: Value.text({
|
||||
name: 'Path',
|
||||
description: 'The fully qualified path to the backup directory',
|
||||
placeholder: 'e.g. /Backups/my-folder',
|
||||
required: true,
|
||||
masked: false,
|
||||
warning: null,
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
required: { default: null },
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user