mirror of https://github.com/TZGyn/shortener
added option to add custom domain to project in project settings page
parent
10485ac9a0
commit
9c1e72ed86
@ -1,4 +1,14 @@
|
||||
ORIGIN=http://localhost:3000
|
||||
PROTOCOL_HEADER=x-forwarded-proto
|
||||
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
|
||||
PRIVATE_HOSTING_PROVIDER= # optional, (options: railway), if you don't have a hosting provider, leave it blank
|
||||
|
||||
# Railway config (if you have a Railway hosting provider)
|
||||
PRIVATE_RAILWAY_API_KEY=
|
||||
PRIVATE_RAILWAY_ENVIRONMENT_ID=
|
||||
PRIVATE_RAILWAY_PROJECT_ID=
|
||||
PRIVATE_RAILWAY_SERVICE_ID=
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TABLE "project" ADD COLUMN "domain_status" varchar(255) DEFAULT 'verified' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "project" ADD COLUMN "enable_custom_domain" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "project" ADD COLUMN "custom_ip" varchar(255);--> statement-breakpoint
|
||||
ALTER TABLE "project" ADD COLUMN "custom_domain_id" varchar(255);
|
||||
@ -0,0 +1,367 @@
|
||||
{
|
||||
"id": "a4ffb6fc-ede8-431d-bd69-2429fbdc3ebd",
|
||||
"prevId": "f5e2ea5c-7287-40ae-928a-3914f03045b5",
|
||||
"version": "6",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.project": {
|
||||
"name": "project",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"uuid": {
|
||||
"name": "uuid",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"qr_background": {
|
||||
"name": "qr_background",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'#ffffff'"
|
||||
},
|
||||
"qr_foreground": {
|
||||
"name": "qr_foreground",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'#000000'"
|
||||
},
|
||||
"domain_status": {
|
||||
"name": "domain_status",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'verified'"
|
||||
},
|
||||
"enable_custom_domain": {
|
||||
"name": "enable_custom_domain",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"custom_ip": {
|
||||
"name": "custom_ip",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"custom_domain_id": {
|
||||
"name": "custom_domain_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"custom_domain": {
|
||||
"name": "custom_domain",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.setting": {
|
||||
"name": "setting",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"qr_background": {
|
||||
"name": "qr_background",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"qr_foreground": {
|
||||
"name": "qr_foreground",
|
||||
"type": "varchar(7)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.shortener": {
|
||||
"name": "shortener",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"link": {
|
||||
"name": "link",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ios": {
|
||||
"name": "ios",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"ios_link": {
|
||||
"name": "ios_link",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"android": {
|
||||
"name": "android",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"android_link": {
|
||||
"name": "android_link",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"shortener_code_unique": {
|
||||
"name": "shortener_code_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"uuid": {
|
||||
"name": "uuid",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.visitor": {
|
||||
"name": "visitor",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"shortener_id": {
|
||||
"name": "shortener_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"country_code": {
|
||||
"name": "country_code",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"device_type": {
|
||||
"name": "device_type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"device_vendor": {
|
||||
"name": "device_vendor",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"os": {
|
||||
"name": "os",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"browser": {
|
||||
"name": "browser",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</div>
|
||||
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { HeadingLevel } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
|
||||
level?: HeadingLevel;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let level: $$Props["level"] = "h5";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={level}
|
||||
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { type Variant, alertVariants } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: Variant;
|
||||
};
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let variant: $$Props["variant"] = "default";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
|
||||
<slot />
|
||||
</div>
|
||||
@ -0,0 +1,33 @@
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
import Root from "./alert.svelte";
|
||||
import Description from "./alert-description.svelte";
|
||||
import Title from "./alert-title.svelte";
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: "relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof alertVariants>["variant"];
|
||||
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { env } from '$env/dynamic/private'
|
||||
|
||||
export const checkDomainAvailable = async (domain: string) => {
|
||||
if (env.PRIVATE_HOSTING_PROVIDER === 'railway') {
|
||||
const { railwayClient } = await import('./railway')
|
||||
|
||||
const available = await railwayClient.checkDomain(domain)
|
||||
|
||||
return available
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const createCustomDomain = async (domain: string) => {
|
||||
if (env.PRIVATE_HOSTING_PROVIDER === 'railway') {
|
||||
const { railwayClient } = await import('./railway')
|
||||
|
||||
const response = await railwayClient.createCustomDomain(domain)
|
||||
|
||||
if (!response.success) {
|
||||
return { success: false, id: undefined, ip: undefined } as const
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id: response.domain.id,
|
||||
ip: response.domain.record,
|
||||
} as const
|
||||
} else {
|
||||
return { id: undefined, ip: undefined } as const
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteCustomDomain = async (domain: string | null) => {
|
||||
if (!domain) {
|
||||
return { success: true } as const
|
||||
}
|
||||
if (env.PRIVATE_HOSTING_PROVIDER === 'railway') {
|
||||
const { railwayClient } = await import('./railway')
|
||||
|
||||
const response = await railwayClient.deleteCustomDomain(domain)
|
||||
|
||||
if (!response.success) {
|
||||
return { success: false } as const
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
} as const
|
||||
} else {
|
||||
return { success: true } as const
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
import { env } from '$env/dynamic/private'
|
||||
|
||||
class RailwayAPI {
|
||||
constructor(private apiKey: string) {}
|
||||
|
||||
private sendRequest(query: string, variables: any) {
|
||||
return fetch(`https://backboard.railway.app/graphql/v2`, {
|
||||
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 customDomainCreate($input: CustomDomainCreateInput!) {
|
||||
customDomainCreate(input: $input) {
|
||||
__typename
|
||||
createdAt
|
||||
deletedAt
|
||||
domain
|
||||
environmentId
|
||||
id
|
||||
projectId
|
||||
serviceId
|
||||
status {
|
||||
dnsRecords {
|
||||
requiredValue
|
||||
}
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = {
|
||||
input: {
|
||||
domain: domain,
|
||||
environmentId: env.PRIVATE_RAILWAY_ENVIRONMENT_ID,
|
||||
projectId: env.PRIVATE_RAILWAY_PROJECT_ID,
|
||||
serviceId: env.PRIVATE_RAILWAY_SERVICE_ID,
|
||||
},
|
||||
}
|
||||
const response = await this.sendRequest(query, variables)
|
||||
const data: any = await response.json()
|
||||
if (!data.errors) {
|
||||
console.log('Railway Create Domain', data)
|
||||
return {
|
||||
success: true,
|
||||
domain: {
|
||||
id: data.data.customDomainCreate.id as string,
|
||||
record: data.data.customDomainCreate.status.dnsRecords[0]
|
||||
.requiredValue as string,
|
||||
},
|
||||
} as const
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Something went wrong',
|
||||
} as const
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCustomDomain(id: string) {
|
||||
const query = `
|
||||
mutation customDomainDelete($id: String!) {
|
||||
customDomainDelete(id: $id)
|
||||
|
||||
}
|
||||
`
|
||||
const variables = {
|
||||
id: id,
|
||||
}
|
||||
const response = await this.sendRequest(query, variables)
|
||||
if (response.ok) {
|
||||
const data: any = await response.json()
|
||||
console.log('Railway Delete Domain', data)
|
||||
return {
|
||||
success: true,
|
||||
} as const
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const railwayClient = new RailwayAPI(
|
||||
env.PRIVATE_RAILWAY_API_KEY,
|
||||
)
|
||||
Loading…
Reference in New Issue