├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
103 | );
104 | });
105 | FormLabel.displayName = "FormLabel";
106 |
107 | const FormControl = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ ...props }, ref) => {
111 | const { error, formItemId, formDescriptionId, formMessageId } =
112 | useFormField();
113 |
114 | return (
115 |
126 | );
127 | });
128 | FormControl.displayName = "FormControl";
129 |
130 | const FormDescription = React.forwardRef<
131 | HTMLParagraphElement,
132 | React.HTMLAttributes
133 | >(({ className, ...props }, ref) => {
134 | const { formDescriptionId } = useFormField();
135 |
136 | return (
137 |
143 | );
144 | });
145 | FormDescription.displayName = "FormDescription";
146 |
147 | const FormMessage = React.forwardRef<
148 | HTMLParagraphElement,
149 | React.HTMLAttributes
150 | >(({ className, children, ...props }, ref) => {
151 | const { error, formMessageId } = useFormField();
152 | const body = error ? String(error?.message) : children;
153 |
154 | if (!body) {
155 | return null;
156 | }
157 |
158 | return (
159 |
165 | {body}
166 |
167 | );
168 | });
169 | FormMessage.displayName = "FormMessage";
170 |
171 | export {
172 | Form,
173 | FormControl,
174 | FormDescription,
175 | FormField,
176 | FormItem,
177 | FormLabel,
178 | FormMessage,
179 | useFormField,
180 | };
181 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "~/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "~/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "~/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/email-templates/change-email-verification.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Section,
12 | Tailwind,
13 | Text,
14 | } from "@react-email/components";
15 |
16 | interface VerificationEmailTemplateProps {
17 | inviteLink: string;
18 | }
19 |
20 | const VerificationEmailTemplate = ({
21 | inviteLink,
22 | }: VerificationEmailTemplateProps) => {
23 | const previewText = `Verify Your Email Address Change.`;
24 |
25 | return (
26 |
27 |
28 | {previewText}
29 |
30 |
31 |
32 |
33 | {previewText}
34 |
35 |
36 |
42 |
43 |
44 | or copy and paste this URL into your browser:{" "}
45 |
46 | {inviteLink}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | VerificationEmailTemplate.PreviewProps = {
58 | inviteLink: "http://localhost:3000",
59 | };
60 |
61 | export default VerificationEmailTemplate;
62 |
--------------------------------------------------------------------------------
/src/email-templates/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as VerificationEmailTemplate } from "./verification-email";
2 | export { default as ResetPasswordEmailTemplate } from "./reset-password-email";
3 | export { default as ChangeEmailVerificationTemplate } from "./change-email-verification";
4 |
--------------------------------------------------------------------------------
/src/email-templates/reset-password-email.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Section,
12 | Tailwind,
13 | Text,
14 | } from "@react-email/components";
15 |
16 | interface VerificationEmailTemplateProps {
17 | inviteLink: string;
18 | }
19 |
20 | const ResetPasswordEmailTemplate = ({
21 | inviteLink,
22 | }: VerificationEmailTemplateProps) => {
23 | const previewText = `Verify Your Email Address To Reset Your Password.`;
24 |
25 | return (
26 |
27 |
28 | {previewText}
29 |
30 |
31 |
32 |
33 | Verify Your Email Address
34 |
35 |
36 |
42 |
43 |
44 | or copy and paste this URL into your browser:{" "}
45 |
46 | {inviteLink}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | ResetPasswordEmailTemplate.PreviewProps = {
58 | inviteLink: "http://localhost:3000",
59 | };
60 |
61 | export default ResetPasswordEmailTemplate;
62 |
--------------------------------------------------------------------------------
/src/email-templates/verification-email.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Hr,
8 | Html,
9 | Link,
10 | Preview,
11 | Section,
12 | Tailwind,
13 | Text,
14 | } from "@react-email/components";
15 |
16 | interface VerificationEmailTemplateProps {
17 | inviteLink: string;
18 | }
19 |
20 | const VerificationEmailTemplate = ({
21 | inviteLink,
22 | }: VerificationEmailTemplateProps) => {
23 | const previewText = `Verify Your Email Address.`;
24 |
25 | return (
26 |
27 |
28 | {previewText}
29 |
30 |
31 |
32 |
33 | Verify Your Email Address
34 |
35 |
36 |
42 |
43 |
44 | or copy and paste this URL into your browser:{" "}
45 |
46 | {inviteLink}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | VerificationEmailTemplate.PreviewProps = {
58 | inviteLink: "http://localhost:3000",
59 | };
60 |
61 | export default VerificationEmailTemplate;
62 |
--------------------------------------------------------------------------------
/src/env.js:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | /**
6 | * Specify your server-side environment variables schema here. This way you can ensure the app
7 | * isn't built with invalid env vars.
8 | */
9 | server: {
10 | DATABASE_URL: z.string().url(),
11 | BETTER_AUTH_SECRET: z.string(),
12 | BETTER_AUTH_URL: z.string().url(),
13 | GITHUB_CLIENT_ID: z.string(),
14 | GITHUB_CLIENT_SECRET: z.string(),
15 | EMAIL_VERIFICATION_CALLBACK_URL: z.string().url(),
16 | RESERND_API_KEY: z.string(),
17 | EMAIL_FROM: z.string(),
18 | NODE_ENV: z
19 | .enum(["development", "test", "production"])
20 | .default("development"),
21 | },
22 |
23 | /**
24 | * Specify your client-side environment variables schema here. This way you can ensure the app
25 | * isn't built with invalid env vars. To expose them to the client, prefix them with
26 | * `NEXT_PUBLIC_`.
27 | */
28 | client: {
29 | // NEXT_PUBLIC_CLIENTVAR: z.string(),
30 | NEXT_PUBLIC_BETTER_AUTH_URL: z.string().url(),
31 | },
32 |
33 | /**
34 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
35 | * middlewares) or client-side so we need to destruct manually.
36 | */
37 | runtimeEnv: {
38 | DATABASE_URL: process.env.DATABASE_URL,
39 | NODE_ENV: process.env.NODE_ENV,
40 | BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
41 | BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
42 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
43 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
44 | NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
45 | EMAIL_VERIFICATION_CALLBACK_URL:
46 | process.env.EMAIL_VERIFICATION_CALLBACK_URL,
47 | EMAIL_FROM: process.env.EMAIL_FROM,
48 | RESERND_API_KEY: process.env.RESERND_API_KEY,
49 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
50 | },
51 | /**
52 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
53 | * useful for Docker builds.
54 | */
55 | skipValidation: !!process.env.SKIP_ENV_VALIDATION,
56 | /**
57 | * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
58 | * `SOME_VAR=''` will throw an error.
59 | */
60 | emptyStringAsUndefined: true,
61 | });
62 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "~/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/zod.ts:
--------------------------------------------------------------------------------
1 | import { object, string, type z } from "zod";
2 |
3 | const getPasswordSchema = (type: "password" | "confirmPassword") =>
4 | string({ required_error: `${type} is required` })
5 | .min(8, `${type} must be atleast 8 characters`)
6 | .max(32, `${type} can not exceed 32 characters`);
7 |
8 | const getEmailSchema = () =>
9 | string({ required_error: "Email is required" })
10 | .min(1, "Email is required")
11 | .email("Invalid email");
12 |
13 | const getNameSchema = () =>
14 | string({ required_error: "Name is required" })
15 | .min(1, "Name is required")
16 | .max(50, "Name must be less than 50 characters");
17 |
18 | export const signUpSchema = object({
19 | name: getNameSchema(),
20 | email: getEmailSchema(),
21 | password: getPasswordSchema("password"),
22 | // confirmPassword: getPasswordSchema("confirmPassword"),
23 | });
24 | // .refine((data) => data.password === data.confirmPassword, {
25 | // message: "Passwords don't match",
26 | // path: ["confirmPassword"],
27 | // });
28 |
29 | export type SignUpSchemaType = z.infer;
30 |
31 | export const signInSchema = object({
32 | email: getEmailSchema(),
33 | password: getPasswordSchema("password"),
34 | });
35 | export type SignInSchemaType = z.infer;
36 |
37 | export const forgotPasswordSchema = object({
38 | email: getEmailSchema(),
39 | });
40 |
41 | export type ForgotPasswordSchemaType = z.infer;
42 |
43 | export const resetPasswordSchema = object({
44 | password: getPasswordSchema("password"),
45 | confirmPassword: getPasswordSchema("confirmPassword"),
46 | }).refine((data) => data.password === data.confirmPassword, {
47 | message: "Passwords don't match",
48 | path: ["confirmPassword"],
49 | });
50 |
51 | export type ResetPasswordSchemaType = z.infer;
52 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { betterFetch } from "@better-fetch/fetch";
2 | import { NextResponse, type NextRequest } from "next/server";
3 | import { env } from "~/env";
4 | import type { Session } from "~/server/auth";
5 |
6 | const authRoutes = ["/signin", "/signup"];
7 | const passwordRoutes = ["/reset-password", "/forgot-password"];
8 | const adminRoutes = ["/admin"];
9 | // const noAuthRoutes = ["/test"];
10 |
11 | export default async function authMiddleware(request: NextRequest) {
12 | const pathName = request.nextUrl.pathname;
13 |
14 | const isAuthRoute = authRoutes.includes(pathName);
15 | const isPasswordRoute = passwordRoutes.includes(pathName);
16 | const isAdminRoute = adminRoutes.includes(pathName);
17 | // const isOnlyProtectedRoutes = onlyProtectedRoutes.includes(pathName);
18 | // const isNoAuthRoute = noAuthRoutes.includes(pathName);
19 |
20 | // if (isNoAuthRoute) {
21 | // return NextResponse.next();
22 | // }
23 |
24 | const { data: session } = await betterFetch(
25 | "/api/auth/get-session",
26 | {
27 | baseURL: env.BETTER_AUTH_URL,
28 | headers: {
29 | //get the cookie from the request
30 | cookie: request.headers.get("cookie") ?? "",
31 | },
32 | },
33 | );
34 |
35 | console.log("Session: ", session);
36 |
37 | if (!session) {
38 | if (isAuthRoute || isPasswordRoute) {
39 | return NextResponse.next();
40 | }
41 |
42 | return NextResponse.redirect(new URL("/signin", request.url));
43 | }
44 |
45 | if (isAuthRoute || isPasswordRoute) {
46 | return NextResponse.redirect(new URL("/", request.url));
47 | }
48 |
49 | if (isAdminRoute && session.user.role !== "admin") {
50 | return NextResponse.redirect(new URL("/", request.url));
51 | }
52 |
53 | return NextResponse.next();
54 | }
55 |
56 | export const config = {
57 | matcher: [
58 | "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|$).*)",
59 | ],
60 | };
61 |
--------------------------------------------------------------------------------
/src/server/api/root.ts:
--------------------------------------------------------------------------------
1 | import { postRouter } from "~/server/api/routers/post";
2 | import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
3 |
4 | /**
5 | * This is the primary router for your server.
6 | *
7 | * All routers added in /api/routers should be manually added here.
8 | */
9 | export const appRouter = createTRPCRouter({
10 | post: postRouter,
11 | });
12 |
13 | // export type definition of API
14 | export type AppRouter = typeof appRouter;
15 |
16 | /**
17 | * Create a server-side caller for the tRPC API.
18 | * @example
19 | * const trpc = createCaller(createContext);
20 | * const res = await trpc.post.all();
21 | * ^? Post[]
22 | */
23 | export const createCaller = createCallerFactory(appRouter);
24 |
--------------------------------------------------------------------------------
/src/server/api/routers/post.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import {
4 | createTRPCRouter,
5 | protectedProcedure,
6 | publicProcedure,
7 | } from "~/server/api/trpc";
8 | import { posts } from "~/server/db/schema";
9 |
10 | export const postRouter = createTRPCRouter({
11 | hello: publicProcedure
12 | .input(z.object({ text: z.string() }))
13 | .query(({ input }) => {
14 | return {
15 | greeting: `Hello ${input.text}`,
16 | };
17 | }),
18 |
19 | create: protectedProcedure
20 | .input(z.object({ name: z.string().min(1) }))
21 | .mutation(async ({ ctx, input }) => {
22 | await ctx.db.insert(posts).values({
23 | name: input.name,
24 | createdById: ctx.session.user.id,
25 | });
26 | }),
27 |
28 | getLatest: protectedProcedure.query(async ({ ctx }) => {
29 | const post = await ctx.db.query.posts.findFirst({
30 | orderBy: (posts, { desc }) => [desc(posts.createdAt)],
31 | });
32 |
33 | return post ?? null;
34 | }),
35 |
36 | getSecretMessage: protectedProcedure.query(() => {
37 | return "you can now see this secret message!";
38 | }),
39 | });
40 |
--------------------------------------------------------------------------------
/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 | import { initTRPC, TRPCError } from "@trpc/server";
10 | import superjson from "superjson";
11 | import { ZodError } from "zod";
12 | import { auth } from "~/server/auth";
13 |
14 | import { db } from "~/server/db";
15 |
16 | /**
17 | * 1. CONTEXT
18 | *
19 | * This section defines the "contexts" that are available in the backend API.
20 | *
21 | * These allow you to access things when processing a request, like the database, the session, etc.
22 | *
23 | * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
24 | * wrap this and provides the required context.
25 | *
26 | * @see https://trpc.io/docs/server/context
27 | */
28 | export const createTRPCContext = async (opts: { headers: Headers }) => {
29 | const session = await auth.api.getSession({
30 | headers: opts.headers,
31 | });
32 |
33 | return {
34 | db,
35 | session,
36 | ...opts,
37 | };
38 | };
39 |
40 | /**
41 | * 2. INITIALIZATION
42 | *
43 | * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
44 | * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
45 | * errors on the backend.
46 | */
47 | const t = initTRPC.context().create({
48 | transformer: superjson,
49 | errorFormatter({ shape, error }) {
50 | return {
51 | ...shape,
52 | data: {
53 | ...shape.data,
54 | zodError:
55 | error.cause instanceof ZodError ? error.cause.flatten() : null,
56 | },
57 | };
58 | },
59 | });
60 |
61 | /**
62 | * Create a server-side caller.
63 | *
64 | * @see https://trpc.io/docs/server/server-side-calls
65 | */
66 | export const createCallerFactory = t.createCallerFactory;
67 |
68 | /**
69 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
70 | *
71 | * These are the pieces you use to build your tRPC API. You should import these a lot in the
72 | * "/src/server/api/routers" directory.
73 | */
74 |
75 | /**
76 | * This is how you create new routers and sub-routers in your tRPC API.
77 | *
78 | * @see https://trpc.io/docs/router
79 | */
80 | export const createTRPCRouter = t.router;
81 |
82 | /**
83 | * Middleware for timing procedure execution and adding an artificial delay in development.
84 | *
85 | * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
86 | * network latency that would occur in production but not in local development.
87 | */
88 | const timingMiddleware = t.middleware(async ({ next, path }) => {
89 | const start = Date.now();
90 |
91 | if (t._config.isDev) {
92 | // artificial delay in dev
93 | const waitMs = Math.floor(Math.random() * 400) + 100;
94 | await new Promise((resolve) => setTimeout(resolve, waitMs));
95 | }
96 |
97 | const result = await next();
98 |
99 | const end = Date.now();
100 | console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
101 |
102 | return result;
103 | });
104 |
105 | /**
106 | * Public (unauthenticated) procedure
107 | *
108 | * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
109 | * guarantee that a user querying is authorized, but you can still access user session data if they
110 | * are logged in.
111 | */
112 | export const publicProcedure = t.procedure.use(timingMiddleware);
113 |
114 | /**
115 | * Protected (authenticated) procedure
116 | *
117 | * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
118 | * the session is valid and guarantees `ctx.session.user` is not null.
119 | *
120 | * @see https://trpc.io/docs/procedures
121 | */
122 | export const protectedProcedure = t.procedure
123 | .use(timingMiddleware)
124 | .use(({ ctx, next }) => {
125 | if (!ctx.session?.user) {
126 | throw new TRPCError({ code: "UNAUTHORIZED" });
127 | }
128 |
129 | return next({
130 | ctx: {
131 | // infers the `session` as non-nullable
132 | session: { ...ctx.session, user: ctx.session.user },
133 | },
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/src/server/auth/client.ts:
--------------------------------------------------------------------------------
1 | import { adminClient } from "better-auth/client/plugins";
2 | import { createAuthClient } from "better-auth/react";
3 | import { env } from "~/env";
4 |
5 | export const authClient = createAuthClient({
6 | baseURL: env.NEXT_PUBLIC_BETTER_AUTH_URL, // the base url of your auth server
7 | plugins: [adminClient()],
8 | });
9 |
--------------------------------------------------------------------------------
/src/server/auth/email.ts:
--------------------------------------------------------------------------------
1 | import { render } from "@react-email/render";
2 | import { Resend } from "resend";
3 | import {
4 | ChangeEmailVerificationTemplate,
5 | ResetPasswordEmailTemplate,
6 | VerificationEmailTemplate,
7 | } from "~/email-templates";
8 | import { env } from "~/env";
9 |
10 | export const resend = new Resend(env.RESERND_API_KEY);
11 |
12 | export const sendVerificationEmail = async ({
13 | email,
14 | verificationUrl,
15 | }: {
16 | email: string;
17 | verificationUrl: string;
18 | }) => {
19 | return await resend.emails.send({
20 | from: env.EMAIL_FROM,
21 | to: [email],
22 | subject: "Verify your Email address",
23 | html: await render(
24 | VerificationEmailTemplate({ inviteLink: verificationUrl }),
25 | ),
26 | });
27 | };
28 |
29 | export const sendResetPasswordEmail = async ({
30 | email,
31 | verificationUrl,
32 | }: {
33 | email: string;
34 | verificationUrl: string;
35 | }) => {
36 | return await resend.emails.send({
37 | from: env.EMAIL_FROM,
38 | to: [email],
39 | subject: "Reset Password Link",
40 | react: ResetPasswordEmailTemplate({ inviteLink: verificationUrl }),
41 | });
42 | };
43 |
44 | export const sendChangeEmailVerification = async ({
45 | email,
46 | verificationUrl,
47 | }: {
48 | email: string;
49 | verificationUrl: string;
50 | }) => {
51 | return await resend.emails.send({
52 | from: env.EMAIL_FROM,
53 | to: [email],
54 | subject: "Reset Password Link",
55 | react: ChangeEmailVerificationTemplate({ inviteLink: verificationUrl }),
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/src/server/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { betterAuth, type BetterAuthOptions } from "better-auth";
2 | import { drizzleAdapter } from "better-auth/adapters/drizzle";
3 | import { openAPI, admin } from "better-auth/plugins";
4 | import { headers } from "next/headers";
5 | import { cache } from "react";
6 | import { env } from "~/env";
7 | import {
8 | sendChangeEmailVerification,
9 | sendResetPasswordEmail,
10 | sendVerificationEmail,
11 | } from "~/server/auth/email";
12 | import { db } from "~/server/db";
13 |
14 | export const auth = betterAuth({
15 | database: drizzleAdapter(db, {
16 | provider: "pg",
17 | }),
18 | plugins: [
19 | openAPI(), // /api/auth/reference
20 | admin({
21 | impersonationSessionDuration: 60 * 60 * 24 * 7, // 7 days
22 | }),
23 | ],
24 | session: {
25 | expiresIn: 60 * 60 * 24 * 7, // 7 days
26 | updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated)
27 | cookieCache: {
28 | enabled: true,
29 | maxAge: 5 * 60, // Cache duration in seconds
30 | },
31 | },
32 | user: {
33 | additionalFields: {
34 | isPremium: {
35 | type: "boolean",
36 | required: true,
37 | },
38 | },
39 | changeEmail: {
40 | enabled: true,
41 | sendChangeEmailVerification: async ({ newEmail, url }, _request) => {
42 | const { error } = await sendChangeEmailVerification({
43 | email: newEmail,
44 | verificationUrl: url,
45 | });
46 |
47 | if (error)
48 | return console.log("sendChangeEmailVerification Error: ", error);
49 | },
50 | },
51 | },
52 | rateLimit: {
53 | window: 60, // time window in seconds
54 | max: 5, // max requests in the window
55 | },
56 | socialProviders: {
57 | github: {
58 | clientId: env.GITHUB_CLIENT_ID,
59 | clientSecret: env.GITHUB_CLIENT_SECRET,
60 | redirectURI: env.BETTER_AUTH_URL + "/api/auth/callback/github",
61 | },
62 | },
63 | account: {
64 | accountLinking: {
65 | enabled: true,
66 | trustedProviders: ["github"],
67 | },
68 | },
69 | emailAndPassword: {
70 | enabled: true,
71 | requireEmailVerification: true,
72 | autoSignIn: false,
73 | sendResetPassword: async ({ user, url }) => {
74 | const { error } = await sendResetPasswordEmail({
75 | email: user.email,
76 | verificationUrl: url,
77 | });
78 |
79 | if (error) return console.log("sendResetPasswordEmail Error: ", error);
80 | },
81 | },
82 | emailVerification: {
83 | sendOnSignUp: true,
84 | expiresIn: 60 * 60 * 1, // 1 HOUR
85 | autoSignInAfterVerification: true,
86 | sendVerificationEmail: async ({ user, token }) => {
87 | const verificationUrl = `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${token}&callbackURL=${env.EMAIL_VERIFICATION_CALLBACK_URL}`;
88 | const { error } = await sendVerificationEmail({
89 | email: user.email,
90 | verificationUrl: verificationUrl,
91 | });
92 |
93 | if (error) return console.log("sendVerificationEmail Error: ", error);
94 | },
95 | },
96 | } satisfies BetterAuthOptions);
97 |
98 | export const getServerSession = cache(
99 | async () =>
100 | await auth.api.getSession({
101 | headers: await headers(),
102 | }),
103 | );
104 |
105 | export type Session = typeof auth.$Infer.Session;
106 | export type AuthUserType = Session["user"];
107 |
--------------------------------------------------------------------------------
/src/server/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from "drizzle-orm/postgres-js";
2 | import postgres from "postgres";
3 |
4 | import { env } from "~/env";
5 | import * as schema from "./schema";
6 |
7 | /**
8 | * Cache the database connection in development. This avoids creating a new connection on every HMR
9 | * update.
10 | */
11 | const globalForDb = globalThis as unknown as {
12 | conn: postgres.Sql | undefined;
13 | };
14 |
15 | const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
16 | if (env.NODE_ENV !== "production") globalForDb.conn = conn;
17 |
18 | export const db = drizzle(conn, { schema });
19 |
--------------------------------------------------------------------------------
/src/server/db/schema.ts:
--------------------------------------------------------------------------------
1 | // Example model schema from the Drizzle docs
2 | // https://orm.drizzle.team/docs/sql-schema-declaration
3 |
4 | import { sql } from "drizzle-orm";
5 | import {
6 | boolean,
7 | index,
8 | integer,
9 | pgTableCreator,
10 | text,
11 | timestamp,
12 | varchar,
13 | } from "drizzle-orm/pg-core";
14 |
15 | /**
16 | * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
17 | * database instance for multiple projects.
18 | *
19 | * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
20 | */
21 | export const createTable = pgTableCreator((name) => `t3_better_auth_${name}`);
22 |
23 | export const posts = createTable(
24 | "post",
25 | {
26 | id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
27 | name: varchar("name", { length: 256 }),
28 | createdById: varchar("created_by", { length: 255 })
29 | .notNull()
30 | .references(() => user.id),
31 | createdAt: timestamp("created_at", { withTimezone: true })
32 | .default(sql`CURRENT_TIMESTAMP`)
33 | .notNull(),
34 | updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
35 | () => new Date(),
36 | ),
37 | },
38 | (example) => ({
39 | createdByIdIdx: index("created_by_idx").on(example.createdById),
40 | nameIndex: index("name_idx").on(example.name),
41 | }),
42 | );
43 |
44 | export const user = createTable("user", {
45 | id: text("id").primaryKey(),
46 | name: text("name").notNull(),
47 | email: text("email").notNull().unique(),
48 | emailVerified: boolean("email_verified").notNull(),
49 | image: text("image"),
50 | isPremium: boolean("is_premium").notNull().default(false),
51 | role: text("role"),
52 | banned: boolean("banned"),
53 | banReason: text("ban_reason"),
54 | banExpires: timestamp("ban_expires"),
55 | createdAt: timestamp("created_at").notNull(),
56 | updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
57 | () => new Date(),
58 | ),
59 | });
60 |
61 | export const session = createTable("session", {
62 | id: text("id").primaryKey(),
63 | expiresAt: timestamp("expires_at").notNull(),
64 | token: text("token").notNull().unique(),
65 | createdAt: timestamp("created_at").notNull(),
66 | updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
67 | () => new Date(),
68 | ),
69 | ipAddress: text("ip_address"),
70 | userAgent: text("user_agent"),
71 | userId: text("user_id")
72 | .notNull()
73 | .references(() => user.id),
74 | impersonatedBy: text("impersonated_by"),
75 | });
76 |
77 | export const account = createTable("account", {
78 | id: text("id").primaryKey(),
79 | accountId: text("account_id").notNull(),
80 | providerId: text("provider_id").notNull(),
81 | userId: text("user_id")
82 | .notNull()
83 | .references(() => user.id),
84 | accessToken: text("access_token"),
85 | refreshToken: text("refresh_token"),
86 | idToken: text("id_token"),
87 | accessTokenExpiresAt: timestamp("access_token_expires_at"),
88 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
89 | scope: text("scope"),
90 | password: text("password"),
91 | createdAt: timestamp("created_at").notNull(),
92 | updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
93 | () => new Date(),
94 | ),
95 | });
96 |
97 | export const verification = createTable("verification", {
98 | id: text("id").primaryKey(),
99 | identifier: text("identifier").notNull(),
100 | value: text("value").notNull(),
101 | expiresAt: timestamp("expires_at").notNull(),
102 | createdAt: timestamp("created_at"),
103 | updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
104 | () => new Date(),
105 | ),
106 | });
107 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @layer base {
5 | :root {
6 | --background: 0 0% 100%;
7 | --foreground: 0 0% 3.9%;
8 | --card: 0 0% 100%;
9 | --card-foreground: 0 0% 3.9%;
10 | --popover: 0 0% 100%;
11 | --popover-foreground: 0 0% 3.9%;
12 | --primary: 0 0% 9%;
13 | --primary-foreground: 0 0% 98%;
14 | --secondary: 0 0% 96.1%;
15 | --secondary-foreground: 0 0% 9%;
16 | --muted: 0 0% 96.1%;
17 | --muted-foreground: 0 0% 45.1%;
18 | --accent: 0 0% 96.1%;
19 | --accent-foreground: 0 0% 9%;
20 | --destructive: 0 84.2% 60.2%;
21 | --destructive-foreground: 0 0% 98%;
22 | --border: 0 0% 89.8%;
23 | --input: 0 0% 89.8%;
24 | --ring: 0 0% 3.9%;
25 | --chart-1: 12 76% 61%;
26 | --chart-2: 173 58% 39%;
27 | --chart-3: 197 37% 24%;
28 | --chart-4: 43 74% 66%;
29 | --chart-5: 27 87% 67%;
30 | --radius: 0.5rem
31 | }
32 | .dark {
33 | --background: 0 0% 3.9%;
34 | --foreground: 0 0% 98%;
35 | --card: 0 0% 3.9%;
36 | --card-foreground: 0 0% 98%;
37 | --popover: 0 0% 3.9%;
38 | --popover-foreground: 0 0% 98%;
39 | --primary: 0 0% 98%;
40 | --primary-foreground: 0 0% 9%;
41 | --secondary: 0 0% 14.9%;
42 | --secondary-foreground: 0 0% 98%;
43 | --muted: 0 0% 14.9%;
44 | --muted-foreground: 0 0% 63.9%;
45 | --accent: 0 0% 14.9%;
46 | --accent-foreground: 0 0% 98%;
47 | --destructive: 0 62.8% 30.6%;
48 | --destructive-foreground: 0 0% 98%;
49 | --border: 0 0% 14.9%;
50 | --input: 0 0% 14.9%;
51 | --ring: 0 0% 83.1%;
52 | --chart-1: 220 70% 50%;
53 | --chart-2: 160 60% 45%;
54 | --chart-3: 30 80% 55%;
55 | --chart-4: 280 65% 60%;
56 | --chart-5: 340 75% 55%
57 | }
58 | }
59 | @layer base {
60 | * {
61 | @apply border-border;
62 | }
63 | body {
64 | @apply bg-background text-foreground;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/trpc/query-client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultShouldDehydrateQuery,
3 | QueryClient,
4 | } from "@tanstack/react-query";
5 | import SuperJSON from "superjson";
6 |
7 | export const createQueryClient = () =>
8 | new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | // With SSR, we usually want to set some default staleTime
12 | // above 0 to avoid refetching immediately on the client
13 | staleTime: 30 * 1000,
14 | },
15 | dehydrate: {
16 | serializeData: SuperJSON.serialize,
17 | shouldDehydrateQuery: (query) =>
18 | defaultShouldDehydrateQuery(query) ||
19 | query.state.status === "pending",
20 | },
21 | hydrate: {
22 | deserializeData: SuperJSON.deserialize,
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/trpc/react.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
4 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
5 | import { createTRPCReact } from "@trpc/react-query";
6 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
7 | import { useState } from "react";
8 | import SuperJSON from "superjson";
9 |
10 | import { type AppRouter } from "~/server/api/root";
11 | import { createQueryClient } from "./query-client";
12 |
13 | let clientQueryClientSingleton: QueryClient | undefined = undefined;
14 | const getQueryClient = () => {
15 | if (typeof window === "undefined") {
16 | // Server: always make a new query client
17 | return createQueryClient();
18 | }
19 | // Browser: use singleton pattern to keep the same query client
20 | return (clientQueryClientSingleton ??= createQueryClient());
21 | };
22 |
23 | export const api = createTRPCReact();
24 |
25 | /**
26 | * Inference helper for inputs.
27 | *
28 | * @example type HelloInput = RouterInputs['example']['hello']
29 | */
30 | export type RouterInputs = inferRouterInputs;
31 |
32 | /**
33 | * Inference helper for outputs.
34 | *
35 | * @example type HelloOutput = RouterOutputs['example']['hello']
36 | */
37 | export type RouterOutputs = inferRouterOutputs;
38 |
39 | export function TRPCReactProvider(props: { children: React.ReactNode }) {
40 | const queryClient = getQueryClient();
41 |
42 | const [trpcClient] = useState(() =>
43 | api.createClient({
44 | links: [
45 | loggerLink({
46 | enabled: (op) =>
47 | process.env.NODE_ENV === "development" ||
48 | (op.direction === "down" && op.result instanceof Error),
49 | }),
50 | unstable_httpBatchStreamLink({
51 | transformer: SuperJSON,
52 | url: getBaseUrl() + "/api/trpc",
53 | headers: () => {
54 | const headers = new Headers();
55 | headers.set("x-trpc-source", "nextjs-react");
56 | return headers;
57 | },
58 | }),
59 | ],
60 | })
61 | );
62 |
63 | return (
64 |
65 |
66 | {props.children}
67 |
68 |
69 | );
70 | }
71 |
72 | function getBaseUrl() {
73 | if (typeof window !== "undefined") return window.location.origin;
74 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
75 | return `http://localhost:${process.env.PORT ?? 3000}`;
76 | }
77 |
--------------------------------------------------------------------------------
/src/trpc/server.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import { createHydrationHelpers } from "@trpc/react-query/rsc";
4 | import { headers } from "next/headers";
5 | import { cache } from "react";
6 |
7 | import { createCaller, type AppRouter } from "~/server/api/root";
8 | import { createTRPCContext } from "~/server/api/trpc";
9 | import { createQueryClient } from "./query-client";
10 |
11 | /**
12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
13 | * handling a tRPC call from a React Server Component.
14 | */
15 | const createContext = cache(async () => {
16 | const heads = new Headers(await headers());
17 | heads.set("x-trpc-source", "rsc");
18 |
19 | return createTRPCContext({
20 | headers: heads,
21 | });
22 | });
23 |
24 | const getQueryClient = cache(createQueryClient);
25 | const caller = createCaller(createContext);
26 |
27 | export const { trpc: api, HydrateClient } = createHydrationHelpers(
28 | caller,
29 | getQueryClient
30 | );
31 |
--------------------------------------------------------------------------------
/start-database.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to start a docker container for a local development database
3 |
4 | # TO RUN ON WINDOWS:
5 | # 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
6 | # 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
7 | # 3. Open WSL - `wsl`
8 | # 4. Run this script - `./start-database.sh`
9 |
10 | # On Linux and macOS you can run this script directly - `./start-database.sh`
11 |
12 | DB_CONTAINER_NAME="t3_better_auth-postgres"
13 |
14 | if ! [ -x "$(command -v docker)" ]; then
15 | echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
16 | exit 1
17 | fi
18 |
19 | if ! docker info > /dev/null 2>&1; then
20 | echo "Docker daemon is not running. Please start Docker and try again."
21 | exit 1
22 | fi
23 |
24 | if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
25 | echo "Database container '$DB_CONTAINER_NAME' already running"
26 | exit 0
27 | fi
28 |
29 | if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
30 | docker start "$DB_CONTAINER_NAME"
31 | echo "Existing database container '$DB_CONTAINER_NAME' started"
32 | exit 0
33 | fi
34 |
35 | # import env variables from .env
36 | set -a
37 | source .env
38 |
39 | DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
40 | DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
41 |
42 | if [ "$DB_PASSWORD" = "password" ]; then
43 | echo "You are using the default database password"
44 | read -p "Should we generate a random password for you? [y/N]: " -r REPLY
45 | if ! [[ $REPLY =~ ^[Yy]$ ]]; then
46 | echo "Please change the default password in the .env file and try again"
47 | exit 1
48 | fi
49 | # Generate a random URL-safe password
50 | DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
51 | sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
52 | fi
53 |
54 | docker run -d \
55 | --name $DB_CONTAINER_NAME \
56 | -e POSTGRES_USER="postgres" \
57 | -e POSTGRES_PASSWORD="$DB_PASSWORD" \
58 | -e POSTGRES_DB=t3_better_auth \
59 | -p "$DB_PORT":5432 \
60 | docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
61 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { type Config } from "tailwindcss";
2 | import { fontFamily } from "tailwindcss/defaultTheme";
3 |
4 | export default {
5 | darkMode: ["class"],
6 | content: ["./src/**/*.tsx"],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | sans: [
11 | 'var(--font-geist-sans)',
12 | ...fontFamily.sans
13 | ]
14 | },
15 | borderRadius: {
16 | lg: 'var(--radius)',
17 | md: 'calc(var(--radius) - 2px)',
18 | sm: 'calc(var(--radius) - 4px)'
19 | },
20 | colors: {
21 | background: 'hsl(var(--background))',
22 | foreground: 'hsl(var(--foreground))',
23 | card: {
24 | DEFAULT: 'hsl(var(--card))',
25 | foreground: 'hsl(var(--card-foreground))'
26 | },
27 | popover: {
28 | DEFAULT: 'hsl(var(--popover))',
29 | foreground: 'hsl(var(--popover-foreground))'
30 | },
31 | primary: {
32 | DEFAULT: 'hsl(var(--primary))',
33 | foreground: 'hsl(var(--primary-foreground))'
34 | },
35 | secondary: {
36 | DEFAULT: 'hsl(var(--secondary))',
37 | foreground: 'hsl(var(--secondary-foreground))'
38 | },
39 | muted: {
40 | DEFAULT: 'hsl(var(--muted))',
41 | foreground: 'hsl(var(--muted-foreground))'
42 | },
43 | accent: {
44 | DEFAULT: 'hsl(var(--accent))',
45 | foreground: 'hsl(var(--accent-foreground))'
46 | },
47 | destructive: {
48 | DEFAULT: 'hsl(var(--destructive))',
49 | foreground: 'hsl(var(--destructive-foreground))'
50 | },
51 | border: 'hsl(var(--border))',
52 | input: 'hsl(var(--input))',
53 | ring: 'hsl(var(--ring))',
54 | chart: {
55 | '1': 'hsl(var(--chart-1))',
56 | '2': 'hsl(var(--chart-2))',
57 | '3': 'hsl(var(--chart-3))',
58 | '4': 'hsl(var(--chart-4))',
59 | '5': 'hsl(var(--chart-5))'
60 | }
61 | }
62 | }
63 | },
64 | plugins: [require("tailwindcss-animate")],
65 | } satisfies Config;
66 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Base Options: */
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 |
12 | /* Strictness */
13 | "strict": true,
14 | "noUncheckedIndexedAccess": true,
15 | "checkJs": true,
16 |
17 | /* Bundled projects */
18 | "lib": ["dom", "dom.iterable", "ES2022"],
19 | "noEmit": true,
20 | "module": "ESNext",
21 | "moduleResolution": "Bundler",
22 | "jsx": "preserve",
23 | "plugins": [{ "name": "next" }],
24 | "incremental": true,
25 |
26 | /* Path Aliases */
27 | "baseUrl": ".",
28 | "paths": {
29 | "~/*": ["./src/*"]
30 | }
31 | },
32 | "include": [
33 | ".eslintrc.cjs",
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | "**/*.cjs",
38 | "**/*.js",
39 | ".next/types/**/*.ts"
40 | ],
41 | "exclude": ["node_modules"]
42 | }
43 |
--------------------------------------------------------------------------------