mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
better acme ux (#2820)
* better acme ux * fix patching arrays... again --------- Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
2
patch-db
2
patch-db
Submodule patch-db updated: 36eb59b79e...0df18c651f
@@ -19,7 +19,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|||||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||||
import { FormComponent } from 'src/app/components/form.component'
|
import { FormComponent } from 'src/app/components/form.component'
|
||||||
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
|
||||||
import { ACME_URL, toAcmeName } from 'src/app/util/acme'
|
import { toAcmeName } from 'src/app/util/acme'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
|
|
||||||
export type MappedInterface = T.ServiceInterface & {
|
export type MappedInterface = T.ServiceInterface & {
|
||||||
@@ -167,7 +167,7 @@ export class InterfaceInfoComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showAcme(url: ACME_URL | string | null): Promise<void> {
|
async showAcme(url: string | null): Promise<void> {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'ACME Provider',
|
header: 'ACME Provider',
|
||||||
message: toAcmeName(url),
|
message: toAcmeName(url),
|
||||||
|
|||||||
@@ -25,31 +25,28 @@
|
|||||||
|
|
||||||
<ion-item-divider>Saved Providers</ion-item-divider>
|
<ion-item-divider>Saved Providers</ion-item-divider>
|
||||||
|
|
||||||
<ion-item button detail="false" (click)="presentFormAcme()">
|
|
||||||
<ion-icon slot="start" name="add" color="dark"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
<b>Add Provider</b>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ng-container *ngIf="acme$ | async as acme">
|
<ng-container *ngIf="acme$ | async as acme">
|
||||||
<ion-item *ngFor="let provider of acme | keyvalue">
|
<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-icon slot="start" name="finger-print" size="medium"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ toAcmeName(provider.key) }}</h2>
|
<h2>{{ toAcmeName(provider.url) }}</h2>
|
||||||
<p *ngFor="let contact of provider.value.contact">
|
<p>Contact: {{ provider.contactString }}</p>
|
||||||
Contact: {{ contact }}
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-button
|
<ion-buttons slot="end">
|
||||||
slot="end"
|
<ion-button (click)="editAcme(provider.url, provider.contact)">
|
||||||
fill="clear"
|
<ion-icon slot="start" name="pencil"></ion-icon>
|
||||||
color="danger"
|
</ion-button>
|
||||||
(click)="removeAcme(provider.key)"
|
<ion-button (click)="removeAcme(provider.url)">
|
||||||
>
|
<ion-icon slot="start" name="trash-outline"></ion-icon>
|
||||||
<ion-icon slot="start" name="close"></ion-icon>
|
</ion-button>
|
||||||
Remove
|
</ion-buttons>
|
||||||
</ion-button>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { FormDialogService } from '../../../services/form-dialog.service'
|
|||||||
import { FormComponent } from '../../../components/form.component'
|
import { FormComponent } from '../../../components/form.component'
|
||||||
import { configBuilderToSpec } from '../../../util/configBuilderToSpec'
|
import { configBuilderToSpec } from '../../../util/configBuilderToSpec'
|
||||||
import { ISB, utils } from '@start9labs/start-sdk'
|
import { ISB, utils } from '@start9labs/start-sdk'
|
||||||
import { ACME_Name, ACME_URL, knownACME, toAcmeName } from 'src/app/util/acme'
|
import { knownACME, toAcmeName } from 'src/app/util/acme'
|
||||||
|
import { map } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'acme',
|
selector: 'acme',
|
||||||
@@ -17,7 +18,21 @@ import { ACME_Name, ACME_URL, knownACME, toAcmeName } from 'src/app/util/acme'
|
|||||||
export class ACMEPage {
|
export class ACMEPage {
|
||||||
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
|
||||||
|
|
||||||
acme$ = this.patch.watch$('serverInfo', 'acme')
|
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
|
toAcmeName = toAcmeName
|
||||||
|
|
||||||
@@ -29,21 +44,55 @@ export class ACMEPage {
|
|||||||
private readonly formDialog: FormDialogService,
|
private readonly formDialog: FormDialogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async presentFormAcme() {
|
async addAcme(
|
||||||
|
providers: {
|
||||||
|
url: string
|
||||||
|
contact: string[]
|
||||||
|
contactString: string
|
||||||
|
}[],
|
||||||
|
) {
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add ACME Provider',
|
label: 'Add ACME Provider',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(acmeSpec),
|
spec: await configBuilderToSpec(
|
||||||
|
getAddAcmeSpec(providers.map(p => p.url)),
|
||||||
|
),
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: 'Save',
|
text: 'Save',
|
||||||
handler: async (val: typeof acmeSpec._TYPE) => this.saveAcme(val),
|
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) {
|
async removeAcme(provider: string) {
|
||||||
const loader = this.loader.open('Removing').subscribe()
|
const loader = this.loader.open('Removing').subscribe()
|
||||||
|
|
||||||
@@ -56,18 +105,14 @@ export class ACMEPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveAcme(val: typeof acmeSpec._TYPE) {
|
private async saveAcme(providerUrl: string, contact: string[]) {
|
||||||
|
console.log(providerUrl, contact)
|
||||||
const loader = this.loader.open('Saving').subscribe()
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
const rawUrl =
|
|
||||||
val.provider.selection === 'other'
|
|
||||||
? val.provider.value.url
|
|
||||||
: val.provider.selection
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.api.initAcme({
|
await this.api.initAcme({
|
||||||
provider: new URL(rawUrl).href,
|
provider: new URL(providerUrl).href,
|
||||||
contact: [`mailto:${val.contact}`],
|
contact: contact.map(address => `mailto:${address}`),
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -79,39 +124,56 @@ export class ACMEPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const acmeSpec = ISB.InputSpec.of({
|
const emailListSpec = ISB.Value.list(
|
||||||
provider: ISB.Value.union(
|
ISB.List.text(
|
||||||
{ name: 'Provider', default: knownACME['Let\'s Encrypt'] as any },
|
{
|
||||||
ISB.Variants.of({
|
name: 'Contact Emails',
|
||||||
...Object.entries(knownACME).reduce(
|
description:
|
||||||
(obj, [name, url]) => ({
|
'Needed to obtain a certificate from a Certificate Authority',
|
||||||
...obj,
|
minLength: 1,
|
||||||
[url]: {
|
},
|
||||||
name,
|
{
|
||||||
spec: ISB.InputSpec.of({}),
|
inputmode: 'email',
|
||||||
},
|
patterns: [utils.Patterns.email],
|
||||||
}),
|
},
|
||||||
{},
|
|
||||||
),
|
|
||||||
other: {
|
|
||||||
name: 'Other',
|
|
||||||
spec: ISB.InputSpec.of({
|
|
||||||
url: ISB.Value.text({
|
|
||||||
name: 'URL',
|
|
||||||
default: null,
|
|
||||||
required: true,
|
|
||||||
inputmode: 'url',
|
|
||||||
patterns: [utils.Patterns.url],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
contact: ISB.Value.text({
|
)
|
||||||
name: 'Contact Email',
|
|
||||||
default: null,
|
function getAddAcmeSpec(providers: string[]) {
|
||||||
required: true,
|
const availableAcme = knownACME.filter(acme => !providers.includes(acme.url))
|
||||||
inputmode: 'email',
|
|
||||||
patterns: [utils.Patterns.email],
|
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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1064,7 +1064,7 @@ export class MockApiService extends ApiService {
|
|||||||
op: PatchOp.ADD,
|
op: PatchOp.ADD,
|
||||||
path: `/serverInfo/acme`,
|
path: `/serverInfo/acme`,
|
||||||
value: {
|
value: {
|
||||||
[toAcmeUrl(params.provider)]: { contact: [params.contact] },
|
[toAcmeUrl(params.provider)]: { contact: params.contact },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
acme: {
|
acme: {
|
||||||
[Object.keys(knownACME)[0]]: {
|
[knownACME[0].url]: {
|
||||||
contact: ['mailto:support@start9.com'],
|
contact: ['mailto:support@start9.com'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export function toAcmeName(url: ACME_URL | string | null): ACME_Name | string {
|
export function toAcmeName(url: string | null): string | 'System CA' {
|
||||||
return (
|
return knownACME.find(acme => acme.url === url)?.name || url || 'System CA'
|
||||||
Object.entries(knownACME).find(([_, val]) => val === url)?.[0] ||
|
|
||||||
url ||
|
|
||||||
'System CA'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toAcmeUrl(name: ACME_Name | string): ACME_URL | string {
|
export function toAcmeUrl(name: string): string {
|
||||||
return knownACME[name as ACME_Name] || name
|
return knownACME.find(acme => acme.name === name)?.url || name
|
||||||
}
|
}
|
||||||
|
|
||||||
export const knownACME = {
|
export const knownACME: {
|
||||||
'Let\'s Encrypt': 'https://acme-v02.api.letsencrypt.org/directory',
|
name: string
|
||||||
'Let\'s Encrypt (Staging)':
|
url: string
|
||||||
'https://acme-staging-v02.api.letsencrypt.org/directory',
|
}[] = [
|
||||||
}
|
{
|
||||||
|
name: `Let's Encrypt`,
|
||||||
export type ACME_Name = keyof typeof knownACME
|
url: 'https://acme-v02.api.letsencrypt.org/directory',
|
||||||
|
},
|
||||||
export type ACME_URL = (typeof knownACME)[ACME_Name]
|
{
|
||||||
|
name: `Let's Encrypt (Staging)`,
|
||||||
|
url: 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user