diff --git a/frontend/.env.example b/frontend/.env.example
index 24f1a79..7aa65a1 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -4,9 +4,9 @@ HOST_HEADER=x-forwarded-host
DATABASE_URL=postgres://postgres:password@127.0.0.1:5432/link-shortener
PUBLIC_SHORTENER_URL=localhost:3000
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
# Railway config (if you have a Railway hosting provider)
@@ -15,5 +15,12 @@ PRIVATE_RAILWAY_ENVIRONMENT_ID=
PRIVATE_RAILWAY_PROJECT_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)
PRIVATE_RESEND_API_KEY=
diff --git a/frontend/bun.lockb b/frontend/bun.lockb
index 5302c18..41bf9ce 100755
Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ
diff --git a/frontend/src/lib/server/domain.ts b/frontend/src/lib/server/domain.ts
index 3d94d00..ef5fd69 100644
--- a/frontend/src/lib/server/domain.ts
+++ b/frontend/src/lib/server/domain.ts
@@ -27,6 +27,20 @@ export const createCustomDomain = async (domain: string) => {
id: response.domain.id,
ip: response.domain.record,
} 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 {
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: 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 {
success: true,
} as const
diff --git a/frontend/src/lib/server/flyio.ts b/frontend/src/lib/server/flyio.ts
new file mode 100644
index 0000000..0353e86
--- /dev/null
+++ b/frontend/src/lib/server/flyio.ts
@@ -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)
diff --git a/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-info.svelte b/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-info.svelte
new file mode 100644
index 0000000..efc706d
--- /dev/null
+++ b/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-info.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+ {#if cname_record}
+ CNAME
+ {/if}
+ {#if a_record || aaaa_record}
+ A and AAAA
+ {/if}
+
+ {#if cname_record}
+
+
+
+ Type
+ Value
+
+
+ CNAME
+ {cname_record}
+
+
+
+ {/if}
+ {#if a_record || aaaa_record}
+
+
+ {#if a_record}
+
+
+ Type
+ Value
+
+
+ A
+ {a_record}
+
+
+ {/if}
+ {#if aaaa_record}
+
+
+ Type
+ Value
+
+
+ AAAA
+ {aaaa_record}
+
+
+ {/if}
+
+
+ {/if}
+
diff --git a/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-tooltip.svelte b/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-tooltip.svelte
new file mode 100644
index 0000000..cce878b
--- /dev/null
+++ b/frontend/src/routes/(app)/projects/[id]/settings/(components)/dns-tooltip.svelte
@@ -0,0 +1,61 @@
+
+
+
+
+
+ {#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}
+
+
+ {#if custom_ip}
+
+ {'Create a CNAME/ALIAS record for ' +
+ domain +
+ ' to ' +
+ custom_ip}
+
+ {/if}
+ {#if cname_record}
+
+ {'Create a CNAME/ALIAS record for ' +
+ domain +
+ ' to ' +
+ cname_record}
+
+ {/if}
+ {#if a_record}
+
+ {'Create a A record for ' + domain + ' to ' + a_record}
+
+ {/if}
+
+ {#if aaaa_record}
+
+ {'Create a AAAA record for ' + domain + ' to ' + aaaa_record}
+
+ {/if}
+ {#if !(custom_ip || cname_record || a_record || aaaa_record)}
+
+ {'Public IP not found'}
+
+ {/if}
+
+
diff --git a/frontend/src/routes/(app)/projects/[id]/settings/+page.server.ts b/frontend/src/routes/(app)/projects/[id]/settings/+page.server.ts
index c012a7b..3bc9598 100644
--- a/frontend/src/routes/(app)/projects/[id]/settings/+page.server.ts
+++ b/frontend/src/routes/(app)/projects/[id]/settings/+page.server.ts
@@ -19,10 +19,27 @@ import {
createCustomDomain,
deleteCustomDomain,
} from '$lib/server/domain'
+import { env } from '$env/dynamic/private'
+import { PUBLIC_SHORTENER_IP } from '$env/static/public'
export const load = (async (event) => {
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 {
form: await superValidate(
{
@@ -43,6 +60,9 @@ export const load = (async (event) => {
id: 'deleteProject',
},
),
+ cnameRecord,
+ aRecord,
+ aaaaRecord,
}
}) satisfies PageServerLoad
@@ -201,6 +221,12 @@ export const actions: Actions = {
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(
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
.update(projectTable)
.set({
diff --git a/frontend/src/routes/(app)/projects/[id]/settings/+page.svelte b/frontend/src/routes/(app)/projects/[id]/settings/+page.svelte
index 483bcc7..e0d0e0c 100644
--- a/frontend/src/routes/(app)/projects/[id]/settings/+page.svelte
+++ b/frontend/src/routes/(app)/projects/[id]/settings/+page.svelte
@@ -23,6 +23,8 @@
} from 'lucide-svelte'
import { env } from '$env/dynamic/public'
import { invalidateAll } from '$app/navigation'
+ import DnsInfo from './(components)/dns-info.svelte'
+ import DnsTooltip from './(components)/dns-tooltip.svelte'
export let data
@@ -68,7 +70,7 @@
Custom Domain
-
+
Update project domain.
@@ -79,11 +81,11 @@
{#if data.project.domain_status === 'pending'}
-
+
{:else if data.project.domain_status === 'verified'}
-
+
{:else if data.project.domain_status === 'disabled'}
-
+
{/if}
@@ -95,33 +97,12 @@
custom domain
-
-
-
- {#if data.project.custom_ip}
- {data.project.custom_ip}
- {:else if env.PUBLIC_SHORTENER_IP}
- {env.PUBLIC_SHORTENER_IP}
- {:else}
- {'Public IP not found'}
- {/if}
-
-
- {#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}
-
-
+
{:else}
@@ -144,16 +125,16 @@
Are you absolutely sure?
-
-
-
- Disabling custom domain is not available yet.
-
-
Enabling a custom domain will allow you to use
your project with a custom domain.
+ {#if data.cnameRecord || data.aRecord || data.aaaaRecord}
+
+ {/if}
Cancel
@@ -255,7 +236,7 @@
Settings
-
+
Update project settings.
@@ -268,11 +249,11 @@
Danger Zone
-
+
Delete Project
-
+
Permanently delete your project
diff --git a/redirect/bun.lockb b/redirect/bun.lockb
index 79e61a7..0983070 100755
Binary files a/redirect/bun.lockb and b/redirect/bun.lockb differ
diff --git a/redirect/fly.toml b/redirect/fly.toml
new file mode 100644
index 0000000..e4ec906
--- /dev/null
+++ b/redirect/fly.toml
@@ -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
diff --git a/redirect/src/index.ts b/redirect/src/index.ts
index e73a8ab..869383d 100644
--- a/redirect/src/index.ts
+++ b/redirect/src/index.ts
@@ -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 geoipupdate_account_id = Bun.env.GEOIPUPDATE_ACCOUNT_ID
const geoipupdate_license_key = Bun.env.GEOIPUPDATE_LICENSE_KEY
+const hosting_provider = Bun.env.HOSTING_PROVIDER
const app = new Elysia().use(cors())
@@ -20,7 +21,11 @@ app.get(
const request_domain = request.headers.get('host')
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 =
require('@maxmind/geoip2-node').WebServiceClient