migrate redirect to flyio for cheaper domain certs

main
TZGyn 1 year ago
parent e7edf16138
commit 2723a9391c
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

@ -4,9 +4,9 @@ HOST_HEADER=x-forwarded-host
DATABASE_URL=postgres://postgres:password@127.0.0.1:5432/link-shortener DATABASE_URL=postgres://postgres:password@127.0.0.1:5432/link-shortener
PUBLIC_SHORTENER_URL=localhost:3000 PUBLIC_SHORTENER_URL=localhost:3000
APP_ENV=local APP_ENV=local
PUBLIC_SHORTENER_IP=1.1.1.1 PUBLIC_SHORTENER_IP=1.1.1.1 # ignore if using other hosting providers listed below
PRIVATE_HOSTING_PROVIDER= # optional, (options: railway), if you don't have a hosting provider, leave it blank PRIVATE_HOSTING_PROVIDER= # optional, (options: railway, fly.io), if you don't have a hosting provider, leave it blank
PRIVATE_MAIL_PROVIDER= # optional, (options: resend), if you don't have a mail provider, leave it blank PRIVATE_MAIL_PROVIDER= # optional, (options: resend), if you don't have a mail provider, leave it blank
# Railway config (if you have a Railway hosting provider) # Railway config (if you have a Railway hosting provider)
@ -15,5 +15,12 @@ PRIVATE_RAILWAY_ENVIRONMENT_ID=
PRIVATE_RAILWAY_PROJECT_ID= PRIVATE_RAILWAY_PROJECT_ID=
PRIVATE_RAILWAY_SERVICE_ID= PRIVATE_RAILWAY_SERVICE_ID=
# Fly.io config (if you have a Fly.io hosting provider)
PRIVATE_FLYIO_API_KEY=
PRIVATE_FLYIO_APP_ID=
PRIVATE_FLYIO_CNAME=
PRIVATE_FLYIO_IPV4=
PRIVATE_FLYIO_IPV6=
# Resend config (if you have a Resend hosting provider) # Resend config (if you have a Resend hosting provider)
PRIVATE_RESEND_API_KEY= PRIVATE_RESEND_API_KEY=

Binary file not shown.

@ -27,6 +27,20 @@ export const createCustomDomain = async (domain: string) => {
id: response.domain.id, id: response.domain.id,
ip: response.domain.record, ip: response.domain.record,
} as const } as const
} else if (env.PRIVATE_HOSTING_PROVIDER == 'fly.io') {
const { flyioClient } = await import('./flyio')
const response = await flyioClient.createCustomDomain(domain)
if (!response.success) {
return { success: false, id: undefined, ip: undefined } as const
}
return {
success: true,
id: domain,
ip: undefined,
} as const
} else { } else {
return { id: undefined, ip: undefined } as const return { id: undefined, ip: undefined } as const
} }
@ -45,6 +59,18 @@ export const deleteCustomDomain = async (domain: string | null) => {
return { success: false } as const return { success: false } as const
} }
return {
success: true,
} as const
} else if (env.PRIVATE_HOSTING_PROVIDER === 'fly.io') {
const { flyioClient } = await import('./flyio')
const response = await flyioClient.deleteCustomDomain(domain)
if (!response.success) {
return { success: false } as const
}
return { return {
success: true, success: true,
} as const } as const

@ -0,0 +1,122 @@
import { env } from '$env/dynamic/private'
class FlyioAPI {
constructor(private apiKey: string) {}
private sendRequest(query: string, variables: any) {
return fetch(`https://api.fly.io/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ query, variables }),
})
}
async checkDomain(domain: string) {
const query = `
query customDomainAvailable($domain: String!) {
customDomainAvailable(domain: $domain) {
__typename
available
message
}
}
`
const variables = {
domain: domain,
}
const response = await this.sendRequest(query, variables)
if (response.ok) {
const data: any = await response.json()
if (data.data.customDomainAvailable.available) {
return true
} else {
return false
}
} else {
return false
}
}
async createCustomDomain(domain: string) {
const query = `
mutation($appId: ID!, $hostname: String!) {
addCertificate(appId: $appId, hostname: $hostname) {
certificate {
configured
acmeDnsConfigured
acmeAlpnConfigured
certificateAuthority
certificateRequestedAt
dnsProvider
dnsValidationInstructions
dnsValidationHostname
dnsValidationTarget
hostname
id
source
}
}
}
`
const variables = {
appId: env.PRIVATE_FLYIO_APP_ID,
hostname: domain,
}
const response = await this.sendRequest(query, variables)
const data: any = await response.json()
if (!data.errors) {
return {
success: true,
} as const
} else {
return {
success: false,
message: 'Something went wrong',
} as const
}
}
async deleteCustomDomain(domain: string) {
const query = `
mutation ($appId: ID!, $hostname: String!) {
deleteCertificate(appId: $appId, hostname: $hostname) {
certificate {
configured
acmeDnsConfigured
acmeAlpnConfigured
certificateAuthority
certificateRequestedAt
dnsProvider
dnsValidationInstructions
dnsValidationHostname
dnsValidationTarget
hostname
id
source
}
}
}
`
const variables = {
appId: env.PRIVATE_FLYIO_APP_ID,
hostname: domain,
}
const response = await this.sendRequest(query, variables)
const data: any = await response.json()
if (!data.errors) {
console.log('Fly.io Delete Domain', data)
return {
success: true,
} as const
} else {
return {
success: false,
} as const
}
}
}
export const flyioClient = new FlyioAPI(env.PRIVATE_FLYIO_API_KEY)

@ -0,0 +1,72 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index.js'
import * as Card from '$lib/components/ui/card/index.js'
import { Button } from '$lib/components/ui/button/index.js'
import { Input } from '$lib/components/ui/input/index.js'
import { Label } from '$lib/components/ui/label/index.js'
export let cname_record: string
export let a_record: string
export let aaaa_record: string
const defaultValue = cname_record ? 'cname' : 'a'
</script>
<Tabs.Root value={defaultValue}>
<Tabs.List>
{#if cname_record}
<Tabs.Trigger value="cname">CNAME</Tabs.Trigger>
{/if}
{#if a_record || aaaa_record}
<Tabs.Trigger value="a">A and AAAA</Tabs.Trigger>
{/if}
</Tabs.List>
{#if cname_record}
<Tabs.Content value="cname">
<Card.Root>
<Card.Header class="grid grid-cols-[80px_1fr] space-y-0 py-4">
<Card.Title>Type</Card.Title>
<Card.Title>Value</Card.Title>
</Card.Header>
<Card.Footer class="bg-muted grid grid-cols-[80px_1fr] py-4">
<Card.Description>CNAME</Card.Description>
<Card.Description>{cname_record}</Card.Description>
</Card.Footer>
</Card.Root>
</Tabs.Content>
{/if}
{#if a_record || aaaa_record}
<Tabs.Content value="a">
<div class="flex flex-col gap-4">
{#if a_record}
<Card.Root>
<Card.Header
class="grid grid-cols-[80px_1fr] space-y-0 py-4">
<Card.Title>Type</Card.Title>
<Card.Title>Value</Card.Title>
</Card.Header>
<Card.Footer
class="bg-muted grid grid-cols-[80px_1fr] py-4">
<Card.Description>A</Card.Description>
<Card.Description>{a_record}</Card.Description>
</Card.Footer>
</Card.Root>
{/if}
{#if aaaa_record}
<Card.Root>
<Card.Header
class="grid grid-cols-[80px_1fr] space-y-0 py-4">
<Card.Title>Type</Card.Title>
<Card.Title>Value</Card.Title>
</Card.Header>
<Card.Footer
class="bg-muted grid grid-cols-[80px_1fr] py-4">
<Card.Description>AAAA</Card.Description>
<Card.Description>{aaaa_record}</Card.Description>
</Card.Footer>
</Card.Root>
{/if}
</div>
</Tabs.Content>
{/if}
</Tabs.Root>

@ -0,0 +1,61 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js'
import { InfoIcon } from 'lucide-svelte'
export let domain: string
export let custom_ip: string | null
export let cname_record: string
export let a_record: string
export let aaaa_record: string
</script>
<Tooltip.Root>
<Tooltip.Trigger class="flex items-center gap-1">
<InfoIcon class="h-4 w-4" />
{#if custom_ip}
{custom_ip}
{:else if cname_record}
{cname_record}
{:else if a_record}
{a_record}
{:else if aaaa_record}
{aaaa_record}
{:else}
{'Public IP not found'}
{/if}
</Tooltip.Trigger>
<Tooltip.Content>
{#if custom_ip}
<div>
{'Create a CNAME/ALIAS record for ' +
domain +
' to ' +
custom_ip}
</div>
{/if}
{#if cname_record}
<div>
{'Create a CNAME/ALIAS record for ' +
domain +
' to ' +
cname_record}
</div>
{/if}
{#if a_record}
<div>
{'Create a A record for ' + domain + ' to ' + a_record}
</div>
{/if}
{#if aaaa_record}
<div>
{'Create a AAAA record for ' + domain + ' to ' + aaaa_record}
</div>
{/if}
{#if !(custom_ip || cname_record || a_record || aaaa_record)}
<div>
{'Public IP not found'}
</div>
{/if}
</Tooltip.Content>
</Tooltip.Root>

@ -19,10 +19,27 @@ import {
createCustomDomain, createCustomDomain,
deleteCustomDomain, deleteCustomDomain,
} from '$lib/server/domain' } from '$lib/server/domain'
import { env } from '$env/dynamic/private'
import { PUBLIC_SHORTENER_IP } from '$env/static/public'
export const load = (async (event) => { export const load = (async (event) => {
const { project } = await event.parent() const { project } = await event.parent()
let cnameRecord = ''
let aRecord = ''
let aaaaRecord = ''
const provider = env.PRIVATE_HOSTING_PROVIDER
if (provider === 'fly.io') {
cnameRecord = env.PRIVATE_FLYIO_CNAME
aRecord = env.PRIVATE_FLYIO_IPV4
aaaaRecord = env.PRIVATE_FLYIO_IPV6
} else if (provider === 'railway') {
} else {
aRecord = PUBLIC_SHORTENER_IP
}
return { return {
form: await superValidate( form: await superValidate(
{ {
@ -43,6 +60,9 @@ export const load = (async (event) => {
id: 'deleteProject', id: 'deleteProject',
}, },
), ),
cnameRecord,
aRecord,
aaaaRecord,
} }
}) satisfies PageServerLoad }) satisfies PageServerLoad
@ -201,6 +221,12 @@ export const actions: Actions = {
return setError(form, 'domain', 'Domain is not available') return setError(form, 'domain', 'Domain is not available')
} }
const customDomain = await createCustomDomain(form.data.domain)
if (!customDomain.success) {
return setError(form, 'domain', 'Cannot create custom domain')
}
const deleteOldCustomDomain = await deleteCustomDomain( const deleteOldCustomDomain = await deleteCustomDomain(
existingProject.custom_domain_id, existingProject.custom_domain_id,
) )
@ -213,12 +239,6 @@ export const actions: Actions = {
) )
} }
const customDomain = await createCustomDomain(form.data.domain)
if (!customDomain.success) {
return setError(form, 'domain', 'Cannot create custom domain')
}
await db await db
.update(projectTable) .update(projectTable)
.set({ .set({

@ -23,6 +23,8 @@
} from 'lucide-svelte' } from 'lucide-svelte'
import { env } from '$env/dynamic/public' import { env } from '$env/dynamic/public'
import { invalidateAll } from '$app/navigation' import { invalidateAll } from '$app/navigation'
import DnsInfo from './(components)/dns-info.svelte'
import DnsTooltip from './(components)/dns-tooltip.svelte'
export let data export let data
@ -68,7 +70,7 @@
<div class="max-w-2xl space-y-6 px-10 py-4"> <div class="max-w-2xl space-y-6 px-10 py-4">
<div> <div>
<h3 class="text-lg font-medium">Custom Domain</h3> <h3 class="text-lg font-medium">Custom Domain</h3>
<p class="text-sm text-muted-foreground"> <p class="text-muted-foreground text-sm">
Update project domain. Update project domain.
</p> </p>
</div> </div>
@ -79,11 +81,11 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class=""> <div class="">
{#if data.project.domain_status === 'pending'} {#if data.project.domain_status === 'pending'}
<CircleDashedIcon class="h-8 w-8 text-warning" /> <CircleDashedIcon class="text-warning h-8 w-8" />
{:else if data.project.domain_status === 'verified'} {:else if data.project.domain_status === 'verified'}
<CircleCheckBigIcon class="h-8 w-8 text-success" /> <CircleCheckBigIcon class="text-success h-8 w-8" />
{:else if data.project.domain_status === 'disabled'} {:else if data.project.domain_status === 'disabled'}
<CircleXIcon class="h-8 w-8 text-destructive" /> <CircleXIcon class="text-destructive h-8 w-8" />
{/if} {/if}
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
@ -95,33 +97,12 @@
<Card.Description>custom domain</Card.Description> <Card.Description>custom domain</Card.Description>
<Card.Description> <Card.Description>
<Tooltip.Root> <DnsTooltip
<Tooltip.Trigger class="flex items-center gap-1"> domain={data.project.custom_domain}
<InfoIcon class="h-4 w-4" /> custom_ip={data.project.custom_ip}
{#if data.project.custom_ip} a_record={data.aRecord}
{data.project.custom_ip} aaaa_record={data.aaaaRecord}
{:else if env.PUBLIC_SHORTENER_IP} cname_record={data.cnameRecord} />
{env.PUBLIC_SHORTENER_IP}
{:else}
{'Public IP not found'}
{/if}
</Tooltip.Trigger>
<Tooltip.Content>
{#if data.project.custom_ip}
{'Create a CNAME record for ' +
data.project.custom_domain +
' to ' +
data.project.custom_ip}
{:else if env.PUBLIC_SHORTENER_IP}
{'Create a A record for ' +
data.project.custom_domain +
' to ' +
env.PUBLIC_SHORTENER_IP}
{:else}
{'Public IP not found'}
{/if}
</Tooltip.Content>
</Tooltip.Root>
</Card.Description> </Card.Description>
{:else} {:else}
<Card.Title> <Card.Title>
@ -144,16 +125,16 @@
<AlertDialog.Title> <AlertDialog.Title>
Are you absolutely sure? Are you absolutely sure?
</AlertDialog.Title> </AlertDialog.Title>
<Alert.Root variant="destructive">
<TriangleAlertIcon class="h-4 w-4" />
<Alert.Description>
Disabling custom domain is not available yet.
</Alert.Description>
</Alert.Root>
<AlertDialog.Description> <AlertDialog.Description>
Enabling a custom domain will allow you to use Enabling a custom domain will allow you to use
your project with a custom domain. your project with a custom domain.
</AlertDialog.Description> </AlertDialog.Description>
{#if data.cnameRecord || data.aRecord || data.aaaaRecord}
<DnsInfo
cname_record={data.cnameRecord}
a_record={data.aRecord}
aaaa_record={data.aaaaRecord} />
{/if}
</AlertDialog.Header> </AlertDialog.Header>
<AlertDialog.Footer> <AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel> <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
@ -255,7 +236,7 @@
<div> <div>
<h3 class="text-lg font-medium">Settings</h3> <h3 class="text-lg font-medium">Settings</h3>
<p class="text-sm text-muted-foreground"> <p class="text-muted-foreground text-sm">
Update project settings. Update project settings.
</p> </p>
</div> </div>
@ -268,11 +249,11 @@
<div> <div>
<h3 class="text-lg font-medium">Danger Zone</h3> <h3 class="text-lg font-medium">Danger Zone</h3>
</div> </div>
<div class="rounded-lg border border-destructive"> <div class="border-destructive rounded-lg border">
<div class="flex items-center justify-between p-4"> <div class="flex items-center justify-between p-4">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-sm">Delete Project</span> <span class="text-sm">Delete Project</span>
<span class="text-xs text-muted-foreground"> <span class="text-muted-foreground text-xs">
Permanently delete your project Permanently delete your project
</span> </span>
</div> </div>

Binary file not shown.

@ -0,0 +1,22 @@
# fly.toml app configuration file generated for kon on 2024-07-28T10:01:44+08:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'kon'
primary_region = 'dfw'
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'off'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1

@ -7,6 +7,7 @@ const fallback_url = Bun.env.FALLBACK_URL ?? 'https://app.kon.sh'
const app_url = Bun.env.APP_URL ?? 'kon.sh' const app_url = Bun.env.APP_URL ?? 'kon.sh'
const geoipupdate_account_id = Bun.env.GEOIPUPDATE_ACCOUNT_ID const geoipupdate_account_id = Bun.env.GEOIPUPDATE_ACCOUNT_ID
const geoipupdate_license_key = Bun.env.GEOIPUPDATE_LICENSE_KEY const geoipupdate_license_key = Bun.env.GEOIPUPDATE_LICENSE_KEY
const hosting_provider = Bun.env.HOSTING_PROVIDER
const app = new Elysia().use(cors()) const app = new Elysia().use(cors())
@ -20,7 +21,11 @@ app.get(
const request_domain = request.headers.get('host') const request_domain = request.headers.get('host')
const domain = request_domain !== app_url ? request_domain : null const domain = request_domain !== app_url ? request_domain : null
const ip = request.headers.get('x-forwarded-for') const ip = request.headers.get(
hosting_provider === 'fly.io'
? 'Fly-Client-IP'
: 'x-forwarded-for'
)
const WebServiceClient = const WebServiceClient =
require('@maxmind/geoip2-node').WebServiceClient require('@maxmind/geoip2-node').WebServiceClient

Loading…
Cancel
Save