mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +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
|
||||
interfaceId: string
|
||||
}
|
||||
@Input({ required: true }) serviceInterface!: ServiceInterfaceWithAddresses
|
||||
@Input({ required: true }) serviceInterface!: MappedServiceInterface
|
||||
}
|
||||
|
||||
export type ServiceInterfaceWithAddresses = T.ServiceInterface & {
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
addresses: {
|
||||
clearnet: AddressDetails[]
|
||||
local: AddressDetails[]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ISB, IST, T, utils } from '@start9labs/start-sdk'
|
||||
import { TuiDialogOptions } from '@taiga-ui/core'
|
||||
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 { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
@@ -49,44 +50,80 @@ export function getClearnetSpec({
|
||||
)
|
||||
}
|
||||
|
||||
export type AddressDetails = {
|
||||
label?: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export function getMultihostAddresses(
|
||||
// @TODO Aiden audit
|
||||
export function getAddresses(
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
config: ConfigService,
|
||||
): {
|
||||
clearnet: AddressDetails[]
|
||||
clearnet: (AddressDetails & { acme: string | null })[]
|
||||
local: AddressDetails[]
|
||||
tor: AddressDetails[]
|
||||
} {
|
||||
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 tor: AddressDetails[] = []
|
||||
|
||||
hostnamesInfo.forEach(hostnameInfo => {
|
||||
utils.addressHostToUrl(addressInfo, hostnameInfo).forEach(url => {
|
||||
// Onion
|
||||
if (hostnameInfo.kind === 'onion') {
|
||||
tor.push({ url })
|
||||
// IP
|
||||
hostnames.forEach(h => {
|
||||
const addresses = utils.addressHostToUrl(addressInfo, h)
|
||||
|
||||
addresses.forEach(url => {
|
||||
if (h.kind === 'onion') {
|
||||
tor.push({
|
||||
label: `Tor${
|
||||
addresses.length > 1
|
||||
? ` (${new URL(url).protocol.replace(':', '').toUpperCase()})`
|
||||
: ''
|
||||
}`,
|
||||
url,
|
||||
})
|
||||
} else {
|
||||
// Domain
|
||||
if (hostnameInfo.hostname.kind === 'domain') {
|
||||
clearnet.push({ url })
|
||||
// Local
|
||||
const hostnameKind = h.hostname.kind
|
||||
|
||||
if (hostnameKind === 'domain') {
|
||||
clearnet.push({
|
||||
label: 'Domain',
|
||||
url,
|
||||
acme: host.domains[h.hostname.domain]?.acme,
|
||||
})
|
||||
} else {
|
||||
const hostnameKind = hostnameInfo.hostname.kind
|
||||
local.push({
|
||||
label:
|
||||
hostnameKind === 'local'
|
||||
? 'Local'
|
||||
: `${hostnameInfo.networkInterfaceId} (${hostnameKind})`,
|
||||
: `${h.networkInterfaceId} (${hostnameKind})`,
|
||||
url,
|
||||
})
|
||||
}
|
||||
@@ -99,4 +136,16 @@ export function getMultihostAddresses(
|
||||
local,
|
||||
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
|
||||
getInterfaces(pkg?: PackageDataEntry): T.ServiceInterface[] {
|
||||
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 {
|
||||
return info && this.isRunning
|
||||
? this.config.launchableAddress(info, this.pkg.hosts)
|
||||
getHref(ui?: T.ServiceInterface): string | null {
|
||||
return ui && this.isRunning
|
||||
? this.config.launchableAddress(ui, this.pkg.hosts)
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
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({
|
||||
template: `
|
||||
@@ -22,6 +23,7 @@ import { getMultihostAddresses } from '../../../components/interfaces/interface.
|
||||
})
|
||||
export class ServiceInterfaceRoute {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
readonly context = {
|
||||
packageId: getPkgId(),
|
||||
@@ -40,7 +42,11 @@ export class ServiceInterfaceRoute {
|
||||
]).pipe(
|
||||
map(([iFace, hosts]) => ({
|
||||
...iFace,
|
||||
addresses: getMultihostAddresses(iFace, hosts[iFace.addressInfo.hostId]),
|
||||
addresses: getAddresses(
|
||||
iFace,
|
||||
hosts[iFace.addressInfo.hostId],
|
||||
this.config,
|
||||
),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon } from '@taiga-ui/core'
|
||||
import { BackupTarget } from 'src/app/services/api/api.types'
|
||||
import { BackupType } from '../types/backup-type'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -102,7 +102,8 @@ export class BackupsUpcomingComponent {
|
||||
readonly targets = toSignal(from(this.api.getBackupTargets({})))
|
||||
readonly current = toSignal(
|
||||
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 || {})),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} 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 { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
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>
|
||||
}
|
||||
@case (0) {
|
||||
@if (showDevTools$ | async) {
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="tertiary-solid"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
Reinstall
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="tertiary-solid"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
Reinstall
|
||||
</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,8 +111,6 @@ export class MarketplaceControlsComponent {
|
||||
@Input()
|
||||
localFlavor!: boolean
|
||||
|
||||
readonly showDevTools$ = inject(ClientStorageService).showDevTools$
|
||||
|
||||
async tryInstall() {
|
||||
const currentUrl = await firstValueFrom(
|
||||
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 { PatchDB } from 'patch-db-client'
|
||||
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 { knownACME, toAcmeName } from 'src/app/util/acme'
|
||||
import { knownACME, toAcmeName } from 'src/app/utils/acme'
|
||||
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({
|
||||
selector: 'acme',
|
||||
templateUrl: 'acme.page.html',
|
||||
styleUrls: ['acme.page.scss'],
|
||||
template: ``,
|
||||
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'
|
||||
|
||||
acme$ = this.patch.watch$('serverInfo', 'acme').pipe(
|
||||
@@ -36,14 +46,6 @@ export class ACMEPage {
|
||||
|
||||
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(
|
||||
providers: {
|
||||
url: string
|
||||
@@ -28,6 +28,16 @@ import { EmailInfoComponent } from './info.component'
|
||||
*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"
|
||||
@@ -80,6 +90,8 @@ export class SettingsEmailComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
testAddress = ''
|
||||
isSaved = false
|
||||
|
||||
readonly spec: Promise<IST.InputSpec> = configBuilderToSpec(
|
||||
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()
|
||||
|
||||
try {
|
||||
await this.api.configureEmail(
|
||||
inputSpec.constants.customSmtp.validator.unsafeCast(value),
|
||||
)
|
||||
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 {
|
||||
@@ -109,7 +127,7 @@ export class SettingsEmailComponent {
|
||||
const loader = this.loader.open('Sending...').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.testEmail({
|
||||
await this.api.testSmtp({
|
||||
to: this.testAddress,
|
||||
...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 {
|
||||
InterfaceComponent,
|
||||
ServiceInterfaceWithAddresses,
|
||||
MappedServiceInterface,
|
||||
} 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'
|
||||
|
||||
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({
|
||||
template: `
|
||||
<app-interface
|
||||
@@ -23,43 +41,17 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
imports: [CommonModule, InterfaceComponent],
|
||||
})
|
||||
export class StartOsUiComponent {
|
||||
readonly ui$: Observable<ServiceInterfaceWithAddresses> = inject(
|
||||
PatchDB<DataModel>,
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
readonly ui$: Observable<MappedServiceInterface> = inject<PatchDB<DataModel>>(
|
||||
PatchDB,
|
||||
)
|
||||
.watch$('serverInfo', 'ui')
|
||||
.watch$('serverInfo')
|
||||
.pipe(
|
||||
map(hosts => {
|
||||
const serviceInterface: T.ServiceInterface = {
|
||||
id: 'startos-ui',
|
||||
name: 'StartOS UI',
|
||||
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),
|
||||
}
|
||||
}),
|
||||
map(server => ({
|
||||
...iface,
|
||||
public: server.host.bindings[iface.addressInfo.internalPort].net.public,
|
||||
addresses: getAddresses(iface, server.host, this.config),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RouterPortComponent } from './table.component'
|
||||
<ng-container *ngIf="server$ | async as server">
|
||||
<router-info [enabled]="!server.network.wanConfig.upnp" />
|
||||
<table
|
||||
*ngIf="server.ui | primaryIp as ip"
|
||||
*ngIf="server.host.hostnameInfo[80] | primaryIp as ip"
|
||||
tuiTextfieldAppearance="unstyled"
|
||||
tuiTextfieldSize="m"
|
||||
[tuiTextfieldLabelOutside]="true"
|
||||
|
||||
@@ -5,6 +5,13 @@ export default [
|
||||
path: '',
|
||||
component: SettingsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'acme',
|
||||
loadComponent: () =>
|
||||
import('./routes/acme/acme.component').then(
|
||||
m => m.SettingsACMEComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'email',
|
||||
loadComponent: () =>
|
||||
|
||||
@@ -107,27 +107,6 @@ export class SettingsService {
|
||||
icon: '@tui.monitor',
|
||||
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': [
|
||||
{
|
||||
@@ -150,6 +129,29 @@ export class SettingsService {
|
||||
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> {
|
||||
|
||||
@@ -17,7 +17,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
canActivate: [UnauthGuard],
|
||||
canActivate: [UnauthGuard, stateNot(['error', 'initializing'])],
|
||||
loadChildren: () =>
|
||||
import('./routes/login/login.module').then(m => m.LoginPageModule),
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Server`,
|
||||
theme: 'Dark',
|
||||
desktop: ['lnd'],
|
||||
marketplace: {
|
||||
selectedUrl: 'https://registry.start9.com/',
|
||||
knownHosts: {
|
||||
@@ -32,6 +31,23 @@ export const mockPatchData: DataModel = {
|
||||
id: 'abcdefgh',
|
||||
version,
|
||||
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: {
|
||||
eth0: {
|
||||
public: false,
|
||||
@@ -72,11 +88,12 @@ export const mockPatchData: DataModel = {
|
||||
packageVersionCompat: '>=0.3.0 <=0.3.6',
|
||||
postInitMigrationTodos: [],
|
||||
statusInfo: {
|
||||
currentBackup: null,
|
||||
// currentBackup: null,
|
||||
updated: false,
|
||||
updateProgress: null,
|
||||
restarting: false,
|
||||
shuttingDown: false,
|
||||
backupProgress: {},
|
||||
},
|
||||
hostname: 'random-words',
|
||||
host: {
|
||||
@@ -194,9 +211,8 @@ export const mockPatchData: DataModel = {
|
||||
platform: 'x86_64-nonfree',
|
||||
zram: true,
|
||||
governor: 'performance',
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
arch: 'x86_64',
|
||||
ram: 8 * 1024 * 1024 * 1024,
|
||||
devices: [],
|
||||
},
|
||||
packageData: {
|
||||
bitcoind: {
|
||||
|
||||
@@ -101,19 +101,7 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(
|
||||
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 ''
|
||||
|
||||
launchableAddress(ui: T.ServiceInterface, hosts: T.Hosts): string {
|
||||
const host = hosts[ui.addressInfo.hostId]
|
||||
|
||||
if (!host) return ''
|
||||
|
||||
@@ -20,7 +20,7 @@ export class EOSService {
|
||||
)
|
||||
|
||||
readonly backingUp$ = this.patch
|
||||
.watch$('serverInfo', 'statusInfo', 'currentBackup')
|
||||
.watch$('serverInfo', 'statusInfo', 'backupProgress')
|
||||
.pipe(
|
||||
map(obj => !!obj),
|
||||
distinctUntilChanged(),
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
switchMap,
|
||||
distinctUntilChanged,
|
||||
ReplaySubject,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@@ -3,7 +3,16 @@ import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type DataModel = {
|
||||
ui: UIData
|
||||
serverInfo: ServerInfo
|
||||
serverInfo: Omit<
|
||||
T.Public['serverInfo'],
|
||||
'wifi' | 'unreadNotificationCount'
|
||||
> & {
|
||||
network: NetworkInfo
|
||||
unreadNotifications: {
|
||||
count: number
|
||||
recent: ServerNotifications
|
||||
}
|
||||
}
|
||||
packageData: Record<string, PackageDataEntry>
|
||||
}
|
||||
|
||||
@@ -17,7 +26,6 @@ export type UIData = {
|
||||
}
|
||||
ackInstructions: Record<string, boolean>
|
||||
theme: string
|
||||
desktop: readonly string[]
|
||||
}
|
||||
|
||||
export type UIMarketplaceData = {
|
||||
@@ -33,30 +41,6 @@ export type UIStore = {
|
||||
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 = {
|
||||
wifi: WiFiInfo
|
||||
start9ToSubdomain: Omit<Domain, 'provider'> | null
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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'
|
||||
|
||||
export async function getServerInfo(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<ServerInfo> {
|
||||
): Promise<DataModel['serverInfo']> {
|
||||
return firstValueFrom(patch.watch$('serverInfo'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user