├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── README ├── t3-siwe-medium.jpg └── t3-siwe-screenshot.png ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma ├── migrations │ ├── 20230408212409_init │ │ └── migration.sql │ ├── 20230409205305_create_siwe_structure │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public └── favicon.ico ├── src ├── env.mjs ├── pages │ ├── _app.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ └── todos.tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ ├── example.ts │ │ │ └── todos.ts │ │ └── trpc.ts │ ├── auth.ts │ └── db.ts ├── styles │ └── globals.css └── utils │ └── api.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="file:./db.sqlite" 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="" 21 | NEXTAUTH_URL="http://localhost:3000" 22 | 23 | # Next Auth Discord Provider 24 | DISCORD_CLIENT_ID="" 25 | DISCORD_CLIENT_SECRET="" 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App With Sign-In With Ethereum 2 | 3 | An implementation of [T3 Stack](https://create.t3.gg/) with [Sign-In With Ethereum](https://login.xyz). 4 | 5 | !["T3-Stack-SIWE"](/README/t3-siwe-screenshot.png) 6 | 7 | ## Tutorials 8 | 9 | 📘 Full walkthrough article on how to [Combine Sign-In With Ethereum With Create-T3-App](https://codingwithmanny.medium.com/combine-sign-in-with-ethereum-with-create-t3-app-8f54604caeeb). 10 | 11 | ![Combine Sign-In With Ethereum With Create-T3-App](README/t3-siwe-medium.jpg) 12 | 13 | --- 14 | 15 | ## Requirement 16 | 17 | - NVM or Node `v18.15.0` 18 | 19 | --- 20 | 21 | ## next-auth@4.21.1 Fix 22 | 23 | With the latest version of `next-auth@4.21.1` it drops the headers which doesn't allow the nonce to be read from the csrf cookie. 24 | 25 | The solution to this can be found in this branch: 26 | 27 | [fix/next-auth-4.21.1](https://github.com/codingwithmanny/t3-app-siwe/tree/fix/next-auth-4.21.1) 28 | 29 | --- 30 | 31 | ## Getting Started 32 | 33 | Follow all these steps to get up and running locally. 34 | 35 | ### Dependencies 36 | 37 | ```bash 38 | # FROM: ./ 39 | 40 | pnpm install; # npm install; 41 | ``` 42 | 43 | ### Environment Variable File 44 | 45 | ```bash 46 | # FROM: ./ 47 | 48 | cp .env.example .env; 49 | ``` 50 | 51 | **File:** `.env` 52 | 53 | ```toml 54 | # When adding additional environment variables, the schema in "/src/env.mjs" 55 | # should be updated accordingly. 56 | 57 | # Prisma 58 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 59 | DATABASE_URL="file:./db.sqlite" 60 | 61 | # Next Auth 62 | # You can generate a new secret on the command line with: 63 | # openssl rand -base64 32 64 | # https://next-auth.js.org/configuration/options#secret 65 | NEXTAUTH_SECRET="A-REALLY-LONG-SECRET-PASSWORD-32" 66 | NEXTAUTH_URL="http://localhost:3000" 67 | 68 | # Next Auth Discord Provider 69 | DISCORD_CLIENT_ID="" 70 | DISCORD_CLIENT_SECRET="" 71 | ``` 72 | 73 | ### Database Migration 74 | 75 | ```bash 76 | # FROM: ./ 77 | 78 | npx prisma migrate dev; 79 | ``` 80 | 81 | ### Local Development 82 | 83 | ```bash 84 | # FROM: ./ 85 | 86 | pnpm dev; # npm run dev; 87 | 88 | # Expected Output: 89 | # > test-t3-app@0.1.0 dev /Users/username/path/to/t3-app-siwe 90 | # > next dev 91 | 92 | # ready - started server on 0.0.0.0:3000, url: http://localhost:3000 93 | ``` 94 | 95 | ### Database Client (Prisma Studio) 96 | 97 | ```bash 98 | # FROM: ./ 99 | 100 | npx prisma studio; 101 | 102 | # Expected Output: 103 | # Environment variables loaded from .env 104 | # Prisma schema loaded from prisma/schema.prisma 105 | # Prisma Studio is up on http://localhost:5555 106 | ``` 107 | 108 | --- 109 | 110 | built by [@codingwithmanny](https://twitter.com/codingwithmanny) 111 | 112 | 113 | -------------------------------------------------------------------------------- /README/t3-siwe-medium.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingwithmanny/t3-app-siwe/fe900c78364f69f3ecdff3a2b7cedee54cf23a31/README/t3-siwe-medium.jpg -------------------------------------------------------------------------------- /README/t3-siwe-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingwithmanny/t3-app-siwe/fe900c78364f69f3ecdff3a2b7cedee54cf23a31/README/t3-siwe-screenshot.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 3 | * This is especially useful for Docker builds. 4 | */ 5 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs")); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you have the "experimental: { appDir: true }" setting enabled, then you 13 | * must comment the below `i18n` config out. 14 | * 15 | * @see https://github.com/vercel/next.js/issues/41980 16 | */ 17 | i18n: { 18 | locales: ["en"], 19 | defaultLocale: "en", 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3-app-siwe", 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 | "@codingwithmanny/blockies": "^1.1.0", 14 | "@next-auth/prisma-adapter": "^1.0.5", 15 | "@prisma/client": "^4.11.0", 16 | "@tanstack/react-query": "^4.28.0", 17 | "@trpc/client": "^10.18.0", 18 | "@trpc/next": "^10.18.0", 19 | "@trpc/react-query": "^10.18.0", 20 | "@trpc/server": "^10.18.0", 21 | "ethers": "^5", 22 | "next": "^13.2.4", 23 | "next-auth": "^4.20.1", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "siwe": "^1.1.6", 27 | "superjson": "1.12.2", 28 | "wagmi": "^0.12.10", 29 | "zod": "^3.21.4" 30 | }, 31 | "devDependencies": { 32 | "@types/eslint": "^8.21.3", 33 | "@types/node": "^18.15.5", 34 | "@types/prettier": "^2.7.2", 35 | "@types/react": "^18.0.28", 36 | "@types/react-dom": "^18.0.11", 37 | "@typescript-eslint/eslint-plugin": "^5.56.0", 38 | "@typescript-eslint/parser": "^5.56.0", 39 | "autoprefixer": "^10.4.14", 40 | "eslint": "^8.36.0", 41 | "eslint-config-next": "^13.2.4", 42 | "postcss": "^8.4.21", 43 | "prettier": "^2.8.6", 44 | "prettier-plugin-tailwindcss": "^0.2.6", 45 | "prisma": "^4.11.0", 46 | "tailwindcss": "^3.3.0", 47 | "typescript": "^5.0.2" 48 | }, 49 | "ct3aMetadata": { 50 | "initVersion": "7.10.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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/20230408212409_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Example" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" DATETIME NOT NULL 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Account" ( 10 | "id" TEXT NOT NULL PRIMARY KEY, 11 | "userId" TEXT NOT NULL, 12 | "type" TEXT NOT NULL, 13 | "provider" TEXT NOT NULL, 14 | "providerAccountId" TEXT NOT NULL, 15 | "refresh_token" TEXT, 16 | "access_token" TEXT, 17 | "expires_at" INTEGER, 18 | "token_type" TEXT, 19 | "scope" TEXT, 20 | "id_token" TEXT, 21 | "session_state" TEXT, 22 | CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 23 | ); 24 | 25 | -- CreateTable 26 | CREATE TABLE "Session" ( 27 | "id" TEXT NOT NULL PRIMARY KEY, 28 | "sessionToken" TEXT NOT NULL, 29 | "userId" TEXT NOT NULL, 30 | "expires" DATETIME NOT NULL, 31 | CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 32 | ); 33 | 34 | -- CreateTable 35 | CREATE TABLE "User" ( 36 | "id" TEXT NOT NULL PRIMARY KEY, 37 | "name" TEXT, 38 | "email" TEXT, 39 | "emailVerified" DATETIME, 40 | "image" TEXT 41 | ); 42 | 43 | -- CreateTable 44 | CREATE TABLE "VerificationToken" ( 45 | "identifier" TEXT NOT NULL, 46 | "token" TEXT NOT NULL, 47 | "expires" DATETIME NOT NULL 48 | ); 49 | 50 | -- CreateIndex 51 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 52 | 53 | -- CreateIndex 54 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 55 | 56 | -- CreateIndex 57 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 58 | 59 | -- CreateIndex 60 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 61 | 62 | -- CreateIndex 63 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 64 | -------------------------------------------------------------------------------- /prisma/migrations/20230409205305_create_siwe_structure/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Example` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost. 6 | - You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost. 7 | - A unique constraint covering the columns `[address]` on the table `User` will be added. If there are existing duplicate values, this will fail. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "User" ADD COLUMN "address" TEXT; 12 | 13 | -- DropTable 14 | PRAGMA foreign_keys=off; 15 | DROP TABLE "Example"; 16 | PRAGMA foreign_keys=on; 17 | 18 | -- DropTable 19 | PRAGMA foreign_keys=off; 20 | DROP TABLE "Session"; 21 | PRAGMA foreign_keys=on; 22 | 23 | -- DropTable 24 | PRAGMA foreign_keys=off; 25 | DROP TABLE "VerificationToken"; 26 | PRAGMA foreign_keys=on; 27 | 28 | -- CreateTable 29 | CREATE TABLE "Todo" ( 30 | "id" TEXT NOT NULL PRIMARY KEY, 31 | "task" TEXT NOT NULL, 32 | "userId" TEXT NOT NULL, 33 | "completed" BOOLEAN NOT NULL DEFAULT false, 34 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 35 | "updatedAt" DATETIME NOT NULL, 36 | CONSTRAINT "Todo_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 37 | ); 38 | 39 | -- CreateIndex 40 | CREATE UNIQUE INDEX "User_address_key" ON "User"("address"); 41 | -------------------------------------------------------------------------------- /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 = "sqlite" -------------------------------------------------------------------------------- /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 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | // NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below 11 | // Further reading: 12 | // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema 13 | // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | // Revised Example table 18 | model Todo { 19 | id String @id @default(cuid()) 20 | task String 21 | userId String 22 | completed Boolean @default(false) 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 26 | } 27 | 28 | // Necessary for Next auth 29 | model Account { 30 | id String @id @default(cuid()) 31 | userId String 32 | type String 33 | provider String 34 | providerAccountId String 35 | refresh_token String? // @db.Text 36 | access_token String? // @db.Text 37 | expires_at Int? 38 | token_type String? 39 | scope String? 40 | id_token String? // @db.Text 41 | session_state String? 42 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 43 | 44 | @@unique([provider, providerAccountId]) 45 | } 46 | model User { 47 | id String @id @default(cuid()) 48 | name String? 49 | // New address 50 | address String? @unique 51 | email String? @unique 52 | emailVerified DateTime? 53 | image String? 54 | accounts Account[] 55 | todos Todo[] 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingwithmanny/t3-app-siwe/fe900c78364f69f3ecdff3a2b7cedee54cf23a31/public/favicon.ico -------------------------------------------------------------------------------- /src/env.mjs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | /** 4 | * Specify your server-side environment variables schema here. This way you can ensure the app isn't 5 | * built with invalid env vars. 6 | */ 7 | const server = z.object({ 8 | DATABASE_URL: z.string().url(), 9 | NODE_ENV: z.enum(["development", "test", "production"]), 10 | NEXTAUTH_SECRET: 11 | process.env.NODE_ENV === "production" 12 | ? z.string().min(1) 13 | : z.string().min(1).optional(), 14 | NEXTAUTH_URL: z.preprocess( 15 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL 16 | // Since NextAuth.js automatically uses the VERCEL_URL if present. 17 | (str) => process.env.VERCEL_URL ?? str, 18 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL 19 | process.env.VERCEL ? z.string().min(1) : z.string().url(), 20 | ), 21 | // Add `.min(1) on ID and SECRET if you want to make sure they're not empty 22 | DISCORD_CLIENT_ID: z.string(), 23 | DISCORD_CLIENT_SECRET: z.string(), 24 | }); 25 | 26 | /** 27 | * Specify your client-side environment variables schema here. This way you can ensure the app isn't 28 | * built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`. 29 | */ 30 | const client = z.object({ 31 | // NEXT_PUBLIC_CLIENTVAR: z.string().min(1), 32 | }); 33 | 34 | /** 35 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 36 | * middlewares) or client-side so we need to destruct manually. 37 | * 38 | * @type {Record | keyof z.infer, string | undefined>} 39 | */ 40 | const processEnv = { 41 | DATABASE_URL: process.env.DATABASE_URL, 42 | NODE_ENV: process.env.NODE_ENV, 43 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, 44 | NEXTAUTH_URL: process.env.NEXTAUTH_URL, 45 | DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, 46 | DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET, 47 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 48 | }; 49 | 50 | // Don't touch the part below 51 | // -------------------------- 52 | 53 | const merged = server.merge(client); 54 | 55 | /** @typedef {z.input} MergedInput */ 56 | /** @typedef {z.infer} MergedOutput */ 57 | /** @typedef {z.SafeParseReturnType} MergedSafeParseReturn */ 58 | 59 | let env = /** @type {MergedOutput} */ (process.env); 60 | 61 | if (!!process.env.SKIP_ENV_VALIDATION == false) { 62 | const isServer = typeof window === "undefined"; 63 | 64 | const parsed = /** @type {MergedSafeParseReturn} */ ( 65 | isServer 66 | ? merged.safeParse(processEnv) // on server we can validate all env vars 67 | : client.safeParse(processEnv) // on client we can only validate the ones that are exposed 68 | ); 69 | 70 | if (parsed.success === false) { 71 | console.error( 72 | "❌ Invalid environment variables:", 73 | parsed.error.flatten().fieldErrors, 74 | ); 75 | throw new Error("Invalid environment variables"); 76 | } 77 | 78 | env = new Proxy(parsed.data, { 79 | get(target, prop) { 80 | if (typeof prop !== "string") return undefined; 81 | // Throw a descriptive error if a server-side env var is accessed on the client 82 | // Otherwise it would just be returning `undefined` and be annoying to debug 83 | if (!isServer && !prop.startsWith("NEXT_PUBLIC_")) 84 | throw new Error( 85 | process.env.NODE_ENV === "production" 86 | ? "❌ Attempted to access a server-side environment variable on the client" 87 | : `❌ Attempted to access server-side environment variable '${prop}' on the client`, 88 | ); 89 | return target[/** @type {keyof typeof target} */ (prop)]; 90 | }, 91 | }); 92 | } 93 | 94 | export { env }; 95 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import { type AppType } from "next/app"; 4 | import { type Session } from "next-auth"; 5 | import { SessionProvider } from "next-auth/react"; 6 | import { api } from "~/utils/api"; 7 | import "~/styles/globals.css"; 8 | // SIWE Integration 9 | import { WagmiConfig, createClient, configureChains } from "wagmi"; 10 | import { mainnet, polygon, optimism, arbitrum } from "wagmi/chains"; 11 | import { publicProvider } from "wagmi/providers/public"; 12 | 13 | // Config 14 | // ======================================================== 15 | /** 16 | * Configure chains supported 17 | */ 18 | const { provider } = configureChains( 19 | [mainnet, polygon, optimism, arbitrum], 20 | [publicProvider()] 21 | ); 22 | 23 | /** 24 | * Configure client with providers and allow for auto wallet connection 25 | */ 26 | const client = createClient({ 27 | autoConnect: true, 28 | provider, 29 | }); 30 | 31 | // App Wrapper Component 32 | // ======================================================== 33 | const MyApp: AppType<{ session: Session | null }> = ({ 34 | Component, 35 | pageProps: { session, ...pageProps }, 36 | }) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | // Exports 47 | // ======================================================== 48 | export default api.withTRPC(MyApp); -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | import NextAuth, { type NextAuthOptions } from "next-auth"; 5 | import { authOptions } from "~/server/auth"; 6 | 7 | // Auth 8 | // ======================================================== 9 | const Auth = (req: NextApiRequest, res: NextApiResponse) => { 10 | const authOpts: NextAuthOptions = authOptions({ req }); 11 | 12 | const isDefaultSigninPage = req.method === "GET" && req?.query?.nextauth?.includes("signin"); 13 | 14 | // Hide Sign-In with Ethereum from default sign page 15 | if (isDefaultSigninPage) { 16 | // Removes from the authOptions.providers array 17 | authOpts.providers.pop(); 18 | } 19 | 20 | return NextAuth(req, res, authOpts) as typeof NextAuth; 21 | }; 22 | 23 | // Exports 24 | // ======================================================== 25 | export default Auth; -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "~/env.mjs"; 4 | import { createTRPCContext } from "~/server/api/trpc"; 5 | import { appRouter } from "~/server/api/root"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext: createTRPCContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error( 15 | `❌ tRPC failed on ${path ?? ""}: ${error.message}`, 16 | ); 17 | } 18 | : undefined, 19 | }); 20 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import { type NextPage } from "next"; 4 | import Head from "next/head"; 5 | import Link from "next/link"; 6 | import Image from "next/image"; 7 | import { useEffect, useState } from "react"; 8 | import { getCsrfToken, signIn, signOut, useSession } from "next-auth/react"; 9 | import { api } from "~/utils/api"; 10 | import { renderDataURI } from "@codingwithmanny/blockies"; 11 | // SIWE Integration 12 | import { SiweMessage } from "siwe"; 13 | import { useAccount, useConnect, useDisconnect, useSignMessage, useNetwork } from "wagmi"; 14 | import { InjectedConnector } from 'wagmi/connectors/injected'; 15 | 16 | // Auth Component 17 | // ======================================================== 18 | const AuthShowcase: React.FC = () => { 19 | // Hooks 20 | const { data: sessionData } = useSession(); 21 | const { data: secretMessage } = api.example.getSecretMessage.useQuery( 22 | undefined, // no input 23 | { enabled: sessionData?.user !== undefined }, 24 | ); 25 | // State 26 | const [showConnection, setShowConnection] = useState(false); 27 | 28 | // Wagmi Hooks 29 | const { signMessageAsync } = useSignMessage(); 30 | const { address, isConnected } = useAccount(); 31 | const { connect } = useConnect({ 32 | connector: new InjectedConnector(), 33 | }); 34 | const { disconnect } = useDisconnect(); 35 | const { chain } = useNetwork(); 36 | 37 | // Functions 38 | /** 39 | * Attempts SIWE and establish session 40 | */ 41 | const onClickSignIn = async () => { 42 | try { 43 | const message = new SiweMessage({ 44 | domain: window.location.host, 45 | address: address, 46 | statement: "Sign in with Ethereum to the app.", 47 | uri: window.location.origin, 48 | version: "1", 49 | chainId: chain?.id, 50 | // nonce is used from CSRF token 51 | nonce: await getCsrfToken(), 52 | }) 53 | const signature = await signMessageAsync({ 54 | message: message.prepareMessage(), 55 | }) 56 | await signIn("credentials", { 57 | message: JSON.stringify(message), 58 | redirect: false, 59 | signature, 60 | }); 61 | } catch (error) { 62 | window.alert(error); 63 | } 64 | }; 65 | 66 | /** 67 | * Sign user out 68 | */ 69 | const onClickSignOut = async () => { 70 | await signOut(); 71 | }; 72 | 73 | // Hooks 74 | /** 75 | * Handles hydration issue 76 | * only show after the window has finished loading 77 | */ 78 | useEffect(() => { 79 | setShowConnection(true); 80 | }, []); 81 | 82 | // Render 83 | return ( 84 |
85 | {sessionData 86 | ?
87 | {sessionData ?
88 | 89 | {sessionData?.user?.id ? {`${sessionData.user.id}`} : null} 90 | {JSON.stringify(sessionData)} 91 |
: null} 92 | {secretMessage ?

93 | 94 | {secretMessage} 95 |

: null} 96 | 97 | 103 |
104 | : showConnection 105 | ?
106 | {isConnected 107 | ? 113 | : null 114 | } 115 |
116 | : null 117 | } 118 | {showConnection 119 | ?
120 | {address 121 | ?

122 | {address} 123 |

124 | : null 125 | } 126 | 132 |
133 | : null} 134 |
135 | ); 136 | }; 137 | 138 | // Page Component 139 | // ======================================================== 140 | const Home: NextPage = () => { 141 | // Requests 142 | const hello = api.example.hello.useQuery({ text: "from tRPC" }); 143 | 144 | // Render 145 | return ( 146 | <> 147 | 148 | Create T3 App SIWE 149 | 150 | 151 | 152 |
153 |
154 |

155 | Create T3 App SIWE 156 |

157 |
158 | 163 |

First Steps →

164 |
165 | Just the basics - Everything you need to know to set up your 166 | database and authentication. 167 |
168 | 169 | 174 |

Documentation →

175 |
176 | Learn more about Create T3 App, the libraries it uses, and how 177 | to deploy it. 178 |
179 | 180 |
181 |
182 |
183 | {/* HERE - new todo link */} 184 | (Protected) Todos Page 185 |
186 |

187 | {hello.data ? hello.data.greeting : "Loading tRPC query..."} 188 |

189 | 190 |
191 |
192 |
193 | 194 | ); 195 | }; 196 | 197 | // Exports 198 | // ======================================================== 199 | export default Home; 200 | -------------------------------------------------------------------------------- /src/pages/todos.tsx: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import { type NextPage } from "next"; 4 | import Link from "next/link"; 5 | import { useSession } from "next-auth/react"; 6 | import { api } from "~/utils/api"; 7 | import { useEffect, useState } from "react"; 8 | 9 | // Page Component 10 | // ======================================================== 11 | const Todos: NextPage = () => { 12 | // State / Props / Hooks 13 | const { data: sessionData } = useSession(); 14 | const [todos, setTodos] = useState<{ task: string; id: string; completed: boolean; }[]>([]); 15 | const [newTodo, setNewTodo] = useState(''); 16 | 17 | // Requests 18 | // - All 19 | const { 20 | data: todosAllData, 21 | isLoading: todosAllIsLoading, 22 | refetch: todosAllRefetch 23 | } = api.todos.all.useQuery( 24 | undefined, // no input 25 | { 26 | // Disable request if no session data 27 | enabled: sessionData?.user !== undefined, 28 | onSuccess: () => { 29 | setNewTodo(''); // reset input form 30 | } 31 | }, 32 | ); 33 | // - Add 34 | const { 35 | mutate: todosAddMutate, 36 | isLoading: todosAddIsLoading, 37 | } = api.todos.add.useMutation({ 38 | onSuccess: async () => { 39 | await todosAllRefetch(); 40 | } 41 | }); 42 | // - Remove 43 | const { 44 | mutate: todosRemoveMutate, 45 | isLoading: todosRemoveIsLoading, 46 | } = api.todos.remove.useMutation({ 47 | onSuccess: async () => { 48 | await todosAllRefetch(); 49 | } 50 | }); 51 | // - Update 52 | const { 53 | mutate: todosUpdateMutate, 54 | isLoading: todosUpdateIsLoading, 55 | } = api.todos.update.useMutation({ 56 | onSuccess: async () => { 57 | await todosAllRefetch(); 58 | } 59 | }); 60 | // Handle loading for all requests to disable buttons and inputs 61 | const isRequestLoading = todosAllIsLoading || todosAddIsLoading || todosRemoveIsLoading || todosUpdateIsLoading; 62 | 63 | // Functions 64 | /** 65 | * 66 | * @param event 67 | */ 68 | const onSubmitForm = (event: React.FormEvent) => { 69 | console.group('onSubmitForm'); 70 | event.preventDefault(); 71 | 72 | const formData = new FormData(event.currentTarget); 73 | const todoValue = formData.get('todo') as string || ''; 74 | console.log({ todoValue }); 75 | 76 | todosAddMutate({ task: todoValue }); 77 | console.groupEnd(); 78 | }; 79 | 80 | /** 81 | * 82 | * @param id 83 | * @returns 84 | */ 85 | const onClickToggleDone = (id: string) => () => { 86 | console.group('onClickToggleDone'); 87 | console.log({ id }); 88 | 89 | todosUpdateMutate({ 90 | id, 91 | completed: !todos.find(i => i.id === id)?.completed 92 | }) 93 | console.groupEnd(); 94 | }; 95 | 96 | /** 97 | * 98 | * @param id 99 | * @returns 100 | */ 101 | const onClickDelete = (id: string) => () => { 102 | console.group('onClickDelete'); 103 | console.log({ id }); 104 | 105 | todosRemoveMutate({ 106 | id 107 | }); 108 | console.groupEnd(); 109 | }; 110 | 111 | // Hooks 112 | useEffect(() => { 113 | console.log({ todosAllData }); 114 | if (todosAllIsLoading) return; 115 | setTodos(todosAllData || []); 116 | // eslint-disable-next-line react-hooks/exhaustive-deps 117 | }, [todosAllData]); 118 | 119 | // When rendering client side don't display anything until loading is complete 120 | // if (typeof window !== "undefined" && loading) return null 121 | // If no session exists, display access denied message 122 | return ( 123 | <> 124 |
125 |
126 |

127 | {!sessionData ? 'Access Denied' : 'Todos'} 128 |

129 |
130 |
131 | Home Page 132 |
133 | {sessionData ?
134 |
135 |
136 | 137 | setNewTodo(e.target.value)} className="disabled:opacity-30 h-10 bg-white rounded px-4 w-full" type="text" name="todo" value={newTodo} /> 138 |
139 |
140 | 141 |
142 |
143 | {todos.length === 0 144 | ?

(No todos yet!)

145 | :
    146 | {todos.map((todo, key) =>
  • 147 | {todo.task} 148 | 149 | 150 | 151 | 152 |
  • )} 153 |
154 | } 155 |
: null} 156 |
157 | 158 | ); 159 | }; 160 | 161 | // Exports 162 | // ======================================================== 163 | export default Todos; -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter } from "~/server/api/trpc"; 2 | import { exampleRouter } from "~/server/api/routers/example"; 3 | import { todosRouter } from "~/server/api/routers/todos"; 4 | 5 | /** 6 | * This is the primary router for your server. 7 | * 8 | * All routers added in /api/routers should be manually added here. 9 | */ 10 | export const appRouter = createTRPCRouter({ 11 | example: exampleRouter, 12 | todos: todosRouter 13 | }); 14 | 15 | // export type definition of API 16 | export type AppRouter = typeof appRouter; 17 | -------------------------------------------------------------------------------- /src/server/api/routers/example.ts: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import { z } from "zod"; 4 | import { 5 | createTRPCRouter, 6 | publicProcedure, 7 | protectedProcedure, 8 | } from "~/server/api/trpc"; 9 | 10 | // Router 11 | // ======================================================== 12 | export const exampleRouter = createTRPCRouter({ 13 | hello: publicProcedure 14 | .input(z.object({ text: z.string() })) 15 | .query(({ input }) => { 16 | return { 17 | greeting: `Hello ${input.text}`, 18 | }; 19 | }), 20 | getSecretMessage: protectedProcedure.query(() => { 21 | return "you can now see this secret message!"; 22 | }), 23 | }); 24 | -------------------------------------------------------------------------------- /src/server/api/routers/todos.ts: -------------------------------------------------------------------------------- 1 | // Imports 2 | // ======================================================== 3 | import { z } from "zod"; 4 | import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; 5 | 6 | // Router 7 | // ======================================================== 8 | export const todosRouter = createTRPCRouter({ 9 | /** 10 | * All todos belonging to the session user 11 | */ 12 | all: protectedProcedure.query(async ({ ctx }) => { 13 | const todos = await ctx.prisma.todo.findMany({ 14 | where: { 15 | userId: ctx.session.user.id 16 | } 17 | }); 18 | return todos; 19 | }), 20 | /** 21 | * Add todo belonging to the session user 22 | */ 23 | add: protectedProcedure 24 | .input(z.object({ task: z.string() })) 25 | .mutation(async ({ input, ctx }) => { 26 | return await ctx.prisma.todo.create({ 27 | data: { 28 | task: input.task, 29 | userId: ctx.session.user.id 30 | } 31 | }) 32 | }), 33 | /** 34 | * Remove todo belonging to the session user* 35 | */ 36 | remove: protectedProcedure 37 | .input(z.object({ id: z.string() })) 38 | .mutation(async ({ input, ctx }) => { 39 | return await ctx.prisma.todo.deleteMany({ 40 | where: { 41 | id: input.id, 42 | userId: ctx.session.user.id 43 | } 44 | }) 45 | }), 46 | /** 47 | * Update todo belonging to the session user 48 | */ 49 | update: protectedProcedure 50 | .input(z.object({ 51 | id: z.string(), 52 | task: z.string().optional(), 53 | completed: z.boolean().optional() 54 | })) 55 | .mutation(async ({ input, ctx }) => { 56 | console.log({ input }); 57 | const data: { task?: string, completed?: boolean } = {}; 58 | if (input.task) { 59 | data.task = input.task; 60 | } 61 | if (typeof input.completed !== "undefined") { 62 | data.completed = input.completed; 63 | } 64 | 65 | return await ctx.prisma.todo.updateMany({ 66 | data, 67 | where: { 68 | id: input.id, 69 | userId: ctx.session.user.id 70 | } 71 | }); 72 | }) 73 | }); 74 | -------------------------------------------------------------------------------- /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 | /** 11 | * 1. CONTEXT 12 | * 13 | * This section defines the "contexts" that are available in the backend API. 14 | * 15 | * These allow you to access things when processing a request, like the database, the session, etc. 16 | */ 17 | import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; 18 | import { type Session } from "next-auth"; 19 | 20 | import { getServerAuthSession } from "~/server/auth"; 21 | import { prisma } from "~/server/db"; 22 | 23 | type CreateContextOptions = { 24 | session: Session | null; 25 | }; 26 | 27 | /** 28 | * This helper generates the "internals" for a tRPC context. If you need to use it, you can export 29 | * it from here. 30 | * 31 | * Examples of things you may need it for: 32 | * - testing, so we don't have to mock Next.js' req/res 33 | * - tRPC's `createSSGHelpers`, where we don't have req/res 34 | * 35 | * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts 36 | */ 37 | const createInnerTRPCContext = (opts: CreateContextOptions) => { 38 | return { 39 | session: opts.session, 40 | prisma, 41 | }; 42 | }; 43 | 44 | /** 45 | * This is the actual context you will use in your router. It will be used to process every request 46 | * that goes through your tRPC endpoint. 47 | * 48 | * @see https://trpc.io/docs/context 49 | */ 50 | export const createTRPCContext = async (opts: CreateNextContextOptions) => { 51 | const { req, res } = opts; 52 | 53 | // Get the session from the server using the getServerSession wrapper function 54 | const session = await getServerAuthSession({ req, res }); 55 | 56 | return createInnerTRPCContext({ 57 | session, 58 | }); 59 | }; 60 | 61 | /** 62 | * 2. INITIALIZATION 63 | * 64 | * This is where the tRPC API is initialized, connecting the context and transformer. We also parse 65 | * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation 66 | * errors on the backend. 67 | */ 68 | import { initTRPC, TRPCError } from "@trpc/server"; 69 | import superjson from "superjson"; 70 | import { ZodError } from "zod"; 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 | // Imports 2 | // ======================================================== 3 | import { type GetServerSidePropsContext } from "next"; 4 | import { getServerSession, type NextAuthOptions, type DefaultSession } from "next-auth"; 5 | import { prisma } from "~/server/db"; 6 | // SIWE Integration 7 | import type { CtxOrReq } from "next-auth/client/_utils"; 8 | import CredentialsProvider from "next-auth/providers/credentials"; 9 | import { SiweMessage } from "siwe"; 10 | import { getCsrfToken } from "next-auth/react"; 11 | import type { Session } from "next-auth"; 12 | 13 | // Types 14 | // ======================================================== 15 | /** 16 | * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` 17 | * object and keep type safety. 18 | * 19 | * @see https://next-auth.js.org/getting-started/typescript#module-augmentation 20 | */ 21 | declare module "next-auth" { 22 | interface Session extends DefaultSession { 23 | user: { 24 | id: string; 25 | // ...other properties 26 | // role: UserRole; 27 | } & DefaultSession["user"]; 28 | } 29 | 30 | // interface User { 31 | // // ...other properties 32 | // // role: UserRole; 33 | // } 34 | } 35 | 36 | // Auth Options 37 | // ======================================================== 38 | /** 39 | * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. 40 | * 41 | * @see https://next-auth.js.org/configuration/options 42 | */ 43 | export const authOptions: (ctxReq: CtxOrReq) => NextAuthOptions = ({ req }) => ({ 44 | callbacks: { 45 | // token.sub will refer to the id of the wallet address 46 | session: ({ session, token }) => ({ 47 | ...session, 48 | user: { 49 | ...session.user, 50 | id: token.sub 51 | }, 52 | } as Session & { user: { id: string; }}), 53 | // OTHER CALLBACKS to take advantage of but not needed 54 | // signIn: async (params: { // Used to control if a user is allowed to sign in 55 | // user: User | AdapterUser 56 | // account: Account | null 57 | // // Not used for credentials 58 | // profile?: Profile 59 | // // Not user 60 | // email?: { 61 | // verificationRequest?: boolean 62 | // } 63 | // /** If Credentials provider is used, it contains the user credentials */ 64 | // credentials?: Record 65 | // }) => { return true; }, 66 | // redirect: async (params: { // Used for a callback url but not used with credentials 67 | // /** URL provided as callback URL by the client */ 68 | // url: string 69 | // /** Default base URL of site (can be used as fallback) */ 70 | // baseUrl: string 71 | // }) => { 72 | // return params.baseUrl; 73 | // }, 74 | // jwt: async ( // Callback whenever JWT created (i.e. at sign in) 75 | // params: { 76 | // token: JWT 77 | // user: User | AdapterUser 78 | // account: Account | null 79 | // profile?: Profile 80 | // trigger?: "signIn" | "signUp" | "update" 81 | // /** @deprecated use `trigger === "signUp"` instead */ 82 | // isNewUser?: boolean 83 | // session?: any 84 | // } 85 | // ) => { 86 | // return params.token; 87 | // } 88 | }, 89 | // OTHER OPTIONS (not needed) 90 | // secret: process.env.NEXTAUTH_SECRET, // in case you want pass this along for other functionality 91 | // adapter: PrismaAdapter(prisma), // Not meant for type 'credentials' (used for db sessions) 92 | // jwt: { // Custom functionlaity for jwt encoding/decoding 93 | // encode: async ({ token, secret, maxAge }: JWTEncodeParams) => { 94 | // return encode({ 95 | // token, 96 | // secret, 97 | // maxAge, 98 | // }) 99 | // }, 100 | // decode: async ({ token, secret }: JWTDecodeParams) => { 101 | // return decode({ token, secret }) 102 | // } 103 | // }, 104 | // session: { // Credentials defaults to this strategy 105 | // strategy: 'jwt', 106 | // maxAge: 2592000, 107 | // updateAge: 86400, 108 | // generateSessionToken: () => 'SomeValue' 109 | // }, 110 | // events: { // Callback events 111 | // signIn: async (message: { 112 | // user: User 113 | // account: Account | null 114 | // profile?: Profile 115 | // isNewUser?: boolean 116 | // }) => {}, 117 | // signOut: async (message: { session: Session; token: JWT }) => {}, 118 | // createUser: async (message: { user: User }) => {}, 119 | // updateUser: async (message: { user: User }) => {}, 120 | // linkAccount: async (message: { 121 | // user: User | AdapterUser 122 | // account: Account 123 | // profile: User | AdapterUser 124 | // }) => {}, 125 | // session: async (message: { session: Session; token: JWT }) => {} 126 | // }, 127 | providers: [ 128 | CredentialsProvider({ 129 | // ! Don't add this 130 | // - it will assume more than one auth provider 131 | // - and redirect to a sign-in page meant for oauth 132 | // - id: 'siwe', 133 | name: "Ethereum", 134 | type: "credentials", // default for Credentials 135 | // Default values if it was a form 136 | credentials: { 137 | message: { 138 | label: "Message", 139 | type: "text", 140 | placeholder: "0x0", 141 | }, 142 | signature: { 143 | label: "Signature", 144 | type: "text", 145 | placeholder: "0x0", 146 | }, 147 | }, 148 | authorize: async (credentials) => { 149 | try { 150 | const siwe = new SiweMessage(JSON.parse(credentials?.message as string ?? "{}") as Partial); 151 | const nonce = await getCsrfToken({ req }); 152 | const fields = await siwe.validate(credentials?.signature || "") 153 | 154 | if (fields.nonce !== nonce) { 155 | return null; 156 | } 157 | 158 | // Check if user exists 159 | let user = await prisma.user.findUnique({ 160 | where: { 161 | address: fields.address 162 | } 163 | }); 164 | // Create new user if doesn't exist 165 | if (!user) { 166 | user = await prisma.user.create({ 167 | data: { 168 | address: fields.address 169 | } 170 | }); 171 | // create account 172 | await prisma.account.create({ 173 | data: { 174 | userId: user.id, 175 | type: "credentials", 176 | provider: "Ethereum", 177 | providerAccountId: fields.address 178 | } 179 | }); 180 | } 181 | 182 | return { 183 | // Pass user id instead of address 184 | // id: fields.address 185 | id: user.id 186 | }; 187 | } catch (error) { 188 | // Uncomment or add logging if needed 189 | console.error({ error }); 190 | return null; 191 | } 192 | }, 193 | }) 194 | /** 195 | * ...add more providers here. 196 | * 197 | * Most other providers require a bit more work than the Discord provider. For example, the 198 | * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account 199 | * model. Refer to the NextAuth.js docs for the provider you want to use. Example: 200 | * 201 | * @see https://next-auth.js.org/providers/github 202 | */ 203 | ], 204 | }); 205 | 206 | // Auth Session 207 | // ======================================================== 208 | /** 209 | * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. 210 | * 211 | * @see https://next-auth.js.org/configuration/nextjs 212 | */ 213 | export const getServerAuthSession = async (ctx: { 214 | req: GetServerSidePropsContext["req"]; 215 | res: GetServerSidePropsContext["res"]; 216 | }) => { 217 | // Changed from authOptions to authOption(ctx) 218 | // This allows use to retrieve the csrf token to verify as the nonce 219 | return getServerSession(ctx.req, ctx.res, authOptions(ctx)); 220 | }; 221 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "~/env.mjs"; 4 | 5 | const globalForPrisma = globalThis as unknown as { 6 | prisma: PrismaClient | undefined; 7 | }; 8 | 9 | export const prisma = 10 | globalForPrisma.prisma ?? 11 | new PrismaClient({ 12 | log: 13 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 14 | }); 15 | 16 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 17 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /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 | 12 | import { type AppRouter } from "~/server/api/root"; 13 | 14 | const getBaseUrl = () => { 15 | if (typeof window !== "undefined") return ""; // browser should use relative url 16 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 17 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 18 | }; 19 | 20 | /** A set of type-safe react-query hooks for your tRPC API. */ 21 | export const api = createTRPCNext({ 22 | config() { 23 | return { 24 | /** 25 | * Transformer used for data de-serialization from the server. 26 | * 27 | * @see https://trpc.io/docs/data-transformers 28 | */ 29 | transformer: superjson, 30 | 31 | /** 32 | * Links used to determine request flow from client to server. 33 | * 34 | * @see https://trpc.io/docs/links 35 | */ 36 | links: [ 37 | loggerLink({ 38 | enabled: (opts) => 39 | process.env.NODE_ENV === "development" || 40 | (opts.direction === "down" && opts.result instanceof Error), 41 | }), 42 | httpBatchLink({ 43 | url: `${getBaseUrl()}/api/trpc`, 44 | }), 45 | ], 46 | }; 47 | }, 48 | /** 49 | * Whether tRPC should await queries when server rendering pages. 50 | * 51 | * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false 52 | */ 53 | ssr: false, 54 | }); 55 | 56 | /** 57 | * Inference helper for inputs. 58 | * 59 | * @example type HelloInput = RouterInputs['example']['hello'] 60 | */ 61 | export type RouterInputs = inferRouterInputs; 62 | 63 | /** 64 | * Inference helper for outputs. 65 | * 66 | * @example type HelloOutput = RouterOutputs['example']['hello'] 67 | */ 68 | export type RouterOutputs = inferRouterOutputs; 69 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "noUncheckedIndexedAccess": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": ["./src/*"] 22 | } 23 | }, 24 | "include": [ 25 | ".eslintrc.cjs", 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | "**/*.cjs", 30 | "**/*.mjs" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | --------------------------------------------------------------------------------