diff --git a/showcase.json b/showcase.json index 5dc76edc..73c42c93 100644 --- a/showcase.json +++ b/showcase.json @@ -1,112 +1,12 @@ [ - "xlog", - "diygod", - "rss3", - "crossbell-blog", - "hey", - "song", - "atlas-thinking", - "joshua", - "jeff", - "crossbell-io-updates", - "kanikig", - "isheep", - "henryqw", - "hominsu", - "stars", - "somehacker", - "novonine", - "qfdk", - "fblauer", - "anxiete", - "kumasanki", - "jingfelix", - "tcj", - "atlas", - "meow-7218", - "erection", - "jowno", - "walt", - "adazz", - "ark", - "hans", - "isolcat", - "henryq", - "duoyu", - "candinya", - "shang", - "toast", - "pseudoyu", - "caos", - "nanami", - "xbox", - "vidorra", - "ivyheretochill", - "wiming", - "zsakvo", - "wuhang2003", - "bladedragon", - "qiuxuanluo", - "saltad", - "arthurwu", - "laoyan", - "baxx", - "kmkmkm", - "fuyuan", - "ming5ming", - "jameshih", - "cryptogirls", - "daidr", - "mekal", - "tommy-1329", - "jackyoung-5042", - "moojok", - "maji-may-7766", - "bocaibocaieth-1274", - "soptq-6165", - "yeungyeah", - "xnor", - "chenyyds", - "0xcavin", - "shuxinfeng", - "tea-24", - "revery", - "hsinyau", - "rootless", - "haydenull", - "walk2future", - "liuyejinghong", - "sion", - "magren", - "lolik", - "yuhangch", - "ssshooter", - "baiyi-5736", - "kendrickzou-8", - "nafidurmus", - "dagora", - "aiirobyte", - "onweb3", - "anghunk", - "agrislis", - "jigsaw", - "sakura-6601", - "bai-2800", - "rustynail", - "ximu-3371", - "xn--37q595dihas5a-4315", - "batgirl", - "exodus-exodus", - "xn--psu263e-5088", - "cryptonerdcn", - "fzdwx-2094", - "ovler", - "stz", - "monkey-1593", - "hoodrh", - "outtime", - "bs10081-7290", - "huazidev", - "character-4568", - "lyunvy" + 10, 33446, 572, 45, 153, 12, 506, 30, 595, 45899, 22033, 51494, 51124, 51232, + 40669, 45828, 26375, 49041, 48952, 51408, 51470, 51072, 496, 38034, 47359, + 33335, 50701, 50161, 470, 50312, 21052, 32022, 274, 77, 50168, 51145, 47328, + 630, 383, 51383, 723, 51172, 4583, 50472, 32179, 51316, 33210, 45089, 51094, + 31132, 50140, 37137, 42839, 50114, 51006, 51191, 50843, 46648, 33398, 887, + 51126, 51197, 32085, 1407, 51154, 51104, 40675, 4381, 43051, 50622, 51056, + 33483, 19, 50877, 33420, 50094, 49213, 33276, 50351, 42993, 50216, 50132, + 45763, 50143, 43754, 32168, 500, 47399, 33538, 48951, 33471, 33396, 37223, + 33458, 39133, 20004, 43625, 42787, 43258, 33462, 47109, 46939, 45701, 33233, + 37346, 40671, 33870, 33421 ] diff --git a/src/models/site.model.ts b/src/models/site.model.ts index 3383d754..3fa80e3f 100644 --- a/src/models/site.model.ts +++ b/src/models/site.model.ts @@ -138,8 +138,8 @@ export const getSites = async (input: string[]) => { const result = await client .query( ` - query getCharacters($identities: [String!], $limit: Int) { - characters( where: { handle: { in: $identities } }, orderBy: [{ updatedAt: desc }], take: $limit ) { + query getCharacters($identities: [Int!], $limit: Int) { + characters( where: { characterId: { in: $identities } }, orderBy: [{ updatedAt: desc }], take: $limit ) { handle updatedAt characterId @@ -719,3 +719,31 @@ export async function getMiraBalance(address: string, contract: Contract) { return result } + +export async function checkDomainServer(domain: string, handle: string) { + const dns = await ( + await fetch( + `https://cloudflare-dns.com/dns-query?name=_xlog-challenge.${domain}&type=TXT`, + { + headers: { + accept: "application/dns-json", + }, + }, + ) + ).json() + + const tenant = dns?.Answer?.[0]?.data.replace(/^"|"$/g, "") + if (!tenant || tenant !== handle) { + return false + } else { + return true + } +} + +export async function checkDomain(domain: string, handle: string) { + const check = await ( + await fetch(`/api/check-domain?domain=${domain}&handle=${handle}`) + ).json() + + return check.data +} diff --git a/src/pages/api/check-domain.tsx b/src/pages/api/check-domain.tsx new file mode 100644 index 00000000..17c9cca1 --- /dev/null +++ b/src/pages/api/check-domain.tsx @@ -0,0 +1,18 @@ +import { NextApiRequest, NextApiResponse } from "next" +import { checkDomainServer } from "~/models/site.model" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const { handle, domain } = req.query + + if (!handle || !domain) { + res.status(400).json({ error: "Missing characterId or domain" }) + return + } + + res.status(200).json({ + data: await checkDomainServer(domain as string, handle as string), + }) +} diff --git a/src/pages/api/host2handle.ts b/src/pages/api/host2handle.ts index 776fd8e5..a1c9a049 100644 --- a/src/pages/api/host2handle.ts +++ b/src/pages/api/host2handle.ts @@ -1,6 +1,7 @@ import { NextApiRequest, NextApiResponse } from "next" import { OUR_DOMAIN } from "~/lib/env" import { cacheGet } from "~/lib/redis.server" +import { checkDomainServer } from "~/models/site.model" export default async function handler( req: NextApiRequest, @@ -31,7 +32,10 @@ export default async function handler( char?.metadata?.content?.attributes?.find( (a: any) => a.trait_type === "xlog_custom_domain", )?.value || "" - if (customDomain) { + if ( + customDomain && + (await checkDomainServer(customDomain, subdomain)) + ) { return { redirect: /^https?:\/\//.test(customDomain) ? customDomain diff --git a/src/pages/api/redirection.tsx b/src/pages/api/redirection.tsx new file mode 100644 index 00000000..016f786a --- /dev/null +++ b/src/pages/api/redirection.tsx @@ -0,0 +1,48 @@ +import { NextApiRequest, NextApiResponse } from "next" +import { getSiteLink } from "~/lib/helpers" +import { getDefaultSlug } from "~/lib/helpers" +import { checkDomainServer } from "~/models/site.model" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const { characterId, noteId } = req.query + + if (!characterId) { + res.status(400).json({ error: "Missing characterId" }) + return + } + + const character = await ( + await fetch(`https://indexer.crossbell.io/v1/characters/${characterId}`) + ).json() + + let domain = character?.metadata?.content?.attributes?.find( + (a: any) => a.trait_type === "xlog_custom_domain", + )?.value + if (domain && !(await checkDomainServer(domain, character.handle))) { + domain = undefined + } + let link = getSiteLink({ + domain, + subdomain: character?.handle, + }) + + if (noteId) { + const note = await ( + await fetch( + `https://indexer.crossbell.io/v1/characters/${characterId}/notes/${noteId}`, + ) + ).json() + const slug = + note?.metadata?.content?.attributes?.find( + (a: any) => a.trait_type === "xlog_slug", + )?.value || + getDefaultSlug(note?.metadata?.content?.title, `${characterId}-${noteId}`) + + link += `/${slug}` + } + + res.redirect(link) +} diff --git a/src/pages/dashboard/[subdomain]/settings/domains.tsx b/src/pages/dashboard/[subdomain]/settings/domains.tsx index 1c363a83..4f8bede5 100644 --- a/src/pages/dashboard/[subdomain]/settings/domains.tsx +++ b/src/pages/dashboard/[subdomain]/settings/domains.tsx @@ -1,5 +1,5 @@ import { useRouter } from "next/router" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { useForm } from "react-hook-form" import toast from "react-hot-toast" import { DashboardLayout } from "~/components/dashboard/DashboardLayout" @@ -17,6 +17,7 @@ import { useTranslation } from "next-i18next" import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server" import { GetServerSideProps } from "next" import { serverSidePropsHandler } from "~/lib/server-side-props" +import { checkDomain } from "~/models/site.model" export const getServerSideProps: GetServerSideProps = serverSidePropsHandler( async (ctx) => { @@ -63,7 +64,10 @@ export default function SettingsDomainsPage() { const [customDomain, setCustomDomain] = useState("") form.register("custom_domain", { - onChange: (e) => setCustomDomain(e.target.value), + onChange: (e) => { + setCustomDomain(e.target.value) + toCheckDomain(e.target.value) + }, }) const customSubdomain = customDomain?.split(".").slice(0, -2).join(".") || "" @@ -73,7 +77,9 @@ export default function SettingsDomainsPage() { if (updateSite.data?.code === 0) { toast.success("Saved!") router.replace( - `/dashboard/${updateSite.variables?.subdomain}/settings/domains`, + `/dashboard/${ + updateSite.variables?.subdomain || updateSite.variables?.site + }/settings/domains`, ) } else { toast.error("Failed to update site" + ": " + updateSite.data.message) @@ -83,6 +89,30 @@ export default function SettingsDomainsPage() { } }, [updateSite.isSuccess]) + const [domainCheckResult, setDomainCheckResult] = useState<{ + isLoading: boolean + data?: boolean + }>({ + isLoading: false, + data: true, + }) + const toCheckDomain = useCallback( + (custom: string) => { + if (custom) { + setDomainCheckResult({ + isLoading: true, + }) + checkDomain(custom, subdomain).then((r) => + setDomainCheckResult({ + isLoading: false, + data: r, + }), + ) + } + }, + [subdomain], + ) + const [hasSet, setHasSet] = useState(false) useEffect(() => { if (site.isSuccess && site.data && !hasSet) { @@ -90,8 +120,9 @@ export default function SettingsDomainsPage() { form.setValue("subdomain", site.data.username || "") form.setValue("custom_domain", site.data.custom_domain || "") setCustomDomain(site.data.custom_domain || "") + toCheckDomain(site.data.custom_domain || "") } - }, [form, site.data, site.isSuccess, customDomain, hasSet]) + }, [form, site.data, site.isSuccess, customDomain, hasSet, toCheckDomain]) return ( @@ -143,14 +174,14 @@ export default function SettingsDomainsPage() { {...form.register("custom_domain")} /> {customDomain && ( -
-

+

+

{t( "Set the following record on your DNS provider to active your custom domain", )} :

- +
@@ -173,11 +204,33 @@ export default function SettingsDomainsPage() {
Type
+
+ {domainCheckResult.isLoading ? ( + Checking... + ) : domainCheckResult.data ? ( + DNS check Passed. + ) : ( + + DNS check failed. + + + )} +
)}
-