Skip to content

Commit

Permalink
feat: update more code
Browse files Browse the repository at this point in the history
  • Loading branch information
marcolivierbouch committed Nov 26, 2024
1 parent dd584e5 commit d30603f
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 40 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions apps/configuration-api/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
VERCEL_CUSTOM_DOMAIN_PROXY_EDGE_CONFIG_ID=
VERCEL_TEAM_ID=
VERCEL_PROJECT_ID=
AUTH_BEARER_TOKEN=
77 changes: 68 additions & 9 deletions apps/configuration-api/app/api/assign/route.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -8,25 +9,49 @@ 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
) {
try {
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})
Expand All @@ -38,4 +63,38 @@ export async function PATCH(

return new Response(null, { status: 500 })
}
}
}

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 });
}
}



14 changes: 9 additions & 5 deletions apps/configuration-api/app/api/domain/route.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
});
Expand All @@ -36,3 +38,5 @@ export async function POST(
return new Response("Internal Server Error", { status: 500 })
}
}


2 changes: 1 addition & 1 deletion apps/configuration-api/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CustomDomainConfig from "@/components/custom-domain-config"
export default function Home() {
return (
<div className="container mx-auto py-10">
<h1 className="text-2xl font-bold mb-6">Custom Domain Configuration</h1>
<h1 className="text-2xl font-bold mb-6">Custom Domain Ready</h1>
<CustomDomainConfig />
</div>
)
Expand Down
127 changes: 111 additions & 16 deletions apps/configuration-api/components/custom-domain-config.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -75,7 +161,7 @@ export default function CustomDomainConfig() {
/>
</div>
</div>
<Button onClick={addConfig}>Add Configuration</Button>
<Button onClick={handleAddDomainAndAlias}>Add Configuration</Button>
</form>

<div className="mt-8 space-y-4">
Expand All @@ -91,13 +177,22 @@ export default function CustomDomainConfig() {
<strong>Destination:</strong> {config.destinationPath}
</p>
</div>
<Button
variant="destructive"
size="icon"
onClick={() => removeConfig(config.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<div className="flex space-x-2">
<Button
variant="destructive"
size="icon"
onClick={() => removeConfig(config.sourceDomain, config.slug, config.destinationPath)}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="icon"
onClick={() => refreshDomainStatus(config.sourceDomain)}
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
Expand Down
1 change: 0 additions & 1 deletion apps/configuration-api/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const config: Config = {
},
},
},

plugins: [],
};

Expand Down
15 changes: 7 additions & 8 deletions apps/custom-domain-proxy/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
15 changes: 15 additions & 0 deletions packages/sdk/lib/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit d30603f

Please sign in to comment.