diff --git a/README.md b/README.md index 442e5c6..600724a 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ # Custom Domain Ready + +## How to configure the project +1. Create a new storage edge config. +2. Create a new vercel project and import configuration-api. +3. Create a new project and import custom-domain-proxy. +4. Connect the storage to the project custom-domain-proxy. +5. Add the required env vars for the configuration-api + +```bash +VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID= # You can find the edge config id when you click on the storage +VERCEL_TEAM_ID= # Your vercel team id +VERCEL_PROJECT_ID= # must be the project id of the custom-domain-proxy +AUTH_BEARER_TOKEN= # create your own bearer token that can access the projects +``` + +6. Add a new domain using the configuration-api +7. Finish the domain configuration \ No newline at end of file diff --git a/apps/configuration-api/.env.local.example b/apps/configuration-api/.env.local.example index a9b50c5..191875f 100644 --- a/apps/configuration-api/.env.local.example +++ b/apps/configuration-api/.env.local.example @@ -1,3 +1,4 @@ VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID= VERCEL_TEAM_ID= +VERCEL_PROJECT_ID= AUTH_BEARER_TOKEN= \ No newline at end of file diff --git a/apps/configuration-api/app/api/assign/route.ts b/apps/configuration-api/app/api/assign/route.ts index 14f233b..3318c36 100644 --- a/apps/configuration-api/app/api/assign/route.ts +++ b/apps/configuration-api/app/api/assign/route.ts @@ -1,5 +1,6 @@ -import { addDomainToEdgeConfig, getEdgeconfigItem, updateEdgeConfigItem } from "@customdomainready/sdk"; +import { addDomainToEdgeConfig, getEdgeconfigItem, updateEdgeConfigItem, removeDomainFromEdgeConfig, getAllItemsFromEdgeConfig } from "@customdomainready/sdk"; +import { NextResponse } from "next/server"; import { z } from "zod"; const customDomainSchema = z.object({ @@ -8,6 +9,29 @@ const customDomainSchema = z.object({ destination: z.string() }) +export async function GET( + req: Request, +) { + try { + const response = await getAllItemsFromEdgeConfig(process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN) + console.log(response) + return NextResponse.json({ + response: response.map((item: any) => ({ + id: item.key, + sourceDomain: `https://${item.key.split('-')[0].replace(/_/g, '.')}`, + slug: item.key.split('-').slice(1).join('/').replace(/_/g, '.'), + destinationPath: item.value, + })) + }); + } catch (error) { + console.error(error) + + return new Response("Internal Server Error", { status: 500 }) + } +} + + + export async function PATCH( req: Request ) { @@ -15,18 +39,19 @@ export async function PATCH( const body = await req.json() const payload = customDomainSchema.parse(body) - const sourceURL = new URL(payload.domain, payload.slug) + const domainWithoutProtocol = payload.domain.replace(/^https?:\/\//, ''); + const key = `${domainWithoutProtocol.replace(/\./g, '_')}${payload.slug.replace(/\//g, '-')}`; + console.log(key) // validate if key already exist and if yes update it instead of creating - const existingDomain = await getEdgeconfigItem(sourceURL.toString(), process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN) - console.log(existingDomain) + const existingDomain = await getEdgeconfigItem(key, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); if (existingDomain){ - const updateResponse = await updateEdgeConfigItem(sourceURL.toString(), payload.destination, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN) - console.log(updateResponse) + const updateResponse = await updateEdgeConfigItem(key, payload.destination, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); + console.log(updateResponse); } else { - const createResponse = await addDomainToEdgeConfig(sourceURL.toString(), payload.destination, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN) - console.log(createResponse) + const createResponse = await addDomainToEdgeConfig(key, payload.destination, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); + console.log(createResponse); } return new Response('created', { status: 201}) @@ -38,4 +63,38 @@ export async function PATCH( return new Response(null, { status: 500 }) } -} \ No newline at end of file +} + +export async function DELETE( + req: Request +) { + try { + const body = await req.json(); + const payload = customDomainSchema.parse(body); + + + const domainWithoutProtocol = payload.domain.replace(/^https?:\/\//, ''); + const key = `${domainWithoutProtocol.replace(/\./g, '_')}${payload.slug.replace(/\//g, '-')}`; + console.log(key) + + const existingDomain = await getEdgeconfigItem(key, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); + + if (existingDomain) { + const deleteResponse = await removeDomainFromEdgeConfig(key, process.env.VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); + console.log(deleteResponse); + return new Response('deleted', { status: 204 }); + } else { + return new Response('Domain not found', { status: 404 }); + } + } catch (error) { + console.log(error); + if (error instanceof z.ZodError) { + return new Response(JSON.stringify(error.issues), { status: 422 }); + } + + return new Response(null, { status: 500 }); + } +} + + + \ No newline at end of file diff --git a/apps/configuration-api/app/api/domain/route.ts b/apps/configuration-api/app/api/domain/route.ts index 31c7923..7280cf1 100644 --- a/apps/configuration-api/app/api/domain/route.ts +++ b/apps/configuration-api/app/api/domain/route.ts @@ -1,4 +1,4 @@ -import { addDomainToVercel } from "@customdomainready/sdk"; +import { addDomainToVercel, getDomains, getAllItemsFromEdgeConfig } from "@customdomainready/sdk"; import { NextResponse } from "next/server"; import * as z from "zod" @@ -17,16 +17,18 @@ export async function POST( const domain = payload.domain; + // Check if the domain already exists + const existingDomains = await getDomains(process.env.VERCEL_PROJECT_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); + if (existingDomains.domains.some((d: any) => d.name === domain)) { + return new Response("Domain already exists", { status: 200 }); + } + const response = await addDomainToVercel(domain, process.env.VERCEL_PROJECT_ID!, process.env.VERCEL_TEAM_ID, process.env.AUTH_BEARER_TOKEN); if (response.error) { return new Response(response.error.message, { status: 400 }) } - if (response.code === 409) { - return new Response("Domain already exists", { status: 409 }) - } - return NextResponse.json({ response }); @@ -36,3 +38,5 @@ export async function POST( return new Response("Internal Server Error", { status: 500 }) } } + + diff --git a/apps/configuration-api/app/page.tsx b/apps/configuration-api/app/page.tsx index 0560186..a7661f5 100644 --- a/apps/configuration-api/app/page.tsx +++ b/apps/configuration-api/app/page.tsx @@ -3,7 +3,7 @@ import CustomDomainConfig from "@/components/custom-domain-config" export default function Home() { return (
-

Custom Domain Configuration

+

Custom Domain Ready

) diff --git a/apps/configuration-api/components/custom-domain-config.tsx b/apps/configuration-api/components/custom-domain-config.tsx index 8356d13..79e929d 100644 --- a/apps/configuration-api/components/custom-domain-config.tsx +++ b/apps/configuration-api/components/custom-domain-config.tsx @@ -1,11 +1,11 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Trash2 } from 'lucide-react' +import { Trash2, RefreshCw } from 'lucide-react' interface DomainConfig { id: string @@ -21,16 +21,102 @@ export default function CustomDomainConfig() { slug: "", destinationPath: "", }) + const fetchConfigs = async () => { + try { + const response = await fetch('/api/assign'); + const data = await response.json(); + console.log(data) + setConfigs(data.response.map((domain: any) => ({ + id: domain.id, + sourceDomain: domain.sourceDomain, + slug: domain.slug, + destinationPath: domain.destinationPath, + }))); + } catch (error) { + console.error("Failed to fetch domain configurations:", error); + } + }; + + useEffect(() => { + fetchConfigs(); + }, []); + + const handleAddDomainAndAlias = async () => { + try { + const domainExists = configs.some(config => config.sourceDomain === newConfig.sourceDomain); + + if (!domainExists) { + const domainResponse = await fetch('/api/domain', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ domain: newConfig.sourceDomain }), + }); + + if (!domainResponse.ok) { + throw new Error('Failed to add domain'); + } + } + + const aliasResponse = await fetch('/api/assign', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + domain: newConfig.sourceDomain, + slug: newConfig.slug, + destination: newConfig.destinationPath, + }), + }); + + if (!aliasResponse.ok) { + throw new Error('Failed to create alias'); + } + + const newAlias = await aliasResponse.json(); + setConfigs((prevConfigs) => [...prevConfigs, newAlias]); - const addConfig = () => { - if (newConfig.sourceDomain && newConfig.slug && newConfig.destinationPath) { - setConfigs([...configs, { ...newConfig, id: Date.now().toString() }]) - setNewConfig({ sourceDomain: "", slug: "", destinationPath: "" }) + setNewConfig({ + sourceDomain: "", + slug: "", + destinationPath: "", + }); + + } catch (error) { + console.error("Error adding domain and alias:", error); + } + }; + const removeConfig = async (sourceDomain: string, slug: string, destination: string) => { + try { + const response = await fetch('/api/assign', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ domain: sourceDomain, slug: slug, destination: destination}), + }); + + if (!response.ok) { + throw new Error('Failed to remove config'); + } + + + fetchConfigs(); + } catch (error) { + console.error("Error removing config:", error); } } - const removeConfig = (id: string) => { - setConfigs(configs.filter((config) => config.id !== id)) + const refreshDomainStatus = async (sourceDomain: string) => { + try { + const response = await fetch(`/api/domain/${sourceDomain.replace(/^https?:\/\//, '')}/verify`); + const data = await response.json(); + alert(`Domain Status: ${data.status}`); + } catch (error) { + console.error("Failed to verify domain status:", error); + } } return ( @@ -75,7 +161,7 @@ export default function CustomDomainConfig() { /> - +
@@ -91,13 +177,22 @@ export default function CustomDomainConfig() { Destination: {config.destinationPath}

- +
+ + +
))} diff --git a/apps/configuration-api/tailwind.config.ts b/apps/configuration-api/tailwind.config.ts index 264062d..abf7257 100644 --- a/apps/configuration-api/tailwind.config.ts +++ b/apps/configuration-api/tailwind.config.ts @@ -15,7 +15,6 @@ const config: Config = { }, }, }, - plugins: [], }; diff --git a/apps/custom-domain-proxy/middleware.ts b/apps/custom-domain-proxy/middleware.ts index e05e7cb..35123d4 100644 --- a/apps/custom-domain-proxy/middleware.ts +++ b/apps/custom-domain-proxy/middleware.ts @@ -6,17 +6,16 @@ export const config = { }; export async function middleware(req: Request) { - const url = new URL(req.url); - const path = url.pathname; - const queryParams = url.search; // Capture query parameters - console.log(req.url) - const destination = await get(req.url) - const destinationURL = new URL(destination?.toString() || '') + const urlWithoutProtocol = req.url.replace(/^https?:\/\//, ''); + const key = urlWithoutProtocol.replace(/\./g, '_').replace(/\//g, '-'); + const destination = await get(key); + const destinationURL = new URL(destination?.toString() || ''); - if (destinationURL) { - NextResponse.rewrite(destinationURL) + if (destinationURL) { + destinationURL.search = new URL(req.url).search; // Preserve query params + return NextResponse.rewrite(destinationURL); } return new NextResponse('Not Found', { status: 404 }); diff --git a/packages/sdk/lib/domains.ts b/packages/sdk/lib/domains.ts index eba9d17..9a67523 100644 --- a/packages/sdk/lib/domains.ts +++ b/packages/sdk/lib/domains.ts @@ -4,6 +4,21 @@ import { DomainVerificationResponse, } from "../types"; +export const getDomains = async ( + projectIdVercel: string, + teamIdVercel?: string, + authBearerToken?: string +) => { + return await fetch( + `https://api.vercel.com/v9/projects/${projectIdVercel}/domains${teamIdVercel ? `?teamId=${teamIdVercel}` : ""}`, + { + headers: { + Authorization: `Bearer ${authBearerToken}`, + }, + } + ).then((res) => res.json()); +}; + export const addDomainToVercel = async ( domain: string, projectIdVercel: string, diff --git a/packages/sdk/lib/edge-config.ts b/packages/sdk/lib/edge-config.ts index 4ec257e..0e56fda 100644 --- a/packages/sdk/lib/edge-config.ts +++ b/packages/sdk/lib/edge-config.ts @@ -72,3 +72,44 @@ export const addDomainToEdgeConfig = async ( ).then((res) => res.json()); }; +export const removeDomainFromEdgeConfig = async ( + key: string, + edgeConfigName: string, + teamIdVercel?: string, + authBearerToken?: string +) => { + return await fetch(`https://api.vercel.com/v1/edge-config/${edgeConfigName}/items?edgeConfigId=${edgeConfigName}&teamId=${teamIdVercel}`, + { + method: "PATCH", + headers: { + Authorization: `Bearer ${authBearerToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + items: [ + { + "operation": "delete", + "key": key, + } + ] + }), + } + ).then((res) => res.json()); +}; + + +export const getAllItemsFromEdgeConfig = async ( + edgeConfigName: string, + teamIdVercel?: string, + authBearerToken?: string +) => { + return await fetch(`https://api.vercel.com/v1/edge-config/${edgeConfigName}/items?edgeConfigId=${edgeConfigName}&teamId=${teamIdVercel}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${authBearerToken}`, + "Content-Type": "application/json", + } + } + ).then((res) => res.json()); +};