tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
294
fork
atom
a tool for shared writing and social publishing
294
fork
atom
overview
issues
31
pulls
pipelines
consolidate domain actions
awarm.space
2 days ago
6cccc1a3
4641f287
+224
-318
12 changed files
expand all
collapse all
unified
split
actions
domains
addDomain.ts
addDomainPath.ts
assignDomainToDocument.ts
assignDomainToPublication.ts
deleteDomain.ts
index.ts
removeDomainAssignment.ts
removeDomainRoute.ts
app
[leaflet_id]
actions
ShareOptions
DomainOptions.tsx
components
Domains
AddDomainForm.tsx
DomainSettingsView.tsx
PublicationDomains.tsx
-98
actions/domains/addDomain.ts
···
1
1
-
"use server";
2
2
-
import { Vercel } from "@vercel/sdk";
3
3
-
import { cookies } from "next/headers";
4
4
-
5
5
-
import { Database } from "supabase/database.types";
6
6
-
import { createServerClient } from "@supabase/ssr";
7
7
-
import { getIdentityData } from "actions/getIdentityData";
8
8
-
9
9
-
const VERCEL_TOKEN = process.env.VERCEL_TOKEN;
10
10
-
const vercel = new Vercel({
11
11
-
bearerToken: VERCEL_TOKEN,
12
12
-
});
13
13
-
14
14
-
let supabase = createServerClient<Database>(
15
15
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
16
16
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
17
17
-
{ cookies: {} },
18
18
-
);
19
19
-
20
20
-
export async function addDomain(domain: string) {
21
21
-
let identity = await getIdentityData();
22
22
-
if (!identity || (!identity.email && !identity.atp_did)) return {};
23
23
-
if (
24
24
-
domain.includes("leaflet.pub") &&
25
25
-
(!identity.email ||
26
26
-
![
27
27
-
"celine@hyperlink.academy",
28
28
-
"brendan@hyperlink.academy",
29
29
-
"jared@hyperlink.academy",
30
30
-
"brendan.schlagel@gmail.com",
31
31
-
].includes(identity.email))
32
32
-
)
33
33
-
return {};
34
34
-
return await createDomain(domain, identity.email, identity.id);
35
35
-
}
36
36
-
37
37
-
export async function addPublicationDomain(
38
38
-
domain: string,
39
39
-
publication_uri: string,
40
40
-
) {
41
41
-
let identity = await getIdentityData();
42
42
-
if (!identity || !identity.atp_did) return {};
43
43
-
let { data: publication } = await supabase
44
44
-
.from("publications")
45
45
-
.select("*")
46
46
-
.eq("uri", publication_uri)
47
47
-
.single();
48
48
-
49
49
-
if (publication?.identity_did !== identity.atp_did) return {};
50
50
-
let { error } = await createDomain(domain, null, identity.id);
51
51
-
if (error) return { error };
52
52
-
await supabase.from("publication_domains").insert({
53
53
-
publication: publication_uri,
54
54
-
identity: identity.atp_did,
55
55
-
domain,
56
56
-
});
57
57
-
return {};
58
58
-
}
59
59
-
60
60
-
async function createDomain(
61
61
-
domain: string,
62
62
-
email: string | null,
63
63
-
identity_id: string,
64
64
-
) {
65
65
-
try {
66
66
-
await vercel.projects.addProjectDomain({
67
67
-
idOrName: "prj_9jX4tmYCISnm176frFxk07fF74kG",
68
68
-
teamId: "team_42xaJiZMTw9Sr7i0DcLTae9d",
69
69
-
requestBody: {
70
70
-
name: domain,
71
71
-
},
72
72
-
});
73
73
-
} catch (e) {
74
74
-
console.log(e);
75
75
-
let error: "unknown-error" | "invalid_domain" | "domain_already_in_use" =
76
76
-
"unknown-error";
77
77
-
if ((e as any).rawValue) {
78
78
-
error =
79
79
-
(e as { rawValue?: { error?: { code?: "invalid_domain" } } })?.rawValue
80
80
-
?.error?.code || "unknown-error";
81
81
-
}
82
82
-
if ((e as any).body) {
83
83
-
try {
84
84
-
error = JSON.parse((e as any).body)?.error?.code || "unknown-error";
85
85
-
} catch (e) {}
86
86
-
}
87
87
-
88
88
-
return { error };
89
89
-
}
90
90
-
91
91
-
await supabase.from("custom_domains").insert({
92
92
-
domain,
93
93
-
identity: email,
94
94
-
confirmed: false,
95
95
-
identity_id,
96
96
-
});
97
97
-
return {};
98
98
-
}
-39
actions/domains/addDomainPath.ts
···
1
1
-
"use server";
2
2
-
import { cookies } from "next/headers";
3
3
-
import { Database } from "supabase/database.types";
4
4
-
import { createServerClient } from "@supabase/ssr";
5
5
-
import { getIdentityData } from "actions/getIdentityData";
6
6
-
7
7
-
let supabase = createServerClient<Database>(
8
8
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
9
9
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
10
10
-
{ cookies: {} },
11
11
-
);
12
12
-
export async function addDomainPath({
13
13
-
domain,
14
14
-
view_permission_token,
15
15
-
edit_permission_token,
16
16
-
route,
17
17
-
}: {
18
18
-
domain: string;
19
19
-
view_permission_token: string;
20
20
-
edit_permission_token: string;
21
21
-
route: string;
22
22
-
}) {
23
23
-
let auth_data = await getIdentityData();
24
24
-
if (!auth_data || !auth_data.custom_domains.find((d) => d.domain === domain))
25
25
-
return null;
26
26
-
27
27
-
await supabase
28
28
-
.from("custom_domain_routes")
29
29
-
.delete()
30
30
-
.eq("edit_permission_token", edit_permission_token);
31
31
-
32
32
-
await supabase.from("custom_domain_routes").insert({
33
33
-
domain,
34
34
-
route,
35
35
-
view_permission_token,
36
36
-
edit_permission_token,
37
37
-
});
38
38
-
return true;
39
39
-
}
-43
actions/domains/assignDomainToDocument.ts
···
1
1
-
"use server";
2
2
-
import { Database } from "supabase/database.types";
3
3
-
import { createServerClient } from "@supabase/ssr";
4
4
-
import { getIdentityData } from "actions/getIdentityData";
5
5
-
6
6
-
let supabase = createServerClient<Database>(
7
7
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
8
8
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
9
9
-
{ cookies: {} },
10
10
-
);
11
11
-
12
12
-
export async function assignDomainToDocument({
13
13
-
domain,
14
14
-
route,
15
15
-
view_permission_token,
16
16
-
edit_permission_token,
17
17
-
}: {
18
18
-
domain: string;
19
19
-
route: string;
20
20
-
view_permission_token: string;
21
21
-
edit_permission_token: string;
22
22
-
}) {
23
23
-
let identity = await getIdentityData();
24
24
-
if (!identity || !identity.custom_domains.find((d) => d.domain === domain))
25
25
-
return null;
26
26
-
27
27
-
await Promise.all([
28
28
-
supabase.from("publication_domains").delete().eq("domain", domain),
29
29
-
supabase
30
30
-
.from("custom_domain_routes")
31
31
-
.delete()
32
32
-
.eq("edit_permission_token", edit_permission_token),
33
33
-
]);
34
34
-
35
35
-
await supabase.from("custom_domain_routes").insert({
36
36
-
domain,
37
37
-
route,
38
38
-
view_permission_token,
39
39
-
edit_permission_token,
40
40
-
});
41
41
-
42
42
-
return true;
43
43
-
}
-42
actions/domains/assignDomainToPublication.ts
···
1
1
-
"use server";
2
2
-
import { Database } from "supabase/database.types";
3
3
-
import { createServerClient } from "@supabase/ssr";
4
4
-
import { getIdentityData } from "actions/getIdentityData";
5
5
-
6
6
-
let supabase = createServerClient<Database>(
7
7
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
8
8
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
9
9
-
{ cookies: {} },
10
10
-
);
11
11
-
12
12
-
export async function assignDomainToPublication({
13
13
-
domain,
14
14
-
publication_uri,
15
15
-
}: {
16
16
-
domain: string;
17
17
-
publication_uri: string;
18
18
-
}) {
19
19
-
let identity = await getIdentityData();
20
20
-
if (!identity || !identity.atp_did) return null;
21
21
-
if (!identity.custom_domains.find((d) => d.domain === domain)) return null;
22
22
-
23
23
-
let { data: publication } = await supabase
24
24
-
.from("publications")
25
25
-
.select("*")
26
26
-
.eq("uri", publication_uri)
27
27
-
.single();
28
28
-
if (publication?.identity_did !== identity.atp_did) return null;
29
29
-
30
30
-
await Promise.all([
31
31
-
supabase.from("custom_domain_routes").delete().eq("domain", domain),
32
32
-
supabase.from("publication_domains").delete().eq("domain", domain),
33
33
-
]);
34
34
-
35
35
-
await supabase.from("publication_domains").insert({
36
36
-
publication: publication_uri,
37
37
-
identity: identity.atp_did,
38
38
-
domain,
39
39
-
});
40
40
-
41
41
-
return true;
42
42
-
}
-36
actions/domains/deleteDomain.ts
···
1
1
-
"use server";
2
2
-
import { Database } from "supabase/database.types";
3
3
-
import { createServerClient } from "@supabase/ssr";
4
4
-
import { Vercel } from "@vercel/sdk";
5
5
-
import { getIdentityData } from "actions/getIdentityData";
6
6
-
7
7
-
let supabase = createServerClient<Database>(
8
8
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
9
9
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
10
10
-
{ cookies: {} },
11
11
-
);
12
12
-
13
13
-
const VERCEL_TOKEN = process.env.VERCEL_TOKEN;
14
14
-
const vercel = new Vercel({
15
15
-
bearerToken: VERCEL_TOKEN,
16
16
-
});
17
17
-
export async function deleteDomain({ domain }: { domain: string }) {
18
18
-
let identity = await getIdentityData();
19
19
-
if (!identity || !identity.custom_domains.find((d) => d.domain === domain))
20
20
-
return null;
21
21
-
22
22
-
await Promise.all([
23
23
-
supabase.from("custom_domain_routes").delete().eq("domain", domain),
24
24
-
supabase.from("publication_domains").delete().eq("domain", domain),
25
25
-
]);
26
26
-
await Promise.all([
27
27
-
supabase.from("custom_domains").delete().eq("domain", domain),
28
28
-
vercel.projects.removeProjectDomain({
29
29
-
idOrName: "prj_9jX4tmYCISnm176frFxk07fF74kG",
30
30
-
teamId: "team_42xaJiZMTw9Sr7i0DcLTae9d",
31
31
-
domain,
32
32
-
}),
33
33
-
]);
34
34
-
35
35
-
return true;
36
36
-
}
+210
actions/domains/index.ts
···
1
1
+
"use server";
2
2
+
import { Database } from "supabase/database.types";
3
3
+
import { createServerClient } from "@supabase/ssr";
4
4
+
import { Vercel } from "@vercel/sdk";
5
5
+
import { getIdentityData } from "actions/getIdentityData";
6
6
+
7
7
+
let supabase = createServerClient<Database>(
8
8
+
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
9
9
+
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
10
10
+
{ cookies: {} },
11
11
+
);
12
12
+
13
13
+
const vercel = new Vercel({
14
14
+
bearerToken: process.env.VERCEL_TOKEN,
15
15
+
});
16
16
+
17
17
+
const VERCEL_PROJECT = "prj_9jX4tmYCISnm176frFxk07fF74kG";
18
18
+
const VERCEL_TEAM = "team_42xaJiZMTw9Sr7i0DcLTae9d";
19
19
+
20
20
+
// Shared helpers
21
21
+
// ==============
22
22
+
23
23
+
async function assertOwnsDomain(domain: string) {
24
24
+
let identity = await getIdentityData();
25
25
+
if (!identity || !identity.custom_domains.find((d) => d.domain === domain))
26
26
+
return null;
27
27
+
return identity;
28
28
+
}
29
29
+
30
30
+
// Clear all assignments (routes + publication links) for a domain,
31
31
+
// without deleting the domain itself.
32
32
+
async function clearAllAssignments(domain: string) {
33
33
+
await Promise.all([
34
34
+
supabase.from("custom_domain_routes").delete().eq("domain", domain),
35
35
+
supabase.from("publication_domains").delete().eq("domain", domain),
36
36
+
]);
37
37
+
}
38
38
+
39
39
+
// Adding domains
40
40
+
// ==============
41
41
+
42
42
+
export async function addDomain(domain: string) {
43
43
+
let identity = await getIdentityData();
44
44
+
if (!identity || (!identity.email && !identity.atp_did)) return {};
45
45
+
if (
46
46
+
domain.includes("leaflet.pub") &&
47
47
+
(!identity.email ||
48
48
+
![
49
49
+
"celine@hyperlink.academy",
50
50
+
"brendan@hyperlink.academy",
51
51
+
"jared@hyperlink.academy",
52
52
+
"brendan.schlagel@gmail.com",
53
53
+
].includes(identity.email))
54
54
+
)
55
55
+
return {};
56
56
+
return await createDomain(domain, identity.email, identity.id);
57
57
+
}
58
58
+
59
59
+
async function createDomain(
60
60
+
domain: string,
61
61
+
email: string | null,
62
62
+
identity_id: string,
63
63
+
) {
64
64
+
try {
65
65
+
await vercel.projects.addProjectDomain({
66
66
+
idOrName: VERCEL_PROJECT,
67
67
+
teamId: VERCEL_TEAM,
68
68
+
requestBody: { name: domain },
69
69
+
});
70
70
+
} catch (e) {
71
71
+
console.log(e);
72
72
+
let error: "unknown-error" | "invalid_domain" | "domain_already_in_use" =
73
73
+
"unknown-error";
74
74
+
if ((e as any).rawValue) {
75
75
+
error =
76
76
+
(e as { rawValue?: { error?: { code?: "invalid_domain" } } })?.rawValue
77
77
+
?.error?.code || "unknown-error";
78
78
+
}
79
79
+
if ((e as any).body) {
80
80
+
try {
81
81
+
error = JSON.parse((e as any).body)?.error?.code || "unknown-error";
82
82
+
} catch (e) {}
83
83
+
}
84
84
+
return { error };
85
85
+
}
86
86
+
87
87
+
await supabase.from("custom_domains").insert({
88
88
+
domain,
89
89
+
identity: email,
90
90
+
confirmed: false,
91
91
+
identity_id,
92
92
+
});
93
93
+
return {};
94
94
+
}
95
95
+
96
96
+
// Assigning domains
97
97
+
// =================
98
98
+
99
99
+
// Point a domain at a leaflet document. Clears any existing assignment first,
100
100
+
// since a domain can only point to one thing at a time.
101
101
+
export async function assignDomainToDocument({
102
102
+
domain,
103
103
+
route,
104
104
+
view_permission_token,
105
105
+
edit_permission_token,
106
106
+
}: {
107
107
+
domain: string;
108
108
+
route: string;
109
109
+
view_permission_token: string;
110
110
+
edit_permission_token: string;
111
111
+
}) {
112
112
+
if (!(await assertOwnsDomain(domain))) return null;
113
113
+
114
114
+
await Promise.all([
115
115
+
supabase.from("publication_domains").delete().eq("domain", domain),
116
116
+
supabase
117
117
+
.from("custom_domain_routes")
118
118
+
.delete()
119
119
+
.eq("edit_permission_token", edit_permission_token),
120
120
+
]);
121
121
+
122
122
+
await supabase.from("custom_domain_routes").insert({
123
123
+
domain,
124
124
+
route,
125
125
+
view_permission_token,
126
126
+
edit_permission_token,
127
127
+
});
128
128
+
129
129
+
return true;
130
130
+
}
131
131
+
132
132
+
// Point a domain at a publication. Clears any existing assignment first.
133
133
+
export async function assignDomainToPublication({
134
134
+
domain,
135
135
+
publication_uri,
136
136
+
}: {
137
137
+
domain: string;
138
138
+
publication_uri: string;
139
139
+
}) {
140
140
+
let identity = await getIdentityData();
141
141
+
if (!identity || !identity.atp_did) return null;
142
142
+
if (!identity.custom_domains.find((d) => d.domain === domain)) return null;
143
143
+
144
144
+
let { data: publication } = await supabase
145
145
+
.from("publications")
146
146
+
.select("*")
147
147
+
.eq("uri", publication_uri)
148
148
+
.single();
149
149
+
if (publication?.identity_did !== identity.atp_did) return null;
150
150
+
151
151
+
await clearAllAssignments(domain);
152
152
+
153
153
+
await supabase.from("publication_domains").insert({
154
154
+
publication: publication_uri,
155
155
+
identity: identity.atp_did,
156
156
+
domain,
157
157
+
});
158
158
+
159
159
+
return true;
160
160
+
}
161
161
+
162
162
+
// Removing assignments
163
163
+
// ====================
164
164
+
165
165
+
// Remove all assignments from a domain (routes + publication links),
166
166
+
// but keep the domain itself registered.
167
167
+
export async function removeDomainAssignment({
168
168
+
domain,
169
169
+
}: {
170
170
+
domain: string;
171
171
+
}) {
172
172
+
if (!(await assertOwnsDomain(domain))) return null;
173
173
+
await clearAllAssignments(domain);
174
174
+
return true;
175
175
+
}
176
176
+
177
177
+
// Remove a single route assignment by ID.
178
178
+
export async function removeDomainRoute({ routeId }: { routeId: string }) {
179
179
+
let identity = await getIdentityData();
180
180
+
if (!identity) return null;
181
181
+
182
182
+
let allRoutes = identity.custom_domains.flatMap(
183
183
+
(d) => d.custom_domain_routes,
184
184
+
);
185
185
+
if (!allRoutes.find((r) => r.id === routeId)) return null;
186
186
+
187
187
+
await supabase.from("custom_domain_routes").delete().eq("id", routeId);
188
188
+
189
189
+
return true;
190
190
+
}
191
191
+
192
192
+
// Deleting domains
193
193
+
// ================
194
194
+
195
195
+
// Fully delete a domain: clear all assignments, remove from DB, and remove from Vercel.
196
196
+
export async function deleteDomain({ domain }: { domain: string }) {
197
197
+
if (!(await assertOwnsDomain(domain))) return null;
198
198
+
199
199
+
await clearAllAssignments(domain);
200
200
+
await Promise.all([
201
201
+
supabase.from("custom_domains").delete().eq("domain", domain),
202
202
+
vercel.projects.removeProjectDomain({
203
203
+
idOrName: VERCEL_PROJECT,
204
204
+
teamId: VERCEL_TEAM,
205
205
+
domain,
206
206
+
}),
207
207
+
]);
208
208
+
209
209
+
return true;
210
210
+
}
-27
actions/domains/removeDomainAssignment.ts
···
1
1
-
"use server";
2
2
-
import { Database } from "supabase/database.types";
3
3
-
import { createServerClient } from "@supabase/ssr";
4
4
-
import { getIdentityData } from "actions/getIdentityData";
5
5
-
6
6
-
let supabase = createServerClient<Database>(
7
7
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
8
8
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
9
9
-
{ cookies: {} },
10
10
-
);
11
11
-
12
12
-
export async function removeDomainAssignment({
13
13
-
domain,
14
14
-
}: {
15
15
-
domain: string;
16
16
-
}) {
17
17
-
let identity = await getIdentityData();
18
18
-
if (!identity || !identity.custom_domains.find((d) => d.domain === domain))
19
19
-
return null;
20
20
-
21
21
-
await Promise.all([
22
22
-
supabase.from("custom_domain_routes").delete().eq("domain", domain),
23
23
-
supabase.from("publication_domains").delete().eq("domain", domain),
24
24
-
]);
25
25
-
26
26
-
return true;
27
27
-
}
-25
actions/domains/removeDomainRoute.ts
···
1
1
-
"use server";
2
2
-
import { Database } from "supabase/database.types";
3
3
-
import { createServerClient } from "@supabase/ssr";
4
4
-
import { getIdentityData } from "actions/getIdentityData";
5
5
-
6
6
-
let supabase = createServerClient<Database>(
7
7
-
process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
8
8
-
process.env.SUPABASE_SERVICE_ROLE_KEY as string,
9
9
-
{ cookies: {} },
10
10
-
);
11
11
-
12
12
-
export async function removeDomainRoute({ routeId }: { routeId: string }) {
13
13
-
let identity = await getIdentityData();
14
14
-
if (!identity) return null;
15
15
-
16
16
-
// Verify the route belongs to one of the user's domains
17
17
-
let allRoutes = identity.custom_domains.flatMap(
18
18
-
(d) => d.custom_domain_routes,
19
19
-
);
20
20
-
if (!allRoutes.find((r) => r.id === routeId)) return null;
21
21
-
22
22
-
await supabase.from("custom_domain_routes").delete().eq("id", routeId);
23
23
-
24
24
-
return true;
25
25
-
}
+4
-2
app/[leaflet_id]/actions/ShareOptions/DomainOptions.tsx
···
10
10
import { CustomDomain } from "components/Domains/DomainList";
11
11
import { useLeafletDomains } from "components/PageSWRDataProvider";
12
12
import { useReadOnlyShareLink } from ".";
13
13
-
import { assignDomainToDocument } from "actions/domains/assignDomainToDocument";
14
14
-
import { removeDomainRoute } from "actions/domains/removeDomainRoute";
13
13
+
import {
14
14
+
assignDomainToDocument,
15
15
+
removeDomainRoute,
16
16
+
} from "actions/domains";
15
17
import { useReplicache } from "src/replicache";
16
18
import { AddDomainForm } from "components/Domains/AddDomainForm";
17
19
import { DomainSettingsView } from "components/Domains/DomainSettingsView";
+1
-1
components/Domains/AddDomainForm.tsx
···
4
4
import { Input } from "components/Input";
5
5
import { useSmoker } from "components/Toast";
6
6
import { useIdentityData } from "components/IdentityProvider";
7
7
-
import { addDomain } from "actions/domains/addDomain";
7
7
+
import { addDomain } from "actions/domains";
8
8
import { DotLoader } from "components/utils/DotLoader";
9
9
import { GoToArrow } from "components/Icons/GoToArrow";
10
10
+5
-3
components/Domains/DomainSettingsView.tsx
···
2
2
import { useState } from "react";
3
3
import { useDomainStatus } from "./useDomainStatus";
4
4
import { DotLoader } from "components/utils/DotLoader";
5
5
-
import { deleteDomain } from "actions/domains/deleteDomain";
6
6
-
import { removeDomainAssignment } from "actions/domains/removeDomainAssignment";
7
7
-
import { removeDomainRoute } from "actions/domains/removeDomainRoute";
5
5
+
import {
6
6
+
deleteDomain,
7
7
+
removeDomainAssignment,
8
8
+
removeDomainRoute,
9
9
+
} from "actions/domains";
8
10
import {
9
11
useIdentityData,
10
12
mutateIdentityData,
+4
-2
components/Domains/PublicationDomains.tsx
···
13
13
useNormalizedPublicationRecord,
14
14
} from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider";
15
15
import { updatePublicationBasePath } from "app/lish/createPub/updatePublication";
16
16
-
import { assignDomainToPublication } from "actions/domains/assignDomainToPublication";
17
17
-
import { removeDomainAssignment } from "actions/domains/removeDomainAssignment";
16
16
+
import {
17
17
+
assignDomainToPublication,
18
18
+
removeDomainAssignment,
19
19
+
} from "actions/domains";
18
20
import { PubSettingsHeader } from "app/lish/[did]/[publication]/dashboard/settings/PublicationSettings";
19
21
import { AddDomainForm } from "./AddDomainForm";
20
22
import { DomainSettingsView } from "./DomainSettingsView";