├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── auth-schema.ts ├── bun.lockb ├── components.json ├── drizzle.config.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public └── favicon.ico ├── src ├── app │ ├── (auth) │ │ ├── email-verified │ │ │ └── page.tsx │ │ ├── forgot-password │ │ │ └── page.tsx │ │ ├── reset-password │ │ │ └── page.tsx │ │ ├── signin │ │ │ └── page.tsx │ │ └── signup │ │ │ └── page.tsx │ ├── admin │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts │ ├── layout.tsx │ ├── page.tsx │ └── test │ │ └── page.tsx ├── components │ ├── admin │ │ └── users-table.tsx │ ├── auth │ │ ├── forgot-password-form.tsx │ │ ├── reset-password-form.tsx │ │ ├── signin-form.tsx │ │ ├── signout-button.tsx │ │ └── signup-form.tsx │ ├── loading-button.tsx │ ├── post.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── table.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx ├── email-templates │ ├── change-email-verification.tsx │ ├── index.tsx │ ├── reset-password-email.tsx │ └── verification-email.tsx ├── env.js ├── hooks │ └── use-toast.ts ├── lib │ ├── utils.ts │ └── zod.ts ├── middleware.ts ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ └── post.ts │ │ └── trpc.ts │ ├── auth │ │ ├── client.ts │ │ ├── email.ts │ │ └── index.ts │ └── db │ │ ├── index.ts │ │ └── schema.ts ├── styles │ └── globals.css └── trpc │ ├── query-client.ts │ ├── react.tsx │ └── server.ts ├── start-database.sh ├── 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.js" 10 | # should be updated accordingly. 11 | 12 | # Drizzle 13 | DATABASE_URL="" 14 | 15 | # Better-Auth 16 | BETTER_AUTH_SECRET="" 17 | BETTER_AUTH_URL="" #Base URL of your app 18 | NEXT_PUBLIC_BETTER_AUTH_URL="" #Base URL of your app 19 | EMAIL_VERIFICATION_CALLBACK_URL="" 20 | GITHUB_CLIENT_ID="" 21 | GITHUB_CLIENT_SECRET="" 22 | 23 | # RESEND 24 | RESERND_API_KEY="" 25 | 26 | # Example: 27 | # SERVERVAR="foo" 28 | # NEXT_PUBLIC_CLIENTVAR="bar" 29 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "drizzle" 10 | ], 11 | "extends": [ 12 | "next/core-web-vitals", 13 | "plugin:@typescript-eslint/recommended-type-checked", 14 | "plugin:@typescript-eslint/stylistic-type-checked" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/array-type": "off", 18 | "@typescript-eslint/consistent-type-definitions": "off", 19 | "@typescript-eslint/consistent-type-imports": [ 20 | "warn", 21 | { 22 | "prefer": "type-imports", 23 | "fixStyle": "inline-type-imports" 24 | } 25 | ], 26 | "@typescript-eslint/no-unused-vars": [ 27 | "warn", 28 | { 29 | "argsIgnorePattern": "^_" 30 | } 31 | ], 32 | "@typescript-eslint/require-await": "off", 33 | "@typescript-eslint/no-misused-promises": [ 34 | "error", 35 | { 36 | "checksVoidReturn": { 37 | "attributes": false 38 | } 39 | } 40 | ], 41 | "drizzle/enforce-delete-with-where": [ 42 | "error", 43 | { 44 | "drizzleObjectName": [ 45 | "db", 46 | "ctx.db" 47 | ] 48 | } 49 | ], 50 | "drizzle/enforce-update-with-where": [ 51 | "error", 52 | { 53 | "drizzleObjectName": [ 54 | "db", 55 | "ctx.db" 56 | ] 57 | } 58 | ] 59 | } 60 | } 61 | module.exports = config; -------------------------------------------------------------------------------- /.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 | db.sqlite 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # 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 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | 45 | # idea files 46 | .idea 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Prisma](https://prisma.io) 14 | - [Drizzle](https://orm.drizzle.team) 15 | - [Tailwind CSS](https://tailwindcss.com) 16 | - [tRPC](https://trpc.io) 17 | 18 | ## Learn More 19 | 20 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 21 | 22 | - [Documentation](https://create.t3.gg/) 23 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 24 | 25 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 26 | 27 | ## How do I deploy this? 28 | 29 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 30 | -------------------------------------------------------------------------------- /auth-schema.ts: -------------------------------------------------------------------------------- 1 | import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const user = pgTable("user", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | email: text("email").notNull().unique(), 7 | emailVerified: boolean("email_verified").notNull(), 8 | image: text("image"), 9 | createdAt: timestamp("created_at").notNull(), 10 | updatedAt: timestamp("updated_at").notNull(), 11 | }); 12 | 13 | export const session = pgTable("session", { 14 | id: text("id").primaryKey(), 15 | expiresAt: timestamp("expires_at").notNull(), 16 | token: text("token").notNull().unique(), 17 | createdAt: timestamp("created_at").notNull(), 18 | updatedAt: timestamp("updated_at").notNull(), 19 | ipAddress: text("ip_address"), 20 | userAgent: text("user_agent"), 21 | userId: text("user_id") 22 | .notNull() 23 | .references(() => user.id), 24 | }); 25 | 26 | export const account = pgTable("account", { 27 | id: text("id").primaryKey(), 28 | accountId: text("account_id").notNull(), 29 | providerId: text("provider_id").notNull(), 30 | userId: text("user_id") 31 | .notNull() 32 | .references(() => user.id), 33 | accessToken: text("access_token"), 34 | refreshToken: text("refresh_token"), 35 | idToken: text("id_token"), 36 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 37 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 38 | scope: text("scope"), 39 | password: text("password"), 40 | createdAt: timestamp("created_at").notNull(), 41 | updatedAt: timestamp("updated_at").notNull(), 42 | }); 43 | 44 | export const verification = pgTable("verification", { 45 | id: text("id").primaryKey(), 46 | identifier: text("identifier").notNull(), 47 | value: text("value").notNull(), 48 | expiresAt: timestamp("expires_at").notNull(), 49 | createdAt: timestamp("created_at"), 50 | updatedAt: timestamp("updated_at"), 51 | }); 52 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patelharsh9797/t3_stack_better_auth/28760a9963b1193fa9364c9c274e493d80309ecd/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "drizzle-kit"; 2 | 3 | import { env } from "~/env"; 4 | 5 | export default { 6 | schema: "./src/server/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: env.DATABASE_URL, 10 | }, 11 | tablesFilter: ["t3_better_auth_*"], 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | // output: "standalone", 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3_better_auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build", 8 | "check": "next lint && tsc --noEmit", 9 | "db:generate": "drizzle-kit generate", 10 | "db:migrate": "drizzle-kit migrate", 11 | "db:push": "drizzle-kit push", 12 | "db:studio": "drizzle-kit studio", 13 | "dev": "next dev --turbo", 14 | "email": "email dev --dir src/email-templates", 15 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 16 | "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 17 | "lint": "next lint", 18 | "lint:fix": "next lint --fix", 19 | "preview": "next build && next start", 20 | "start": "next start", 21 | "typecheck": "tsc --noEmit" 22 | }, 23 | "dependencies": { 24 | "@hookform/resolvers": "^3.10.0", 25 | "@radix-ui/react-label": "^2.1.1", 26 | "@radix-ui/react-slot": "^1.1.1", 27 | "@radix-ui/react-toast": "^1.2.5", 28 | "@react-email/components": "^0.0.32", 29 | "@t3-oss/env-nextjs": "^0.10.1", 30 | "@tanstack/react-query": "^5.50.0", 31 | "@trpc/client": "^11.0.0-rc.446", 32 | "@trpc/react-query": "^11.0.0-rc.446", 33 | "@trpc/server": "^11.0.0-rc.446", 34 | "better-auth": "^1.1.14", 35 | "class-variance-authority": "^0.7.1", 36 | "clsx": "^2.1.1", 37 | "drizzle-orm": "^0.33.0", 38 | "geist": "^1.3.0", 39 | "lucide-react": "^0.474.0", 40 | "next": "^15.0.1", 41 | "postgres": "^3.4.4", 42 | "react": "^18.3.1", 43 | "react-dom": "^18.3.1", 44 | "react-hook-form": "^7.54.2", 45 | "resend": "^4.1.1", 46 | "server-only": "^0.0.1", 47 | "superjson": "^2.2.1", 48 | "tailwind-merge": "^2.6.0", 49 | "tailwindcss-animate": "^1.0.7", 50 | "zod": "^3.24.1" 51 | }, 52 | "devDependencies": { 53 | "@types/eslint": "^8.56.10", 54 | "@types/node": "^20.14.10", 55 | "@types/react": "^18.3.3", 56 | "@types/react-dom": "^18.3.0", 57 | "@typescript-eslint/eslint-plugin": "^8.1.0", 58 | "@typescript-eslint/parser": "^8.1.0", 59 | "drizzle-kit": "^0.24.0", 60 | "eslint": "^8.57.0", 61 | "eslint-config-next": "^15.0.1", 62 | "eslint-plugin-drizzle": "^0.2.3", 63 | "postcss": "^8.4.39", 64 | "prettier": "^3.3.2", 65 | "prettier-plugin-tailwindcss": "^0.6.5", 66 | "react-email": "3.0.6", 67 | "tailwindcss": "^3.4.3", 68 | "typescript": "^5.5.3" 69 | }, 70 | "ct3aMetadata": { 71 | "initVersion": "7.38.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patelharsh9797/t3_stack_better_auth/28760a9963b1193fa9364c9c274e493d80309ecd/public/favicon.ico -------------------------------------------------------------------------------- /src/app/(auth)/email-verified/page.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "~/components/ui/button"; 2 | import Link from "next/link"; 3 | 4 | export default function EmailVerifiedPage() { 5 | return ( 6 |
7 |

8 | Email Verified! 9 |

10 |

11 | Your email has been successfully verified. 12 |

13 | 19 | Go to home 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from "~/components/auth/forgot-password-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(auth)/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetPasswordForm } from "~/components/auth/reset-password-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { SigninForm } from "~/components/auth/signin-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignupForm } from "~/components/auth/signup-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import UsersTable from "~/components/admin/users-table"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; 3 | 4 | export default async function AdminDashboard() { 5 | return ( 6 |
7 |
8 |
9 |

Admin Dashboard

10 |

11 | Manage users and view system statistics 12 |

13 |
14 | 15 | 16 | 17 | Users 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | import { auth } from "~/server/auth"; // path to your auth file 3 | 4 | export const { POST, GET } = toNextJsHandler(auth); 5 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { type NextRequest } from "next/server"; 3 | 4 | import { env } from "~/env"; 5 | import { appRouter } from "~/server/api/root"; 6 | import { createTRPCContext } from "~/server/api/trpc"; 7 | 8 | /** 9 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 10 | * handling a HTTP request (e.g. when you make requests from Client Components). 11 | */ 12 | const createContext = async (req: NextRequest) => { 13 | return createTRPCContext({ 14 | headers: req.headers, 15 | }); 16 | }; 17 | 18 | const handler = (req: NextRequest) => 19 | fetchRequestHandler({ 20 | endpoint: "/api/trpc", 21 | req, 22 | router: appRouter, 23 | createContext: () => createContext(req), 24 | onError: 25 | env.NODE_ENV === "development" 26 | ? ({ path, error }) => { 27 | console.error( 28 | `❌ tRPC failed on ${path ?? ""}: ${error.message}` 29 | ); 30 | } 31 | : undefined, 32 | }); 33 | 34 | export { handler as GET, handler as POST }; 35 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "~/styles/globals.css"; 2 | 3 | import { GeistSans } from "geist/font/sans"; 4 | import { type Metadata } from "next"; 5 | 6 | import { Toaster } from "~/components/ui/toaster"; 7 | import { TRPCReactProvider } from "~/trpc/react"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Create T3 App", 11 | description: "Generated by create-t3-app", 12 | icons: [{ rel: "icon", url: "/favicon.ico" }], 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ children: React.ReactNode }>) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import SignoutButton from "~/components/auth/signout-button"; 3 | 4 | import { LatestPost } from "~/components/post"; 5 | import { getServerSession } from "~/server/auth"; 6 | import { api, HydrateClient } from "~/trpc/server"; 7 | 8 | export default async function Home() { 9 | const hello = await api.post.hello({ text: "from tRPC" }); 10 | const session = await getServerSession(); 11 | 12 | if (session?.user) { 13 | void api.post.getLatest.prefetch(); 14 | } 15 | 16 | return ( 17 | 18 |
19 |
20 |

21 | Create T3 App 22 |

23 | {/*
*/} 24 | {/* */} 29 | {/*

First Steps →

*/} 30 | {/*
*/} 31 | {/* Just the basics - Everything you need to know to set up your */} 32 | {/* database and authentication. */} 33 | {/*
*/} 34 | {/* */} 35 | {/* */} 40 | {/*

Documentation →

*/} 41 | {/*
*/} 42 | {/* Learn more about Create T3 App, the libraries it uses, and how */} 43 | {/* to deploy it. */} 44 | {/*
*/} 45 | {/* */} 46 | {/*
*/} 47 |
48 |

49 | {hello ? hello.greeting : "Loading tRPC query..."} 50 |

51 | 52 |
53 |

54 | {session && Logged in as {session.user?.name}} 55 |

56 | 57 | {!session ? ( 58 | 62 | {session ? "Sign out" : "Sign in"} 63 | 64 | ) : ( 65 | 66 | )} 67 |
68 |
69 | 70 | {session?.user && } 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/app/test/page.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "~/trpc/server"; 2 | 3 | const getMsg = async () => { 4 | try { 5 | const msg = await api.post.getSecretMessage(); 6 | return msg; 7 | } catch (error) { 8 | return "⛔ Error"; 9 | } 10 | }; 11 | 12 | const TestPage = async () => { 13 | const secretMessage = await getMsg(); 14 | 15 | return ( 16 |
17 |

Protected Test Page!!

18 |

{secretMessage}

19 |
20 | ); 21 | }; 22 | 23 | export default TestPage; 24 | -------------------------------------------------------------------------------- /src/components/admin/users-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useQuery } from "@tanstack/react-query"; 4 | import { 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "~/components/ui/table"; 12 | import { type AuthUserType } from "~/server/auth"; 13 | import { authClient } from "~/server/auth/client"; 14 | // import ImpersonateUser from "./impersonate-user"; 15 | 16 | export default function UsersTable() { 17 | const { 18 | isLoading, 19 | data: users, 20 | error, 21 | } = useQuery({ 22 | queryKey: ["admin_list_user"], 23 | queryFn: async () => 24 | await authClient.admin.listUsers({ 25 | query: { limit: 10 }, 26 | }), 27 | }); 28 | 29 | if (isLoading) { 30 | return ( 31 |
32 | Loading users... 33 |
34 | ); 35 | } 36 | 37 | if (error || !users?.data?.users) { 38 | return ( 39 |
40 | 41 | Error: {error?.message ?? "Fetch Failed!!!"} 42 | 43 |
44 | ); 45 | } 46 | 47 | return ( 48 | 49 | 50 | 51 | Name 52 | Email 53 | Role 54 | Verified 55 | Premium 56 | Status 57 | Joined 58 | Actions 59 | 60 | 61 | 62 | {(users.data.users as AuthUserType[]).map((user) => ( 63 | 64 | {user.name} 65 | {user.email} 66 | {user.role} 67 | {user.emailVerified ? "Yes" : "No"} 68 | {user.isPremium ? "Yes" : "No"} 69 | 70 | {user.banned ? ( 71 | Banned 72 | ) : ( 73 | Active 74 | )} 75 | 76 | 77 | {new Date(user.createdAt).toLocaleDateString()} 78 | 79 | {/* */} 80 | 81 | ))} 82 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/components/auth/forgot-password-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { type ErrorContext } from "@better-fetch/fetch"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useState } from "react"; 5 | import { useForm } from "react-hook-form"; 6 | import LoadingButton from "~/components/loading-button"; 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardHeader, 12 | CardTitle, 13 | } from "~/components/ui/card"; 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "~/components/ui/form"; 22 | import { Input } from "~/components/ui/input"; 23 | import { useToast } from "~/hooks/use-toast"; 24 | import { cn } from "~/lib/utils"; 25 | import { forgotPasswordSchema, type ForgotPasswordSchemaType } from "~/lib/zod"; 26 | import { authClient } from "~/server/auth/client"; 27 | 28 | export function ForgotPasswordForm({ 29 | className, 30 | ...props 31 | }: React.ComponentPropsWithoutRef<"div">) { 32 | const { toast } = useToast(); 33 | const [pending, setPending] = useState(false); 34 | 35 | const form = useForm({ 36 | resolver: zodResolver(forgotPasswordSchema), 37 | defaultValues: { 38 | email: "", 39 | }, 40 | }); 41 | 42 | const handleForgotPassword = async (values: ForgotPasswordSchemaType) => { 43 | await authClient.forgetPassword( 44 | { 45 | email: values.email, 46 | redirectTo: "/reset-password", 47 | }, 48 | { 49 | onRequest: () => { 50 | setPending(true); 51 | }, 52 | onSuccess: async () => { 53 | toast({ 54 | title: "Success", 55 | description: 56 | "If an account exists, you will receive an email to reset your password.", 57 | }); 58 | }, 59 | onError: (ctx: ErrorContext) => { 60 | toast({ 61 | title: "Something went wrong", 62 | description: ctx.error.message ?? "Something went wrong.", 63 | variant: "destructive", 64 | }); 65 | }, 66 | }, 67 | ); 68 | 69 | setPending(false); 70 | }; 71 | 72 | return ( 73 |
74 | 75 | 76 | Reset Password 77 | 78 | Enter your email below to reset your password. 79 | 80 | 81 | 82 |
83 | 87 | {["email"].map((field) => ( 88 | ( 93 | 94 | 95 | {field.charAt(0).toUpperCase() + field.slice(1)} 96 | 97 | 98 | 106 | 107 | 108 | 109 | )} 110 | /> 111 | ))} 112 | 113 | Send Reset Link 114 | 115 | 116 | 117 |
118 |
119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/components/auth/reset-password-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { type ErrorContext } from "@better-fetch/fetch"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useRouter, useSearchParams } from "next/navigation"; 5 | import { Suspense, useState } from "react"; 6 | import { useForm } from "react-hook-form"; 7 | import LoadingButton from "~/components/loading-button"; 8 | import { 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardHeader, 13 | CardTitle, 14 | } from "~/components/ui/card"; 15 | import { 16 | Form, 17 | FormControl, 18 | FormField, 19 | FormItem, 20 | FormLabel, 21 | FormMessage, 22 | } from "~/components/ui/form"; 23 | import { Input } from "~/components/ui/input"; 24 | import { useToast } from "~/hooks/use-toast"; 25 | import { cn } from "~/lib/utils"; 26 | import { resetPasswordSchema, type ResetPasswordSchemaType } from "~/lib/zod"; 27 | import { authClient } from "~/server/auth/client"; 28 | 29 | function ResetPasswordFormNoSuspense({ 30 | className, 31 | ...props 32 | }: React.ComponentPropsWithoutRef<"div">) { 33 | const router = useRouter(); 34 | const searchParams = useSearchParams(); 35 | const invalid_token_error = searchParams.get("error"); 36 | const token = searchParams.get("token"); 37 | 38 | const { toast } = useToast(); 39 | const [pending, setPending] = useState(false); 40 | 41 | const form = useForm({ 42 | resolver: zodResolver(resetPasswordSchema), 43 | defaultValues: { 44 | password: "", 45 | confirmPassword: "", 46 | }, 47 | }); 48 | 49 | const handleForgotPassword = async (values: ResetPasswordSchemaType) => { 50 | if (!token) { 51 | console.log("No token found!!!"); 52 | return; 53 | } 54 | 55 | await authClient.resetPassword( 56 | { 57 | newPassword: values.password, 58 | token, 59 | }, 60 | { 61 | onRequest: () => { 62 | setPending(true); 63 | }, 64 | onSuccess: async () => { 65 | toast({ 66 | title: "Success", 67 | description: "Password reset successful. Login to continue.", 68 | }); 69 | router.push("/signin"); 70 | }, 71 | onError: (ctx: ErrorContext) => { 72 | toast({ 73 | title: "Something went wrong", 74 | description: ctx.error.message ?? "Something went wrong.", 75 | variant: "destructive", 76 | }); 77 | }, 78 | }, 79 | ); 80 | 81 | setPending(false); 82 | }; 83 | 84 | if (invalid_token_error === "INVALID_TOKEN" || !token) { 85 | return ( 86 |
87 | 88 | 89 | 90 | Invalid Reset Link 91 | 92 | 93 | 94 |
95 |

96 | This password reset link is invalid or has expired. 97 |

98 |
99 |
100 |
101 |
102 | ); 103 | } 104 | 105 | return ( 106 |
107 | 108 | 109 | Reset Password 110 | 111 | 112 |
113 | 117 | {["password", "confirmPassword"].map((field) => ( 118 | ( 123 | 124 | 125 | {field.charAt(0).toUpperCase() + field.slice(1)} 126 | 127 | 128 | 138 | 139 | 140 | 141 | )} 142 | /> 143 | ))} 144 | 145 | Send Reset Link 146 | 147 | 148 | 149 |
150 |
151 |
152 | ); 153 | } 154 | 155 | export function ResetPasswordForm({ 156 | className, 157 | ...props 158 | }: React.ComponentPropsWithoutRef<"div">) { 159 | return ( 160 | 161 | 162 | 163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /src/components/auth/signin-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { type ErrorContext } from "@better-fetch/fetch"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { Github } from "lucide-react"; 5 | import Link from "next/link"; 6 | import { useRouter } from "next/navigation"; 7 | import { useState } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import LoadingButton from "~/components/loading-button"; 10 | import { 11 | Card, 12 | CardContent, 13 | CardDescription, 14 | CardHeader, 15 | CardTitle, 16 | } from "~/components/ui/card"; 17 | import { 18 | Form, 19 | FormControl, 20 | FormField, 21 | FormItem, 22 | FormLabel, 23 | FormMessage, 24 | } from "~/components/ui/form"; 25 | import { Input } from "~/components/ui/input"; 26 | import { useToast } from "~/hooks/use-toast"; 27 | import { cn } from "~/lib/utils"; 28 | import { signInSchema, type SignInSchemaType } from "~/lib/zod"; 29 | import { authClient } from "~/server/auth/client"; 30 | 31 | export function SigninForm({ 32 | className, 33 | ...props 34 | }: React.ComponentPropsWithoutRef<"div">) { 35 | const router = useRouter(); 36 | const { toast } = useToast(); 37 | const [pendingCredentials, setPendingCredentials] = useState(false); 38 | const [pendingGithub, setPendingGithub] = useState(false); 39 | 40 | const form = useForm({ 41 | resolver: zodResolver(signInSchema), 42 | defaultValues: { 43 | email: "", 44 | password: "", 45 | }, 46 | }); 47 | 48 | const handleSignInWithGithub = async () => { 49 | await authClient.signIn.social( 50 | { 51 | provider: "github", 52 | }, 53 | { 54 | onRequest: () => { 55 | setPendingGithub(true); 56 | }, 57 | onSuccess: async () => { 58 | router.refresh(); 59 | // router.push("/"); 60 | }, 61 | onError: (ctx: ErrorContext) => { 62 | toast({ 63 | title: "Something went wrong", 64 | description: ctx.error.message ?? "Something went wrong.", 65 | variant: "destructive", 66 | }); 67 | }, 68 | }, 69 | ); 70 | 71 | setPendingGithub(false); 72 | }; 73 | 74 | const handleCredentialsSignIn = async (values: SignInSchemaType) => { 75 | await authClient.signIn.email( 76 | { 77 | email: values.email, 78 | password: values.password, 79 | }, 80 | { 81 | onRequest: () => { 82 | setPendingCredentials(true); 83 | }, 84 | onSuccess: async () => { 85 | router.push("/"); 86 | }, 87 | onError: (ctx: ErrorContext) => { 88 | toast({ 89 | title: (ctx.error?.code as string) ?? "Something went wrong", 90 | description: ctx.error.message ?? "Something went wrong.", 91 | variant: "destructive", 92 | }); 93 | }, 94 | }, 95 | ); 96 | 97 | setPendingCredentials(false); 98 | }; 99 | 100 | return ( 101 |
102 | 103 | 104 | Signin 105 | 106 | Enter your email below to sign in to your account 107 | 108 | 109 | 110 |
111 | 115 | {["email", "password"].map((field) => ( 116 | ( 121 | 122 | 123 | {field.charAt(0).toUpperCase() + field.slice(1)} 124 | 125 | 126 | 134 | 135 | 136 | {field === "password" && ( 137 |
138 | 142 | Forgot your password? 143 | 144 |
145 | )} 146 |
147 | )} 148 | /> 149 | ))} 150 | 151 | Sign in 152 | 153 | 160 | 161 | Login with Github 162 | 163 |
164 | Don't have an account?{" "} 165 | 166 | Sign up 167 | 168 |
169 | 170 | 171 |
172 |
173 |
174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /src/components/auth/signout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | import LoadingButton from "~/components/loading-button"; 6 | import { authClient } from "~/server/auth/client"; 7 | 8 | export default function SignoutButton() { 9 | const router = useRouter(); 10 | const [pending, setPending] = useState(false); 11 | 12 | const handleSignOut = async () => { 13 | try { 14 | setPending(true); 15 | await authClient.signOut({ 16 | fetchOptions: { 17 | onSuccess: () => { 18 | router.push("/signin"); 19 | // router.refresh(); 20 | }, 21 | }, 22 | }); 23 | } catch (error) { 24 | console.error("Error signing out:", error); 25 | } finally { 26 | setPending(false); 27 | } 28 | }; 29 | 30 | return ( 31 | 36 | Sign Out 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/auth/signup-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/navigation"; 5 | import { useState } from "react"; 6 | import { useForm } from "react-hook-form"; 7 | import LoadingButton from "~/components/loading-button"; 8 | import { Button } from "~/components/ui/button"; 9 | import { 10 | Card, 11 | CardContent, 12 | CardDescription, 13 | CardHeader, 14 | CardTitle, 15 | } from "~/components/ui/card"; 16 | import { 17 | Form, 18 | FormControl, 19 | FormField, 20 | FormItem, 21 | FormLabel, 22 | FormMessage, 23 | } from "~/components/ui/form"; 24 | import { Input } from "~/components/ui/input"; 25 | import { useToast } from "~/hooks/use-toast"; 26 | import { cn } from "~/lib/utils"; 27 | import { signUpSchema, type SignUpSchemaType } from "~/lib/zod"; 28 | import { authClient } from "~/server/auth/client"; 29 | 30 | export function SignupForm({ 31 | className, 32 | ...props 33 | }: React.ComponentPropsWithoutRef<"div">) { 34 | const [pending, setPending] = useState(false); 35 | const { toast } = useToast(); 36 | const router = useRouter(); 37 | const form = useForm({ 38 | resolver: zodResolver(signUpSchema), 39 | defaultValues: { 40 | name: "", 41 | email: "", 42 | password: "", 43 | }, 44 | }); 45 | 46 | const onSubmit = async (values: SignUpSchemaType) => { 47 | await authClient.signUp.email( 48 | { 49 | email: values.email, 50 | password: values.password, 51 | name: values.name, 52 | }, 53 | { 54 | onRequest: () => setPending(true), 55 | onSuccess: () => { 56 | toast({ 57 | title: "Account created", 58 | description: 59 | "Your account has been created. Check your email for a verification link.", 60 | }); 61 | }, 62 | onError: (ctx) => { 63 | console.log("error", ctx); 64 | toast({ 65 | title: "Something went wrong", 66 | description: ctx.error.message ?? "", 67 | }); 68 | }, 69 | }, 70 | ); 71 | 72 | setPending(false); 73 | }; 74 | 75 | return ( 76 |
77 | 78 | 79 | Signup 80 | 81 | Enter your email below to sign up for your account 82 | 83 | 84 | 85 |
86 | 87 | {["name", "email", "password"].map((field) => ( 88 | ( 93 | 94 | 95 | {field.charAt(0).toUpperCase() + field.slice(1)} 96 | 97 | 98 | 110 | 111 | 112 | 113 | )} 114 | /> 115 | ))} 116 | Sign up 117 | {/* */} 120 |
121 | Already have an account?{" "} 122 | 123 | Sign in 124 | 125 |
126 | 127 | 128 |
129 |
130 |
131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/components/loading-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button, type ButtonProps } from "~/components/ui/button"; 2 | import { cn } from "~/lib/utils"; 3 | export default function LoadingButton({ 4 | pending, 5 | children, 6 | onClick, 7 | className = "", 8 | ...props 9 | }: { 10 | pending: boolean; 11 | children: React.ReactNode; 12 | onClick?: () => void; 13 | className?: string; 14 | } & ButtonProps) { 15 | return ( 16 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/post.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | import { api } from "~/trpc/react"; 6 | 7 | export function LatestPost() { 8 | const [latestPost] = api.post.getLatest.useSuspenseQuery(); 9 | 10 | const utils = api.useUtils(); 11 | const [name, setName] = useState(""); 12 | const createPost = api.post.create.useMutation({ 13 | onSuccess: async () => { 14 | await utils.post.invalidate(); 15 | setName(""); 16 | }, 17 | }); 18 | 19 | return ( 20 |
21 | {latestPost ? ( 22 |

Your most recent post: {latestPost.name}

23 | ) : ( 24 |

You have no posts yet.

25 | )} 26 |
{ 28 | e.preventDefault(); 29 | createPost.mutate({ name }); 30 | }} 31 | className="flex flex-col gap-2" 32 | > 33 | setName(e.target.value)} 38 | className="w-full rounded-full px-4 py-2 text-black" 39 | /> 40 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { Slot } from "@radix-ui/react-slot"; 5 | import * as React from "react"; 6 | import { 7 | Controller, 8 | type ControllerProps, 9 | type FieldPath, 10 | type FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form"; 14 | 15 | import { Label } from "~/components/ui/label"; 16 | import { cn } from "~/lib/utils"; 17 | 18 | const Form = FormProvider; 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath, 23 | > = { 24 | name: TName; 25 | }; 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue, 29 | ); 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath, 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext); 46 | const itemContext = React.useContext(FormItemContext); 47 | const { getFieldState, formState } = useFormContext(); 48 | if (!getFieldState || !formState) { 49 | } 50 | const fieldState = getFieldState(fieldContext.name, formState); 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within "); 54 | } 55 | 56 | const { id } = itemContext; 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | }; 66 | }; 67 | 68 | type FormItemContextValue = { 69 | id: string; 70 | }; 71 | 72 | const FormItemContext = React.createContext( 73 | {} as FormItemContextValue, 74 | ); 75 | 76 | const FormItem = React.forwardRef< 77 | HTMLDivElement, 78 | React.HTMLAttributes 79 | >(({ className, ...props }, ref) => { 80 | const id = React.useId(); 81 | 82 | return ( 83 | 84 |
85 | 86 | ); 87 | }); 88 | FormItem.displayName = "FormItem"; 89 | 90 | const FormLabel = React.forwardRef< 91 | React.ElementRef, 92 | React.ComponentPropsWithoutRef 93 | >(({ className, ...props }, ref) => { 94 | const { error, formItemId } = useFormField(); 95 | 96 | return ( 97 |