Skip to content

Commit 4ca4e20

Browse files
authored
Merge pull request #44 from Saasfy/domains
feat(web): add domain page
2 parents 2fcf7fb + 32f1482 commit 4ca4e20

File tree

11 files changed

+766
-0
lines changed

11 files changed

+766
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { withWorkspaceUser } from '@saasfy/api/server';
2+
import { createAdminClient } from '@saasfy/supabase/server';
3+
4+
type VercelError = {
5+
error: {
6+
code: string;
7+
message: string;
8+
};
9+
};
10+
11+
export const POST = withWorkspaceUser<{ domainSlug: string }>(
12+
['owner', 'member'] as const,
13+
async ({ workspace, params }) => {
14+
const supabase = createAdminClient();
15+
16+
const { data: domain } = await supabase
17+
.from('domains')
18+
.select('*')
19+
.eq('slug', params.domainSlug)
20+
.eq('workspace_id', workspace.id)
21+
.single();
22+
23+
if (!domain) {
24+
return Response.json(
25+
{
26+
errors: ['Domain not found'],
27+
domain: null,
28+
},
29+
{
30+
status: 404,
31+
},
32+
);
33+
}
34+
35+
if (domain.verified && domain.configured) {
36+
return Response.json(
37+
{
38+
...domain,
39+
errors: null,
40+
},
41+
{
42+
status: 200,
43+
},
44+
);
45+
}
46+
47+
const [vercelDomainConfigResponse, vercelDomainResponse] = await Promise.all([
48+
fetch(
49+
`https://api.vercel.com/v6/domains/${domain.slug}/config?teamId=${process.env.TEAM_ID_VERCEL}`,
50+
{
51+
method: 'GET',
52+
headers: {
53+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
54+
'Content-Type': 'application/json',
55+
},
56+
},
57+
),
58+
fetch(
59+
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${domain.slug}?teamId=${process.env.TEAM_ID_VERCEL}`,
60+
{
61+
method: 'GET',
62+
headers: {
63+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
64+
'Content-Type': 'application/json',
65+
},
66+
},
67+
),
68+
]);
69+
70+
const vercelDomainConfig = (await vercelDomainConfigResponse.json()) as {
71+
misconfigured: boolean;
72+
error: string;
73+
conflicts: {
74+
name: string;
75+
type: string;
76+
value: string;
77+
}[];
78+
};
79+
80+
const vercelDomain = (await vercelDomainResponse.json()) as {
81+
apexName: string;
82+
name: string;
83+
verified: boolean;
84+
verification: {
85+
domain: string;
86+
reason: string;
87+
type: string;
88+
value: string;
89+
}[];
90+
} & VercelError;
91+
92+
if (vercelDomainResponse.status !== 200) {
93+
return Response.json(
94+
{
95+
...domain,
96+
errors: vercelDomain.error ? [vercelDomain.error.message] : null,
97+
},
98+
{
99+
status: vercelDomainResponse.status,
100+
},
101+
);
102+
}
103+
104+
/**
105+
* If domain is not verified, we try to verify now
106+
*/
107+
let domainVerification = null;
108+
if (!vercelDomain.verified) {
109+
const domainVerificationResponse = await fetch(
110+
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${params.domainSlug}/verify?teamId=${process.env.TEAM_ID_VERCEL}`,
111+
{
112+
method: 'POST',
113+
headers: {
114+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
115+
'Content-Type': 'application/json',
116+
},
117+
},
118+
);
119+
120+
domainVerification = (await domainVerificationResponse.json()) as {
121+
apexName: string;
122+
name: string;
123+
verified: boolean;
124+
verification: {
125+
domain: string;
126+
reason: string;
127+
type: string;
128+
value: string;
129+
}[];
130+
} & VercelError;
131+
132+
if (domainVerificationResponse.status !== 200) {
133+
return Response.json(
134+
{
135+
...domain,
136+
...vercelDomain,
137+
errors: domainVerification.error ? [domainVerification.error.message] : null,
138+
},
139+
{
140+
status: domainVerificationResponse.status,
141+
},
142+
);
143+
}
144+
}
145+
146+
if (
147+
((domainVerification && domainVerification.verified) || vercelDomain.verified) &&
148+
!domain.verified
149+
) {
150+
await supabase.from('domains').update({ verified: true }).eq('id', domain.id);
151+
}
152+
153+
if (!vercelDomainConfig.misconfigured && !domain.configured) {
154+
await supabase.from('domains').update({ configured: true }).eq('id', domain.id);
155+
}
156+
157+
return Response.json({
158+
...domain,
159+
...vercelDomain,
160+
...(domainVerification ? domainVerification : {}),
161+
conflicts: vercelDomainConfig.conflicts,
162+
configured: !vercelDomainConfig.misconfigured,
163+
errors: null,
164+
});
165+
},
166+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { withWorkspaceUser } from '@saasfy/api/server';
2+
import { createAdminClient } from '@saasfy/supabase/server';
3+
4+
export const GET = withWorkspaceUser<{ domainSlug: string }>(
5+
['owner', 'member'] as const,
6+
async ({ workspace, params }) => {
7+
const supabase = createAdminClient();
8+
9+
const { data: domain, error } = await supabase
10+
.from('domains')
11+
.select('*')
12+
.eq('workspace_id', workspace.id)
13+
.eq('slug', params.domainSlug)
14+
.single();
15+
16+
return Response.json(
17+
{
18+
domain,
19+
errors: error ? [error.message] : [],
20+
},
21+
{
22+
status: error ? 500 : 200,
23+
},
24+
);
25+
},
26+
);
27+
28+
export const DELETE = withWorkspaceUser<{ domainSlug: string }>(
29+
['owner', 'member'] as const,
30+
async ({ workspace, params }) => {
31+
const supabase = createAdminClient();
32+
33+
const response = await fetch(
34+
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${params.domainSlug}?teamId=${process.env.TEAM_ID_VERCEL}`,
35+
{
36+
headers: {
37+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
38+
},
39+
method: 'DELETE',
40+
},
41+
);
42+
43+
if (!response.ok) {
44+
return Response.json(
45+
{
46+
errors: ['Failed to delete domain from Vercel'],
47+
},
48+
{
49+
status: response.status,
50+
},
51+
);
52+
}
53+
54+
const { error } = await supabase
55+
.from('domains')
56+
.delete()
57+
.eq('workspace_id', workspace.id)
58+
.eq('slug', params.domainSlug);
59+
60+
return Response.json(
61+
{
62+
errors: error ? [error.message] : [],
63+
},
64+
{
65+
status: error ? 500 : 200,
66+
},
67+
);
68+
},
69+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { withWorkspaceUser } from '@saasfy/api/server';
2+
3+
export const POST = withWorkspaceUser<{ domainSlug: string }>(
4+
['owner', 'member'] as const,
5+
async ({ workspace, params }) => {
6+
const response = await fetch(
7+
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${params.domainSlug}/verify?teamId=${process.env.TEAM_ID_VERCEL}`,
8+
{
9+
headers: {
10+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
11+
'Content-Type': 'application/json',
12+
},
13+
method: 'POST',
14+
},
15+
);
16+
17+
const data = await response.json();
18+
19+
return Response.json(
20+
{
21+
errors: [],
22+
data,
23+
},
24+
{
25+
status: response.status,
26+
},
27+
);
28+
},
29+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { withWorkspaceUser } from '@saasfy/api/server';
2+
import { createAdminClient } from '@saasfy/supabase/server';
3+
4+
export const POST = withWorkspaceUser(
5+
['owner', 'member'] as const,
6+
async ({ req, params, workspace }) => {
7+
const { name } = await req.json();
8+
9+
const supabase = createAdminClient();
10+
11+
const domain = await supabase
12+
.from('domains')
13+
.insert({ slug: name, workspace_id: workspace.id })
14+
.select('*')
15+
.single();
16+
17+
const response = await fetch(
18+
`https://api.vercel.com/v10/projects/${process.env.PROJECT_ID_VERCEL}/domains?teamId=${process.env.TEAM_ID_VERCEL}`,
19+
{
20+
body: JSON.stringify({ name }),
21+
headers: {
22+
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
23+
'Content-Type': 'application/json',
24+
},
25+
method: 'POST',
26+
},
27+
);
28+
29+
const data = await response.json();
30+
31+
if (data.error?.code == 'forbidden') {
32+
return Response.json(
33+
{
34+
errors: ['You do not have permission to add a domain to this workspace.'],
35+
domain: null,
36+
},
37+
{
38+
status: 403,
39+
},
40+
);
41+
} else if (data.error?.code == 'domain_taken') {
42+
return Response.json(
43+
{
44+
errors: ['This domain is already taken.'],
45+
domain: null,
46+
},
47+
{
48+
status: 409,
49+
},
50+
);
51+
} else {
52+
return Response.json({
53+
errors: [],
54+
domain,
55+
});
56+
}
57+
},
58+
);
59+
60+
export const GET = withWorkspaceUser(['owner', 'member'] as const, async ({ workspace }) => {
61+
const supabase = createAdminClient();
62+
63+
const { data: domains, error } = await supabase
64+
.from('domains')
65+
.select('*')
66+
.eq('workspace_id', workspace.id);
67+
68+
return Response.json(
69+
{
70+
domains,
71+
errors: error ? [error.message] : [],
72+
},
73+
{
74+
status: error ? 500 : 200,
75+
},
76+
);
77+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client';
2+
3+
import { useParams, useRouter } from 'next/navigation';
4+
5+
import { Button } from '@saasfy/ui/button';
6+
import { Input } from '@saasfy/ui/input';
7+
8+
export function AddDomainForm() {
9+
const { workspaceSlug } = useParams();
10+
const router = useRouter();
11+
12+
return (
13+
<form
14+
className="flex items-center"
15+
onSubmit={async (event) => {
16+
event.preventDefault();
17+
18+
const formData = new FormData(event.target as HTMLFormElement);
19+
20+
const domain = formData.get('domain') as string;
21+
22+
await fetch(`/api/workspaces/${workspaceSlug}/domains`, {
23+
method: 'POST',
24+
headers: {
25+
'Content-Type': 'application/json',
26+
},
27+
body: JSON.stringify({
28+
name: domain,
29+
}),
30+
});
31+
32+
router.refresh();
33+
}}
34+
>
35+
<Input
36+
placeholder="mywebsite.com"
37+
name="domain"
38+
pattern="^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$"
39+
required
40+
/>
41+
<Button type="submit" className="ml-4">
42+
Add
43+
</Button>
44+
</form>
45+
);
46+
}

0 commit comments

Comments
 (0)