a tool for shared writing and social publishing

consolidate domain actions

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