├── .env.example ├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── docker-compose.yml ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma ├── migrations │ ├── 20230622204208_initial │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public └── favicon.ico ├── src ├── env.mjs ├── middleware.ts ├── pages │ ├── _app.tsx │ ├── api │ │ ├── file-verification │ │ │ └── [domain] │ │ │ │ └── [handle].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── cat.tsx │ ├── components │ │ ├── HandleForm │ │ │ └── index.tsx │ │ └── Select.tsx │ └── index.tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ └── handle.ts │ │ └── trpc.ts │ ├── auth.ts │ ├── db.ts │ └── domainProviders │ │ └── cloudflare.ts ├── styles │ └── globals.css └── utils │ ├── api.ts │ ├── bsky.ts │ ├── domains.ts │ ├── provider.ts │ └── regex.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Prisma 13 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 14 | DATABASE_URL="postgresql://postgres:password@localhost:5432" 15 | 16 | # Next Auth 17 | # You can generate a new secret on the command line with: 18 | # openssl rand -base64 32 19 | # https://next-auth.js.org/configuration/options#secret 20 | NEXTAUTH_SECRET="ZDL24sZ6gi8vtYpDE4e5N+4nvJbYl/dQFXHnGxWwa/U=" 21 | NEXTAUTH_URL="http://localhost:3000" 22 | 23 | # Next Auth Discord Provider 24 | DISCORD_CLIENT_ID="" 25 | DISCORD_CLIENT_SECRET="" 26 | 27 | DOMAINS_CLOUDFLARE="the-gay.cat:ID,woke.cat:ID,lesbian.cat:ID,is-extremely.gay:ID" 28 | DOMAINS_FILE_VERIFICATION="test.localhost" 29 | 30 | NEXT_PUBLIC_DOMAINS_CLOUDFLARE="the-gay.cat,woke.cat,lesbian.cat,is-extremely.gay" 31 | NEXT_PUBLIC_DOMAINS_FILE_VERIFICATION="test.localhost" 32 | 33 | CLOUDFLARE_ID="" 34 | CLOUDFLARE_SECRET="" 35 | 36 | UPSTASH_REDIS_REST_URL="https://upstash.io" 37 | UPSTASH_REDIS_REST_TOKEN="" -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require("path"); 3 | 4 | /** @type {import("eslint").Linter.Config} */ 5 | const config = { 6 | overrides: [ 7 | { 8 | extends: [ 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | ], 11 | files: ["*.ts", "*.tsx"], 12 | parserOptions: { 13 | project: path.join(__dirname, "tsconfig.json"), 14 | }, 15 | }, 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | project: path.join(__dirname, "tsconfig.json"), 20 | }, 21 | plugins: ["@typescript-eslint"], 22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 23 | rules: { 24 | "@typescript-eslint/consistent-type-imports": [ 25 | "warn", 26 | { 27 | prefer: "type-imports", 28 | fixStyle: "inline-type-imports", 29 | }, 30 | ], 31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 32 | }, 33 | }; 34 | 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [SlickDomique] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: domi_zip 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SlickDomique 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Handles 2 | 3 | A project to let you host site that allows people create custom bluesky handles with your domains 4 | 5 | ## Dev 6 | 7 | ``` 8 | yarn 9 | yarn dev 10 | ``` 11 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - make components look better and be reusable 2 | - implement a list of taken handles 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use postgres/example user/password credentials 2 | version: "3.1" 3 | 4 | services: 5 | db: 6 | image: postgres 7 | restart: always 8 | environment: 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: devPassword4412 11 | ports: 12 | - 5432:5432 13 | 14 | adminer: 15 | image: adminer 16 | restart: always 17 | ports: 18 | - 8080:8080 19 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | await import("./src/env.mjs"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config 13 | * out. 14 | * 15 | * @see https://github.com/vercel/next.js/issues/41980 16 | */ 17 | i18n: { 18 | locales: ["en"], 19 | defaultLocale: "en", 20 | }, 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-handles", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "postinstall": "prisma generate", 9 | "lint": "next lint", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "@fontsource/roboto": "^5.0.3", 14 | "@next-auth/prisma-adapter": "^1.0.5", 15 | "@prisma/client": "^4.14.0", 16 | "@t3-oss/env-nextjs": "^0.3.1", 17 | "@tanstack/react-query": "^4.29.7", 18 | "@trpc/client": "^10.26.0", 19 | "@trpc/next": "^10.26.0", 20 | "@trpc/react-query": "^10.26.0", 21 | "@trpc/server": "^10.26.0", 22 | "@upstash/ratelimit": "^0.4.3", 23 | "@upstash/redis": "^1.21.0", 24 | "@vercel/analytics": "^1.3.1", 25 | "next": "^14.2.7", 26 | "next-auth": "^4.22.1", 27 | "nextjs-cors": "^2.1.2", 28 | "prisma": "^4.14.0", 29 | "react": "^18.3.1", 30 | "react-dom": "^18.3.1", 31 | "superjson": "1.12.2", 32 | "zod": "^3.21.4" 33 | }, 34 | "devDependencies": { 35 | "@types/eslint": "^8.37.0", 36 | "@types/node": "^18.16.0", 37 | "@types/prettier": "^2.7.2", 38 | "@types/react": "^18.2.6", 39 | "@types/react-dom": "^18.2.4", 40 | "@typescript-eslint/eslint-plugin": "^5.59.6", 41 | "@typescript-eslint/parser": "^5.59.6", 42 | "autoprefixer": "^10.4.14", 43 | "eslint": "^8.40.0", 44 | "eslint-config-next": "^14.2.7", 45 | "postcss": "^8.4.21", 46 | "prettier": "^2.8.8", 47 | "prettier-plugin-tailwindcss": "^0.2.8", 48 | "tailwindcss": "^3.3.0", 49 | "typescript": "^5.0.4" 50 | }, 51 | "ct3aMetadata": { 52 | "initVersion": "7.14.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20230622204208_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Example" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | 7 | CONSTRAINT "Example_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Handle" ( 12 | "id" TEXT NOT NULL, 13 | "handle" TEXT NOT NULL, 14 | "subdomain" TEXT NOT NULL, 15 | "subdomainValue" TEXT NOT NULL, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | 19 | CONSTRAINT "Handle_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "Account" ( 24 | "id" TEXT NOT NULL, 25 | "userId" TEXT NOT NULL, 26 | "type" TEXT NOT NULL, 27 | "provider" TEXT NOT NULL, 28 | "providerAccountId" TEXT NOT NULL, 29 | "refresh_token" TEXT, 30 | "access_token" TEXT, 31 | "expires_at" INTEGER, 32 | "token_type" TEXT, 33 | "scope" TEXT, 34 | "id_token" TEXT, 35 | "session_state" TEXT, 36 | 37 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 38 | ); 39 | 40 | -- CreateTable 41 | CREATE TABLE "Session" ( 42 | "id" TEXT NOT NULL, 43 | "sessionToken" TEXT NOT NULL, 44 | "userId" TEXT NOT NULL, 45 | "expires" TIMESTAMP(3) NOT NULL, 46 | 47 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 48 | ); 49 | 50 | -- CreateTable 51 | CREATE TABLE "User" ( 52 | "id" TEXT NOT NULL, 53 | "name" TEXT, 54 | "email" TEXT, 55 | "emailVerified" TIMESTAMP(3), 56 | "image" TEXT, 57 | 58 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 59 | ); 60 | 61 | -- CreateTable 62 | CREATE TABLE "VerificationToken" ( 63 | "identifier" TEXT NOT NULL, 64 | "token" TEXT NOT NULL, 65 | "expires" TIMESTAMP(3) NOT NULL 66 | ); 67 | 68 | -- CreateIndex 69 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 70 | 71 | -- CreateIndex 72 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 73 | 74 | -- CreateIndex 75 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 76 | 77 | -- CreateIndex 78 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 79 | 80 | -- CreateIndex 81 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 82 | 83 | -- AddForeignKey 84 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 85 | 86 | -- AddForeignKey 87 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 88 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | previewFeatures = ["jsonProtocol"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below 12 | // Further reading: 13 | // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema 14 | // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string 15 | url = env("DATABASE_URL") 16 | } 17 | 18 | model Example { 19 | id String @id @default(cuid()) 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | } 23 | 24 | model Handle { 25 | id String @id @default(cuid()) 26 | handle String 27 | subdomain String 28 | subdomainValue String 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | } 32 | 33 | // Necessary for Next auth 34 | model Account { 35 | id String @id @default(cuid()) 36 | userId String 37 | type String 38 | provider String 39 | providerAccountId String 40 | refresh_token String? // @db.Text 41 | access_token String? // @db.Text 42 | expires_at Int? 43 | token_type String? 44 | scope String? 45 | id_token String? // @db.Text 46 | session_state String? 47 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 48 | 49 | @@unique([provider, providerAccountId]) 50 | } 51 | 52 | model Session { 53 | id String @id @default(cuid()) 54 | sessionToken String @unique 55 | userId String 56 | expires DateTime 57 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 58 | } 59 | 60 | model User { 61 | id String @id @default(cuid()) 62 | name String? 63 | email String? @unique 64 | emailVerified DateTime? 65 | image String? 66 | accounts Account[] 67 | sessions Session[] 68 | } 69 | 70 | model VerificationToken { 71 | identifier String 72 | token String @unique 73 | expires DateTime 74 | 75 | @@unique([identifier, token]) 76 | } 77 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlickDomique/open-handles/abea052a7c0c40ab936cb4e1c0b1842eaa366beb/public/favicon.ico -------------------------------------------------------------------------------- /src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * Specify your server-side environment variables schema here. This way you can ensure the app 7 | * isn't built with invalid env vars. 8 | */ 9 | server: { 10 | DATABASE_URL: z.string().url(), 11 | NODE_ENV: z.enum(["development", "test", "production"]), 12 | NEXTAUTH_SECRET: 13 | process.env.NODE_ENV === "production" 14 | ? z.string().min(1) 15 | : z.string().min(1).optional(), 16 | NEXTAUTH_URL: z.preprocess( 17 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL 18 | // Since NextAuth.js automatically uses the VERCEL_URL if present. 19 | (str) => process.env.VERCEL_URL ?? str, 20 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL 21 | process.env.VERCEL ? z.string().min(1) : z.string().url() 22 | ), 23 | // Add `.min(1) on ID and SECRET if you want to make sure they're not empty 24 | DISCORD_CLIENT_ID: z.string(), 25 | DISCORD_CLIENT_SECRET: z.string(), 26 | DOMAINS_CLOUDFLARE: z.string(), 27 | CLOUDFLARE_SECRET: z.string(), 28 | }, 29 | 30 | /** 31 | * Specify your client-side environment variables schema here. This way you can ensure the app 32 | * isn't built with invalid env vars. To expose them to the client, prefix them with 33 | * `NEXT_PUBLIC_`. 34 | */ 35 | client: { 36 | // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), 37 | }, 38 | 39 | /** 40 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 41 | * middlewares) or client-side so we need to destruct manually. 42 | */ 43 | runtimeEnv: { 44 | DATABASE_URL: process.env.DATABASE_URL, 45 | NODE_ENV: process.env.NODE_ENV, 46 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, 47 | NEXTAUTH_URL: process.env.NEXTAUTH_URL, 48 | DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, 49 | DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, 50 | DOMAINS_CLOUDFLARE: process.env.DOMAINS_CLOUDFLARE, 51 | CLOUDFLARE_SECRET: process.env.CLOUDFLARE_SECRET, 52 | }, 53 | /** 54 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 55 | * This is especially useful for Docker builds. 56 | */ 57 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 58 | }); 59 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | // RegExp for public files 5 | const PUBLIC_FILE = /\.(.*)$/; // Files 6 | 7 | // eslint-disable-next-line @typescript-eslint/require-await 8 | export async function middleware(req: NextRequest) { 9 | // Clone the URL 10 | const url = req.nextUrl.clone(); 11 | const host = (req.headers.get("host") as string) || ""; 12 | if (url.pathname.includes("_next") || url.pathname.includes("static")) return; 13 | if ( 14 | !url.pathname.startsWith("/.well-known/atproto-did") && 15 | host.includes(".cat") 16 | ) { 17 | url.pathname = "/cat"; 18 | return NextResponse.rewrite(url); 19 | } 20 | 21 | if (!url.pathname.startsWith("/.well-known/atproto-did")) return; 22 | 23 | const fileDomains = process.env.DOMAINS_FILE_VERIFICATION?.split(","); 24 | 25 | try { 26 | const hostUrl = new URL(`https://${host}`).hostname; 27 | const hostName = fileDomains?.find((el) => hostUrl.endsWith(el)); 28 | if (!hostName) return; 29 | 30 | const subdomain = hostUrl.replace(`.${hostName}`, ""); 31 | const mainDomain = hostUrl.replace(`${subdomain}.`, ""); 32 | url.pathname = `/api/file-verification/${mainDomain}/${subdomain}`; 33 | 34 | return NextResponse.rewrite(url); 35 | } catch (e) { 36 | console.error(e); 37 | return; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { type AppType } from "next/app"; 2 | import { api } from "~/utils/api"; 3 | import "~/styles/globals.css"; 4 | 5 | const MyApp: AppType = ({ Component, pageProps: { ...pageProps } }) => { 6 | return ; 7 | }; 8 | 9 | export default api.withTRPC(MyApp); 10 | -------------------------------------------------------------------------------- /src/pages/api/file-verification/[domain]/[handle].ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { prisma } from "~/server/db"; 4 | 5 | type ResponseData = { 6 | message: string; 7 | }; 8 | 9 | export default async function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | const { domain, handle } = req.query; 14 | 15 | if ( 16 | !domain || 17 | !handle || 18 | typeof domain !== "string" || 19 | typeof handle !== "string" 20 | ) { 21 | res.status(400).json({ message: "Invalid input" }); 22 | return; 23 | } 24 | 25 | let savedHandle = null; 26 | try { 27 | savedHandle = await prisma.handle.findFirst({ 28 | where: { 29 | AND: [ 30 | { handle: { equals: handle, mode: "insensitive" } }, 31 | { subdomain: { equals: domain, mode: "insensitive" } }, 32 | ], 33 | }, 34 | }); 35 | } catch (e) { 36 | console.error(e); 37 | throw Error("Could not connect to the database"); 38 | } 39 | 40 | if (savedHandle) { 41 | res 42 | .status(200) 43 | .setHeader("content-type", "text/plain") 44 | .setHeader("Cache-Control", "public, max-age=86400") 45 | .end(`${savedHandle.subdomainValue.replace("did=", "")}`); 46 | return; 47 | } 48 | 49 | res 50 | .status(404) 51 | .setHeader("Cache-Control", "public, max-age=300") 52 | .end("Not found"); 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | import { env } from "~/env.mjs"; 3 | import { appRouter } from "~/server/api/root"; 4 | import { createTRPCContext } from "~/server/api/trpc"; 5 | import cors from "nextjs-cors"; 6 | import { NextApiRequest, NextApiResponse } from "next"; 7 | 8 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 9 | // Enable cors 10 | await cors(req, res); 11 | 12 | return createNextApiHandler({ 13 | router: appRouter, 14 | createContext: createTRPCContext, 15 | onError: 16 | env.NODE_ENV === "development" 17 | ? ({ path, error }) => { 18 | console.error( 19 | `❌ tRPC failed on ${path ?? ""}: ${error.message}` 20 | ); 21 | } 22 | : undefined, 23 | })(req, res); 24 | }; 25 | 26 | export default handler; 27 | -------------------------------------------------------------------------------- /src/pages/cat.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Cat() { 4 | return ( 5 | <> 6 | 7 | Get custom BlueSky handle 8 | 9 | 10 | 11 |
12 | hola! Aquest és un domini que podeu utilitzar per al vostre propi maneig 13 | a bsky.com mitjançant https://handles.domi.zip Més informació sobre 14 | català aquí - 15 | https://llengua.gencat.cat/en/serveis/aprendre_catala/a-catalunya/index.html 16 |
17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/components/HandleForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, Dispatch, SetStateAction, useState } from "react"; 2 | import { api } from "~/utils/api"; 3 | import Select from "../Select"; 4 | import regex from "~/utils/regex"; 5 | import { domains } from "~/utils/domains"; 6 | 7 | interface IMessage { 8 | success: boolean; 9 | content: string; 10 | } 11 | 12 | type Timer = ReturnType; 13 | 14 | export default function HandleForm() { 15 | // const hello = api.example.hello.useQuery({ text: "from tRPC" }); 16 | const [handleValue, sethandleValue] = useState(""); 17 | const [domainValue, setDomainValue] = useState(""); 18 | const [domainName, setDomainName] = useState( 19 | `${Object.keys(domains)[0] || ""}` 20 | ); 21 | 22 | const [handleValueValidator, sethandleValueValidator] = 23 | useState(false); 24 | const [domainValueValidator, setDomainValueValidator] = 25 | useState(false); 26 | 27 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 28 | const domainType = domains[domainName] || "file"; 29 | 30 | const utils = api.useContext(); 31 | const [timer, setTimer] = useState(undefined); 32 | 33 | const recordMutation = api.handle.createNew.useMutation({ 34 | // eslint-disable-next-line @typescript-eslint/require-await 35 | onMutate: async (response) => { 36 | await utils.handle.invalidate(); 37 | }, 38 | }); 39 | 40 | const delayedInput = 41 | ( 42 | onChange: Dispatch>, 43 | action: Dispatch>, 44 | regex: RegExp 45 | ) => 46 | (event: ChangeEvent) => { 47 | onChange(event.target.value); 48 | 49 | clearTimeout(timer); 50 | 51 | const newTimer = setTimeout(() => { 52 | if (event.target.value.length > 0 && !event.target.value.match(regex)) { 53 | action(true); 54 | } else { 55 | action(false); 56 | } 57 | }, 345); 58 | 59 | setTimer(newTimer); 60 | }; 61 | 62 | const addRecord = () => { 63 | if (recordMutation.isLoading) return; 64 | 65 | recordMutation.mutate({ 66 | handleValue, 67 | domainValue, 68 | domainName, 69 | }); 70 | }; 71 | 72 | return ( 73 | <> 74 |
75 | 95 | .{domainName} 96 |
97 |
Upload a text file to:
98 |
99 | https://{handleValue || "customHandle"}.{domainName} 100 | /.well-known/atproto-did 101 |
102 | {handleValueValidator && ( 103 |
104 | custom handle value is wrong (has to start or end with a letter 105 | or number) 106 |
107 | )} 108 |
Value:
109 |
110 | 121 |
122 | {domainValueValidator && ( 123 |
124 | make sure you copied the value correctly 125 |
126 | )} 127 | 128 | ) : ( 129 |
130 |
Domain:
131 |
132 | _atproto. 133 | 144 | .{domainName} 145 |
146 | {handleValueValidator && ( 147 |
148 | custom handle value is wrong (has to start or end with a letter 149 | or number) 150 |
151 | )} 152 |
Type:
153 |
TXT
154 |
Value:
155 |
156 | 167 |
168 | {domainValueValidator && ( 169 |
170 | make sure you copied the value correctly 171 |
172 | )} 173 |
174 | )} 175 |
176 | 200 | {recordMutation.error && ( 201 |
202 | {recordMutation.error.message} 203 |
204 | )} 205 | {recordMutation.isSuccess && ( 206 |
207 | {`Record added succesfully! Click "Verify" button in Bluesky`} 208 |
209 | )} 210 |
211 | 212 | 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /src/pages/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from "react"; 2 | import { domains } from "~/utils/domains"; 3 | 4 | interface IProps { 5 | value: string; 6 | onChange: Dispatch>; 7 | } 8 | 9 | export default function Select({ value, onChange }: IProps) { 10 | const [expanded, setExpanded] = useState(false); 11 | 12 | const options = Object.keys(domains); 13 | 14 | return ( 15 | <> 16 |
17 | 45 | {expanded && ( 46 |
    53 | {options.map((val: string) => ( 54 |
  • { 61 | onChange(val); 62 | setExpanded(false); 63 | }} 64 | > 65 |
    66 | {val} 67 |
    68 | {value === val && ( 69 | 70 | 82 | 83 | )} 84 |
  • 85 | ))} 86 |
87 | )} 88 |
89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import HandleForm from "./components/HandleForm"; 3 | import { useState } from "react"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | export default function Home() { 7 | const [helpVisible, setHelpVisible] = useState(false); 8 | 9 | return ( 10 | <> 11 | 12 | Get custom BlueSky handle 13 | 14 | 15 | 16 |
17 |
18 |
19 |

20 | Get customised BlueSky{" "} 21 | handle 22 |

23 |
24 |
25 |
26 |
27 | 35 |
36 |
37 |
38 |
39 | 1. Go to your settings on bluesky at{" "} 40 | 45 | https://bsky.app/settings 46 | 47 |
48 |
49 | {`2. Click "Change handle" and "I have my own domain" in the popup`} 50 |
51 |
3. Click "No DNS Panel"
52 |
53 | 4. Fill the desired handle with one of the available endings and 54 | copy the domain value to a field below 55 |
56 |
57 | 5. If everything is ok click "Verify Text File" on 58 | bsky.app! 59 |
60 |
61 |
62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 | Free and open source{" "} 71 | 76 | github.com/SlickDomique/open-handles 77 | 78 |
79 |
80 | You can follow the author on bsky at{" "} 81 | 86 | bsky.app/profile/domi.zip 87 | 88 |
89 | If you like my work you can donate on{" "} 90 | 95 | 102 | 106 | 107 | https://ko-fi.com/domi_zip 108 | 109 |
110 |
111 | 112 |
113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter } from "~/server/api/trpc"; 2 | import { handleRouter } from "./routers/handle"; 3 | 4 | /** 5 | * This is the primary router for your server. 6 | * 7 | * All routers added in /api/routers should be manually added here. 8 | */ 9 | export const appRouter = createTRPCRouter({ 10 | handle: handleRouter, 11 | }); 12 | 13 | // export type definition of API 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /src/server/api/routers/handle.ts: -------------------------------------------------------------------------------- 1 | import { env } from "process"; 2 | import { z } from "zod"; 3 | import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; 4 | import { prisma } from "~/server/db"; 5 | import cloudflareProvider from "~/server/domainProviders/cloudflare"; 6 | import regex from "~/utils/regex"; 7 | 8 | import { Ratelimit } from "@upstash/ratelimit"; 9 | import { Redis } from "@upstash/redis"; 10 | import { getProvider } from "~/utils/provider"; 11 | import { getUserProfile } from "~/utils/bsky"; 12 | 13 | export const handleRouter = createTRPCRouter({ 14 | createNew: publicProcedure 15 | .input( 16 | z.object({ 17 | handleValue: z.string().regex(regex.handleValueRegex), 18 | domainValue: z.string().regex(regex.fileDidValue), 19 | domainName: z.string().regex(regex.getDomainNameRegex()), 20 | }) 21 | ) 22 | .mutation(async ({ input }) => { 23 | if (!input.handleValue || !input.domainName || !input.domainValue) { 24 | throw Error("Invalid input"); 25 | } 26 | 27 | let handle = null; 28 | try { 29 | handle = await prisma.handle.findFirst({ 30 | where: { 31 | AND: [ 32 | { handle: { equals: input.handleValue, mode: "insensitive" } }, 33 | { subdomain: { equals: input.domainName, mode: "insensitive" } }, 34 | ], 35 | }, 36 | }); 37 | } catch (e) { 38 | console.error(e); 39 | throw Error("Could not connect to the database"); 40 | } 41 | 42 | if (handle) { 43 | // check the handle owner if it was checked here more than 3 days ago 44 | if (handle.updatedAt.getTime() + 1000 * 60 * 60 * 24 * 3 < Date.now()) { 45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 46 | const bskyUser: any = await getUserProfile( 47 | `${input.handleValue}.${input.domainName}` 48 | ); 49 | 50 | if ( 51 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 52 | bskyUser.status === 400 && 53 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 54 | bskyUser.json.message === "Profile not found" 55 | ) { 56 | await prisma.handle.delete({ 57 | where: { 58 | id: handle.id, 59 | }, 60 | }); 61 | } else { 62 | await prisma.handle.update({ 63 | where: { 64 | id: handle.id, 65 | }, 66 | data: { 67 | updatedAt: new Date(), 68 | }, 69 | }); 70 | 71 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 72 | if (bskyUser.json.did === input.domainValue) { 73 | throw Error("You already use this handle!"); 74 | } else { 75 | throw Error("This handle is already taken!"); 76 | } 77 | } 78 | } else { 79 | throw Error("This handle is already taken!"); 80 | } 81 | } 82 | 83 | const provider = getProvider(input.domainName); 84 | if (provider === "cloudflare") { 85 | const creationResult = await cloudflareProvider.createSubdomain( 86 | `_atproto.${input.handleValue}.${input.domainName}`, 87 | input.domainValue, 88 | input.domainName, 89 | "TXT" 90 | ); 91 | 92 | if (!creationResult) { 93 | throw Error("Something went wrong. Couldn't add your handle"); 94 | } 95 | } 96 | 97 | await prisma.handle.create({ 98 | data: { 99 | handle: input.handleValue, 100 | subdomain: input.domainName, 101 | subdomainValue: input.domainValue, 102 | }, 103 | }); 104 | }), 105 | }); 106 | -------------------------------------------------------------------------------- /src/server/api/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: 3 | * 1. You want to modify request context (see Part 1). 4 | * 2. You want to create a new middleware or type of procedure (see Part 3). 5 | * 6 | * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will 7 | * need to use are documented accordingly near the end. 8 | */ 9 | 10 | import { initTRPC, TRPCError } from "@trpc/server"; 11 | import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; 12 | import { type Session } from "next-auth"; 13 | import superjson from "superjson"; 14 | import { ZodError } from "zod"; 15 | import { getServerAuthSession } from "~/server/auth"; 16 | import { prisma } from "~/server/db"; 17 | 18 | /** 19 | * 1. CONTEXT 20 | * 21 | * This section defines the "contexts" that are available in the backend API. 22 | * 23 | * These allow you to access things when processing a request, like the database, the session, etc. 24 | */ 25 | 26 | type CreateContextOptions = { 27 | session: Session | null; 28 | }; 29 | 30 | /** 31 | * This helper generates the "internals" for a tRPC context. If you need to use it, you can export 32 | * it from here. 33 | * 34 | * Examples of things you may need it for: 35 | * - testing, so we don't have to mock Next.js' req/res 36 | * - tRPC's `createSSGHelpers`, where we don't have req/res 37 | * 38 | * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts 39 | */ 40 | const createInnerTRPCContext = (opts: CreateContextOptions) => { 41 | return { 42 | session: opts.session, 43 | prisma, 44 | }; 45 | }; 46 | 47 | /** 48 | * This is the actual context you will use in your router. It will be used to process every request 49 | * that goes through your tRPC endpoint. 50 | * 51 | * @see https://trpc.io/docs/context 52 | */ 53 | export const createTRPCContext = async (opts: CreateNextContextOptions) => { 54 | const { req, res } = opts; 55 | 56 | // Get the session from the server using the getServerSession wrapper function 57 | const session = await getServerAuthSession({ req, res }); 58 | 59 | return createInnerTRPCContext({ 60 | session, 61 | }); 62 | }; 63 | 64 | /** 65 | * 2. INITIALIZATION 66 | * 67 | * This is where the tRPC API is initialized, connecting the context and transformer. We also parse 68 | * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation 69 | * errors on the backend. 70 | */ 71 | 72 | const t = initTRPC.context().create({ 73 | transformer: superjson, 74 | errorFormatter({ shape, error }) { 75 | return { 76 | ...shape, 77 | data: { 78 | ...shape.data, 79 | zodError: 80 | error.cause instanceof ZodError ? error.cause.flatten() : null, 81 | }, 82 | }; 83 | }, 84 | }); 85 | 86 | /** 87 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) 88 | * 89 | * These are the pieces you use to build your tRPC API. You should import these a lot in the 90 | * "/src/server/api/routers" directory. 91 | */ 92 | 93 | /** 94 | * This is how you create new routers and sub-routers in your tRPC API. 95 | * 96 | * @see https://trpc.io/docs/router 97 | */ 98 | export const createTRPCRouter = t.router; 99 | 100 | /** 101 | * Public (unauthenticated) procedure 102 | * 103 | * This is the base piece you use to build new queries and mutations on your tRPC API. It does not 104 | * guarantee that a user querying is authorized, but you can still access user session data if they 105 | * are logged in. 106 | */ 107 | export const publicProcedure = t.procedure; 108 | 109 | /** Reusable middleware that enforces users are logged in before running the procedure. */ 110 | const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { 111 | if (!ctx.session || !ctx.session.user) { 112 | throw new TRPCError({ code: "UNAUTHORIZED" }); 113 | } 114 | return next({ 115 | ctx: { 116 | // infers the `session` as non-nullable 117 | session: { ...ctx.session, user: ctx.session.user }, 118 | }, 119 | }); 120 | }); 121 | 122 | /** 123 | * Protected (authenticated) procedure 124 | * 125 | * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies 126 | * the session is valid and guarantees `ctx.session.user` is not null. 127 | * 128 | * @see https://trpc.io/docs/procedures 129 | */ 130 | export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); 131 | -------------------------------------------------------------------------------- /src/server/auth.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 2 | import { type GetServerSidePropsContext } from "next"; 3 | import { 4 | getServerSession, 5 | type NextAuthOptions, 6 | type DefaultSession, 7 | } from "next-auth"; 8 | import { prisma } from "~/server/db"; 9 | 10 | /** 11 | * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` 12 | * object and keep type safety. 13 | * 14 | * @see https://next-auth.js.org/getting-started/typescript#module-augmentation 15 | */ 16 | declare module "next-auth" { 17 | interface Session extends DefaultSession { 18 | user: { 19 | id: string; 20 | // ...other properties 21 | // role: UserRole; 22 | } & DefaultSession["user"]; 23 | } 24 | 25 | // interface User { 26 | // // ...other properties 27 | // // role: UserRole; 28 | // } 29 | } 30 | 31 | /** 32 | * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. 33 | * 34 | * @see https://next-auth.js.org/configuration/options 35 | */ 36 | export const authOptions: NextAuthOptions = { 37 | callbacks: { 38 | session: ({ session, user }) => ({ 39 | ...session, 40 | user: { 41 | ...session.user, 42 | id: user.id, 43 | }, 44 | }), 45 | }, 46 | adapter: PrismaAdapter(prisma), 47 | providers: [ 48 | // DiscordProvider({ 49 | // clientId: env.DISCORD_CLIENT_ID, 50 | // clientSecret: env.DISCORD_CLIENT_SECRET, 51 | // }), 52 | /** 53 | * ...add more providers here. 54 | * 55 | * Most other providers require a bit more work than the Discord provider. For example, the 56 | * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account 57 | * model. Refer to the NextAuth.js docs for the provider you want to use. Example: 58 | * 59 | * @see https://next-auth.js.org/providers/github 60 | */ 61 | ], 62 | }; 63 | 64 | /** 65 | * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. 66 | * 67 | * @see https://next-auth.js.org/configuration/nextjs 68 | */ 69 | export const getServerAuthSession = (ctx: { 70 | req: GetServerSidePropsContext["req"]; 71 | res: GetServerSidePropsContext["res"]; 72 | }) => { 73 | return getServerSession(ctx.req, ctx.res, authOptions); 74 | }; 75 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { env } from "~/env.mjs"; 3 | 4 | const globalForPrisma = globalThis as unknown as { 5 | prisma: PrismaClient | undefined; 6 | }; 7 | 8 | export const prisma = 9 | globalForPrisma.prisma ?? 10 | new PrismaClient({ 11 | log: 12 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 13 | }); 14 | 15 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 16 | -------------------------------------------------------------------------------- /src/server/domainProviders/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import { env } from "process"; 2 | 3 | const cloudflareProvider = { 4 | createSubdomain: async ( 5 | domainName: string, 6 | domainValue: string, 7 | zoneName: string, 8 | type: string 9 | ) => { 10 | const zones = env.DOMAINS_CLOUDFLARE?.split(","); 11 | const zoneId = zones?.find((el) => el.startsWith(zoneName))?.split(":")[1]; 12 | 13 | const response = await fetch( 14 | `https://api.cloudflare.com/client/v4/zones/${zoneId || ""}/dns_records`, 15 | { 16 | body: JSON.stringify({ 17 | name: domainName, 18 | type, 19 | content: domainValue, 20 | }), 21 | headers: { 22 | "Content-Type": "application/json", 23 | Authorization: `Bearer ${env.CLOUDFLARE_SECRET || ""}`, 24 | }, 25 | method: "POST", 26 | } 27 | ); 28 | 29 | type CloudflareResponse = { 30 | success?: boolean; 31 | }; 32 | const { success } = (await response.json()) as CloudflareResponse; 33 | return response.status === 200 && success; 34 | }, 35 | }; 36 | 37 | export default cloudflareProvider; 38 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "@fontsource/roboto"; 2 | @import "@fontsource/roboto/300.css"; 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | .triangles { 8 | border-left: 50vw solid #f3f9ff; 9 | border-right: 50vw solid #f3f9ff; 10 | border-bottom: 5vw solid #fff; 11 | } -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which 3 | * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. 4 | * 5 | * We also create a few inference helpers for input and output types. 6 | */ 7 | import { httpBatchLink, loggerLink } from "@trpc/client"; 8 | import { createTRPCNext } from "@trpc/next"; 9 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; 10 | import superjson from "superjson"; 11 | import { type AppRouter } from "~/server/api/root"; 12 | 13 | const getBaseUrl = () => { 14 | if (typeof window !== "undefined") return ""; // browser should use relative url 15 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 16 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 17 | }; 18 | 19 | /** A set of type-safe react-query hooks for your tRPC API. */ 20 | export const api = createTRPCNext({ 21 | config() { 22 | return { 23 | /** 24 | * Transformer used for data de-serialization from the server. 25 | * 26 | * @see https://trpc.io/docs/data-transformers 27 | */ 28 | transformer: superjson, 29 | 30 | /** 31 | * Links used to determine request flow from client to server. 32 | * 33 | * @see https://trpc.io/docs/links 34 | */ 35 | links: [ 36 | loggerLink({ 37 | enabled: (opts) => 38 | process.env.NODE_ENV === "development" || 39 | (opts.direction === "down" && opts.result instanceof Error), 40 | }), 41 | httpBatchLink({ 42 | url: `${getBaseUrl()}/api/trpc`, 43 | }), 44 | ], 45 | }; 46 | }, 47 | /** 48 | * Whether tRPC should await queries when server rendering pages. 49 | * 50 | * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false 51 | */ 52 | ssr: false, 53 | }); 54 | 55 | /** 56 | * Inference helper for inputs. 57 | * 58 | * @example type HelloInput = RouterInputs['example']['hello'] 59 | */ 60 | export type RouterInputs = inferRouterInputs; 61 | 62 | /** 63 | * Inference helper for outputs. 64 | * 65 | * @example type HelloOutput = RouterOutputs['example']['hello'] 66 | */ 67 | export type RouterOutputs = inferRouterOutputs; 68 | -------------------------------------------------------------------------------- /src/utils/bsky.ts: -------------------------------------------------------------------------------- 1 | export const getUserProfile = async (did: string) => { 2 | const response = await fetch( 3 | `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${did}` 4 | ); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment 7 | const json = await response.json(); 8 | return { 9 | status: response.status, 10 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 11 | json, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/domains.ts: -------------------------------------------------------------------------------- 1 | export enum DomainType { 2 | CLOUDFLARE = "cloudflare", 3 | FILE = "file", 4 | } 5 | 6 | const fileDomains = 7 | process.env.NEXT_PUBLIC_DOMAINS_FILE_VERIFICATION?.split(","); 8 | const cloudflareDomains = 9 | process.env.NEXT_PUBLIC_DOMAINS_CLOUDFLARE?.split(","); 10 | 11 | export const domains: { [domain: string]: DomainType } = {}; 12 | 13 | fileDomains?.forEach((domain) => { 14 | domains[domain] = DomainType.FILE; 15 | }); 16 | 17 | cloudflareDomains?.forEach((domain) => { 18 | domains[domain] = DomainType.CLOUDFLARE; 19 | }); 20 | -------------------------------------------------------------------------------- /src/utils/provider.ts: -------------------------------------------------------------------------------- 1 | import { env } from "process"; 2 | 3 | export const getProvider = (domain: string) => { 4 | const cloudflareVerifiedDomains = env.DOMAINS_CLOUDFLARE?.split(","); 5 | const fileVerifiedDomains = env.DOMAINS_FILE_VERIFICATION?.split(","); 6 | 7 | if (cloudflareVerifiedDomains?.find((el) => el.split(":")[0] === domain)) { 8 | return "cloudflare"; 9 | } else if (fileVerifiedDomains?.find((el) => el === domain)) { 10 | return "file"; 11 | } 12 | 13 | return null; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/regex.ts: -------------------------------------------------------------------------------- 1 | import { env } from "process"; 2 | 3 | const regex = { 4 | getDomainNameRegex: () => { 5 | const domains = [ 6 | ...(env.DOMAINS_FILE_VERIFICATION?.split(",") || []), 7 | ...(env.DOMAINS_CLOUDFLARE?.split(",").map((val) => val.split(":")[0]) || 8 | []), 9 | ].join("|"); 10 | 11 | return new RegExp(`^(${domains})$`); 12 | }, 13 | domain: /^((?!-)[A-Za-z0–9-]{1, 63}(?