Skip to content

Commit

Permalink
wip: site cert handling
Browse files Browse the repository at this point in the history
  • Loading branch information
arpowers committed Mar 19, 2024
1 parent ff7308b commit 669ff32
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 41 deletions.
36 changes: 35 additions & 1 deletion @fiction/core/utils/test/url.ci.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
import { describe, expect, it } from 'vitest'

import { displayDomain, getDomainFavicon, getUrlPath, refineRoute, safeUrl, standardizeUrlOrPath, updateUrl, urlPath } from '../url'
import { displayDomain, getDomainFavicon, getUrlPath, refineRoute, safeUrl, standardizeUrlOrPath, updateUrl, urlPath, validHost } from '../url'

describe('validHost', () => {
it('should return the hostname for a valid URL with protocol and path', () => {
expect(validHost('http://www.fiction.com/path')).toBe('www.fiction.com')
})

it('should return the hostname for a valid URL with HTTPS and query', () => {
expect(validHost('https://foo.com?query=string')).toBe('foo.com')
})

it('should return the hostname for a valid host without protocol', () => {
expect(validHost('foo.what.foo.com')).toBe('foo.what.foo.com')
})

it('should return false for an invalid URL', () => {
expect(validHost('invalid-url')).toBe(false)
})

it('should return false for a URL missing top-level domain', () => {
expect(validHost('http://localhost')).toBe(false)
})

it('should handle URLs with subdomains correctly', () => {
expect(validHost('http://sub.domain.fiction.com')).toBe('sub.domain.fiction.com')
})

it('should return false for a URL with spaces', () => {
expect(validHost('http://www. fiction.com')).toBe(false)
})

it('should return the hostname for a URL with port number', () => {
expect(validHost('http://www.fiction.com:8080')).toBe('www.fiction.com')
})
})

describe('refineRoute', () => {
it('should handle misc vars', () => {
Expand Down
12 changes: 12 additions & 0 deletions @fiction/core/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
/**
* get host part of URL, useful for domain handling
*/
export function validHost(host?: string) {
if (!host)
return false

// Updated pattern to handle query strings or fragments
const pattern = /^(?:http:\/\/|https:\/\/)?([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/
const match = host.match(pattern)
return match ? match[1] : false
}
export function refineRoute(
routePath: string,
replacers?: Record<string, unknown>,
Expand Down
6 changes: 5 additions & 1 deletion @fiction/plugin-sites/el/InputAi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ function updateGeneration(opt: InputOptionGeneration, value: InputOptionGenerati
[opt.key]: { ...opt, ...value },
}
}
const numFields = vue.computed(() => {
return Object.keys(card.value?.generation.inputConfig.value || {}).length
})
</script>

<template>
Expand All @@ -50,7 +54,7 @@ function updateGeneration(opt: InputOptionGeneration, value: InputOptionGenerati
:loading="loading"
>
<span class="i-tabler-sparkles text-base" />
<span>Generate</span>
<span>Generate ({{ numFields }})</span>
</ElButton>
</div>

Expand Down
18 changes: 3 additions & 15 deletions @fiction/plugin-sites/el/ToolPageGlobal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { InputOption } from '@fiction/ui'
import ElInput from '@fiction/ui/ElInput.vue'
import ElForm from '@fiction/ui/ElForm.vue'
import type { Site } from '../site'
import { requestManagePage } from '../utils/region'
import { updateSite } from '../utils/site'
import { saveSite, updateSite } from '../utils/site'
import type { EditorTool } from './tools'
import ElTool from './ElTool.vue'
import ToolForm from './ToolForm.vue'
Expand All @@ -15,7 +14,6 @@ const props = defineProps({
tool: { type: Object as vue.PropType<EditorTool>, required: true },
})
const control = props.site.settings.fictionSites
const loading = vue.ref(false)
const options = [
Expand All @@ -26,7 +24,7 @@ const options = [
input: 'group',
options: [
new InputOption({ key: 'title', label: 'Site Title', input: 'InputText', isRequired: true }),
new InputOption({ key: 'userConfig.faviconUrl', label: 'Favicon (32px x 32px)', input: 'InputMediaUpload' }),
new InputOption({ key: 'userConfig.faviconUrl', label: 'Favicon', input: 'InputMediaUpload' }),
new InputOption({ key: 'userConfig.timeZone', label: 'Site Time Zone', input: 'InputTimezone' }),
new InputOption({ key: 'userConfig.languageCode', label: 'Site Language Code', input: 'InputText', placeholder: 'en' }),
],
Expand All @@ -50,18 +48,8 @@ vue.onMounted(() => {
async function save() {
loading.value = true
await requestManagePage({
site: props.site,
_action: 'upsert',
regionCard: props.site.editPageConfig.value,
delay: 400,
successMessage: 'Page Saved',
})
await saveSite({ site: props.site, successMessage: 'Settings saved' })
loading.value = false
props.site.editPageConfig.value = {}
control.useTool({ toolId: 'pages' })
}
const v = vue.computed({
Expand Down
39 changes: 18 additions & 21 deletions @fiction/plugin-sites/el/ToolPagePublish.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useService, vue } from '@fiction/core'
import { InputOption } from '@fiction/ui'
import ElButton from '@fiction/ui/ElButton.vue'
import ElForm from '@fiction/ui/ElForm.vue'
import ElModalConfirm from '@fiction/ui/ElModalConfirm.vue'
import type { Site } from '../site'
import type { TableSiteConfig } from '../tables'
import { tableNames } from '../tables'
Expand All @@ -27,12 +28,12 @@ function getSuffixUrl() {
const options = [
new InputOption({
key: 'editor.hidePublishing',
label: 'Publishing',
label: 'Domain',
input: 'group',
options: [
new InputOption({
key: 'subDomain',
label: 'Development / Staging Domain',
label: 'Fiction Domain',
input: 'InputUsername',
isRequired: true,
props: {
Expand All @@ -44,7 +45,7 @@ const options = [
}),
new InputOption({
key: 'customDomains',
label: 'Production Domain (Custom)',
label: 'Custom Domain',
input: vue.defineAsyncComponent(() => import('./InputCustomDomains.vue')),
isRequired: true,
props: {
Expand All @@ -59,49 +60,45 @@ const options = [
const tempSite = vue.ref<Partial<TableSiteConfig>>({})
const v = vue.computed({
get: () => ({ ...props.site.toConfig(), ...tempSite.value }),
set: v => (tempSite.value = v),
get: () => ({ ...props.site.toConfig(), ...props.site.editor.value.tempSite }),
set: v => (props.site.editor.value.tempSite = v),
})
async function save() {
const confirmed = confirm('Make sure to update your DNS settings to point to the new domain')
if (!confirmed)
return
loading.value = true
await saveSite({ site: props.site, delayUntilSaveConfig: tempSite.value, successMessage: 'Settings saved' })
tempSite.value = {}
await saveSite({ site: props.site, delayUntilSaveConfig: props.site.editor.value.tempSite, successMessage: 'Settings saved' })
props.site.editor.value.tempSite = {}
loading.value = false
}
function reset() {
tempSite.value = {}
}
const showConfirm = vue.ref(false)
</script>

<template>
<ElTool
:actions="[]"
v-bind="props"
>
<ElForm @submit="save()">
<ElForm @submit="showConfirm = true">
<ToolForm v-model="v" :options="options" :site="site" />

<div class="text-right px-4 py-2 border-t border-theme-200 dark:border-theme-600 pt-4 space-x-4 flex justify-between">
<ElButton btn="default" @click="reset()">
Reset
</ElButton>
<ElButton :loading="loading" type="submit" btn="primary" :disabled="Object.keys(tempSite).length === 0">
Save Domain Settings
<ElButton :loading="loading" type="submit" btn="primary" :disabled="Object.keys(props.site.editor.value.tempSite).length === 0">
Publish Domain Changes
</ElButton>
</div>
</ElForm>
<ElModalConfirm
v-model:vis="showConfirm"
title="Domain Changes"
sub="Changes to your sub domain or custom domain will take effect immediately. You'll need to make appropriate changes with your domain provider. Are you sure you want to proceed?"
@confirmed="save()"
/>
</ElTool>
</template>

<style lang="less" scoped>
.region-setting-input {
--input-bg: theme('colors.theme.100');
}
</style>activeSiteHostname,
8 changes: 6 additions & 2 deletions @fiction/plugin-sites/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { DataFilter, EndpointMeta, EndpointResponse } from '@fiction/core'
import { AdminQuery } from '@fiction/plugin-admin'
import { deepMerge } from '@fiction/core'
import type { Knex } from 'knex'
import type { CardConfigPortable, TableCardConfig, TableSiteConfig } from './tables'
import type { CardConfigPortable, TableCardConfig, TableDomainConfig, TableSiteConfig } from './tables'
import { tableNames } from './tables'
import { incrementSlugId } from './util'
import { updateSiteCerts } from './utils/cert'
import { Card } from './card'
import type { FictionSites, SitesPluginSettings } from '.'

Expand Down Expand Up @@ -174,7 +175,7 @@ export class ManagePage extends SitesQuery {
}

type CreateManageSiteParams = { _action: 'create', fields: Partial<TableSiteConfig>, userId: string, orgId: string }
type EditManageSiteParams = { _action: 'update' | 'delete', fields: Partial<TableSiteConfig>, where: { siteId?: string, subDomain?: string }, userId: string, orgId: string }
type EditManageSiteParams = { _action: 'update' | 'delete', publishDomains?: boolean, fields: Partial<TableSiteConfig>, where: { siteId?: string, subDomain?: string }, userId: string, orgId: string }
type GetManageSiteParams = { _action: 'retrieve', where: { siteId?: string, subDomain?: string }, userId?: string, orgId?: string }

type ManageSiteParams = (CreateManageSiteParams | EditManageSiteParams | GetManageSiteParams) & { caller?: string, successMessage?: string }
Expand Down Expand Up @@ -345,6 +346,9 @@ export class ManageSite extends SitesQuery {
await Promise.all(upsertPromises)
}

if (params.publishDomains)
await updateSiteCerts({ site: data, fictionSites: this.settings.fictionSites, fictionDb, flyIoAppId: this.settings.flyIoAppId }, meta)

message = 'site saved'
}

Expand Down
2 changes: 2 additions & 0 deletions @fiction/plugin-sites/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type EditorState = {
selectedCardId: string
selectedPageId: string
tempPage: CardConfigPortable
tempSite: Partial<TableSiteConfig>
selectedRegionId: PageRegion | undefined
savedCardOrder: Record<string, string[]>
}
Expand Down Expand Up @@ -83,6 +84,7 @@ export class Site<T extends SiteSettings = SiteSettings> extends FictionObject<T
selectedRegionId: 'main',
savedCardOrder: {},
tempPage: {},
tempSite: {},
...this.settings.editor,
})

Expand Down
49 changes: 49 additions & 0 deletions @fiction/plugin-sites/utils/cert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { type EndpointMeta, type FictionDb, _stop, prepareFields } from '@fiction/core'
import type { TableDomainConfig, TableSiteConfig } from '../tables'
import { tableNames } from '../tables'
import type { FictionSites } from '..'

export async function updateSiteCerts(args: { site: Partial<TableSiteConfig>, fictionSites: FictionSites, fictionDb: FictionDb, flyIoAppId: string }, meta: EndpointMeta) {
const { site, fictionSites, fictionDb, flyIoAppId } = args
const { siteId, customDomains } = site

const db = fictionDb.client()

const existingDomains = await db(tableNames.domains).select<TableDomainConfig[]>('*').where({ siteId })

await Promise.all(existingDomains.map(async (domain) => {
if (!customDomains?.some(d => d.hostname === domain.hostname)) {
const result = await fictionSites.queries.ManageCert.serve({ _action: 'delete', hostname: domain.hostname }, { caller: 'updateSiteCerts' })
if (result.status !== 'success')
throw _stop('cert not deleted', { data: { domain, result } })

await db(tableNames.domains).delete().where({ hostname: domain.hostname, siteId })
}
}))

const domains = (await Promise.all(customDomains?.map(async (domain) => {
if (!domain.hostname || existingDomains.some(d => d.hostname === domain.hostname))
return

const result = await fictionSites.queries.ManageCert.serve({
_action: 'create',
hostname: domain.hostname,
siteId,
appId: flyIoAppId,
}, { caller: 'updateSiteCerts' })

if (result.status !== 'success')
throw _stop('cert not created', { data: { domain, result } })

if (result.data) {
const prepped = prepareFields({ type: 'internal', fields: { ...domain, ...result.data }, table: tableNames.domains, meta, fictionDb })
return (await db(tableNames.domains)
.insert({ siteId, hostname: domain.hostname, ...prepped })
.onConflict(['hostname', 'site_id'])
.merge()
.returning<TableDomainConfig[]>('*'))[0]
}
}) || []) || []).filter(Boolean) as TableDomainConfig[]

return domains
}
3 changes: 2 additions & 1 deletion @fiction/ui/ElModalConfirm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ const loading = vue.ref(false)
async function confirmed() {
loading.value = true
emit('confirmed', true)
await waitFor(500)
await waitFor(100)
loading.value = false
emit('update:vis', false)
}
</script>

Expand Down

0 comments on commit 669ff32

Please sign in to comment.