mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
compiles
This commit is contained in:
@@ -1,62 +0,0 @@
|
|||||||
<ion-item *ngIf="iFace">
|
|
||||||
<ion-icon
|
|
||||||
slot="start"
|
|
||||||
size="large"
|
|
||||||
[name]="
|
|
||||||
iFace.type === 'ui'
|
|
||||||
? 'desktop-outline'
|
|
||||||
: iFace.type === 'api'
|
|
||||||
? 'terminal-outline'
|
|
||||||
: 'people-outline'
|
|
||||||
"
|
|
||||||
></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h1>{{ iFace.name }}</h1>
|
|
||||||
<h2>{{ iFace.description }}</h2>
|
|
||||||
<ion-button style="margin-right: 8px" (click)="presentDomainForm()">
|
|
||||||
Add Domain
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
[color]="iFace.public ? 'danger' : 'success'"
|
|
||||||
(click)="togglePublic()"
|
|
||||||
>
|
|
||||||
Make {{ iFace.public ? 'Private' : 'Public' }}
|
|
||||||
</ion-button>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
<div *ngIf="iFace" style="padding-left: 64px">
|
|
||||||
<ion-item *ngFor="let address of iFace.addresses">
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ address.name }}</h2>
|
|
||||||
<p>{{ address.url }}</p>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="address.isDomain"
|
|
||||||
color="danger"
|
|
||||||
(click)="removeStandard(address.url)"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</ion-button>
|
|
||||||
<ion-button
|
|
||||||
*ngIf="address.isOnion"
|
|
||||||
color="danger"
|
|
||||||
(click)="removeOnion(address.url)"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</ion-button>
|
|
||||||
</ion-label>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button *ngIf="address.isDomain" (click)="showAcme(address.acme)">
|
|
||||||
<ion-icon name="finger-print"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button *ngIf="iFace.type === 'ui'" (click)="launch(address.url)">
|
|
||||||
<ion-icon name="open-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button (click)="showQR(address.url)">
|
|
||||||
<ion-icon name="qr-code-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button (click)="copy(address.url)">
|
|
||||||
<ion-icon name="copy-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-item>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
p {
|
|
||||||
font-family: 'Courier New';
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
import { Component, Inject, Input } from '@angular/core'
|
|
||||||
import { WINDOW } from '@ng-web-apis/common'
|
|
||||||
import {
|
|
||||||
AlertController,
|
|
||||||
ModalController,
|
|
||||||
ToastController,
|
|
||||||
} from '@ionic/angular'
|
|
||||||
import {
|
|
||||||
copyToClipboard,
|
|
||||||
ErrorService,
|
|
||||||
LoadingService,
|
|
||||||
} from '@start9labs/shared'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { QRComponent } from 'src/app/components/qr/qr.component'
|
|
||||||
import { firstValueFrom } from 'rxjs'
|
|
||||||
import { ISB, T, utils } from '@start9labs/start-sdk'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
|
||||||
import { FormComponent } from 'src/app/components/form.component'
|
|
||||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
|
||||||
import { toAcmeName } from 'src/app/util/acme'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
|
|
||||||
export type MappedInterface = T.ServiceInterface & {
|
|
||||||
addresses: MappedAddress[]
|
|
||||||
public: boolean
|
|
||||||
}
|
|
||||||
export type MappedAddress = {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
isDomain: boolean
|
|
||||||
isOnion: boolean
|
|
||||||
acme: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'interface-info',
|
|
||||||
templateUrl: './interface-info.component.html',
|
|
||||||
styleUrls: ['./interface-info.component.scss'],
|
|
||||||
})
|
|
||||||
export class InterfaceInfoComponent {
|
|
||||||
@Input() pkgId?: string
|
|
||||||
@Input() iFace!: MappedInterface
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly toastCtrl: ToastController,
|
|
||||||
private readonly modalCtrl: ModalController,
|
|
||||||
private readonly errorService: ErrorService,
|
|
||||||
private readonly loader: LoadingService,
|
|
||||||
private readonly api: ApiService,
|
|
||||||
private readonly formDialog: FormDialogService,
|
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
|
||||||
private readonly config: ConfigService,
|
|
||||||
@Inject(WINDOW) private readonly windowRef: Window,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
launch(url: string): void {
|
|
||||||
this.windowRef.open(url, '_blank', 'noreferrer')
|
|
||||||
}
|
|
||||||
|
|
||||||
async togglePublic() {
|
|
||||||
const loader = this.loader
|
|
||||||
.open(`Making ${this.iFace.public ? 'private' : 'public'}`)
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
internalPort: this.iFace.addressInfo.internalPort,
|
|
||||||
public: !this.iFace.public,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgBindingSetPubic({
|
|
||||||
...params,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
package: this.pkgId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverBindingSetPubic(params)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async presentDomainForm() {
|
|
||||||
const acme = await firstValueFrom(this.patch.watch$('serverInfo', 'acme'))
|
|
||||||
|
|
||||||
const spec = getDomainSpec(Object.keys(acme))
|
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
|
||||||
label: 'Add Domain',
|
|
||||||
data: {
|
|
||||||
spec: await configBuilderToSpec(spec),
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Save',
|
|
||||||
handler: async (val: typeof spec._TYPE) => {
|
|
||||||
if (val.type.selection === 'standard') {
|
|
||||||
return this.saveStandard(
|
|
||||||
val.type.value.domain,
|
|
||||||
val.type.value.acme,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return this.saveTor(val.type.value.key)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeStandard(url: string) {
|
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
domain: new URL(url).hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgRemoveDomain({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverRemoveDomain(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeOnion(url: string) {
|
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
onion: new URL(url).hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgRemoveOnion({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverRemoveOnion(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showAcme(url: string | null): Promise<void> {
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: 'ACME Provider',
|
|
||||||
message: toAcmeName(url),
|
|
||||||
})
|
|
||||||
await alert.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async showQR(text: string): Promise<void> {
|
|
||||||
const modal = await this.modalCtrl.create({
|
|
||||||
component: QRComponent,
|
|
||||||
componentProps: {
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
cssClass: 'qr-modal',
|
|
||||||
})
|
|
||||||
await modal.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
async copy(address: string): Promise<void> {
|
|
||||||
let message = ''
|
|
||||||
await copyToClipboard(address || '').then(success => {
|
|
||||||
message = success
|
|
||||||
? 'Copied to clipboard!'
|
|
||||||
: 'Failed to copy to clipboard.'
|
|
||||||
})
|
|
||||||
|
|
||||||
const toast = await this.toastCtrl.create({
|
|
||||||
header: message,
|
|
||||||
position: 'bottom',
|
|
||||||
duration: 1000,
|
|
||||||
})
|
|
||||||
await toast.present()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveStandard(domain: string, acme: string) {
|
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
domain,
|
|
||||||
acme: acme === 'none' ? null : acme,
|
|
||||||
private: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgAddDomain({
|
|
||||||
...params,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverAddDomain(params)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveTor(key: string | null) {
|
|
||||||
const loader = this.loader.open('Creating onion address').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
let onion = key
|
|
||||||
? await this.api.addTorKey({ key })
|
|
||||||
: await this.api.generateTorKey({})
|
|
||||||
onion = `${onion}.onion`
|
|
||||||
|
|
||||||
if (this.pkgId) {
|
|
||||||
await this.api.pkgAddOnion({
|
|
||||||
onion,
|
|
||||||
package: this.pkgId,
|
|
||||||
host: this.iFace.addressInfo.hostId,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.api.serverAddOnion({ onion })
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
return false
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDomainSpec(acme: string[]) {
|
|
||||||
return ISB.InputSpec.of({
|
|
||||||
type: ISB.Value.union(
|
|
||||||
{ name: 'Type', default: 'standard' },
|
|
||||||
ISB.Variants.of({
|
|
||||||
standard: {
|
|
||||||
name: 'Standard',
|
|
||||||
spec: ISB.InputSpec.of({
|
|
||||||
domain: ISB.Value.text({
|
|
||||||
name: 'Domain',
|
|
||||||
description: 'The domain or subdomain you want to use',
|
|
||||||
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
|
|
||||||
required: true,
|
|
||||||
default: null,
|
|
||||||
patterns: [utils.Patterns.domain],
|
|
||||||
}),
|
|
||||||
acme: ISB.Value.select({
|
|
||||||
name: 'ACME Provider',
|
|
||||||
description:
|
|
||||||
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
|
|
||||||
values: acme.reduce(
|
|
||||||
(obj, url) => ({
|
|
||||||
...obj,
|
|
||||||
[url]: toAcmeName(url),
|
|
||||||
}),
|
|
||||||
{ none: 'None (use system Root CA)' } as Record<string, string>,
|
|
||||||
),
|
|
||||||
default: '',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onion: {
|
|
||||||
name: 'Onion',
|
|
||||||
spec: ISB.InputSpec.of({
|
|
||||||
key: ISB.Value.text({
|
|
||||||
name: 'Private Key (optional)',
|
|
||||||
description:
|
|
||||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.',
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
patterns: [utils.Patterns.base64],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAddresses(
|
|
||||||
serviceInterface: T.ServiceInterface,
|
|
||||||
host: T.Host,
|
|
||||||
config: ConfigService,
|
|
||||||
): MappedAddress[] {
|
|
||||||
const addressInfo = serviceInterface.addressInfo
|
|
||||||
|
|
||||||
let hostnames = host.hostnameInfo[addressInfo.internalPort]
|
|
||||||
|
|
||||||
hostnames = hostnames.filter(
|
|
||||||
h =>
|
|
||||||
config.isLocalhost() ||
|
|
||||||
h.kind !== 'ip' ||
|
|
||||||
h.hostname.kind !== 'ipv6' ||
|
|
||||||
!h.hostname.value.startsWith('fe80::'),
|
|
||||||
)
|
|
||||||
if (config.isLocalhost()) {
|
|
||||||
const local = hostnames.find(
|
|
||||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
|
||||||
)
|
|
||||||
if (local) {
|
|
||||||
hostnames.unshift({
|
|
||||||
kind: 'ip',
|
|
||||||
networkInterfaceId: 'lo',
|
|
||||||
public: false,
|
|
||||||
hostname: {
|
|
||||||
kind: 'local',
|
|
||||||
port: local.hostname.port,
|
|
||||||
sslPort: local.hostname.sslPort,
|
|
||||||
value: 'localhost',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mappedAddresses = hostnames.flatMap(h => {
|
|
||||||
let name = ''
|
|
||||||
let isDomain = false
|
|
||||||
let isOnion = false
|
|
||||||
let acme: string | null = null
|
|
||||||
|
|
||||||
if (h.kind === 'onion') {
|
|
||||||
name = `Tor`
|
|
||||||
isOnion = true
|
|
||||||
} else {
|
|
||||||
const hostnameKind = h.hostname.kind
|
|
||||||
|
|
||||||
if (hostnameKind === 'domain') {
|
|
||||||
name = 'Domain'
|
|
||||||
isDomain = true
|
|
||||||
acme = host.domains[h.hostname.domain]?.acme
|
|
||||||
} else {
|
|
||||||
name =
|
|
||||||
hostnameKind === 'local'
|
|
||||||
? 'Local'
|
|
||||||
: `${h.networkInterfaceId} (${hostnameKind})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
|
||||||
if (addresses.length > 1) {
|
|
||||||
return addresses.map(url => ({
|
|
||||||
name: `${name} (${new URL(url).protocol
|
|
||||||
.replace(':', '')
|
|
||||||
.toUpperCase()})`,
|
|
||||||
url,
|
|
||||||
isDomain,
|
|
||||||
isOnion,
|
|
||||||
acme,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
return addresses.map(url => ({
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
isDomain,
|
|
||||||
isOnion,
|
|
||||||
acme,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return mappedAddresses.filter(
|
|
||||||
(value, index, self) => index === self.findIndex(t => t.url === value.url),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { InterfaceInfoComponent } from './interface-info.component'
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [InterfaceInfoComponent],
|
|
||||||
imports: [CommonModule, IonicModule],
|
|
||||||
exports: [InterfaceInfoComponent],
|
|
||||||
})
|
|
||||||
export class InterfaceInfoModule {}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { RouterModule, Routes } from '@angular/router'
|
|
||||||
import { ACMEPage } from './acme.page'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: ACMEPage,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [CommonModule, IonicModule, RouterModule.forChild(routes)],
|
|
||||||
declarations: [ACMEPage],
|
|
||||||
})
|
|
||||||
export class ACMEPageModule {}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button defaultHref="system"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>ACME</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding-top with-widgets">
|
|
||||||
<ion-item-group>
|
|
||||||
<!-- always -->
|
|
||||||
<ion-item>
|
|
||||||
<ion-label>
|
|
||||||
<h2>
|
|
||||||
Register with one or more ACME providers such as Let's Encrypt in
|
|
||||||
order to generate SSL (https) certificates on-demand for clearnet
|
|
||||||
hosting
|
|
||||||
<a [href]="docsUrl" target="_blank" rel="noreferrer">
|
|
||||||
View instructions
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item-divider>Saved Providers</ion-item-divider>
|
|
||||||
|
|
||||||
<ng-container *ngIf="acme$ | async as acme">
|
|
||||||
<ion-item button detail="false" (click)="addAcme(acme)">
|
|
||||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<b>Add Provider</b>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item *ngFor="let provider of acme">
|
|
||||||
<ion-icon slot="start" name="finger-print" size="medium"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ toAcmeName(provider.url) }}</h2>
|
|
||||||
<p>Contact: {{ provider.contactString }}</p>
|
|
||||||
</ion-label>
|
|
||||||
<ion-buttons slot="end">
|
|
||||||
<ion-button (click)="editAcme(provider.url, provider.contact)">
|
|
||||||
<ion-icon slot="start" name="pencil"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-button (click)="removeAcme(provider.url)">
|
|
||||||
<ion-icon slot="start" name="trash-outline"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
</ion-item-group>
|
|
||||||
</ion-content>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
|
||||||
import { TuiInputModule } from '@taiga-ui/kit'
|
|
||||||
import {
|
|
||||||
TuiNotificationModule,
|
|
||||||
TuiTextfieldControllerModule,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { EmailPage } from './email.page'
|
|
||||||
import { FormModule } from 'src/app/components/form/form.module'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { TuiErrorModule, TuiModeModule } from '@taiga-ui/core'
|
|
||||||
import { TuiAppearanceModule, TuiButtonModule } from '@taiga-ui/experimental'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: EmailPage,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild(routes),
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
TuiButtonModule,
|
|
||||||
TuiInputModule,
|
|
||||||
FormModule,
|
|
||||||
TuiNotificationModule,
|
|
||||||
TuiTextfieldControllerModule,
|
|
||||||
TuiAppearanceModule,
|
|
||||||
TuiModeModule,
|
|
||||||
TuiErrorModule,
|
|
||||||
],
|
|
||||||
declarations: [EmailPage],
|
|
||||||
})
|
|
||||||
export class EmailPageModule {}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-title>Email</ion-title>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button defaultHref="system"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding">
|
|
||||||
<tui-notification>
|
|
||||||
Fill out the form below to connect to an external SMTP server. With your
|
|
||||||
permission, installed services can use the SMTP server to send emails. To
|
|
||||||
grant permission to a particular service, visit that service's "Actions"
|
|
||||||
page. Not all services support sending emails.
|
|
||||||
<a
|
|
||||||
href="https://docs.start9.com/latest/user-manual/0.3.5.x/smtp"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
View instructions
|
|
||||||
</a>
|
|
||||||
</tui-notification>
|
|
||||||
<ng-container *ngIf="form$ | async as form">
|
|
||||||
<form [formGroup]="form" [style.text-align]="'right'">
|
|
||||||
<h3 class="g-title">SMTP Credentials</h3>
|
|
||||||
<form-group
|
|
||||||
*ngIf="spec | async as resolved"
|
|
||||||
[spec]="resolved"
|
|
||||||
></form-group>
|
|
||||||
<button
|
|
||||||
*ngIf="isSaved"
|
|
||||||
tuiButton
|
|
||||||
appearance="destructive"
|
|
||||||
[style.margin-top.rem]="1"
|
|
||||||
[style.margin-right.rem]="1"
|
|
||||||
(click)="save(null)"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
[style.margin-top.rem]="1"
|
|
||||||
[disabled]="form.invalid"
|
|
||||||
(click)="save(form.value)"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form [style.text-align]="'right'">
|
|
||||||
<h3 class="g-title">Send Test Email</h3>
|
|
||||||
<tui-input
|
|
||||||
[(ngModel)]="testAddress"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
>
|
|
||||||
To Address
|
|
||||||
<input tuiTextfield inputmode="email" />
|
|
||||||
</tui-input>
|
|
||||||
<button
|
|
||||||
tuiButton
|
|
||||||
appearance="secondary"
|
|
||||||
[style.margin-top.rem]="1"
|
|
||||||
[disabled]="!testAddress || form.invalid"
|
|
||||||
(click)="sendTestEmail(form.value)"
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ng-container>
|
|
||||||
</ion-content>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
form {
|
|
||||||
padding-top: 24px;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 30rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import { IST, inputSpec } from '@start9labs/start-sdk'
|
|
||||||
import { TuiDialogService } from '@taiga-ui/core'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { switchMap, tap } from 'rxjs'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { FormService } from 'src/app/services/form.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'email-page',
|
|
||||||
templateUrl: './email.page.html',
|
|
||||||
styleUrls: ['./email.page.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class EmailPage {
|
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
|
||||||
private readonly formService = inject(FormService)
|
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
|
|
||||||
isSaved = false
|
|
||||||
testAddress = ''
|
|
||||||
|
|
||||||
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
|
||||||
inputSpec.constants.customSmtp,
|
|
||||||
)
|
|
||||||
readonly form$ = this.patch.watch$('serverInfo', 'smtp').pipe(
|
|
||||||
tap(value => (this.isSaved = !!value)),
|
|
||||||
switchMap(async value =>
|
|
||||||
this.formService.createForm(await this.spec, value),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async save(
|
|
||||||
value: typeof inputSpec.constants.customSmtp._TYPE | null,
|
|
||||||
): Promise<void> {
|
|
||||||
const loader = this.loader.open('Saving...').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (value) {
|
|
||||||
await this.api.setSmtp(value)
|
|
||||||
this.isSaved = true
|
|
||||||
} else {
|
|
||||||
await this.api.clearSmtp({})
|
|
||||||
this.isSaved = false
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
|
|
||||||
const loader = this.loader.open('Sending email...').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.testSmtp({
|
|
||||||
to: this.testAddress,
|
|
||||||
...value,
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
return this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dialogs
|
|
||||||
.open(
|
|
||||||
`A test email has been sent to ${this.testAddress}.<br /><br /><b>Check your spam folder and mark as not spam</b>`,
|
|
||||||
{
|
|
||||||
label: 'Success',
|
|
||||||
size: 's',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -104,10 +104,10 @@ export class InterfaceComponent {
|
|||||||
packageId: string
|
packageId: string
|
||||||
interfaceId: string
|
interfaceId: string
|
||||||
}
|
}
|
||||||
@Input({ required: true }) serviceInterface!: ServiceInterfaceWithAddresses
|
@Input({ required: true }) serviceInterface!: MappedServiceInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceInterfaceWithAddresses = T.ServiceInterface & {
|
export type MappedServiceInterface = T.ServiceInterface & {
|
||||||
addresses: {
|
addresses: {
|
||||||
clearnet: AddressDetails[]
|
clearnet: AddressDetails[]
|
||||||
local: AddressDetails[]
|
local: AddressDetails[]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ISB, IST, T, utils } from '@start9labs/start-sdk'
|
import { ISB, IST, T, utils } from '@start9labs/start-sdk'
|
||||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||||
import { TuiConfirmData } from '@taiga-ui/kit'
|
import { TuiConfirmData } from '@taiga-ui/kit'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
import { NetworkInfo } from 'src/app/services/patch-db/data-model'
|
||||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
|
||||||
@@ -49,44 +50,80 @@ export function getClearnetSpec({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddressDetails = {
|
// @TODO Aiden audit
|
||||||
label?: string
|
export function getAddresses(
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMultihostAddresses(
|
|
||||||
serviceInterface: T.ServiceInterface,
|
serviceInterface: T.ServiceInterface,
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
|
config: ConfigService,
|
||||||
): {
|
): {
|
||||||
clearnet: AddressDetails[]
|
clearnet: (AddressDetails & { acme: string | null })[]
|
||||||
local: AddressDetails[]
|
local: AddressDetails[]
|
||||||
tor: AddressDetails[]
|
tor: AddressDetails[]
|
||||||
} {
|
} {
|
||||||
const addressInfo = serviceInterface.addressInfo
|
const addressInfo = serviceInterface.addressInfo
|
||||||
const hostnamesInfo = host.hostnameInfo[addressInfo.internalPort]
|
|
||||||
|
|
||||||
const clearnet: AddressDetails[] = []
|
let hostnames = host.hostnameInfo[addressInfo.internalPort]
|
||||||
|
|
||||||
|
hostnames = hostnames.filter(
|
||||||
|
h =>
|
||||||
|
config.isLocalhost() ||
|
||||||
|
h.kind !== 'ip' ||
|
||||||
|
h.hostname.kind !== 'ipv6' ||
|
||||||
|
!h.hostname.value.startsWith('fe80::'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (config.isLocalhost()) {
|
||||||
|
const local = hostnames.find(
|
||||||
|
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (local) {
|
||||||
|
hostnames.unshift({
|
||||||
|
kind: 'ip',
|
||||||
|
networkInterfaceId: 'lo',
|
||||||
|
public: false,
|
||||||
|
hostname: {
|
||||||
|
kind: 'local',
|
||||||
|
port: local.hostname.port,
|
||||||
|
sslPort: local.hostname.sslPort,
|
||||||
|
value: 'localhost',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearnet: (AddressDetails & { acme: string | null })[] = []
|
||||||
const local: AddressDetails[] = []
|
const local: AddressDetails[] = []
|
||||||
const tor: AddressDetails[] = []
|
const tor: AddressDetails[] = []
|
||||||
|
|
||||||
hostnamesInfo.forEach(hostnameInfo => {
|
hostnames.forEach(h => {
|
||||||
utils.addressHostToUrl(addressInfo, hostnameInfo).forEach(url => {
|
const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||||
// Onion
|
|
||||||
if (hostnameInfo.kind === 'onion') {
|
addresses.forEach(url => {
|
||||||
tor.push({ url })
|
if (h.kind === 'onion') {
|
||||||
// IP
|
tor.push({
|
||||||
|
label: `Tor${
|
||||||
|
addresses.length > 1
|
||||||
|
? ` (${new URL(url).protocol.replace(':', '').toUpperCase()})`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
url,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Domain
|
const hostnameKind = h.hostname.kind
|
||||||
if (hostnameInfo.hostname.kind === 'domain') {
|
|
||||||
clearnet.push({ url })
|
if (hostnameKind === 'domain') {
|
||||||
// Local
|
clearnet.push({
|
||||||
|
label: 'Domain',
|
||||||
|
url,
|
||||||
|
acme: host.domains[h.hostname.domain]?.acme,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const hostnameKind = hostnameInfo.hostname.kind
|
|
||||||
local.push({
|
local.push({
|
||||||
label:
|
label:
|
||||||
hostnameKind === 'local'
|
hostnameKind === 'local'
|
||||||
? 'Local'
|
? 'Local'
|
||||||
: `${hostnameInfo.networkInterfaceId} (${hostnameKind})`,
|
: `${h.networkInterfaceId} (${hostnameKind})`,
|
||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -99,4 +136,16 @@ export function getMultihostAddresses(
|
|||||||
local,
|
local,
|
||||||
tor,
|
tor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @TODO Aiden what was going on here in 036?
|
||||||
|
// return mappedAddresses.filter(
|
||||||
|
// (value, index, self) => index === self.findIndex(t => t.url === value.url),
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabel(name: string, url: string, multiple: boolean) {}
|
||||||
|
|
||||||
|
export type AddressDetails = {
|
||||||
|
label: string
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,18 @@ export class UILaunchComponent {
|
|||||||
@tuiPure
|
@tuiPure
|
||||||
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
|
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
|
||||||
return pkg
|
return pkg
|
||||||
? Object.values(pkg.serviceInterfaces).filter(({ type }) => type === 'ui')
|
? Object.values(pkg.serviceInterfaces).filter(
|
||||||
|
i =>
|
||||||
|
i.type === 'ui' &&
|
||||||
|
(i.addressInfo.scheme === 'http' ||
|
||||||
|
i.addressInfo.sslScheme === 'https'),
|
||||||
|
)
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
getHref(info?: T.ServiceInterface): string | null {
|
getHref(ui?: T.ServiceInterface): string | null {
|
||||||
return info && this.isRunning
|
return ui && this.isRunning
|
||||||
? this.config.launchableAddress(info, this.pkg.hosts)
|
? this.config.launchableAddress(ui, this.pkg.hosts)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { combineLatest, map } from 'rxjs'
|
import { combineLatest, map } from 'rxjs'
|
||||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getMultihostAddresses } from '../../../components/interfaces/interface.utils'
|
import { getAddresses } from '../../../components/interfaces/interface.utils'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -22,6 +23,7 @@ import { getMultihostAddresses } from '../../../components/interfaces/interface.
|
|||||||
})
|
})
|
||||||
export class ServiceInterfaceRoute {
|
export class ServiceInterfaceRoute {
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
readonly context = {
|
readonly context = {
|
||||||
packageId: getPkgId(),
|
packageId: getPkgId(),
|
||||||
@@ -40,7 +42,11 @@ export class ServiceInterfaceRoute {
|
|||||||
]).pipe(
|
]).pipe(
|
||||||
map(([iFace, hosts]) => ({
|
map(([iFace, hosts]) => ({
|
||||||
...iFace,
|
...iFace,
|
||||||
addresses: getMultihostAddresses(iFace, hosts[iFace.addressInfo.hostId]),
|
addresses: getAddresses(
|
||||||
|
iFace,
|
||||||
|
hosts[iFace.addressInfo.hostId],
|
||||||
|
this.config,
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { Exver } from '@start9labs/shared'
|
|
||||||
import { TuiIcon } from '@taiga-ui/core'
|
import { TuiIcon } from '@taiga-ui/core'
|
||||||
import { BackupTarget } from 'src/app/services/api/api.types'
|
|
||||||
import { BackupType } from '../types/backup-type'
|
import { BackupType } from '../types/backup-type'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export class BackupsUpcomingComponent {
|
|||||||
readonly targets = toSignal(from(this.api.getBackupTargets({})))
|
readonly targets = toSignal(from(this.api.getBackupTargets({})))
|
||||||
readonly current = toSignal(
|
readonly current = toSignal(
|
||||||
inject<PatchDB<DataModel>>(PatchDB)
|
inject<PatchDB<DataModel>>(PatchDB)
|
||||||
.watch$('serverInfo', 'statusInfo', 'currentBackup', 'job')
|
// @TODO remove "as any" once this feature is real
|
||||||
|
.watch$('serverInfo', 'statusInfo', 'currentBackup' as any, 'job')
|
||||||
.pipe(map(job => job || {})),
|
.pipe(map(job => job || {})),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
DataModel,
|
DataModel,
|
||||||
PackageDataEntry,
|
PackageDataEntry,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
|
||||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||||
@@ -60,16 +59,14 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@case (0) {
|
@case (0) {
|
||||||
@if (showDevTools$ | async) {
|
<button
|
||||||
<button
|
tuiButton
|
||||||
tuiButton
|
type="button"
|
||||||
type="button"
|
appearance="tertiary-solid"
|
||||||
appearance="tertiary-solid"
|
(click)="tryInstall()"
|
||||||
(click)="tryInstall()"
|
>
|
||||||
>
|
Reinstall
|
||||||
Reinstall
|
</button>
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,8 +111,6 @@ export class MarketplaceControlsComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
localFlavor!: boolean
|
localFlavor!: boolean
|
||||||
|
|
||||||
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
|
|
||||||
|
|
||||||
async tryInstall() {
|
async tryInstall() {
|
||||||
const currentUrl = await firstValueFrom(
|
const currentUrl = await firstValueFrom(
|
||||||
this.marketplaceService.getRegistryUrl$(),
|
this.marketplaceService.getRegistryUrl$(),
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import { Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { DataModel } from '../../../services/patch-db/data-model'
|
|
||||||
import { FormDialogService } from '../../../services/form-dialog.service'
|
|
||||||
import { FormComponent } from '../../../components/form.component'
|
|
||||||
import { configBuilderToSpec } from '../../../util/configBuilderToSpec'
|
|
||||||
import { ISB, utils } from '@start9labs/start-sdk'
|
import { ISB, utils } from '@start9labs/start-sdk'
|
||||||
import { knownACME, toAcmeName } from 'src/app/util/acme'
|
import { knownACME, toAcmeName } from 'src/app/utils/acme'
|
||||||
import { map } from 'rxjs'
|
import { map } from 'rxjs'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||||
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
|
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'acme',
|
selector: 'acme',
|
||||||
templateUrl: 'acme.page.html',
|
template: ``,
|
||||||
styleUrls: ['acme.page.scss'],
|
styles: [],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
})
|
})
|
||||||
export class ACMEPage {
|
export class SettingsACMEComponent {
|
||||||
|
private readonly formDialog = inject(FormDialogService)
|
||||||
|
private readonly loader = inject(LoadingService)
|
||||||
|
private readonly errorService = inject(ErrorService)
|
||||||
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
|
||||||
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
||||||
|
|
||||||
acme$ = this.patch.watch$('serverInfo', 'acme').pipe(
|
acme$ = this.patch.watch$('serverInfo', 'acme').pipe(
|
||||||
@@ -36,14 +46,6 @@ export class ACMEPage {
|
|||||||
|
|
||||||
toAcmeName = toAcmeName
|
toAcmeName = toAcmeName
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly loader: LoadingService,
|
|
||||||
private readonly errorService: ErrorService,
|
|
||||||
private readonly api: ApiService,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
|
||||||
private readonly formDialog: FormDialogService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async addAcme(
|
async addAcme(
|
||||||
providers: {
|
providers: {
|
||||||
url: string
|
url: string
|
||||||
@@ -28,6 +28,16 @@ import { EmailInfoComponent } from './info.component'
|
|||||||
*ngIf="spec | async as resolved"
|
*ngIf="spec | async as resolved"
|
||||||
[spec]="resolved"
|
[spec]="resolved"
|
||||||
></form-group>
|
></form-group>
|
||||||
|
<button
|
||||||
|
*ngIf="isSaved"
|
||||||
|
tuiButton
|
||||||
|
appearance="destructive"
|
||||||
|
[style.margin-top.rem]="1"
|
||||||
|
[style.margin-right.rem]="1"
|
||||||
|
(click)="save(null)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
tuiButton
|
tuiButton
|
||||||
[style.margin-top.rem]="1"
|
[style.margin-top.rem]="1"
|
||||||
@@ -80,6 +90,8 @@ export class SettingsEmailComponent {
|
|||||||
private readonly api = inject(ApiService)
|
private readonly api = inject(ApiService)
|
||||||
|
|
||||||
testAddress = ''
|
testAddress = ''
|
||||||
|
isSaved = false
|
||||||
|
|
||||||
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
||||||
inputSpec.constants.customSmtp,
|
inputSpec.constants.customSmtp,
|
||||||
)
|
)
|
||||||
@@ -91,13 +103,19 @@ export class SettingsEmailComponent {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async save(value: unknown): Promise<void> {
|
async save(
|
||||||
|
value: typeof inputSpec.constants.customSmtp._TYPE | null,
|
||||||
|
): Promise<void> {
|
||||||
const loader = this.loader.open('Saving...').subscribe()
|
const loader = this.loader.open('Saving...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.configureEmail(
|
if (value) {
|
||||||
inputSpec.constants.customSmtp.validator.unsafeCast(value),
|
await this.api.setSmtp(value)
|
||||||
)
|
this.isSaved = true
|
||||||
|
} else {
|
||||||
|
await this.api.clearSmtp({})
|
||||||
|
this.isSaved = false
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -109,7 +127,7 @@ export class SettingsEmailComponent {
|
|||||||
const loader = this.loader.open('Sending...').subscribe()
|
const loader = this.loader.open('Sending...').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.testEmail({
|
await this.api.testSmtp({
|
||||||
to: this.testAddress,
|
to: this.testAddress,
|
||||||
...form.value,
|
...form.value,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
inject,
|
|
||||||
TemplateRef,
|
|
||||||
} from '@angular/core'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
|
||||||
import {
|
|
||||||
TuiAlertService,
|
|
||||||
TuiDialogService,
|
|
||||||
TuiIcon,
|
|
||||||
TuiLabel,
|
|
||||||
TuiTitle,
|
|
||||||
} from '@taiga-ui/core'
|
|
||||||
import { TUI_CONFIRM, TuiCheckbox } from '@taiga-ui/kit'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
|
||||||
import { filter } from 'rxjs'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
template: `
|
|
||||||
<ng-container *ngIf="server$ | async as server">
|
|
||||||
<button class="g-action" (click)="reset(tor)">
|
|
||||||
<tui-icon icon="@tui.rotate-cw" />
|
|
||||||
<div tuiTitle>
|
|
||||||
<strong>Reset Tor</strong>
|
|
||||||
<div tuiSubtitle>
|
|
||||||
Resetting the Tor daemon on your server may resolve Tor connectivity
|
|
||||||
issues.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button class="g-action" (click)="zram(server.zram)">
|
|
||||||
<tui-icon [icon]="server.zram ? '@tui.zap-off' : '@tui.zap'" />
|
|
||||||
<div tuiTitle>
|
|
||||||
<strong>{{ server.zram ? 'Disable' : 'Enable' }} zram</strong>
|
|
||||||
<div tuiSubtitle>
|
|
||||||
Zram creates compressed swap in memory, resulting in faster I/O for
|
|
||||||
low RAM devices
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-template #tor>
|
|
||||||
<p *ngIf="isTor">
|
|
||||||
You are currently connected over Tor. If you reset the Tor daemon, you
|
|
||||||
will lose connectivity until it comes back online.
|
|
||||||
</p>
|
|
||||||
<p *ngIf="!isTor">Reset Tor?</p>
|
|
||||||
<p>
|
|
||||||
Optionally wipe state to forcibly acquire new guard nodes. It is
|
|
||||||
recommended to try without wiping state first.
|
|
||||||
</p>
|
|
||||||
<label tuiLabel>
|
|
||||||
<input type="checkbox" tuiCheckbox [(ngModel)]="wipe" />
|
|
||||||
Wipe state
|
|
||||||
</label>
|
|
||||||
</ng-template>
|
|
||||||
`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
TuiTitle,
|
|
||||||
TuiIcon,
|
|
||||||
TuiLabel,
|
|
||||||
TuiCheckbox,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SettingsExperimentalComponent {
|
|
||||||
private readonly loader = inject(LoadingService)
|
|
||||||
private readonly errorService = inject(ErrorService)
|
|
||||||
private readonly api = inject(ApiService)
|
|
||||||
private readonly dialogs = inject(TuiDialogService)
|
|
||||||
private readonly alerts = inject(TuiAlertService)
|
|
||||||
|
|
||||||
readonly server$ = inject<PatchDB<DataModel>>(PatchDB).watch$('server-info')
|
|
||||||
readonly isTor = inject(ConfigService).isTor()
|
|
||||||
|
|
||||||
wipe = false
|
|
||||||
|
|
||||||
reset(content: TemplateRef<any>) {
|
|
||||||
this.wipe = false
|
|
||||||
this.dialogs
|
|
||||||
.open(TUI_CONFIRM, {
|
|
||||||
label: this.isTor ? 'Warning' : 'Confirm',
|
|
||||||
data: {
|
|
||||||
content,
|
|
||||||
yes: 'Reset',
|
|
||||||
no: 'Cancel',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.resetTor(this.wipe))
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resetTor(wipeState: boolean) {
|
|
||||||
const loader = this.loader.open('Resetting Tor...').subscribe()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.resetTor({
|
|
||||||
'wipe-state': wipeState,
|
|
||||||
reason: 'User triggered',
|
|
||||||
})
|
|
||||||
this.alerts.open('Tor reset in progress').subscribe()
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,29 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { Observable, map } from 'rxjs'
|
import { Observable, map } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
InterfaceComponent,
|
InterfaceComponent,
|
||||||
ServiceInterfaceWithAddresses,
|
MappedServiceInterface,
|
||||||
} from 'src/app/routes/portal/components/interfaces/interface.component'
|
} from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||||
import { getMultihostAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||||
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
|
const iface: T.ServiceInterface = {
|
||||||
|
id: '',
|
||||||
|
name: 'StartOS User Interface',
|
||||||
|
description:
|
||||||
|
'The primary user interface for your StartOS server, accessible from any browser.',
|
||||||
|
type: 'ui' as const,
|
||||||
|
masked: false,
|
||||||
|
addressInfo: {
|
||||||
|
hostId: '',
|
||||||
|
internalPort: 80,
|
||||||
|
scheme: 'http',
|
||||||
|
sslScheme: 'https',
|
||||||
|
suffix: '',
|
||||||
|
username: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<app-interface
|
<app-interface
|
||||||
@@ -23,43 +41,17 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
|||||||
imports: [CommonModule, InterfaceComponent],
|
imports: [CommonModule, InterfaceComponent],
|
||||||
})
|
})
|
||||||
export class StartOsUiComponent {
|
export class StartOsUiComponent {
|
||||||
readonly ui$: Observable<ServiceInterfaceWithAddresses> = inject(
|
private readonly config = inject(ConfigService)
|
||||||
PatchDB<DataModel>,
|
|
||||||
|
readonly ui$: Observable<MappedServiceInterface> = inject<PatchDB<DataModel>>(
|
||||||
|
PatchDB,
|
||||||
)
|
)
|
||||||
.watch$('serverInfo', 'ui')
|
.watch$('serverInfo')
|
||||||
.pipe(
|
.pipe(
|
||||||
map(hosts => {
|
map(server => ({
|
||||||
const serviceInterface: T.ServiceInterface = {
|
...iface,
|
||||||
id: 'startos-ui',
|
public: server.host.bindings[iface.addressInfo.internalPort].net.public,
|
||||||
name: 'StartOS UI',
|
addresses: getAddresses(iface, server.host, this.config),
|
||||||
description: 'The primary web user interface for StartOS',
|
})),
|
||||||
type: 'ui',
|
|
||||||
hasPrimary: false,
|
|
||||||
masked: false,
|
|
||||||
addressInfo: {
|
|
||||||
hostId: '',
|
|
||||||
username: null,
|
|
||||||
internalPort: 80,
|
|
||||||
scheme: 'http',
|
|
||||||
sslScheme: 'https',
|
|
||||||
suffix: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO Aiden confirm this is correct
|
|
||||||
const host: T.Host = {
|
|
||||||
kind: 'multi',
|
|
||||||
bindings: {},
|
|
||||||
hostnameInfo: {
|
|
||||||
80: hosts,
|
|
||||||
},
|
|
||||||
addresses: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...serviceInterface,
|
|
||||||
addresses: getMultihostAddresses(serviceInterface, host),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { RouterPortComponent } from './table.component'
|
|||||||
<ng-container *ngIf="server$ | async as server">
|
<ng-container *ngIf="server$ | async as server">
|
||||||
<router-info [enabled]="!server.network.wanConfig.upnp" />
|
<router-info [enabled]="!server.network.wanConfig.upnp" />
|
||||||
<table
|
<table
|
||||||
*ngIf="server.ui | primaryIp as ip"
|
*ngIf="server.host.hostnameInfo[80] | primaryIp as ip"
|
||||||
tuiTextfieldAppearance="unstyled"
|
tuiTextfieldAppearance="unstyled"
|
||||||
tuiTextfieldSize="m"
|
tuiTextfieldSize="m"
|
||||||
[tuiTextfieldLabelOutside]="true"
|
[tuiTextfieldLabelOutside]="true"
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export default [
|
|||||||
path: '',
|
path: '',
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'acme',
|
||||||
|
loadComponent: () =>
|
||||||
|
import('./routes/acme/acme.component').then(
|
||||||
|
m => m.SettingsACMEComponent,
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'email',
|
path: 'email',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
|
|||||||
@@ -107,27 +107,6 @@ export class SettingsService {
|
|||||||
icon: '@tui.monitor',
|
icon: '@tui.monitor',
|
||||||
routerLink: 'ui',
|
routerLink: 'ui',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Restart',
|
|
||||||
icon: '@tui.refresh-cw',
|
|
||||||
description: 'Restart Start OS server',
|
|
||||||
action: () => this.promptPower('Restart'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Shutdown',
|
|
||||||
icon: '@tui.power',
|
|
||||||
description: 'Turn Start OS server off',
|
|
||||||
action: () => this.promptPower('Shutdown'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Logout',
|
|
||||||
icon: '@tui.log-out',
|
|
||||||
description: 'Log off from Start OS',
|
|
||||||
action: () => {
|
|
||||||
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
|
||||||
this.auth.setUnverified()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
'Privacy and Security': [
|
'Privacy and Security': [
|
||||||
{
|
{
|
||||||
@@ -150,6 +129,29 @@ export class SettingsService {
|
|||||||
routerLink: 'sessions',
|
routerLink: 'sessions',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
Power: [
|
||||||
|
{
|
||||||
|
title: 'Restart',
|
||||||
|
icon: '@tui.refresh-cw',
|
||||||
|
description: 'Restart Start OS server',
|
||||||
|
action: () => this.promptPower('Restart'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Shutdown',
|
||||||
|
icon: '@tui.power',
|
||||||
|
description: 'Turn Start OS server off',
|
||||||
|
action: () => this.promptPower('Shutdown'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Logout',
|
||||||
|
icon: '@tui.log-out',
|
||||||
|
description: 'Log off from Start OS',
|
||||||
|
action: () => {
|
||||||
|
this.api.logout({}).catch(e => console.error('Failed to log out', e))
|
||||||
|
this.auth.setUnverified()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setOutboundProxy(): Promise<void> {
|
private async setOutboundProxy(): Promise<void> {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
canActivate: [UnauthGuard],
|
canActivate: [UnauthGuard, stateNot(['error', 'initializing'])],
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./routes/login/login.module').then(m => m.LoginPageModule),
|
import('./routes/login/login.module').then(m => m.LoginPageModule),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const mockPatchData: DataModel = {
|
|||||||
ui: {
|
ui: {
|
||||||
name: `Matt's Server`,
|
name: `Matt's Server`,
|
||||||
theme: 'Dark',
|
theme: 'Dark',
|
||||||
desktop: ['lnd'],
|
|
||||||
marketplace: {
|
marketplace: {
|
||||||
selectedUrl: 'https://registry.start9.com/',
|
selectedUrl: 'https://registry.start9.com/',
|
||||||
knownHosts: {
|
knownHosts: {
|
||||||
@@ -32,6 +31,23 @@ export const mockPatchData: DataModel = {
|
|||||||
id: 'abcdefgh',
|
id: 'abcdefgh',
|
||||||
version,
|
version,
|
||||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||||
|
network: {
|
||||||
|
wifi: {
|
||||||
|
enabled: false,
|
||||||
|
interface: 'wlan0',
|
||||||
|
ssids: [],
|
||||||
|
selected: null,
|
||||||
|
lastRegion: null,
|
||||||
|
},
|
||||||
|
start9ToSubdomain: null,
|
||||||
|
domains: [],
|
||||||
|
wanConfig: {
|
||||||
|
upnp: true,
|
||||||
|
forwards: [],
|
||||||
|
},
|
||||||
|
proxies: [],
|
||||||
|
outboundProxy: null,
|
||||||
|
},
|
||||||
networkInterfaces: {
|
networkInterfaces: {
|
||||||
eth0: {
|
eth0: {
|
||||||
public: false,
|
public: false,
|
||||||
@@ -72,11 +88,12 @@ export const mockPatchData: DataModel = {
|
|||||||
packageVersionCompat: '>=0.3.0 <=0.3.6',
|
packageVersionCompat: '>=0.3.0 <=0.3.6',
|
||||||
postInitMigrationTodos: [],
|
postInitMigrationTodos: [],
|
||||||
statusInfo: {
|
statusInfo: {
|
||||||
currentBackup: null,
|
// currentBackup: null,
|
||||||
updated: false,
|
updated: false,
|
||||||
updateProgress: null,
|
updateProgress: null,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
shuttingDown: false,
|
shuttingDown: false,
|
||||||
|
backupProgress: {},
|
||||||
},
|
},
|
||||||
hostname: 'random-words',
|
hostname: 'random-words',
|
||||||
host: {
|
host: {
|
||||||
@@ -194,9 +211,8 @@ export const mockPatchData: DataModel = {
|
|||||||
platform: 'x86_64-nonfree',
|
platform: 'x86_64-nonfree',
|
||||||
zram: true,
|
zram: true,
|
||||||
governor: 'performance',
|
governor: 'performance',
|
||||||
passwordHash:
|
ram: 8 * 1024 * 1024 * 1024,
|
||||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
devices: [],
|
||||||
arch: 'x86_64',
|
|
||||||
},
|
},
|
||||||
packageData: {
|
packageData: {
|
||||||
bitcoind: {
|
bitcoind: {
|
||||||
|
|||||||
@@ -101,19 +101,7 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||||
launchableAddress(
|
launchableAddress(ui: T.ServiceInterface, hosts: T.Hosts): string {
|
||||||
interfaces: PackageDataEntry['serviceInterfaces'],
|
|
||||||
hosts: T.Hosts,
|
|
||||||
): string {
|
|
||||||
const ui = Object.values(interfaces).find(
|
|
||||||
i =>
|
|
||||||
i.type === 'ui' &&
|
|
||||||
(i.addressInfo.scheme === 'http' ||
|
|
||||||
i.addressInfo.sslScheme === 'https'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ui) return ''
|
|
||||||
|
|
||||||
const host = hosts[ui.addressInfo.hostId]
|
const host = hosts[ui.addressInfo.hostId]
|
||||||
|
|
||||||
if (!host) return ''
|
if (!host) return ''
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class EOSService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
readonly backingUp$ = this.patch
|
readonly backingUp$ = this.patch
|
||||||
.watch$('serverInfo', 'statusInfo', 'currentBackup')
|
.watch$('serverInfo', 'statusInfo', 'backupProgress')
|
||||||
.pipe(
|
.pipe(
|
||||||
map(obj => !!obj),
|
map(obj => !!obj),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
tap,
|
|
||||||
} from 'rxjs'
|
} from 'rxjs'
|
||||||
import { RR } from 'src/app/services/api/api.types'
|
import { RR } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ import { T } from '@start9labs/start-sdk'
|
|||||||
|
|
||||||
export type DataModel = {
|
export type DataModel = {
|
||||||
ui: UIData
|
ui: UIData
|
||||||
serverInfo: ServerInfo
|
serverInfo: Omit<
|
||||||
|
T.Public['serverInfo'],
|
||||||
|
'wifi' | 'unreadNotificationCount'
|
||||||
|
> & {
|
||||||
|
network: NetworkInfo
|
||||||
|
unreadNotifications: {
|
||||||
|
count: number
|
||||||
|
recent: ServerNotifications
|
||||||
|
}
|
||||||
|
}
|
||||||
packageData: Record<string, PackageDataEntry>
|
packageData: Record<string, PackageDataEntry>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +26,6 @@ export type UIData = {
|
|||||||
}
|
}
|
||||||
ackInstructions: Record<string, boolean>
|
ackInstructions: Record<string, boolean>
|
||||||
theme: string
|
theme: string
|
||||||
desktop: readonly string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UIMarketplaceData = {
|
export type UIMarketplaceData = {
|
||||||
@@ -33,30 +41,6 @@ export type UIStore = {
|
|||||||
name?: string
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerInfo = {
|
|
||||||
id: string
|
|
||||||
version: string
|
|
||||||
country: string
|
|
||||||
ui: T.HostnameInfo[]
|
|
||||||
network: NetworkInfo
|
|
||||||
lastBackup: string | null
|
|
||||||
unreadNotifications: {
|
|
||||||
count: number
|
|
||||||
recent: ServerNotifications
|
|
||||||
}
|
|
||||||
statusInfo: ServerStatusInfo
|
|
||||||
eosVersionCompat: string
|
|
||||||
pubkey: string
|
|
||||||
caFingerprint: string
|
|
||||||
ntpSynced: boolean
|
|
||||||
smtp: T.SmtpValue | null
|
|
||||||
passwordHash: string
|
|
||||||
platform: string
|
|
||||||
arch: string
|
|
||||||
governor: string | null
|
|
||||||
zram: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NetworkInfo = {
|
export type NetworkInfo = {
|
||||||
wifi: WiFiInfo
|
wifi: WiFiInfo
|
||||||
start9ToSubdomain: Omit<Domain, 'provider'> | null
|
start9ToSubdomain: Omit<Domain, 'provider'> | null
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel, ServerInfo } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { firstValueFrom } from 'rxjs'
|
import { firstValueFrom } from 'rxjs'
|
||||||
|
|
||||||
export async function getServerInfo(
|
export async function getServerInfo(
|
||||||
patch: PatchDB<DataModel>,
|
patch: PatchDB<DataModel>,
|
||||||
): Promise<ServerInfo> {
|
): Promise<DataModel['serverInfo']> {
|
||||||
return firstValueFrom(patch.watch$('serverInfo'))
|
return firstValueFrom(patch.watch$('serverInfo'))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user