mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<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>
|
||||
@@ -0,0 +1,3 @@
|
||||
p {
|
||||
font-family: 'Courier New';
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
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),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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 {}
|
||||
@@ -0,0 +1,18 @@
|
||||
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 {}
|
||||
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
179
web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts
Normal file
179
web/projects/ui/src/app/pages/server-routes/acme/acme.page.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Component } 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 { map } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'acme',
|
||||
templateUrl: 'acme.page.html',
|
||||
styleUrls: ['acme.page.scss'],
|
||||
})
|
||||
export class ACMEPage {
|
||||
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
||||
|
||||
acme$ = this.patch.watch$('serverInfo', 'acme').pipe(
|
||||
map(acme => {
|
||||
const providerUrls = Object.keys(acme)
|
||||
return providerUrls.map(url => {
|
||||
const contact = acme[url].contact.map(mailto =>
|
||||
mailto.replace('mailto:', ''),
|
||||
)
|
||||
return {
|
||||
url,
|
||||
contact,
|
||||
contactString: contact.join(', '),
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
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
|
||||
contact: string[]
|
||||
contactString: string
|
||||
}[],
|
||||
) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
getAddAcmeSpec(providers.map(p => p.url)),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (
|
||||
val: ReturnType<typeof getAddAcmeSpec>['_TYPE'],
|
||||
) => {
|
||||
const providerUrl =
|
||||
val.provider.selection === 'other'
|
||||
? val.provider.value.url
|
||||
: val.provider.selection
|
||||
|
||||
return this.saveAcme(providerUrl, val.contact)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async editAcme(provider: string, contact: string[]) {
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Edit ACME Provider',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(editAcmeSpec),
|
||||
buttons: [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: async (val: typeof editAcmeSpec._TYPE) =>
|
||||
this.saveAcme(provider, val.contact),
|
||||
},
|
||||
],
|
||||
value: { contact },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async removeAcme(provider: string) {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.removeAcme({ provider })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAcme(providerUrl: string, contact: string[]) {
|
||||
console.log(providerUrl, contact)
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.initAcme({
|
||||
provider: new URL(providerUrl).href,
|
||||
contact: contact.map(address => `mailto:${address}`),
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emailListSpec = ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: 'Contact Emails',
|
||||
description:
|
||||
'Needed to obtain a certificate from a Certificate Authority',
|
||||
minLength: 1,
|
||||
},
|
||||
{
|
||||
inputmode: 'email',
|
||||
patterns: [utils.Patterns.email],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
function getAddAcmeSpec(providers: string[]) {
|
||||
const availableAcme = knownACME.filter(acme => !providers.includes(acme.url))
|
||||
|
||||
return ISB.InputSpec.of({
|
||||
provider: ISB.Value.union(
|
||||
{ name: 'Provider', default: (availableAcme[0]?.url as any) || 'other' },
|
||||
ISB.Variants.of({
|
||||
...availableAcme.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.url]: {
|
||||
name: curr.name,
|
||||
spec: ISB.InputSpec.of({}),
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: ISB.InputSpec.of({
|
||||
url: ISB.Value.text({
|
||||
name: 'URL',
|
||||
default: null,
|
||||
required: true,
|
||||
inputmode: 'url',
|
||||
patterns: [utils.Patterns.url],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
contact: emailListSpec,
|
||||
})
|
||||
}
|
||||
|
||||
const editAcmeSpec = ISB.InputSpec.of({
|
||||
contact: emailListSpec,
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
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 {}
|
||||
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
@@ -0,0 +1,9 @@
|
||||
form {
|
||||
padding-top: 24px;
|
||||
margin: auto;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}*
|
||||
{{ spec.name }} *
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Control } from '../control'
|
||||
@Component({
|
||||
selector: 'form-toggle',
|
||||
templateUrl: './form-toggle.component.html',
|
||||
host: { class: 'g-toggle' },
|
||||
host: { style: 'display: flex' },
|
||||
})
|
||||
export class FormToggleComponent extends Control<
|
||||
IST.ValueSpecToggle,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { endWith, ReplaySubject, shareReplay, Subject, switchMap } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
endWith,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Injectable({
|
||||
@@ -9,11 +19,34 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
export class SideloadService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly guid$ = new Subject<string>()
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
readonly progress$ = this.guid$.pipe(
|
||||
switchMap(guid =>
|
||||
this.api.openWebsocket$<T.FullProgress>(guid).pipe(endWith(null)),
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
closeObserver: {
|
||||
next: event => {
|
||||
if (event.code !== 1000) {
|
||||
this.errorService.handleError(event.reason)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
tap(p => {
|
||||
if (p.overall === true) {
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
}),
|
||||
endWith(null),
|
||||
),
|
||||
),
|
||||
catchError(e => {
|
||||
this.errorService.handleError('Websocket connection broken. Try again.')
|
||||
return EMPTY
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
|
||||
@@ -1615,7 +1615,43 @@ export module Mock {
|
||||
},
|
||||
internal: {
|
||||
name: 'Internal',
|
||||
spec: ISB.InputSpec.of({}),
|
||||
spec: ISB.InputSpec.of({
|
||||
listitems: ISB.Value.list(
|
||||
ISB.List.text(
|
||||
{
|
||||
name: 'RPC Allowed IPs',
|
||||
minLength: 1,
|
||||
maxLength: 10,
|
||||
default: ['192.168.1.1'],
|
||||
description:
|
||||
'external ip addresses that are authorized to access your Bitcoin node',
|
||||
warning:
|
||||
'Any IP you allow here will have RPC access to your Bitcoin node.',
|
||||
},
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
regex:
|
||||
'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
|
||||
description:
|
||||
'must be a valid ipv4, ipv6, or domain name',
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
),
|
||||
name: ISB.Value.text({
|
||||
name: 'Name',
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [
|
||||
{
|
||||
regex: '^[a-zA-Z]+$',
|
||||
description: 'Must contain only letters.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
external: {
|
||||
name: 'External',
|
||||
@@ -1763,6 +1799,10 @@ export module Mock {
|
||||
},
|
||||
'bitcoin-node': {
|
||||
selection: 'internal',
|
||||
value: {
|
||||
listitems: ['192.168.1.1', '192.1681.23'],
|
||||
name: 'Matt',
|
||||
},
|
||||
},
|
||||
port: 20,
|
||||
rpcallowip: undefined,
|
||||
@@ -1797,6 +1837,15 @@ export module Mock {
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
rpc: {
|
||||
name: 'Set RPC',
|
||||
description: 'Create RPC Credentials',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
properties: {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
@@ -1820,7 +1869,6 @@ export module Mock {
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'Web UI',
|
||||
description:
|
||||
@@ -1837,7 +1885,6 @@ export module Mock {
|
||||
},
|
||||
rpc: {
|
||||
id: 'rpc',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'RPC',
|
||||
description:
|
||||
@@ -1854,7 +1901,6 @@ export module Mock {
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
hasPrimary: true,
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
@@ -1873,9 +1919,23 @@ export module Mock {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
@@ -1928,7 +1988,8 @@ export module Mock {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
@@ -1939,7 +2000,8 @@ export module Mock {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
@@ -1956,17 +2018,45 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
@@ -2016,7 +2106,6 @@ export module Mock {
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'Web UI',
|
||||
description: 'A launchable web app for Bitcoin Proxy',
|
||||
@@ -2061,11 +2150,29 @@ export module Mock {
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
actions: {},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Config',
|
||||
description: 'LND needs configuration before starting',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
connect: {
|
||||
name: 'Connect',
|
||||
description: 'View LND connection details',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
grpc: {
|
||||
id: 'grpc',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'GRPC',
|
||||
description:
|
||||
@@ -2082,7 +2189,6 @@ export module Mock {
|
||||
},
|
||||
lndconnect: {
|
||||
id: 'lndconnect',
|
||||
hasPrimary: false,
|
||||
masked: true,
|
||||
name: 'LND Connect',
|
||||
description:
|
||||
@@ -2099,7 +2205,6 @@ export module Mock {
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
hasPrimary: true,
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
@@ -2136,6 +2241,24 @@ export module Mock {
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
config: {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND needs configuration before starting',
|
||||
},
|
||||
},
|
||||
connect: {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'connect',
|
||||
severity: 'important',
|
||||
reason: 'View LND connection details',
|
||||
},
|
||||
},
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
request: {
|
||||
@@ -2147,10 +2270,24 @@ export module Mock {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoind/rpc': {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'rpc',
|
||||
severity: 'important',
|
||||
reason: `LND want's its own RPC credentials`,
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
DomainInfo,
|
||||
NetworkStrategy,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { FetchLogsReq, FetchLogsRes, Log } from '@start9labs/shared'
|
||||
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
|
||||
import { Dump } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo } from '@start9labs/shared'
|
||||
@@ -118,6 +118,17 @@ export module RR {
|
||||
} // server.proxy.set-outbound
|
||||
export type SetOsOutboundProxyRes = null
|
||||
|
||||
// smtp
|
||||
|
||||
export type SetSMTPReq = T.SmtpValue // server.set-smtp
|
||||
export type SetSMTPRes = null
|
||||
|
||||
export type ClearSMTPReq = {} // server.clear-smtp
|
||||
export type ClearSMTPRes = null
|
||||
|
||||
export type TestSMTPReq = SetSMTPReq & { to: string } // server.test-smtp
|
||||
export type TestSMTPRes = null
|
||||
|
||||
// sessions
|
||||
|
||||
export type GetSessionsReq = {} // sessions.list
|
||||
@@ -326,18 +337,83 @@ export module RR {
|
||||
export type CreateBackupRes = null
|
||||
|
||||
// package
|
||||
// @TODO Matt I just copy-pasted those types from minor
|
||||
export type GetPackageLogsReq = {
|
||||
id: string
|
||||
before: boolean
|
||||
cursor?: string
|
||||
limit?: number
|
||||
} // package.logs
|
||||
export type GetPackageLogsRes = {
|
||||
entries: Log[]
|
||||
startCursor?: string
|
||||
endCursor?: string
|
||||
|
||||
export type InitAcmeReq = {
|
||||
provider: 'letsencrypt' | 'letsencrypt-staging' | string
|
||||
contact: string[]
|
||||
}
|
||||
export type InitAcmeRes = null
|
||||
|
||||
export type RemoveAcmeReq = {
|
||||
provider: string
|
||||
}
|
||||
export type RemoveAcmeRes = null
|
||||
|
||||
export type AddTorKeyReq = {
|
||||
// net.tor.key.add
|
||||
key: string
|
||||
}
|
||||
export type GenerateTorKeyReq = {} // net.tor.key.generate
|
||||
export type AddTorKeyRes = string // onion address without .onion suffix
|
||||
|
||||
export type ServerBindingSetPublicReq = {
|
||||
// server.host.binding.set-public
|
||||
internalPort: number
|
||||
public: boolean | null // default true
|
||||
}
|
||||
export type BindingSetPublicRes = null
|
||||
|
||||
export type ServerAddOnionReq = {
|
||||
// server.host.address.onion.add
|
||||
onion: string // address *with* .onion suffix
|
||||
}
|
||||
export type AddOnionRes = null
|
||||
|
||||
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
|
||||
export type RemoveOnionRes = null
|
||||
|
||||
export type ServerAddDomainReq = {
|
||||
// server.host.address.domain.add
|
||||
domain: string // FQDN
|
||||
private: boolean
|
||||
acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null
|
||||
}
|
||||
export type ServerAddDomainRes = null
|
||||
|
||||
export type ServerRemoveDomainReq = {
|
||||
// server.host.address.domain.remove
|
||||
domain: string // FQDN
|
||||
}
|
||||
export type RemoveDomainRes = null
|
||||
|
||||
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
|
||||
// package.host.binding.set-public
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgAddOnionReq = ServerAddOnionReq & {
|
||||
// package.host.address.onion.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
|
||||
|
||||
export type PkgAddDomainReq = ServerAddDomainReq & {
|
||||
// package.host.address.domain.add
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type PkgRemoveDomainReq = ServerRemoveDomainReq & {
|
||||
// package.host.address.domain.remove
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
|
||||
export type GetPackageLogsReq = GetServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = GetServerLogsRes
|
||||
|
||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||
@@ -601,10 +677,10 @@ export type ServerNotification<T extends number> = {
|
||||
export type NotificationData<T> = T extends 0
|
||||
? null
|
||||
: T extends 1
|
||||
? BackupReport
|
||||
: T extends 2
|
||||
? string
|
||||
: any
|
||||
? BackupReport
|
||||
: T extends 2
|
||||
? string
|
||||
: any
|
||||
|
||||
export type BackupReport = {
|
||||
server: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { RPCOptions } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { Observable } from 'rxjs'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
|
||||
export abstract class ApiService {
|
||||
// http
|
||||
@@ -32,7 +33,7 @@ export abstract class ApiService {
|
||||
abstract openWebsocket$<T>(
|
||||
guid: string,
|
||||
config?: RR.WebsocketConfig<T>,
|
||||
): Observable<T>
|
||||
): WebSocketSubject<T>
|
||||
|
||||
// state
|
||||
|
||||
@@ -137,6 +138,14 @@ export abstract class ApiService {
|
||||
params: RR.SetOsOutboundProxyReq,
|
||||
): Promise<RR.SetOsOutboundProxyRes>
|
||||
|
||||
// smtp
|
||||
|
||||
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
||||
|
||||
abstract clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes>
|
||||
|
||||
abstract testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes>
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
abstract registryRequest<T>(
|
||||
@@ -221,14 +230,6 @@ export abstract class ApiService {
|
||||
|
||||
abstract deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes>
|
||||
|
||||
// email
|
||||
|
||||
abstract testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes>
|
||||
|
||||
abstract configureEmail(
|
||||
params: RR.ConfigureEmailReq,
|
||||
): Promise<RR.ConfigureEmailRes>
|
||||
|
||||
// ssh
|
||||
|
||||
abstract getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes>
|
||||
@@ -342,4 +343,48 @@ export abstract class ApiService {
|
||||
abstract setServiceOutboundProxy(
|
||||
params: RR.SetServiceOutboundProxyReq,
|
||||
): Promise<RR.SetServiceOutboundProxyRes>
|
||||
|
||||
abstract initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes>
|
||||
|
||||
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
|
||||
|
||||
abstract addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract generateTorKey(
|
||||
params: RR.GenerateTorKeyReq,
|
||||
): Promise<RR.AddTorKeyRes>
|
||||
|
||||
abstract serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
|
||||
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract serverAddDomain(
|
||||
params: RR.ServerAddDomainReq,
|
||||
): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract serverRemoveDomain(
|
||||
params: RR.ServerRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes>
|
||||
|
||||
abstract pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes>
|
||||
|
||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
|
||||
abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes>
|
||||
|
||||
abstract pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes>
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
|
||||
import { ApiService } from './embassy-api.service'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
@@ -95,7 +95,7 @@ export class LiveApiService extends ApiService {
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T> = {},
|
||||
): Observable<T> {
|
||||
): WebSocketSubject<T> {
|
||||
const { location } = this.document.defaultView!
|
||||
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
const host = location.host
|
||||
@@ -458,16 +458,18 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'wifi.delete', params })
|
||||
}
|
||||
|
||||
// email
|
||||
// smtp
|
||||
|
||||
async testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes> {
|
||||
return this.rpcRequest({ method: 'email.test', params })
|
||||
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.set-smtp', params })
|
||||
}
|
||||
|
||||
async configureEmail(
|
||||
params: RR.ConfigureEmailReq,
|
||||
): Promise<RR.ConfigureEmailRes> {
|
||||
return this.rpcRequest({ method: 'email.configure', params })
|
||||
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.clear-smtp', params })
|
||||
}
|
||||
|
||||
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
|
||||
return this.rpcRequest({ method: 'server.test-smtp', params })
|
||||
}
|
||||
|
||||
// ssh
|
||||
@@ -640,6 +642,118 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
|
||||
}
|
||||
|
||||
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.acme.delete',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.acme.init',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.tor.key.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'net.tor.key.generate',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.ServerBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.binding.set-public',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.onion.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.onion.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverAddDomain(
|
||||
params: RR.ServerAddDomainReq,
|
||||
): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async serverRemoveDomain(
|
||||
params: RR.ServerRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.address.domain.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-public',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.onion.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.onion.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.add',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.address.domain.remove',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
urlOverride?: string,
|
||||
|
||||
@@ -19,16 +19,7 @@ import {
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { BackupTargetType, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import {
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
Subject,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
|
||||
import { mockPatchData } from './mock-patch'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -38,6 +29,8 @@ import {
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAcmeUrl } from 'src/app/utils/acme'
|
||||
|
||||
const PROGRESS: T.FullProgress = {
|
||||
overall: {
|
||||
@@ -113,11 +106,11 @@ export class MockApiService extends ApiService {
|
||||
openWebsocket$<T>(
|
||||
guid: string,
|
||||
config: RR.WebsocketConfig<T> = {},
|
||||
): Observable<T> {
|
||||
): WebSocketSubject<T> {
|
||||
if (guid === 'db-guid') {
|
||||
return this.mockWsSource$.pipe<any>(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
) as WebSocketSubject<T>
|
||||
} else if (guid === 'logs-guid') {
|
||||
return interval(50).pipe<any>(
|
||||
map((_, index) => {
|
||||
@@ -126,16 +119,16 @@ export class MockApiService extends ApiService {
|
||||
if (index === 100) throw new Error('HAAHHA')
|
||||
return Mock.ServerLogs[0]
|
||||
}),
|
||||
)
|
||||
) as WebSocketSubject<T>
|
||||
} else if (guid === 'init-progress-guid') {
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
) as WebSocketSubject<T>
|
||||
} else if (guid === 'sideload-progress-guid') {
|
||||
config.openObserver?.next(new Event(''))
|
||||
return from(this.initProgress()).pipe(
|
||||
startWith(PROGRESS),
|
||||
) as Observable<T>
|
||||
) as WebSocketSubject<T>
|
||||
} else {
|
||||
throw new Error('invalid guid type')
|
||||
}
|
||||
@@ -768,16 +761,9 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
// email
|
||||
// smtp
|
||||
|
||||
async testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
async configureEmail(
|
||||
params: RR.ConfigureEmailReq,
|
||||
): Promise<RR.ConfigureEmailRes> {
|
||||
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
@@ -791,6 +777,25 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/smtp',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// ssh
|
||||
|
||||
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
|
||||
@@ -1344,6 +1349,283 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/acme`,
|
||||
value: {
|
||||
[toAcmeUrl(params.provider)]: { contact: params.contact },
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const regex = new RegExp('/', 'g')
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/acme/${params.provider.replace(regex, '~1')}`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'vanityabcdefghijklmnop'
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'abcdefghijklmnopqrstuv'
|
||||
}
|
||||
|
||||
async serverBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/host/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/onions/0`,
|
||||
value: params.onion,
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
value: params.onion,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/onions/0`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/-1`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async serverRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/domains/${params.domain}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingSetPubic(
|
||||
params: RR.PkgBindingSetPublicReq,
|
||||
): Promise<RR.BindingSetPublicRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`,
|
||||
value: params.public,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
||||
value: params.onion,
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
value: params.onion,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: Operation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
|
||||
value: {
|
||||
[params.domain]: { public: !params.private, acme: params.acme },
|
||||
},
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
domain: params.domain,
|
||||
subdomain: null,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgRemoveDomain(
|
||||
params: RR.PkgRemoveDomainReq,
|
||||
): Promise<RR.RemoveDomainRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch: RemoveOperation[] = [
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`,
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async initProgress(): Promise<T.FullProgress> {
|
||||
const progress = JSON.parse(JSON.stringify(PROGRESS))
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Mock } from './api.fixures'
|
||||
import { knownACME } from 'src/app/utils/acme'
|
||||
const version = require('../../../../../../package.json').version
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
@@ -26,94 +28,49 @@ export const mockPatchData: DataModel = {
|
||||
ackInstructions: {},
|
||||
},
|
||||
serverInfo: {
|
||||
arch: 'x86_64',
|
||||
id: 'abcdefgh',
|
||||
version: '0.3.5.1',
|
||||
country: 'us',
|
||||
ui: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1111,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'myveryownspecialtoraddress.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.1.5',
|
||||
port: null,
|
||||
sslPort: 1111,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'elan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
|
||||
port: null,
|
||||
sslPort: 1111,
|
||||
},
|
||||
},
|
||||
],
|
||||
network: {
|
||||
domains: [],
|
||||
start9ToSubdomain: null,
|
||||
wifi: {
|
||||
enabled: false,
|
||||
lastRegion: null,
|
||||
interface: 'test',
|
||||
ssids: [],
|
||||
selected: null,
|
||||
},
|
||||
wanConfig: {
|
||||
upnp: false,
|
||||
forwards: [
|
||||
{
|
||||
assigned: 443,
|
||||
override: null,
|
||||
target: 443,
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
assigned: 80,
|
||||
override: null,
|
||||
target: 80,
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
assigned: 8332,
|
||||
override: null,
|
||||
target: 8332,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
proxies: [],
|
||||
outboundProxy: null,
|
||||
},
|
||||
version,
|
||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
networkInterfaces: {
|
||||
eth0: {
|
||||
public: false,
|
||||
ipInfo: {
|
||||
scopeId: 1,
|
||||
deviceType: 'ethernet',
|
||||
subnets: ['10.0.0.2/24'],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
},
|
||||
wlan0: {
|
||||
public: false,
|
||||
ipInfo: {
|
||||
scopeId: 2,
|
||||
deviceType: 'wireless',
|
||||
subnets: [
|
||||
'10.0.90.12/24',
|
||||
'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64',
|
||||
],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
acme: {
|
||||
[knownACME[0].url]: {
|
||||
contact: ['mailto:support@start9.com'],
|
||||
},
|
||||
},
|
||||
unreadNotifications: {
|
||||
count: 4,
|
||||
recent: Mock.Notifications,
|
||||
},
|
||||
eosVersionCompat: '>=0.3.0 <=0.3.0.1',
|
||||
// password is asdfasdf
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
packageVersionCompat: '>=0.3.0 <=0.3.6',
|
||||
postInitMigrationTodos: [],
|
||||
statusInfo: {
|
||||
currentBackup: null,
|
||||
updated: false,
|
||||
@@ -121,6 +78,109 @@ export const mockPatchData: DataModel = {
|
||||
restarting: false,
|
||||
shuttingDown: false,
|
||||
},
|
||||
hostname: 'random-words',
|
||||
host: {
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: null,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
preferredExternalPort: 80,
|
||||
addSsl: {
|
||||
preferredExternalPort: 443,
|
||||
alpn: { specified: ['http/1.1', 'h2'] },
|
||||
},
|
||||
secure: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
domains: {},
|
||||
onions: ['myveryownspecialtoraddress'],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'eth0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'wlan0',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'myveryownspecialtoraddress.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
|
||||
ntpSynced: false,
|
||||
@@ -171,6 +231,15 @@ export const mockPatchData: DataModel = {
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
rpc: {
|
||||
name: 'Set RPC',
|
||||
description: 'Create RPC Credentials',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
properties: {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
@@ -194,7 +263,6 @@ export const mockPatchData: DataModel = {
|
||||
serviceInterfaces: {
|
||||
ui: {
|
||||
id: 'ui',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'Web UI',
|
||||
description:
|
||||
@@ -211,7 +279,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
rpc: {
|
||||
id: 'rpc',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'RPC',
|
||||
description:
|
||||
@@ -228,7 +295,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
hasPrimary: true,
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
@@ -247,9 +313,23 @@ export const mockPatchData: DataModel = {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
@@ -302,7 +382,8 @@ export const mockPatchData: DataModel = {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
@@ -313,7 +394,8 @@ export const mockPatchData: DataModel = {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
@@ -330,17 +412,45 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
addresses: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
@@ -388,11 +498,29 @@ export const mockPatchData: DataModel = {
|
||||
status: {
|
||||
main: 'stopped',
|
||||
},
|
||||
actions: {},
|
||||
actions: {
|
||||
config: {
|
||||
name: 'Config',
|
||||
description: 'LND needs configuration before starting',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
connect: {
|
||||
name: 'Connect',
|
||||
description: 'View LND connection details',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: true,
|
||||
group: null,
|
||||
},
|
||||
},
|
||||
serviceInterfaces: {
|
||||
grpc: {
|
||||
id: 'grpc',
|
||||
hasPrimary: false,
|
||||
masked: false,
|
||||
name: 'GRPC',
|
||||
description:
|
||||
@@ -409,7 +537,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
lndconnect: {
|
||||
id: 'lndconnect',
|
||||
hasPrimary: false,
|
||||
masked: true,
|
||||
name: 'LND Connect',
|
||||
description:
|
||||
@@ -426,7 +553,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
p2p: {
|
||||
id: 'p2p',
|
||||
hasPrimary: true,
|
||||
masked: false,
|
||||
name: 'P2P',
|
||||
description:
|
||||
@@ -464,6 +590,24 @@ export const mockPatchData: DataModel = {
|
||||
developerKey: 'developer-key',
|
||||
outboundProxy: null,
|
||||
requestedActions: {
|
||||
config: {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'config',
|
||||
severity: 'critical',
|
||||
reason: 'LND needs configuration before starting',
|
||||
},
|
||||
},
|
||||
connect: {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'lnd',
|
||||
actionId: 'connect',
|
||||
severity: 'important',
|
||||
reason: 'View LND connection details',
|
||||
},
|
||||
},
|
||||
'bitcoind/config': {
|
||||
active: true,
|
||||
request: {
|
||||
@@ -475,10 +619,24 @@ export const mockPatchData: DataModel = {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
color: '#ffffff',
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'bitcoind/rpc': {
|
||||
active: true,
|
||||
request: {
|
||||
packageId: 'bitcoind',
|
||||
actionId: 'rpc',
|
||||
severity: 'important',
|
||||
reason: `LND want's its own RPC credentials`,
|
||||
input: {
|
||||
kind: 'partial',
|
||||
value: {
|
||||
rpcsettings: {
|
||||
rpcuser: 'lnd',
|
||||
},
|
||||
testnet: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from './patch-db/data-model'
|
||||
|
||||
const {
|
||||
gitHash,
|
||||
@@ -28,6 +28,7 @@ export class ConfigService {
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
supportsWebSockets = !!window.WebSocket
|
||||
|
||||
isTor(): boolean {
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
@@ -40,35 +41,54 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return (
|
||||
this.hostname === 'localhost' ||
|
||||
(useMocks && mocks.maskAs === 'localhost')
|
||||
)
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost' || this.hostname === '127.0.0.1'
|
||||
}
|
||||
|
||||
isIpv4(): boolean {
|
||||
return isValidIpv4(this.hostname) || (useMocks && mocks.maskAs === 'ipv4')
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv4'
|
||||
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isLanIpv4(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv4'
|
||||
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname) &&
|
||||
(this.hostname.startsWith('192.168.') ||
|
||||
this.hostname.startsWith('10.') ||
|
||||
(this.hostname.startsWith('172.') &&
|
||||
!![this.hostname.split('.').map(Number)[1]].filter(
|
||||
n => n >= 16 && n < 32,
|
||||
).length))
|
||||
}
|
||||
|
||||
isIpv6(): boolean {
|
||||
return isValidIpv6(this.hostname) || (useMocks && mocks.maskAs === 'ipv6')
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv6'
|
||||
: new RegExp(utils.Patterns.ipv6.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isClearnet(): boolean {
|
||||
return (
|
||||
(useMocks && mocks.maskAs === 'clearnet') ||
|
||||
(!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isIpv4() &&
|
||||
!this.isIpv6())
|
||||
)
|
||||
return useMocks
|
||||
? mocks.maskAs === 'clearnet'
|
||||
: this.isHttps() &&
|
||||
!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isLanIpv4() &&
|
||||
!this.isIpv6()
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
@@ -82,46 +102,155 @@ export class ConfigService {
|
||||
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(
|
||||
ui: T.ServiceInterface,
|
||||
hosts: PackageDataEntry['hosts'],
|
||||
interfaces: PackageDataEntry['serviceInterfaces'],
|
||||
hosts: T.Hosts,
|
||||
): string {
|
||||
if (
|
||||
ui.type !== 'ui' ||
|
||||
(ui.addressInfo.scheme !== 'http' && ui.addressInfo.sslScheme !== 'https')
|
||||
const ui = Object.values(interfaces).find(
|
||||
i =>
|
||||
i.type === 'ui' &&
|
||||
(i.addressInfo.scheme === 'http' ||
|
||||
i.addressInfo.sslScheme === 'https'),
|
||||
)
|
||||
return ''
|
||||
|
||||
const hostnameInfo =
|
||||
hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort]
|
||||
if (!ui) return ''
|
||||
|
||||
const host = hosts[ui.addressInfo.hostId]
|
||||
|
||||
if (!host) return ''
|
||||
|
||||
let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort]
|
||||
hostnameInfo = hostnameInfo.filter(
|
||||
h =>
|
||||
this.isLocalhost() ||
|
||||
h.kind !== 'ip' ||
|
||||
h.hostname.kind !== 'ipv6' ||
|
||||
!h.hostname.value.startsWith('fe80::'),
|
||||
)
|
||||
if (this.isLocalhost()) {
|
||||
const local = hostnameInfo.find(
|
||||
h => h.kind === 'ip' && h.hostname.kind === 'local',
|
||||
)
|
||||
if (local) {
|
||||
hostnameInfo.unshift({
|
||||
kind: 'ip',
|
||||
networkInterfaceId: 'lo',
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
port: local.hostname.port,
|
||||
sslPort: local.hostname.sslPort,
|
||||
value: 'localhost',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!hostnameInfo) return ''
|
||||
|
||||
const addressInfo = ui.addressInfo
|
||||
const scheme = this.isHttps()
|
||||
? ui.addressInfo.sslScheme === 'https'
|
||||
? 'https'
|
||||
: 'http'
|
||||
: ui.addressInfo.scheme === 'http'
|
||||
? 'http'
|
||||
: 'https'
|
||||
const username = addressInfo.username ? addressInfo.username + '@' : ''
|
||||
const suffix = addressInfo.suffix || ''
|
||||
const url = new URL(`${scheme}://${username}placeholder${suffix}`)
|
||||
|
||||
const onionHostname = hostnameInfo.find(h => h.kind === 'onion')
|
||||
?.hostname as T.OnionHostname | undefined
|
||||
|
||||
if (this.isTor() && onionHostname) {
|
||||
url.hostname = onionHostname.value
|
||||
} else {
|
||||
const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as
|
||||
| T.IpHostname
|
||||
const url = new URL(`https://${username}placeholder${suffix}`)
|
||||
const use = (hostname: {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}) => {
|
||||
url.hostname = hostname.value
|
||||
const useSsl =
|
||||
hostname.port && hostname.sslPort ? this.isHttps() : !!hostname.sslPort
|
||||
url.protocol = useSsl
|
||||
? `${addressInfo.sslScheme || 'https'}:`
|
||||
: `${addressInfo.scheme || 'http'}:`
|
||||
const port = useSsl ? hostname.sslPort : hostname.port
|
||||
const omitPort = useSsl
|
||||
? ui.addressInfo.sslScheme === 'https' && port === 443
|
||||
: ui.addressInfo.scheme === 'http' && port === 80
|
||||
if (!omitPort && port) url.port = String(port)
|
||||
}
|
||||
const useFirst = (
|
||||
hostnames: (
|
||||
| {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
| undefined
|
||||
)[],
|
||||
) => {
|
||||
const first = hostnames.find(h => h)
|
||||
if (first) {
|
||||
use(first)
|
||||
}
|
||||
return !!first
|
||||
}
|
||||
|
||||
if (!ipHostname) return ''
|
||||
const ipHostnames = hostnameInfo
|
||||
.filter(h => h.kind === 'ip')
|
||||
.map(h => h.hostname) as T.IpHostname[]
|
||||
const domainHostname = ipHostnames
|
||||
.filter(h => h.kind === 'domain')
|
||||
.map(h => h as T.IpHostname & { kind: 'domain' })
|
||||
.map(h => ({
|
||||
value: h.domain,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const wanIpHostname = hostnameInfo
|
||||
.filter(h => h.kind === 'ip' && h.public && h.hostname.kind !== 'domain')
|
||||
.map(h => h.hostname as Exclude<T.IpHostname, { kind: 'domain' }>)
|
||||
.map(h => ({
|
||||
value: h.value,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const onionHostname = hostnameInfo
|
||||
.filter(h => h.kind === 'onion')
|
||||
.map(h => h as T.HostnameInfo & { kind: 'onion' })
|
||||
.map(h => ({
|
||||
value: h.hostname.value,
|
||||
sslPort: h.hostname.sslPort,
|
||||
port: h.hostname.port,
|
||||
}))[0]
|
||||
const localHostname = ipHostnames
|
||||
.filter(h => h.kind === 'local')
|
||||
.map(h => h as T.IpHostname & { kind: 'local' })
|
||||
.map(h => ({ value: h.value, sslPort: h.sslPort, port: h.port }))[0]
|
||||
|
||||
url.hostname = this.hostname
|
||||
url.port = String(ipHostname.sslPort || ipHostname.port)
|
||||
if (this.isClearnet()) {
|
||||
if (
|
||||
!useFirst([domainHostname, wanIpHostname, onionHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.isTor()) {
|
||||
if (
|
||||
!useFirst([onionHostname, domainHostname, wanIpHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.isIpv6()) {
|
||||
const ipv6Hostname = ipHostnames.find(h => h.kind === 'ipv6') as {
|
||||
kind: 'ipv6'
|
||||
value: string
|
||||
scopeId: number
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
|
||||
if (!useFirst([ipv6Hostname, localHostname])) {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// ipv4 or .local or localhost
|
||||
|
||||
if (!localHostname) return ''
|
||||
|
||||
use({
|
||||
value: this.hostname,
|
||||
port: localHostname.port,
|
||||
sslPort: localHostname.sslPort,
|
||||
})
|
||||
}
|
||||
|
||||
return url.href
|
||||
@@ -130,32 +259,6 @@ export class ConfigService {
|
||||
getHost(): string {
|
||||
return this.host
|
||||
}
|
||||
|
||||
private isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidIpv4(address: string): boolean {
|
||||
const regexExp =
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||
return regexExp.test(address)
|
||||
}
|
||||
|
||||
export function isValidIpv6(address: string): boolean {
|
||||
const regexExp =
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi
|
||||
return regexExp.test(address)
|
||||
}
|
||||
|
||||
export function removeProtocol(str: string): string {
|
||||
if (str.startsWith('http://')) return str.slice(7)
|
||||
if (str.startsWith('https://')) return str.slice(8)
|
||||
return str
|
||||
}
|
||||
|
||||
export function removePort(str: string): string {
|
||||
return str.split(':')[0]
|
||||
}
|
||||
|
||||
export function hasUi(
|
||||
|
||||
@@ -124,11 +124,6 @@ export class FormService {
|
||||
),
|
||||
listValidators(spec),
|
||||
)
|
||||
case 'file':
|
||||
return this.formBuilder.control(
|
||||
currentValue || null,
|
||||
fileValidators(spec),
|
||||
)
|
||||
case 'union':
|
||||
return this.getUnionObject(spec, currentValue)
|
||||
case 'toggle':
|
||||
@@ -136,7 +131,7 @@ export class FormService {
|
||||
return this.formBuilder.control(value)
|
||||
case 'select':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value)
|
||||
return this.formBuilder.control(value, [Validators.required])
|
||||
case 'multiselect':
|
||||
value = currentValue === undefined ? spec.default : currentValue
|
||||
return this.formBuilder.control(value, multiselectValidators(spec))
|
||||
|
||||
21
web/projects/ui/src/app/utils/acme.ts
Normal file
21
web/projects/ui/src/app/utils/acme.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function toAcmeName(url: string | null): string | 'System CA' {
|
||||
return knownACME.find(acme => acme.url === url)?.name || url || 'System CA'
|
||||
}
|
||||
|
||||
export function toAcmeUrl(name: string): string {
|
||||
return knownACME.find(acme => acme.name === name)?.url || name
|
||||
}
|
||||
|
||||
export const knownACME: {
|
||||
name: string
|
||||
url: string
|
||||
}[] = [
|
||||
{
|
||||
name: `Let's Encrypt`,
|
||||
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
},
|
||||
{
|
||||
name: `Let's Encrypt (Staging)`,
|
||||
url: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
||||
},
|
||||
]
|
||||
@@ -20,5 +20,5 @@
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user