mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
various bug, improve smtp
This commit is contained in:
@@ -1,41 +1,57 @@
|
|||||||
import { SmtpValue } from '../../types'
|
|
||||||
import { GetSystemSmtp, Patterns } from '../../util'
|
import { GetSystemSmtp, Patterns } from '../../util'
|
||||||
import { InputSpec, InputSpecOf } from './builder/inputSpec'
|
import { InputSpec } from './builder/inputSpec'
|
||||||
import { Value } from './builder/value'
|
import { Value } from './builder/value'
|
||||||
import { Variants } from './builder/variants'
|
import { Variants } from './builder/variants'
|
||||||
|
|
||||||
|
const securityVariants = Variants.of({
|
||||||
|
tls: {
|
||||||
|
name: 'TLS',
|
||||||
|
spec: InputSpec.of({
|
||||||
|
port: Value.dynamicText(async () => ({
|
||||||
|
name: 'Port',
|
||||||
|
required: true,
|
||||||
|
default: '465',
|
||||||
|
disabled: 'Fixed for TLS',
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
starttls: {
|
||||||
|
name: 'STARTTLS',
|
||||||
|
spec: InputSpec.of({
|
||||||
|
port: Value.select({
|
||||||
|
name: 'Port',
|
||||||
|
default: '587',
|
||||||
|
values: { '25': '25', '587': '587', '2525': '2525' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an SMTP field spec with provider-specific defaults pre-filled.
|
* Creates an SMTP field spec with provider-specific defaults pre-filled.
|
||||||
*/
|
*/
|
||||||
function smtpFields(
|
function smtpFields(
|
||||||
defaults: {
|
defaults: {
|
||||||
host?: string
|
host?: string
|
||||||
port?: number
|
|
||||||
security?: 'starttls' | 'tls'
|
security?: 'starttls' | 'tls'
|
||||||
|
hostDisabled?: boolean
|
||||||
} = {},
|
} = {},
|
||||||
): InputSpec<SmtpValue> {
|
) {
|
||||||
return InputSpec.of<InputSpecOf<SmtpValue>>({
|
const hostSpec = Value.text({
|
||||||
host: Value.text({
|
name: 'Host',
|
||||||
name: 'Host',
|
required: true,
|
||||||
required: true,
|
default: defaults.host ?? null,
|
||||||
default: defaults.host ?? null,
|
placeholder: 'smtp.example.com',
|
||||||
placeholder: 'smtp.example.com',
|
})
|
||||||
}),
|
|
||||||
port: Value.number({
|
return InputSpec.of({
|
||||||
name: 'Port',
|
host: defaults.hostDisabled
|
||||||
required: true,
|
? hostSpec.withDisabled('Fixed for this provider')
|
||||||
default: defaults.port ?? 587,
|
: hostSpec,
|
||||||
min: 1,
|
security: Value.union({
|
||||||
max: 65535,
|
|
||||||
integer: true,
|
|
||||||
}),
|
|
||||||
security: Value.select({
|
|
||||||
name: 'Connection Security',
|
name: 'Connection Security',
|
||||||
default: defaults.security ?? 'starttls',
|
default: defaults.security ?? 'tls',
|
||||||
values: {
|
variants: securityVariants,
|
||||||
starttls: 'STARTTLS',
|
|
||||||
tls: 'TLS',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
from: Value.text({
|
from: Value.text({
|
||||||
name: 'From Address',
|
name: 'From Address',
|
||||||
@@ -68,44 +84,47 @@ export const customSmtp = smtpFields()
|
|||||||
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
|
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
|
||||||
*/
|
*/
|
||||||
export const smtpProviderVariants = Variants.of({
|
export const smtpProviderVariants = Variants.of({
|
||||||
|
none: {
|
||||||
|
name: 'None',
|
||||||
|
spec: InputSpec.of({}),
|
||||||
|
},
|
||||||
gmail: {
|
gmail: {
|
||||||
name: 'Gmail',
|
name: 'Gmail',
|
||||||
spec: smtpFields({
|
spec: smtpFields({
|
||||||
host: 'smtp.gmail.com',
|
host: 'smtp.gmail.com',
|
||||||
port: 587,
|
security: 'tls',
|
||||||
security: 'starttls',
|
hostDisabled: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
ses: {
|
ses: {
|
||||||
name: 'Amazon SES',
|
name: 'Amazon SES',
|
||||||
spec: smtpFields({
|
spec: smtpFields({
|
||||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||||
port: 587,
|
security: 'tls',
|
||||||
security: 'starttls',
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
sendgrid: {
|
sendgrid: {
|
||||||
name: 'SendGrid',
|
name: 'SendGrid',
|
||||||
spec: smtpFields({
|
spec: smtpFields({
|
||||||
host: 'smtp.sendgrid.net',
|
host: 'smtp.sendgrid.net',
|
||||||
port: 587,
|
security: 'tls',
|
||||||
security: 'starttls',
|
hostDisabled: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
mailgun: {
|
mailgun: {
|
||||||
name: 'Mailgun',
|
name: 'Mailgun',
|
||||||
spec: smtpFields({
|
spec: smtpFields({
|
||||||
host: 'smtp.mailgun.org',
|
host: 'smtp.mailgun.org',
|
||||||
port: 587,
|
security: 'tls',
|
||||||
security: 'starttls',
|
hostDisabled: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
protonmail: {
|
protonmail: {
|
||||||
name: 'Proton Mail',
|
name: 'Proton Mail',
|
||||||
spec: smtpFields({
|
spec: smtpFields({
|
||||||
host: 'smtp.protonmail.ch',
|
host: 'smtp.protonmail.ch',
|
||||||
port: 587,
|
security: 'tls',
|
||||||
security: 'starttls',
|
hostDisabled: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
@@ -121,7 +140,7 @@ export const smtpProviderVariants = Variants.of({
|
|||||||
export const systemSmtpSpec = InputSpec.of({
|
export const systemSmtpSpec = InputSpec.of({
|
||||||
provider: Value.union({
|
provider: Value.union({
|
||||||
name: 'Provider',
|
name: 'Provider',
|
||||||
default: null as any,
|
default: 'none',
|
||||||
variants: smtpProviderVariants,
|
variants: smtpProviderVariants,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
export * as inputSpecTypes from './actions/input/inputSpecTypes'
|
export * as inputSpecTypes from './actions/input/inputSpecTypes'
|
||||||
|
export {
|
||||||
|
CurrentDependenciesResult,
|
||||||
|
OptionalDependenciesOf as OptionalDependencies,
|
||||||
|
RequiredDependenciesOf as RequiredDependencies,
|
||||||
|
} from './dependencies/setupDependencies'
|
||||||
|
export * from './osBindings'
|
||||||
|
export { SDKManifest } from './types/ManifestTypes'
|
||||||
|
export { Effects }
|
||||||
import { InputSpec as InputSpecClass } from './actions/input/builder/inputSpec'
|
import { InputSpec as InputSpecClass } from './actions/input/builder/inputSpec'
|
||||||
|
|
||||||
import {
|
|
||||||
DependencyRequirement,
|
|
||||||
NamedHealthCheckResult,
|
|
||||||
Manifest,
|
|
||||||
ServiceInterface,
|
|
||||||
ActionId,
|
|
||||||
} from './osBindings'
|
|
||||||
import { Affine, StringObject, ToKebab } from './util'
|
|
||||||
import { Action, Actions } from './actions/setupActions'
|
import { Action, Actions } from './actions/setupActions'
|
||||||
import { Effects } from './Effects'
|
import { Effects } from './Effects'
|
||||||
import { ExtendedVersion, VersionRange } from './exver'
|
import { ExtendedVersion, VersionRange } from './exver'
|
||||||
export { Effects }
|
import {
|
||||||
export * from './osBindings'
|
ActionId,
|
||||||
export { SDKManifest } from './types/ManifestTypes'
|
DependencyRequirement,
|
||||||
export {
|
Manifest,
|
||||||
RequiredDependenciesOf as RequiredDependencies,
|
NamedHealthCheckResult,
|
||||||
OptionalDependenciesOf as OptionalDependencies,
|
ServiceInterface,
|
||||||
CurrentDependenciesResult,
|
} from './osBindings'
|
||||||
} from './dependencies/setupDependencies'
|
import { StringObject, ToKebab } from './util'
|
||||||
|
|
||||||
/** An object that can be built into a terminable daemon process. */
|
/** An object that can be built into a terminable daemon process. */
|
||||||
export type DaemonBuildable = {
|
export type DaemonBuildable = {
|
||||||
|
|||||||
@@ -324,6 +324,14 @@ function enabledAddresses(addr: DerivedAddressInfo): HostnameInfo[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out localhost and IPv6 link-local hostnames from a list.
|
||||||
|
* Equivalent to the `nonLocal` filter on `Filled` addresses.
|
||||||
|
*/
|
||||||
|
export function filterNonLocal(hostnames: HostnameInfo[]): HostnameInfo[] {
|
||||||
|
return filterRec(hostnames, nonLocalFilter, false)
|
||||||
|
}
|
||||||
|
|
||||||
export const filledAddress = (
|
export const filledAddress = (
|
||||||
host: Host,
|
host: Host,
|
||||||
addressInfo: AddressInfo,
|
addressInfo: AddressInfo,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {
|
|||||||
GetServiceInterface,
|
GetServiceInterface,
|
||||||
getServiceInterface,
|
getServiceInterface,
|
||||||
filledAddress,
|
filledAddress,
|
||||||
|
filterNonLocal,
|
||||||
} from './getServiceInterface'
|
} from './getServiceInterface'
|
||||||
export { getServiceInterfaces } from './getServiceInterfaces'
|
export { getServiceInterfaces } from './getServiceInterfaces'
|
||||||
export { once } from './once'
|
export { once } from './once'
|
||||||
|
|||||||
@@ -34,110 +34,121 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (!shuttingDown) {
|
@if (!shuttingDown) {
|
||||||
<section tuiCardLarge="compact">
|
<section tuiCardLarge="compact">
|
||||||
<header tuiHeader>
|
<header tuiHeader>
|
||||||
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
<h2 tuiTitle>{{ 'Select Drives' | i18n }}</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
<tui-loader />
|
<tui-loader />
|
||||||
} @else if (drives.length === 0) {
|
} @else if (drives.length === 0) {
|
||||||
<p class="no-drives">
|
<p class="no-drives">
|
||||||
{{
|
{{
|
||||||
'No drives found. Please connect a drive and click Refresh.' | i18n
|
'No drives found. Please connect a drive and click Refresh.'
|
||||||
}}
|
| i18n
|
||||||
</p>
|
}}
|
||||||
} @else {
|
</p>
|
||||||
<tui-textfield [stringify]="stringify">
|
|
||||||
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
|
||||||
@if (mobile) {
|
|
||||||
<select
|
|
||||||
tuiSelect
|
|
||||||
[(ngModel)]="selectedOsDrive"
|
|
||||||
[items]="drives"
|
|
||||||
></select>
|
|
||||||
} @else {
|
|
||||||
<input tuiSelect [(ngModel)]="selectedOsDrive" />
|
|
||||||
}
|
|
||||||
@if (!mobile) {
|
|
||||||
<tui-data-list-wrapper
|
|
||||||
new
|
|
||||||
*tuiTextfieldDropdown
|
|
||||||
[items]="drives"
|
|
||||||
[itemContent]="driveContent"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
|
||||||
</tui-textfield>
|
|
||||||
|
|
||||||
<tui-textfield [stringify]="stringify">
|
|
||||||
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
|
||||||
@if (mobile) {
|
|
||||||
<select
|
|
||||||
tuiSelect
|
|
||||||
[(ngModel)]="selectedDataDrive"
|
|
||||||
(ngModelChange)="onDataDriveChange($event)"
|
|
||||||
[items]="drives"
|
|
||||||
></select>
|
|
||||||
} @else {
|
|
||||||
<input
|
|
||||||
tuiSelect
|
|
||||||
[(ngModel)]="selectedDataDrive"
|
|
||||||
(ngModelChange)="onDataDriveChange($event)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
@if (!mobile) {
|
|
||||||
<tui-data-list-wrapper
|
|
||||||
new
|
|
||||||
*tuiTextfieldDropdown
|
|
||||||
[items]="drives"
|
|
||||||
[itemContent]="driveContent"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
@if (preserveData === true) {
|
|
||||||
<tui-icon
|
|
||||||
icon="@tui.database"
|
|
||||||
style="color: var(--tui-status-positive); pointer-events: none"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
@if (preserveData === false) {
|
|
||||||
<tui-icon
|
|
||||||
icon="@tui.database-zap"
|
|
||||||
style="color: var(--tui-status-negative); pointer-events: none"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
|
||||||
</tui-textfield>
|
|
||||||
|
|
||||||
<ng-template #driveContent let-drive>
|
|
||||||
<div class="drive-item">
|
|
||||||
<span class="drive-name">
|
|
||||||
{{ drive.vendor || ('Unknown' | i18n) }}
|
|
||||||
{{ drive.model || ('Drive' | i18n) }}
|
|
||||||
</span>
|
|
||||||
<small>
|
|
||||||
{{ formatCapacity(drive.capacity) }} · {{ drive.logicalname }}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
}
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
@if (drives.length === 0) {
|
|
||||||
<button tuiButton appearance="secondary" (click)="refresh()">
|
|
||||||
{{ 'Refresh' | i18n }}
|
|
||||||
</button>
|
|
||||||
} @else {
|
} @else {
|
||||||
<button
|
<tui-textfield
|
||||||
tuiButton
|
[stringify]="stringify"
|
||||||
[disabled]="!selectedOsDrive || !selectedDataDrive"
|
[disabledItemHandler]="osDisabled"
|
||||||
(click)="continue()"
|
|
||||||
>
|
>
|
||||||
{{ 'Continue' | i18n }}
|
<label tuiLabel>{{ 'OS Drive' | i18n }}</label>
|
||||||
</button>
|
@if (mobile) {
|
||||||
|
<select
|
||||||
|
tuiSelect
|
||||||
|
[ngModel]="selectedOsDrive"
|
||||||
|
(ngModelChange)="onOsDriveChange($event)"
|
||||||
|
[items]="drives"
|
||||||
|
></select>
|
||||||
|
} @else {
|
||||||
|
<input
|
||||||
|
tuiSelect
|
||||||
|
[ngModel]="selectedOsDrive"
|
||||||
|
(ngModelChange)="onOsDriveChange($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
@if (!mobile) {
|
||||||
|
<tui-data-list-wrapper
|
||||||
|
new
|
||||||
|
*tuiTextfieldDropdown
|
||||||
|
[items]="drives"
|
||||||
|
[itemContent]="driveContent"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<tui-icon [tuiTooltip]="osDriveTooltip" />
|
||||||
|
</tui-textfield>
|
||||||
|
|
||||||
|
<tui-textfield
|
||||||
|
[stringify]="stringify"
|
||||||
|
[disabledItemHandler]="dataDisabled"
|
||||||
|
>
|
||||||
|
<label tuiLabel>{{ 'Data Drive' | i18n }}</label>
|
||||||
|
@if (mobile) {
|
||||||
|
<select
|
||||||
|
tuiSelect
|
||||||
|
[(ngModel)]="selectedDataDrive"
|
||||||
|
(ngModelChange)="onDataDriveChange($event)"
|
||||||
|
[items]="drives"
|
||||||
|
></select>
|
||||||
|
} @else {
|
||||||
|
<input
|
||||||
|
tuiSelect
|
||||||
|
[(ngModel)]="selectedDataDrive"
|
||||||
|
(ngModelChange)="onDataDriveChange($event)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
@if (!mobile) {
|
||||||
|
<tui-data-list-wrapper
|
||||||
|
new
|
||||||
|
*tuiTextfieldDropdown
|
||||||
|
[items]="drives"
|
||||||
|
[itemContent]="driveContent"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
@if (preserveData === true) {
|
||||||
|
<tui-icon
|
||||||
|
icon="@tui.database"
|
||||||
|
style="color: var(--tui-status-positive); pointer-events: none"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
@if (preserveData === false) {
|
||||||
|
<tui-icon
|
||||||
|
icon="@tui.database-zap"
|
||||||
|
style="color: var(--tui-status-negative); pointer-events: none"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<tui-icon [tuiTooltip]="dataDriveTooltip" />
|
||||||
|
</tui-textfield>
|
||||||
|
|
||||||
|
<ng-template #driveContent let-drive>
|
||||||
|
<div class="drive-item">
|
||||||
|
<span class="drive-name">
|
||||||
|
{{ driveName(drive) }}
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
{{ formatCapacity(drive.capacity) }} · {{ drive.logicalname }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
}
|
}
|
||||||
</footer>
|
|
||||||
</section>
|
<footer>
|
||||||
|
@if (drives.length === 0) {
|
||||||
|
<button tuiButton appearance="secondary" (click)="refresh()">
|
||||||
|
{{ 'Refresh' | i18n }}
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
tuiButton
|
||||||
|
[disabled]="!selectedOsDrive || !selectedDataDrive"
|
||||||
|
(click)="continue()"
|
||||||
|
>
|
||||||
|
{{ 'Continue' | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -198,6 +209,10 @@ export default class DrivesPage {
|
|||||||
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive.',
|
'The drive where your StartOS data (services, settings, etc.) will be stored. This can be the same as the OS drive or a separate drive.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private readonly MIN_OS = 18 * 2 ** 30 // 18 GiB
|
||||||
|
private readonly MIN_DATA = 20 * 2 ** 30 // 20 GiB
|
||||||
|
private readonly MIN_BOTH = 38 * 2 ** 30 // 38 GiB
|
||||||
|
|
||||||
drives: DiskInfo[] = []
|
drives: DiskInfo[] = []
|
||||||
loading = true
|
loading = true
|
||||||
shuttingDown = false
|
shuttingDown = false
|
||||||
@@ -206,10 +221,17 @@ export default class DrivesPage {
|
|||||||
selectedDataDrive: DiskInfo | null = null
|
selectedDataDrive: DiskInfo | null = null
|
||||||
preserveData: boolean | null = null
|
preserveData: boolean | null = null
|
||||||
|
|
||||||
|
readonly osDisabled = (drive: DiskInfo): boolean =>
|
||||||
|
drive.capacity < this.MIN_OS
|
||||||
|
|
||||||
|
dataDisabled = (drive: DiskInfo): boolean => drive.capacity < this.MIN_DATA
|
||||||
|
|
||||||
|
readonly driveName = (drive: DiskInfo): string =>
|
||||||
|
[drive.vendor, drive.model].filter(Boolean).join(' ') ||
|
||||||
|
this.i18n.transform('Unknown Drive')
|
||||||
|
|
||||||
readonly stringify = (drive: DiskInfo | null) =>
|
readonly stringify = (drive: DiskInfo | null) =>
|
||||||
drive
|
drive ? this.driveName(drive) : ''
|
||||||
? `${drive.vendor || this.i18n.transform('Unknown')} ${drive.model || this.i18n.transform('Drive')}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
formatCapacity(bytes: number): string {
|
formatCapacity(bytes: number): string {
|
||||||
const gb = bytes / 1e9
|
const gb = bytes / 1e9
|
||||||
@@ -231,6 +253,22 @@ export default class DrivesPage {
|
|||||||
await this.loadDrives()
|
await this.loadDrives()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOsDriveChange(osDrive: DiskInfo | null) {
|
||||||
|
this.selectedOsDrive = osDrive
|
||||||
|
this.dataDisabled = (drive: DiskInfo) => {
|
||||||
|
if (osDrive && drive.logicalname === osDrive.logicalname) {
|
||||||
|
return drive.capacity < this.MIN_BOTH
|
||||||
|
}
|
||||||
|
return drive.capacity < this.MIN_DATA
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear data drive if it's now invalid
|
||||||
|
if (this.selectedDataDrive && this.dataDisabled(this.selectedDataDrive)) {
|
||||||
|
this.selectedDataDrive = null
|
||||||
|
this.preserveData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onDataDriveChange(drive: DiskInfo | null) {
|
onDataDriveChange(drive: DiskInfo | null) {
|
||||||
this.preserveData = null
|
this.preserveData = null
|
||||||
|
|
||||||
@@ -400,7 +438,7 @@ export default class DrivesPage {
|
|||||||
|
|
||||||
private async loadDrives() {
|
private async loadDrives() {
|
||||||
try {
|
try {
|
||||||
this.drives = await this.api.getDisks()
|
this.drives = (await this.api.getDisks()).filter(d => d.capacity > 0)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -191,7 +191,118 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GiB = 2 ** 30
|
||||||
|
|
||||||
const MOCK_DISKS: DiskInfo[] = [
|
const MOCK_DISKS: DiskInfo[] = [
|
||||||
|
// 0 capacity - should be hidden entirely
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdd',
|
||||||
|
vendor: 'Generic',
|
||||||
|
model: 'Card Reader',
|
||||||
|
partitions: [],
|
||||||
|
capacity: 0,
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
// 10 GiB - too small for OS and data; also tests both vendor+model null
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sde',
|
||||||
|
vendor: null,
|
||||||
|
model: null,
|
||||||
|
partitions: [
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sde1',
|
||||||
|
label: null,
|
||||||
|
capacity: 10 * GiB,
|
||||||
|
used: null,
|
||||||
|
startOs: {},
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capacity: 10 * GiB,
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
// 18 GiB - exact OS boundary; tests vendor null with model present
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdf',
|
||||||
|
vendor: null,
|
||||||
|
model: 'SATA Flash Drive',
|
||||||
|
partitions: [
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdf1',
|
||||||
|
label: null,
|
||||||
|
capacity: 18 * GiB,
|
||||||
|
used: null,
|
||||||
|
startOs: {},
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capacity: 18 * GiB,
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
// 20 GiB - exact data boundary; tests vendor present with model null
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdg',
|
||||||
|
vendor: 'PNY',
|
||||||
|
model: null,
|
||||||
|
partitions: [
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdg1',
|
||||||
|
label: null,
|
||||||
|
capacity: 20 * GiB,
|
||||||
|
used: null,
|
||||||
|
startOs: {},
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capacity: 20 * GiB,
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
// 30 GiB - OK for OS or data alone, too small for both (< 38 GiB)
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdh',
|
||||||
|
vendor: 'SanDisk',
|
||||||
|
model: 'Ultra',
|
||||||
|
partitions: [
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdh1',
|
||||||
|
label: null,
|
||||||
|
capacity: 30 * GiB,
|
||||||
|
used: null,
|
||||||
|
startOs: {},
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capacity: 30 * GiB,
|
||||||
|
guid: null,
|
||||||
|
},
|
||||||
|
// 30 GiB with existing StartOS data - tests preserve/overwrite + capacity constraint
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdi',
|
||||||
|
vendor: 'Kingston',
|
||||||
|
model: 'A400',
|
||||||
|
partitions: [
|
||||||
|
{
|
||||||
|
logicalname: '/dev/sdi1',
|
||||||
|
label: null,
|
||||||
|
capacity: 30 * GiB,
|
||||||
|
used: null,
|
||||||
|
startOs: {
|
||||||
|
'small-server-id': {
|
||||||
|
hostname: 'small-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: 'small-existing-guid',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
capacity: 30 * GiB,
|
||||||
|
guid: 'small-existing-guid',
|
||||||
|
},
|
||||||
|
// 500 GB - large, always OK
|
||||||
{
|
{
|
||||||
logicalname: '/dev/sda',
|
logicalname: '/dev/sda',
|
||||||
vendor: 'Samsung',
|
vendor: 'Samsung',
|
||||||
@@ -209,6 +320,7 @@ const MOCK_DISKS: DiskInfo[] = [
|
|||||||
capacity: 500000000000,
|
capacity: 500000000000,
|
||||||
guid: null,
|
guid: null,
|
||||||
},
|
},
|
||||||
|
// 1 TB with existing StartOS data
|
||||||
{
|
{
|
||||||
logicalname: '/dev/sdb',
|
logicalname: '/dev/sdb',
|
||||||
vendor: 'Crucial',
|
vendor: 'Crucial',
|
||||||
@@ -235,6 +347,7 @@ const MOCK_DISKS: DiskInfo[] = [
|
|||||||
capacity: 1000000000000,
|
capacity: 1000000000000,
|
||||||
guid: 'existing-guid',
|
guid: 'existing-guid',
|
||||||
},
|
},
|
||||||
|
// 2 TB
|
||||||
{
|
{
|
||||||
logicalname: '/dev/sdc',
|
logicalname: '/dev/sdc',
|
||||||
vendor: 'WD',
|
vendor: 'WD',
|
||||||
|
|||||||
@@ -644,7 +644,6 @@ export default {
|
|||||||
706: 'Beibehalten',
|
706: 'Beibehalten',
|
||||||
707: 'Überschreiben',
|
707: 'Überschreiben',
|
||||||
708: 'Entsperren',
|
708: 'Entsperren',
|
||||||
709: 'Laufwerk',
|
|
||||||
710: 'Übertragen',
|
710: 'Übertragen',
|
||||||
711: 'Die Liste ist leer',
|
711: 'Die Liste ist leer',
|
||||||
712: 'Jetzt neu starten',
|
712: 'Jetzt neu starten',
|
||||||
@@ -709,4 +708,6 @@ export default {
|
|||||||
779: 'Öffentlich',
|
779: 'Öffentlich',
|
||||||
780: 'Privat',
|
780: 'Privat',
|
||||||
781: 'Lokal',
|
781: 'Lokal',
|
||||||
|
782: 'Unbekanntes Laufwerk',
|
||||||
|
783: 'Muss eine gültige E-Mail-Adresse sein',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -644,7 +644,6 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Preserve': 706,
|
'Preserve': 706,
|
||||||
'Overwrite': 707,
|
'Overwrite': 707,
|
||||||
'Unlock': 708,
|
'Unlock': 708,
|
||||||
'Drive': 709, // the noun, a storage device
|
|
||||||
'Transfer': 710, // the verb
|
'Transfer': 710, // the verb
|
||||||
'The list is empty': 711,
|
'The list is empty': 711,
|
||||||
'Restart now': 712,
|
'Restart now': 712,
|
||||||
@@ -709,4 +708,6 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'Public': 779, // as in, publicly accessible
|
'Public': 779, // as in, publicly accessible
|
||||||
'Private': 780, // as in, privately accessible
|
'Private': 780, // as in, privately accessible
|
||||||
'Local': 781, // as in, locally accessible
|
'Local': 781, // as in, locally accessible
|
||||||
|
'Unknown Drive': 782,
|
||||||
|
'Must be a valid email address': 783,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,7 +644,6 @@ export default {
|
|||||||
706: 'Conservar',
|
706: 'Conservar',
|
||||||
707: 'Sobrescribir',
|
707: 'Sobrescribir',
|
||||||
708: 'Desbloquear',
|
708: 'Desbloquear',
|
||||||
709: 'Unidad',
|
|
||||||
710: 'Transferir',
|
710: 'Transferir',
|
||||||
711: 'La lista está vacía',
|
711: 'La lista está vacía',
|
||||||
712: 'Reiniciar ahora',
|
712: 'Reiniciar ahora',
|
||||||
@@ -709,4 +708,6 @@ export default {
|
|||||||
779: 'Público',
|
779: 'Público',
|
||||||
780: 'Privado',
|
780: 'Privado',
|
||||||
781: 'Local',
|
781: 'Local',
|
||||||
|
782: 'Unidad desconocida',
|
||||||
|
783: 'Debe ser una dirección de correo electrónico válida',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -644,7 +644,6 @@ export default {
|
|||||||
706: 'Conserver',
|
706: 'Conserver',
|
||||||
707: 'Écraser',
|
707: 'Écraser',
|
||||||
708: 'Déverrouiller',
|
708: 'Déverrouiller',
|
||||||
709: 'Disque',
|
|
||||||
710: 'Transférer',
|
710: 'Transférer',
|
||||||
711: 'La liste est vide',
|
711: 'La liste est vide',
|
||||||
712: 'Redémarrer maintenant',
|
712: 'Redémarrer maintenant',
|
||||||
@@ -709,4 +708,6 @@ export default {
|
|||||||
779: 'Public',
|
779: 'Public',
|
||||||
780: 'Privé',
|
780: 'Privé',
|
||||||
781: 'Local',
|
781: 'Local',
|
||||||
|
782: 'Lecteur inconnu',
|
||||||
|
783: 'Doit être une adresse e-mail valide',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -644,7 +644,6 @@ export default {
|
|||||||
706: 'Zachowaj',
|
706: 'Zachowaj',
|
||||||
707: 'Nadpisz',
|
707: 'Nadpisz',
|
||||||
708: 'Odblokuj',
|
708: 'Odblokuj',
|
||||||
709: 'Dysk',
|
|
||||||
710: 'Przenieś',
|
710: 'Przenieś',
|
||||||
711: 'Lista jest pusta',
|
711: 'Lista jest pusta',
|
||||||
712: 'Uruchom ponownie teraz',
|
712: 'Uruchom ponownie teraz',
|
||||||
@@ -709,4 +708,6 @@ export default {
|
|||||||
779: 'Publiczny',
|
779: 'Publiczny',
|
||||||
780: 'Prywatny',
|
780: 'Prywatny',
|
||||||
781: 'Lokalny',
|
781: 'Lokalny',
|
||||||
|
782: 'Nieznany dysk',
|
||||||
|
783: 'Musi być prawidłowy adres e-mail',
|
||||||
} satisfies i18n
|
} satisfies i18n
|
||||||
|
|||||||
@@ -116,7 +116,12 @@ export class InterfaceService {
|
|||||||
gatewayMap.set(gateway.id, gateway)
|
gatewayMap.set(gateway.id, gateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const h of addr.available) {
|
const available =
|
||||||
|
this.config.accessType === 'localhost'
|
||||||
|
? addr.available
|
||||||
|
: utils.filterNonLocal(addr.available)
|
||||||
|
|
||||||
|
for (const h of available) {
|
||||||
const gatewayIds = getGatewayIds(h)
|
const gatewayIds = getGatewayIds(h)
|
||||||
for (const gid of gatewayIds) {
|
for (const gid of gatewayIds) {
|
||||||
const list = groupMap.get(gid)
|
const list = groupMap.get(gid)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ interface ActionItem {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [TuiTitle],
|
imports: [TuiTitle],
|
||||||
host: {
|
host: {
|
||||||
'[disabled]': '!!disabled() || inactive()',
|
'[attr.disabled]': '(!!disabled() || inactive()) || null',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class ServiceActionComponent {
|
export class ServiceActionComponent {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
ChangeDetectionStrategy,
|
import { FormControl, ReactiveFormsModule } from '@angular/forms'
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
signal,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -15,11 +10,11 @@ import {
|
|||||||
i18nPipe,
|
i18nPipe,
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { inputSpec } from '@start9labs/start-sdk'
|
import { inputSpec, utils } from '@start9labs/start-sdk'
|
||||||
import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core'
|
import { TuiButton, TuiError, TuiTextfield, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiHeader } from '@taiga-ui/layout'
|
import { TuiHeader } from '@taiga-ui/layout'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { Subscription, switchMap, tap } from 'rxjs'
|
import { switchMap } from 'rxjs'
|
||||||
import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component'
|
import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormService } from 'src/app/services/form.service'
|
import { FormService } from 'src/app/services/form.service'
|
||||||
@@ -27,17 +22,6 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
|
||||||
const PROVIDER_HINTS: Record<string, string> = {
|
|
||||||
gmail:
|
|
||||||
'Requires an App Password. Enable 2FA in your Google account, then generate an App Password.',
|
|
||||||
ses: 'Use SMTP credentials (not IAM credentials). Update the host to match your SES region.',
|
|
||||||
sendgrid:
|
|
||||||
"Username is 'apikey' (literal). Password is your SendGrid API key.",
|
|
||||||
mailgun: 'Use SMTP credentials from your Mailgun domain settings.',
|
|
||||||
protonmail:
|
|
||||||
'Requires a Proton for Business account. Use your Proton email as username.',
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectProviderKey(host: string | undefined): string {
|
function detectProviderKey(host: string | undefined): string {
|
||||||
if (!host) return 'other'
|
if (!host) return 'other'
|
||||||
const providers: Record<string, string> = {
|
const providers: Record<string, string> = {
|
||||||
@@ -61,8 +45,8 @@ function detectProviderKey(host: string | undefined): string {
|
|||||||
</a>
|
</a>
|
||||||
{{ 'SMTP' | i18n }}
|
{{ 'SMTP' | i18n }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@if (form$ | async; as form) {
|
@if (form$ | async; as data) {
|
||||||
<form [formGroup]="form">
|
<form [formGroup]="data.form">
|
||||||
<header tuiHeader="body-l">
|
<header tuiHeader="body-l">
|
||||||
<h3 tuiTitle>
|
<h3 tuiTitle>
|
||||||
<b>
|
<b>
|
||||||
@@ -80,59 +64,54 @@ function detectProviderKey(host: string | undefined): string {
|
|||||||
</b>
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</header>
|
</header>
|
||||||
@if (spec | async; as resolved) {
|
<form-group [spec]="data.spec" />
|
||||||
<form-group [spec]="resolved" />
|
|
||||||
}
|
|
||||||
@if (providerHint()) {
|
|
||||||
<p class="provider-hint">{{ providerHint() }}</p>
|
|
||||||
}
|
|
||||||
<footer>
|
<footer>
|
||||||
@if (isSaved) {
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
size="l"
|
|
||||||
appearance="secondary-destructive"
|
|
||||||
(click)="save(null)"
|
|
||||||
>
|
|
||||||
{{ 'Delete' | i18n }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
size="l"
|
size="l"
|
||||||
[disabled]="form.invalid || form.pristine"
|
[disabled]="data.form.invalid || data.form.pristine"
|
||||||
(click)="save(form.value)"
|
(click)="save(data.form.value)"
|
||||||
>
|
>
|
||||||
{{ 'Save' | i18n }}
|
{{ 'Save' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
<form>
|
@if (data.form.value.provider?.selection !== 'none') {
|
||||||
<header tuiHeader="body-l">
|
<form>
|
||||||
<h3 tuiTitle>
|
<header tuiHeader="body-l">
|
||||||
<b>{{ 'Send test email' | i18n }}</b>
|
<h3 tuiTitle>
|
||||||
</h3>
|
<b>{{ 'Send test email' | i18n }}</b>
|
||||||
</header>
|
</h3>
|
||||||
<tui-textfield>
|
</header>
|
||||||
<label tuiLabel>Name Lastname <email@example.com></label>
|
<tui-textfield>
|
||||||
<input
|
<label tuiLabel>email@example.com</label>
|
||||||
tuiTextfield
|
<input
|
||||||
inputmode="email"
|
tuiTextfield
|
||||||
[(ngModel)]="testAddress"
|
inputmode="email"
|
||||||
[ngModelOptions]="{ standalone: true }"
|
[formControl]="testEmailControl"
|
||||||
|
/>
|
||||||
|
</tui-textfield>
|
||||||
|
<tui-error
|
||||||
|
[error]="
|
||||||
|
!testEmailControl.pristine && isEmailInvalid
|
||||||
|
? ('Must be a valid email address' | i18n)
|
||||||
|
: null
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</tui-textfield>
|
<footer>
|
||||||
<footer>
|
<button
|
||||||
<button
|
tuiButton
|
||||||
tuiButton
|
size="l"
|
||||||
size="l"
|
[disabled]="
|
||||||
[disabled]="!testAddress || form.invalid"
|
!testEmailControl.value || isEmailInvalid || data.form.invalid
|
||||||
(click)="sendTestEmail(form.value)"
|
"
|
||||||
>
|
(click)="sendTestEmail(data.form.value)"
|
||||||
{{ 'Send' | i18n }}
|
>
|
||||||
</button>
|
{{ 'Send' | i18n }}
|
||||||
</footer>
|
</button>
|
||||||
</form>
|
</footer>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
@@ -150,20 +129,14 @@ function detectProviderKey(host: string | undefined): string {
|
|||||||
footer {
|
footer {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-hint {
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormGroupComponent,
|
FormGroupComponent,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiError,
|
||||||
TuiTextfield,
|
TuiTextfield,
|
||||||
TuiHeader,
|
TuiHeader,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
@@ -182,49 +155,58 @@ export default class SystemEmailComponent {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
private readonly i18n = inject(i18nPipe)
|
||||||
|
|
||||||
readonly providerHint = signal('')
|
private readonly emailRegex = new RegExp(utils.Patterns.email.regex)
|
||||||
private providerSub: Subscription | null = null
|
readonly testEmailControl = new FormControl('')
|
||||||
|
|
||||||
testAddress = ''
|
get isEmailInvalid(): boolean {
|
||||||
isSaved = false
|
const value = this.testEmailControl.value
|
||||||
|
return !!value && !this.emailRegex.test(value)
|
||||||
readonly spec = configBuilderToSpec(inputSpec.constants.systemSmtpSpec)
|
}
|
||||||
|
|
||||||
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
|
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
|
||||||
tap(value => {
|
|
||||||
this.isSaved = !!value
|
|
||||||
}),
|
|
||||||
switchMap(async value => {
|
switchMap(async value => {
|
||||||
const spec = await this.spec
|
const spec = await configBuilderToSpec(inputSpec.constants.systemSmtpSpec)
|
||||||
|
|
||||||
const formData = value
|
const formData = value
|
||||||
? { provider: { selection: detectProviderKey(value.host), value } }
|
? {
|
||||||
|
provider: {
|
||||||
|
selection: detectProviderKey(value.host),
|
||||||
|
value: {
|
||||||
|
host: value.host,
|
||||||
|
security: {
|
||||||
|
selection: value.security,
|
||||||
|
value: { port: String(value.port) },
|
||||||
|
},
|
||||||
|
from: value.from,
|
||||||
|
username: value.username,
|
||||||
|
password: value.password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
: undefined
|
: undefined
|
||||||
const form = this.formService.createForm(spec, formData)
|
const form = this.formService.createForm(spec, formData)
|
||||||
|
|
||||||
// Watch provider selection for hints
|
return { form, spec }
|
||||||
this.providerSub?.unsubscribe()
|
|
||||||
const selectionCtrl = form.get('provider.selection')
|
|
||||||
if (selectionCtrl) {
|
|
||||||
this.providerHint.set(PROVIDER_HINTS[selectionCtrl.value] || '')
|
|
||||||
this.providerSub = selectionCtrl.valueChanges.subscribe(key => {
|
|
||||||
this.providerHint.set(PROVIDER_HINTS[key] || '')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return form
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
async save(formValue: Record<string, any> | null): Promise<void> {
|
private getSmtpValue(formValue: Record<string, any>) {
|
||||||
|
const { security, ...rest } = formValue['provider'].value
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
security: security.selection,
|
||||||
|
port: Number(security.value.port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(formValue: Record<string, any>): Promise<void> {
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (formValue) {
|
if (formValue['provider'].selection === 'none') {
|
||||||
await this.api.setSmtp(formValue['provider'].value)
|
|
||||||
this.isSaved = true
|
|
||||||
} else {
|
|
||||||
await this.api.clearSmtp({})
|
await this.api.clearSmtp({})
|
||||||
this.isSaved = false
|
} else {
|
||||||
|
await this.api.setSmtp(this.getSmtpValue(formValue))
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
@@ -234,21 +216,22 @@ export default class SystemEmailComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendTestEmail(formValue: Record<string, any>) {
|
async sendTestEmail(formValue: Record<string, any>) {
|
||||||
const smtpValue = formValue['provider'].value
|
const smtpValue = this.getSmtpValue(formValue)
|
||||||
|
const address = this.testEmailControl.value!
|
||||||
const loader = this.loader.open('Sending email').subscribe()
|
const loader = this.loader.open('Sending email').subscribe()
|
||||||
const success =
|
const success =
|
||||||
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}. <i>${this.i18n.transform('Check your spam folder and mark as not spam.')}</i>` as i18nKey
|
`${this.i18n.transform('A test email has been sent to')} ${address}. <i>${this.i18n.transform('Check your spam folder and mark as not spam.')}</i>` as i18nKey
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.testSmtp({
|
await this.api.testSmtp({
|
||||||
...smtpValue,
|
...smtpValue,
|
||||||
password: smtpValue.password || '',
|
password: smtpValue.password || '',
|
||||||
to: this.testAddress,
|
to: address,
|
||||||
})
|
})
|
||||||
this.dialog
|
this.dialog
|
||||||
.openAlert(success, { label: 'Success', size: 's' })
|
.openAlert(success, { label: 'Success', size: 's' })
|
||||||
.subscribe()
|
.subscribe()
|
||||||
this.testAddress = ''
|
this.testEmailControl.reset()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export class LiveApiService extends ApiService {
|
|||||||
// wifi
|
// wifi
|
||||||
|
|
||||||
async enableWifi(params: T.SetWifiEnabledParams): Promise<null> {
|
async enableWifi(params: T.SetWifiEnabledParams): Promise<null> {
|
||||||
return this.rpcRequest({ method: 'wifi.enable', params })
|
return this.rpcRequest({ method: 'wifi.set-enabled', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWifi(params: {}, timeout?: number): Promise<T.WifiListInfo> {
|
async getWifi(params: {}, timeout?: number): Promise<T.WifiListInfo> {
|
||||||
@@ -685,9 +685,7 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgAddPrivateDomain(
|
async pkgAddPrivateDomain(params: PkgAddPrivateDomainReq): Promise<boolean> {
|
||||||
params: PkgAddPrivateDomainReq,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.host.address.domain.private.add',
|
method: 'package.host.address.domain.private.add',
|
||||||
params,
|
params,
|
||||||
|
|||||||
Reference in New Issue
Block a user