From d5d1fd4a22b6c9aac66b12041ea3f54c7cf04b9b Mon Sep 17 00:00:00 2001 From: Nicholas Ly Date: Tue, 9 Sep 2025 20:59:51 -0500 Subject: [PATCH] Clerk user authentication --- .env.example | 9 +- package-lock.json | 189 ++++++- package.json | 1 + src/app/(app)/study/page.tsx | 11 +- src/app/(auth)/login/[[...login]]/page.tsx | 9 + src/app/(auth)/login/login-form.tsx | 66 --- src/app/(auth)/login/page.tsx | 37 -- .../(auth)/register/[[...register]]/page.tsx | 9 + src/app/(auth)/register/page.tsx | 39 -- src/app/(auth)/register/register-form.tsx | 69 --- src/app/(marketing)/page.tsx | 7 +- src/app/api/attempts/route.ts | 12 +- src/app/api/login/route.ts | 60 -- src/app/api/logout/route.ts | 25 - src/app/api/register/route.ts | 60 -- src/app/api/webhooks/[[...webhook]]/route.ts | 61 +++ src/app/layout.tsx | 18 +- src/components/custom/header.tsx | 35 +- src/components/custom/logout-button.tsx | 14 - src/db/migrations/0000_flimsy_mad_thinker.sql | 16 - ...e_thundra.sql => 0000_pink_retro_girl.sql} | 29 +- src/db/migrations/0002_harsh_spectrum.sql | 1 - src/db/migrations/0003_good_zeigeist.sql | 19 - src/db/migrations/meta/0000_snapshot.json | 383 ++++++++++++- src/db/migrations/meta/0001_snapshot.json | 406 -------------- src/db/migrations/meta/0002_snapshot.json | 406 -------------- src/db/migrations/meta/0003_snapshot.json | 518 ------------------ src/db/migrations/meta/_journal.json | 25 +- src/db/{queries.ts => prepared.ts} | 38 +- src/db/schema.ts | 19 +- src/db/types.ts | 2 - src/env.ts | 20 +- src/hooks/use-login.tsx | 54 -- src/hooks/use-logout.tsx | 43 -- src/hooks/use-register.tsx | 54 -- src/lib/auth/index.ts | 72 --- src/lib/auth/passwords.ts | 27 - src/lib/auth/sessions.ts | 160 ------ src/lib/validation.ts | 17 - src/middleware.ts | 69 +-- 40 files changed, 732 insertions(+), 2377 deletions(-) create mode 100644 src/app/(auth)/login/[[...login]]/page.tsx delete mode 100644 src/app/(auth)/login/login-form.tsx delete mode 100644 src/app/(auth)/login/page.tsx create mode 100644 src/app/(auth)/register/[[...register]]/page.tsx delete mode 100644 src/app/(auth)/register/page.tsx delete mode 100644 src/app/(auth)/register/register-form.tsx delete mode 100644 src/app/api/login/route.ts delete mode 100644 src/app/api/logout/route.ts delete mode 100644 src/app/api/register/route.ts create mode 100644 src/app/api/webhooks/[[...webhook]]/route.ts delete mode 100644 src/components/custom/logout-button.tsx delete mode 100644 src/db/migrations/0000_flimsy_mad_thinker.sql rename src/db/migrations/{0001_polite_thundra.sql => 0000_pink_retro_girl.sql} (58%) delete mode 100644 src/db/migrations/0002_harsh_spectrum.sql delete mode 100644 src/db/migrations/0003_good_zeigeist.sql delete mode 100644 src/db/migrations/meta/0001_snapshot.json delete mode 100644 src/db/migrations/meta/0002_snapshot.json delete mode 100644 src/db/migrations/meta/0003_snapshot.json rename src/db/{queries.ts => prepared.ts} (53%) delete mode 100644 src/hooks/use-login.tsx delete mode 100644 src/hooks/use-logout.tsx delete mode 100644 src/hooks/use-register.tsx delete mode 100644 src/lib/auth/index.ts delete mode 100644 src/lib/auth/passwords.ts delete mode 100644 src/lib/auth/sessions.ts diff --git a/.env.example b/.env.example index 35dd34c..1b80392 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ DATABASE_URL="" -DATABASE_POOLER_URL="" \ No newline at end of file +DATABASE_POOLER_URL="" +CLERK_SECRET_KEY="" +CLERK_WEBHOOK_SIGNING_SECRET="" +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" +NEXT_PUBLIC_CLERK_SIGN_IN_URL="/login" +NEXT_PUBLIC_CLERK_SIGN_UP_URL="/register" +NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL="/study" +NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL="/study" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2437728..0370cbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "anko", "version": "0.1.0", "dependencies": { + "@clerk/nextjs": "^6.31.10", "@date-fns/utc": "^2.1.1", "@hookform/resolvers": "^5.2.1", "@neondatabase/serverless": "^1.0.1", @@ -194,6 +195,104 @@ "node": ">=6.9.0" } }, + "node_modules/@clerk/backend": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.13.0.tgz", + "integrity": "sha512-MjEeDjlteMcEC+RUmrtlwkrJ+1Zv7Gb+atCg6mD+1N8/01v66LjYWKk2eQ+T0v+znQkSiFS8JPQ8yqaLH2sbqg==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^3.24.2", + "@clerk/types": "^4.85.0", + "cookie": "1.0.2", + "standardwebhooks": "^1.0.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + } + }, + "node_modules/@clerk/clerk-react": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.46.2.tgz", + "integrity": "sha512-Gk2YVg4k72ZSCJu/fYw5GQuku1vwfHrMmYRt1/2+vyDdotZsFjKA0AcH3cPkzHlVBuCwYonN+7BfQBBq3HzX9g==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^3.24.2", + "@clerk/types": "^4.85.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + } + }, + "node_modules/@clerk/nextjs": { + "version": "6.31.10", + "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.31.10.tgz", + "integrity": "sha512-pRfSPLzg0Hlp5AiQ7kNB/2FejIFpFx6JP4z0Scd+H73rPcR3YddQC45s6U0xWtCg5z7cjinlYhCQZIg6bikrTw==", + "license": "MIT", + "dependencies": { + "@clerk/backend": "^2.13.0", + "@clerk/clerk-react": "^5.46.2", + "@clerk/shared": "^3.24.2", + "@clerk/types": "^4.85.0", + "server-only": "0.0.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "next": "^13.5.7 || ^14.2.25 || ^15.2.3", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + } + }, + "node_modules/@clerk/shared": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.24.2.tgz", + "integrity": "sha512-gGnB08PWdM6gHVhlyrQ32xwWt/bIyg8nQhrxUCnNSf/xlPC2mFUtx24JYLsFRjbIGg/uA3z2qVnllBMkxejIDA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@clerk/types": "^4.85.0", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0", + "swr": "2.3.4" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/types": { + "version": "4.85.0", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.85.0.tgz", + "integrity": "sha512-0s7KZbWpX4e9/CHl29sYGGBUBbZa7qlXp74OaaXfm1WTzYGiOOTyyE54j2WTz91+TZxIZZu5UA5rfn9qc79hYA==", + "license": "MIT", + "dependencies": { + "csstype": "3.1.3" + }, + "engines": { + "node": ">=18.17.0" + } + }, "node_modules/@date-fns/utc": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@date-fns/utc/-/utc-2.1.1.tgz", @@ -2970,6 +3069,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -4540,6 +4645,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4559,7 +4673,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4694,6 +4807,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -5634,6 +5756,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -5872,6 +6000,12 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6532,6 +6666,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8053,6 +8196,12 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8301,6 +8450,22 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8500,6 +8665,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -8890,6 +9068,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b65ee61..1521d3b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "db:seed": "tsx ./src/db/scripts/seed.ts" }, "dependencies": { + "@clerk/nextjs": "^6.31.10", "@date-fns/utc": "^2.1.1", "@hookform/resolvers": "^5.2.1", "@neondatabase/serverless": "^1.0.1", diff --git a/src/app/(app)/study/page.tsx b/src/app/(app)/study/page.tsx index 3f5ac61..938be53 100644 --- a/src/app/(app)/study/page.tsx +++ b/src/app/(app)/study/page.tsx @@ -1,17 +1,14 @@ -import { redirect } from "next/navigation"; import { Suspense } from "react"; import Header from "@/components/custom/header"; import Loading from "@/components/custom/loading"; import ProblemsDataTable from "@/components/custom/problems-data-table"; -import { getStudyProblemCollections } from "@/db/queries"; -import { auth } from "@/lib/auth"; +import { getStudyProblemCollections } from "@/db/prepared"; +import { auth } from "@clerk/nextjs/server"; export default async function Page() { - const { user } = await auth(); - if (!user) redirect("/"); - - const promise = getStudyProblemCollections.execute({ userId: user.id }); + const { userId } = await auth(); + const promise = getStudyProblemCollections.execute({ userId }); return (
diff --git a/src/app/(auth)/login/[[...login]]/page.tsx b/src/app/(auth)/login/[[...login]]/page.tsx new file mode 100644 index 0000000..7a49c53 --- /dev/null +++ b/src/app/(auth)/login/[[...login]]/page.tsx @@ -0,0 +1,9 @@ +import { SignIn } from "@clerk/nextjs"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/app/(auth)/login/login-form.tsx b/src/app/(auth)/login/login-form.tsx deleted file mode 100644 index 4e4181b..0000000 --- a/src/app/(auth)/login/login-form.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useLogin } from "@/hooks/use-login"; -import { loginFormSchema } from "@/lib/validation"; -import { zodResolver } from "@hookform/resolvers/zod"; - -export function LogInForm() { - const { login, isPending } = useLogin(); - - const form = useForm>({ - resolver: zodResolver(loginFormSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - return ( -
- - ( - - Email address - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - - - - ); -} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx deleted file mode 100644 index 23cec4d..0000000 --- a/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from "next/link"; -import { redirect } from "next/navigation"; - -import { Button } from "@/components/ui/button"; -import { auth } from "@/lib/auth"; - -import { LogInForm } from "./login-form"; - -export default async function Page() { - const { user } = await auth(); - if (user) redirect("/"); - - return ( -
-
-
-

Sign in

-

- Log in to your account with your email address and password. -

-
- -

- Don't have an account?  - -

-
-
- ); -} diff --git a/src/app/(auth)/register/[[...register]]/page.tsx b/src/app/(auth)/register/[[...register]]/page.tsx new file mode 100644 index 0000000..a0026c5 --- /dev/null +++ b/src/app/(auth)/register/[[...register]]/page.tsx @@ -0,0 +1,9 @@ +import { SignUp } from "@clerk/nextjs"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx deleted file mode 100644 index 8f504e4..0000000 --- a/src/app/(auth)/register/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Link from "next/link"; -import { redirect } from "next/navigation"; - -import { Button } from "@/components/ui/button"; -import { auth } from "@/lib/auth"; - -import { RegisterForm } from "./register-form"; - -export default async function Page() { - const { user } = await auth(); - if (user) redirect("/"); - - return ( -
-
-
-

- Create an account -

-

- Create an account using your email address and password. -

-
- -

- Already have an account?  - -

-
-
- ); -} diff --git a/src/app/(auth)/register/register-form.tsx b/src/app/(auth)/register/register-form.tsx deleted file mode 100644 index bf6a73c..0000000 --- a/src/app/(auth)/register/register-form.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useRegister } from "@/hooks/use-register"; -import { registerFormSchema } from "@/lib/validation"; -import { zodResolver } from "@hookform/resolvers/zod"; - -export function RegisterForm() { - const { register, isPending } = useRegister(); - - const form = useForm>({ - resolver: zodResolver(registerFormSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - return ( -
- - ( - - Email address - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - - - - ); -} diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index 8665718..5d2132c 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -1,13 +1,8 @@ import Link from "next/link"; -import { redirect } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { auth } from "@/lib/auth"; - -export default async function Page() { - const { user } = await auth(); - if (user) redirect("/study"); +export default function Page() { return (
diff --git a/src/app/api/attempts/route.ts b/src/app/api/attempts/route.ts index 1098231..657a7df 100644 --- a/src/app/api/attempts/route.ts +++ b/src/app/api/attempts/route.ts @@ -2,13 +2,13 @@ import { and, eq } from "drizzle-orm"; import { db } from "@/db"; import { attemptsTable, studiesTable } from "@/db/schema"; -import { getCurrentSession } from "@/lib/auth/sessions"; import { calculateDueDate, calculateNewEase, calculateNewInterval, } from "@/lib/repetition"; import { attemptSchema } from "@/lib/validation"; +import { auth } from "@clerk/nextjs/server"; /** * Saves a new study attempt and applies the spaced repetition algorithm. @@ -16,8 +16,8 @@ import { attemptSchema } from "@/lib/validation"; * @returns An HTTP response. */ export async function POST(request: Request): Promise { - const { user } = await getCurrentSession(); - if (!user) return new Response(null, { status: 401 }); + const { userId } = await auth(); + if (!userId) return new Response(null, { status: 401 }); const body = await request.json(); const { success, data } = attemptSchema.safeParse(body); @@ -29,9 +29,7 @@ export async function POST(request: Request): Promise { const [study] = await tx .select() .from(studiesTable) - .where( - and(eq(studiesTable.id, studyId), eq(studiesTable.userId, user.id)), - ) + .where(and(eq(studiesTable.id, studyId), eq(studiesTable.userId, userId))) .limit(1); if (!study) return new Response(null, { status: 404 }); @@ -47,7 +45,7 @@ export async function POST(request: Request): Promise { await tx .insert(attemptsTable) - .values({ userId: user.id, studyId: study.id, rating }); + .values({ userId: userId, studyId: study.id, rating }); return new Response(null, { status: 200 }); }); diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts deleted file mode 100644 index cc9c9d4..0000000 --- a/src/app/api/login/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { eq } from "drizzle-orm"; - -import { db } from "@/db"; -import { seedUser } from "@/db/queries"; -import { usersTable } from "@/db/schema"; -import { verifyPassword } from "@/lib/auth/passwords"; -import { - createSession, - generateSessionToken, - getCurrentSession, - invalidateExpiredSessions, - setSessionTokenCookie, -} from "@/lib/auth/sessions"; -import { loginFormSchema } from "@/lib/validation"; - -/** - * Logs in a user, creating a new database session. - * @param request The HTTP request. - * @returns An HTTP response. - */ -export async function POST(request: Request): Promise { - // TODO: Implement rate limiting - - const { session: currentSession } = await getCurrentSession(); - if (currentSession) return new Response(null, { status: 403 }); - - const body = await request.json(); - const { success, data } = loginFormSchema.safeParse(body); - if (!success) return new Response(null, { status: 400 }); - - const { email, password } = data; - - const [user] = await db - .select() - .from(usersTable) - .where(eq(usersTable.email, email)) - .limit(1); - - // Avoid returning at early here to prevent malicious actors from easily - // discovering genuine usernames via error message or response time. - - const isCorrectPassword = user - ? await verifyPassword(user.passwordHash, password) - : false; - - if (!user || !isCorrectPassword) { - // TODO: Implement throttling on failed login attempts - return new Response(null, { status: 401 }); - } - - await seedUser(user.id); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, session.expiresAt); - - invalidateExpiredSessions(); - - return new Response(null, { status: 200 }); -} diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts deleted file mode 100644 index 1c5afc5..0000000 --- a/src/app/api/logout/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - deleteSessionTokenCookie, - getCurrentSession, - invalidateExpiredSessions, - invalidateSession, -} from "@/lib/auth/sessions"; - -/** - * Logs out a user, invalidating their session and expired sessions. - * @param request The HTTP request. - * @returns An HTTP response. - */ -export async function DELETE(): Promise { - const { session } = await getCurrentSession(); - if (!session) return new Response(null, { status: 401 }); - - await Promise.all([ - invalidateSession(session.id), - deleteSessionTokenCookie(), - ]); - - invalidateExpiredSessions(); - - return new Response(null, { status: 200 }); -} diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts deleted file mode 100644 index 384c9d9..0000000 --- a/src/app/api/register/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { eq } from "drizzle-orm"; - -import { db } from "@/db"; -import { seedUser } from "@/db/queries"; -import { usersTable } from "@/db/schema"; -import { hashPassword } from "@/lib/auth/passwords"; -import { - createSession, - generateSessionToken, - getCurrentSession, - setSessionTokenCookie, -} from "@/lib/auth/sessions"; -import { registerFormSchema } from "@/lib/validation"; - -/** - * Creates a new user and logs them in. - * @param request The HTTP request. - * @returns An HTTP response. - */ -export async function POST(request: Request): Promise { - // TODO: Implement rate limiting - - const { session: currentSession } = await getCurrentSession(); - if (currentSession) return new Response(null, { status: 403 }); - - const body = await request.json(); - const { success, data } = registerFormSchema.safeParse(body); - if (!success) return new Response(null, { status: 400 }); - - const { email, password } = data; - - const [existingUser] = await db - .select() - .from(usersTable) - .where(eq(usersTable.email, email)) - .limit(1); - - if (existingUser?.email === email) return new Response(null, { status: 409 }); - - const passwordHash = await hashPassword(password); - - const user = await db.transaction(async (tx) => { - const [user] = await tx - .insert(usersTable) - .values({ email, passwordHash }) - .returning(); - - return user!; - }); - - // TODO: Implement email verification - - await seedUser(user.id); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, session.expiresAt); - - return new Response(null, { status: 200 }); -} diff --git a/src/app/api/webhooks/[[...webhook]]/route.ts b/src/app/api/webhooks/[[...webhook]]/route.ts new file mode 100644 index 0000000..c6f33c8 --- /dev/null +++ b/src/app/api/webhooks/[[...webhook]]/route.ts @@ -0,0 +1,61 @@ +import { eq } from "drizzle-orm"; +import { NextRequest } from "next/server"; + +import { db } from "@/db"; +import { problemsTable, studiesTable, usersTable } from "@/db/schema"; +import { verifyWebhook } from "@clerk/nextjs/webhooks"; + +async function handleUserCreate(userId: string) { + await db.transaction(async (tx) => { + const problemsPromise = tx.select().from(problemsTable); + const userPromise = tx + .insert(usersTable) + .values({ id: userId }) + .returning(); + + const [problems, [user]] = await Promise.all([ + problemsPromise, + userPromise, + ]); + + if (!user) throw new Error("Failed to create new user."); + if (problems.length <= 0) return; + + await tx + .insert(studiesTable) + .values( + problems.map((problem) => ({ + userId: user!.id, + problemId: problem.id, + })), + ) + .onConflictDoNothing(); + }); +} + +async function handleUserDelete(userId?: string) { + if (!userId) return; + + await db.delete(usersTable).where(eq(usersTable.id, userId)); +} + +export async function POST(request: NextRequest): Promise { + try { + const event = await verifyWebhook(request); + + console.log(`Received "${event.type}" webhook with ID: ${event.data.id}`); + + if (event.type === "user.created") { + await handleUserCreate(event.data.id); + } + + if (event.type === "user.deleted") { + await handleUserDelete(event.data.id); + } + + return new Response(null, { status: 200 }); + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response(null, { status: 400 }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 32ba0d7..7540494 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata, Viewport } from "next"; import { NuqsAdapter } from "nuqs/adapters/next/app"; +import { ClerkProvider } from "@clerk/nextjs"; + import "./globals.css"; import { Toaster } from "@/components/ui/sonner"; @@ -20,11 +22,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} - - - + + + + + {children} + + + + + ); } diff --git a/src/components/custom/header.tsx b/src/components/custom/header.tsx index 0df71d6..1f9ff18 100644 --- a/src/components/custom/header.tsx +++ b/src/components/custom/header.tsx @@ -1,30 +1,25 @@ -import Link from "next/link"; - -import { auth } from "@/lib/auth"; - -import { Button } from "../ui/button"; -import LogoutButton from "./logout-button"; - -export default async function Header() { - const { user } = await auth(); +import { + SignInButton, + SignUpButton, + SignedIn, + SignedOut, + UserButton, +} from "@clerk/nextjs"; +export default function Header() { return (

anko

- {user && } - {!user && ( - <> - - - - )} + + + + + + +
); diff --git a/src/components/custom/logout-button.tsx b/src/components/custom/logout-button.tsx deleted file mode 100644 index 3a86647..0000000 --- a/src/components/custom/logout-button.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { useLogout } from "@/hooks/use-logout"; - -export default function LogoutButton() { - const { logout, isPending } = useLogout(); - - return ( - - ); -} diff --git a/src/db/migrations/0000_flimsy_mad_thinker.sql b/src/db/migrations/0000_flimsy_mad_thinker.sql deleted file mode 100644 index 7bc95f0..0000000 --- a/src/db/migrations/0000_flimsy_mad_thinker.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE "sessions" ( - "id" text PRIMARY KEY NOT NULL, - "user_id" uuid NOT NULL, - "expires_at" timestamp with time zone NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "users" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "email" text NOT NULL, - "password_hash" text NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE cascade; \ No newline at end of file diff --git a/src/db/migrations/0001_polite_thundra.sql b/src/db/migrations/0000_pink_retro_girl.sql similarity index 58% rename from src/db/migrations/0001_polite_thundra.sql rename to src/db/migrations/0000_pink_retro_girl.sql index 0e8f1cd..3962f9f 100644 --- a/src/db/migrations/0001_polite_thundra.sql +++ b/src/db/migrations/0000_pink_retro_girl.sql @@ -1,14 +1,27 @@ CREATE TYPE "public"."difficulty_enum" AS ENUM('Easy', 'Medium', 'Hard');--> statement-breakpoint CREATE TYPE "public"."rating_enum" AS ENUM('again', 'hard', 'good', 'easy');--> statement-breakpoint -CREATE TYPE "public"."topic_enum" AS ENUM('Arrays & Hashing', 'Two Pointers', 'Sliding Window', 'Stack', 'Binary Search', 'Linked List', 'Trees', 'Graphs', 'Advanced Graphs', '1-D Dynamic Programming', '2-D Dynamic Programming', 'Greedy', 'Intervals', 'Math & Geometry', 'Bit Manipulation');--> statement-breakpoint +CREATE TYPE "public"."topic_enum" AS ENUM('Arrays & Hashing', 'Two Pointers', 'Sliding Window', 'Stack', 'Binary Search', 'Linked List', 'Trees', 'Heap / Priority Queue', 'Backtracking', 'Tries', 'Graphs', 'Advanced Graphs', '1-D Dynamic Programming', '2-D Dynamic Programming', 'Greedy', 'Intervals', 'Math & Geometry', 'Bit Manipulation');--> statement-breakpoint CREATE TABLE "attempts" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, + "user_id" text NOT NULL, "study_id" uuid NOT NULL, "rating" "rating_enum" NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint +CREATE TABLE "collection_problems" ( + "collection_id" uuid NOT NULL, + "problem_id" uuid NOT NULL, + CONSTRAINT "collection_problems_collection_id_problem_id_pk" PRIMARY KEY("collection_id","problem_id") +); +--> statement-breakpoint +CREATE TABLE "collections" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + CONSTRAINT "collections_name_unique" UNIQUE("name") +); +--> statement-breakpoint CREATE TABLE "problems" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "title" text NOT NULL, @@ -24,16 +37,24 @@ CREATE TABLE "problems" ( --> statement-breakpoint CREATE TABLE "studies" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, + "user_id" text NOT NULL, "problem_id" uuid NOT NULL, "interval" real DEFAULT 1 NOT NULL, "ease" real DEFAULT 2.2 NOT NULL, - "due_at" timestamp with time zone DEFAULT now() NOT NULL, + "due_at" timestamp with time zone DEFAULT '1970-01-01T00:00:00.000Z' NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "studies_userId_problemId_unique" UNIQUE("user_id","problem_id") +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint ALTER TABLE "attempts" ADD CONSTRAINT "attempts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "attempts" ADD CONSTRAINT "attempts_study_id_studies_id_fk" FOREIGN KEY ("study_id") REFERENCES "public"."studies"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "collection_problems" ADD CONSTRAINT "collection_problems_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "collection_problems" ADD CONSTRAINT "collection_problems_problem_id_problems_id_fk" FOREIGN KEY ("problem_id") REFERENCES "public"."problems"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "studies" ADD CONSTRAINT "studies_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "studies" ADD CONSTRAINT "studies_problem_id_problems_id_fk" FOREIGN KEY ("problem_id") REFERENCES "public"."problems"("id") ON DELETE cascade ON UPDATE cascade; \ No newline at end of file diff --git a/src/db/migrations/0002_harsh_spectrum.sql b/src/db/migrations/0002_harsh_spectrum.sql deleted file mode 100644 index 048bf95..0000000 --- a/src/db/migrations/0002_harsh_spectrum.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "studies" ALTER COLUMN "due_at" SET DEFAULT '1970-01-01T00:00:00.000Z'; \ No newline at end of file diff --git a/src/db/migrations/0003_good_zeigeist.sql b/src/db/migrations/0003_good_zeigeist.sql deleted file mode 100644 index b0c1279..0000000 --- a/src/db/migrations/0003_good_zeigeist.sql +++ /dev/null @@ -1,19 +0,0 @@ -ALTER TYPE "public"."topic_enum" ADD VALUE 'Heap / Priority Queue' BEFORE 'Graphs';--> statement-breakpoint -ALTER TYPE "public"."topic_enum" ADD VALUE 'Backtracking' BEFORE 'Graphs';--> statement-breakpoint -ALTER TYPE "public"."topic_enum" ADD VALUE 'Tries' BEFORE 'Graphs';--> statement-breakpoint -CREATE TABLE "collection_problems" ( - "collection_id" uuid NOT NULL, - "problem_id" uuid NOT NULL, - CONSTRAINT "collection_problems_collection_id_problem_id_pk" PRIMARY KEY("collection_id","problem_id") -); ---> statement-breakpoint -CREATE TABLE "collections" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "name" text NOT NULL, - "description" text NOT NULL, - CONSTRAINT "collections_name_unique" UNIQUE("name") -); ---> statement-breakpoint -ALTER TABLE "collection_problems" ADD CONSTRAINT "collection_problems_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint -ALTER TABLE "collection_problems" ADD CONSTRAINT "collection_problems_problem_id_problems_id_fk" FOREIGN KEY ("problem_id") REFERENCES "public"."problems"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint -ALTER TABLE "studies" ADD CONSTRAINT "studies_userId_problemId_unique" UNIQUE("user_id","problem_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json index dcbd732..8cc7dd0 100644 --- a/src/db/migrations/meta/0000_snapshot.json +++ b/src/db/migrations/meta/0000_snapshot.json @@ -1,28 +1,36 @@ { - "id": "923aa293-daad-41aa-adb8-61df7a3819e3", + "id": "6b32c5f2-3655-4205-8d6b-9697c3b46629", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "public.sessions": { - "name": "sessions", + "public.attempts": { + "name": "attempts", "schema": "", "columns": { "id": { "name": "id", - "type": "text", + "type": "uuid", "primaryKey": true, - "notNull": true + "notNull": true, + "default": "gen_random_uuid()" }, "user_id": { "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "study_id": { + "name": "study_id", "type": "uuid", "primaryKey": false, "notNull": true }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", + "rating": { + "name": "rating", + "type": "rating_enum", + "typeSchema": "public", "primaryKey": false, "notNull": true }, @@ -36,12 +44,29 @@ }, "indexes": {}, "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", + "attempts_user_id_users_id_fk": { + "name": "attempts_user_id_users_id_fk", + "tableFrom": "attempts", "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "attempts_study_id_studies_id_fk": { + "name": "attempts_study_id_studies_id_fk", + "tableFrom": "attempts", + "tableTo": "studies", + "columnsFrom": [ + "study_id" + ], + "columnsTo": [ + "id" + ], "onDelete": "cascade", "onUpdate": "cascade" } @@ -52,8 +77,68 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.users": { - "name": "users", + "public.collection_problems": { + "name": "collection_problems", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "collection_problems_collection_id_collections_id_fk": { + "name": "collection_problems_collection_id_collections_id_fk", + "tableFrom": "collection_problems", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "collection_problems_problem_id_problems_id_fk": { + "name": "collection_problems_problem_id_problems_id_fk", + "tableFrom": "collection_problems", + "tableTo": "problems", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "collection_problems_collection_id_problem_id_pk": { + "name": "collection_problems_collection_id_problem_id_pk", + "columns": [ + "collection_id", + "problem_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", "schema": "", "columns": { "id": { @@ -63,18 +148,166 @@ "notNull": true, "default": "gen_random_uuid()" }, - "email": { - "name": "email", + "name": { + "name": "name", "type": "text", "primaryKey": false, "notNull": true }, - "password_hash": { - "name": "password_hash", + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "collections_name_unique": { + "name": "collections_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problems": { + "name": "problems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solution": { + "name": "solution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "difficulty": { + "name": "difficulty", + "type": "difficulty_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "topic_enum", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "problems_title_unique": { + "name": "problems_title_unique", + "nullsNotDistinct": false, + "columns": [ + "title" + ] + }, + "problems_url_unique": { + "name": "problems_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + }, + "problems_solution_unique": { + "name": "problems_solution_unique", + "nullsNotDistinct": false, + "columns": [ + "solution" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.studies": { + "name": "studies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", "type": "text", "primaryKey": false, "notNull": true }, + "problem_id": { + "name": "problem_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "ease": { + "name": "ease", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 2.2 + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'1970-01-01T00:00:00.000Z'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -84,21 +317,121 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "studies_user_id_users_id_fk": { + "name": "studies_user_id_users_id_fk", + "tableFrom": "studies", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "studies_problem_id_problems_id_fk": { + "name": "studies_problem_id_problems_id_fk", + "tableFrom": "studies", + "tableTo": "problems", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", + "studies_userId_problemId_unique": { + "name": "studies_userId_problemId_unique", "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "user_id", + "problem_id" + ] } }, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.difficulty_enum": { + "name": "difficulty_enum", + "schema": "public", + "values": [ + "Easy", + "Medium", + "Hard" + ] + }, + "public.rating_enum": { + "name": "rating_enum", + "schema": "public", + "values": [ + "again", + "hard", + "good", + "easy" + ] + }, + "public.topic_enum": { + "name": "topic_enum", + "schema": "public", + "values": [ + "Arrays & Hashing", + "Two Pointers", + "Sliding Window", + "Stack", + "Binary Search", + "Linked List", + "Trees", + "Heap / Priority Queue", + "Backtracking", + "Tries", + "Graphs", + "Advanced Graphs", + "1-D Dynamic Programming", + "2-D Dynamic Programming", + "Greedy", + "Intervals", + "Math & Geometry", + "Bit Manipulation" + ] } }, - "enums": {}, "schemas": {}, "sequences": {}, "roles": {}, @@ -109,4 +442,4 @@ "schemas": {}, "tables": {} } -} +} \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json deleted file mode 100644 index 28f97a6..0000000 --- a/src/db/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,406 +0,0 @@ -{ - "id": "f839cdc3-af1d-45dc-baff-7e3a28ed9416", - "prevId": "923aa293-daad-41aa-adb8-61df7a3819e3", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.attempts": { - "name": "attempts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "study_id": { - "name": "study_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "rating": { - "name": "rating", - "type": "rating_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "attempts_user_id_users_id_fk": { - "name": "attempts_user_id_users_id_fk", - "tableFrom": "attempts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "attempts_study_id_studies_id_fk": { - "name": "attempts_study_id_studies_id_fk", - "tableFrom": "attempts", - "tableTo": "studies", - "columnsFrom": [ - "study_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.problems": { - "name": "problems", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "solution": { - "name": "solution", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "difficulty": { - "name": "difficulty", - "type": "difficulty_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "topic": { - "name": "topic", - "type": "topic_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "problems_title_unique": { - "name": "problems_title_unique", - "nullsNotDistinct": false, - "columns": [ - "title" - ] - }, - "problems_url_unique": { - "name": "problems_url_unique", - "nullsNotDistinct": false, - "columns": [ - "url" - ] - }, - "problems_solution_unique": { - "name": "problems_solution_unique", - "nullsNotDistinct": false, - "columns": [ - "solution" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.studies": { - "name": "studies", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "problem_id": { - "name": "problem_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "interval": { - "name": "interval", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "ease": { - "name": "ease", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 2.2 - }, - "due_at": { - "name": "due_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "studies_user_id_users_id_fk": { - "name": "studies_user_id_users_id_fk", - "tableFrom": "studies", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "studies_problem_id_problems_id_fk": { - "name": "studies_problem_id_problems_id_fk", - "tableFrom": "studies", - "tableTo": "problems", - "columnsFrom": [ - "problem_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.difficulty_enum": { - "name": "difficulty_enum", - "schema": "public", - "values": [ - "Easy", - "Medium", - "Hard" - ] - }, - "public.rating_enum": { - "name": "rating_enum", - "schema": "public", - "values": [ - "again", - "hard", - "good", - "easy" - ] - }, - "public.topic_enum": { - "name": "topic_enum", - "schema": "public", - "values": [ - "Arrays & Hashing", - "Two Pointers", - "Sliding Window", - "Stack", - "Binary Search", - "Linked List", - "Trees", - "Graphs", - "Advanced Graphs", - "1-D Dynamic Programming", - "2-D Dynamic Programming", - "Greedy", - "Intervals", - "Math & Geometry", - "Bit Manipulation" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0002_snapshot.json b/src/db/migrations/meta/0002_snapshot.json deleted file mode 100644 index ec78af8..0000000 --- a/src/db/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,406 +0,0 @@ -{ - "id": "8094a954-87b3-40f0-84e9-7c5567a52e39", - "prevId": "f839cdc3-af1d-45dc-baff-7e3a28ed9416", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.attempts": { - "name": "attempts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "study_id": { - "name": "study_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "rating": { - "name": "rating", - "type": "rating_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "attempts_user_id_users_id_fk": { - "name": "attempts_user_id_users_id_fk", - "tableFrom": "attempts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "attempts_study_id_studies_id_fk": { - "name": "attempts_study_id_studies_id_fk", - "tableFrom": "attempts", - "tableTo": "studies", - "columnsFrom": [ - "study_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.problems": { - "name": "problems", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "solution": { - "name": "solution", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "difficulty": { - "name": "difficulty", - "type": "difficulty_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "topic": { - "name": "topic", - "type": "topic_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "problems_title_unique": { - "name": "problems_title_unique", - "nullsNotDistinct": false, - "columns": [ - "title" - ] - }, - "problems_url_unique": { - "name": "problems_url_unique", - "nullsNotDistinct": false, - "columns": [ - "url" - ] - }, - "problems_solution_unique": { - "name": "problems_solution_unique", - "nullsNotDistinct": false, - "columns": [ - "solution" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.studies": { - "name": "studies", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "problem_id": { - "name": "problem_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "interval": { - "name": "interval", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "ease": { - "name": "ease", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 2.2 - }, - "due_at": { - "name": "due_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "'1970-01-01T00:00:00.000Z'" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "studies_user_id_users_id_fk": { - "name": "studies_user_id_users_id_fk", - "tableFrom": "studies", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "studies_problem_id_problems_id_fk": { - "name": "studies_problem_id_problems_id_fk", - "tableFrom": "studies", - "tableTo": "problems", - "columnsFrom": [ - "problem_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.difficulty_enum": { - "name": "difficulty_enum", - "schema": "public", - "values": [ - "Easy", - "Medium", - "Hard" - ] - }, - "public.rating_enum": { - "name": "rating_enum", - "schema": "public", - "values": [ - "again", - "hard", - "good", - "easy" - ] - }, - "public.topic_enum": { - "name": "topic_enum", - "schema": "public", - "values": [ - "Arrays & Hashing", - "Two Pointers", - "Sliding Window", - "Stack", - "Binary Search", - "Linked List", - "Trees", - "Graphs", - "Advanced Graphs", - "1-D Dynamic Programming", - "2-D Dynamic Programming", - "Greedy", - "Intervals", - "Math & Geometry", - "Bit Manipulation" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0003_snapshot.json b/src/db/migrations/meta/0003_snapshot.json deleted file mode 100644 index 27a5b85..0000000 --- a/src/db/migrations/meta/0003_snapshot.json +++ /dev/null @@ -1,518 +0,0 @@ -{ - "id": "99a7b557-6ac4-4fc4-9117-89a454358102", - "prevId": "8094a954-87b3-40f0-84e9-7c5567a52e39", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.attempts": { - "name": "attempts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "study_id": { - "name": "study_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "rating": { - "name": "rating", - "type": "rating_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "attempts_user_id_users_id_fk": { - "name": "attempts_user_id_users_id_fk", - "tableFrom": "attempts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "attempts_study_id_studies_id_fk": { - "name": "attempts_study_id_studies_id_fk", - "tableFrom": "attempts", - "tableTo": "studies", - "columnsFrom": [ - "study_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.collection_problems": { - "name": "collection_problems", - "schema": "", - "columns": { - "collection_id": { - "name": "collection_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "problem_id": { - "name": "problem_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "collection_problems_collection_id_collections_id_fk": { - "name": "collection_problems_collection_id_collections_id_fk", - "tableFrom": "collection_problems", - "tableTo": "collections", - "columnsFrom": [ - "collection_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "collection_problems_problem_id_problems_id_fk": { - "name": "collection_problems_problem_id_problems_id_fk", - "tableFrom": "collection_problems", - "tableTo": "problems", - "columnsFrom": [ - "problem_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": { - "collection_problems_collection_id_problem_id_pk": { - "name": "collection_problems_collection_id_problem_id_pk", - "columns": [ - "collection_id", - "problem_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.collections": { - "name": "collections", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "collections_name_unique": { - "name": "collections_name_unique", - "nullsNotDistinct": false, - "columns": [ - "name" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.problems": { - "name": "problems", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "url": { - "name": "url", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "solution": { - "name": "solution", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "difficulty": { - "name": "difficulty", - "type": "difficulty_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "topic": { - "name": "topic", - "type": "topic_enum", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "problems_title_unique": { - "name": "problems_title_unique", - "nullsNotDistinct": false, - "columns": [ - "title" - ] - }, - "problems_url_unique": { - "name": "problems_url_unique", - "nullsNotDistinct": false, - "columns": [ - "url" - ] - }, - "problems_solution_unique": { - "name": "problems_solution_unique", - "nullsNotDistinct": false, - "columns": [ - "solution" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.studies": { - "name": "studies", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "problem_id": { - "name": "problem_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "interval": { - "name": "interval", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "ease": { - "name": "ease", - "type": "real", - "primaryKey": false, - "notNull": true, - "default": 2.2 - }, - "due_at": { - "name": "due_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "'1970-01-01T00:00:00.000Z'" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "studies_user_id_users_id_fk": { - "name": "studies_user_id_users_id_fk", - "tableFrom": "studies", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "studies_problem_id_problems_id_fk": { - "name": "studies_problem_id_problems_id_fk", - "tableFrom": "studies", - "tableTo": "problems", - "columnsFrom": [ - "problem_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "studies_userId_problemId_unique": { - "name": "studies_userId_problemId_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id", - "problem_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.difficulty_enum": { - "name": "difficulty_enum", - "schema": "public", - "values": [ - "Easy", - "Medium", - "Hard" - ] - }, - "public.rating_enum": { - "name": "rating_enum", - "schema": "public", - "values": [ - "again", - "hard", - "good", - "easy" - ] - }, - "public.topic_enum": { - "name": "topic_enum", - "schema": "public", - "values": [ - "Arrays & Hashing", - "Two Pointers", - "Sliding Window", - "Stack", - "Binary Search", - "Linked List", - "Trees", - "Heap / Priority Queue", - "Backtracking", - "Tries", - "Graphs", - "Advanced Graphs", - "1-D Dynamic Programming", - "2-D Dynamic Programming", - "Greedy", - "Intervals", - "Math & Geometry", - "Bit Manipulation" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 2827774..3576bfb 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -5,29 +5,8 @@ { "idx": 0, "version": "7", - "when": 1757224956234, - "tag": "0000_flimsy_mad_thinker", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1757380857141, - "tag": "0001_polite_thundra", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1757394287679, - "tag": "0002_harsh_spectrum", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1757444313809, - "tag": "0003_good_zeigeist", + "when": 1757463683257, + "tag": "0000_pink_retro_girl", "breakpoints": true } ] diff --git a/src/db/queries.ts b/src/db/prepared.ts similarity index 53% rename from src/db/queries.ts rename to src/db/prepared.ts index eb99c8f..1399a64 100644 --- a/src/db/queries.ts +++ b/src/db/prepared.ts @@ -1,13 +1,4 @@ -import { - InferInsertModel, - and, - asc, - desc, - eq, - getTableColumns, - isNull, - sql, -} from "drizzle-orm"; +import { asc, desc, eq, getTableColumns, sql } from "drizzle-orm"; import { db } from "."; import { @@ -41,30 +32,3 @@ export const getStudyProblemCollections = db asc(problemsTable.topic), ) .prepare("get_study_problem_collections"); - -export async function seedUser(userId: string) { - await db.transaction(async (tx) => { - const problems = await tx - .select({ ...getTableColumns(problemsTable) }) - .from(problemsTable) - .leftJoin( - studiesTable, - and( - eq(studiesTable.problemId, problemsTable.id), - eq(studiesTable.userId, userId), - ), - ) - .where(isNull(studiesTable.id)); - - if (problems.length <= 0) return; - - const studies: InferInsertModel[] = problems.map( - (problem) => ({ - userId, - problemId: problem.id, - }), - ); - - await tx.insert(studiesTable).values(studies); - }); -} diff --git a/src/db/schema.ts b/src/db/schema.ts index 467a2e0..1e4ac14 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -37,22 +37,7 @@ export const ratingEnum = pgEnum("rating_enum", [ ]); export const usersTable = pgTable("users", (t) => ({ - id: t.uuid().primaryKey().defaultRandom(), - email: t.text().notNull().unique(), - passwordHash: t.text().notNull(), - createdAt: t.timestamp({ withTimezone: true }).notNull().defaultNow(), -})); - -export const sessionsTable = pgTable("sessions", (t) => ({ id: t.text().primaryKey(), - userId: t - .uuid() - .notNull() - .references(() => usersTable.id, { - onDelete: "cascade", - onUpdate: "cascade", - }), - expiresAt: t.timestamp({ withTimezone: true }).notNull(), createdAt: t.timestamp({ withTimezone: true }).notNull().defaultNow(), })); @@ -98,7 +83,7 @@ export const studiesTable = pgTable( (t) => ({ id: t.uuid().primaryKey().defaultRandom(), userId: t - .uuid() + .text() .notNull() .references(() => usersTable.id, { onDelete: "cascade", @@ -130,7 +115,7 @@ export const studiesTable = pgTable( export const attemptsTable = pgTable("attempts", (t) => ({ id: t.uuid().primaryKey().defaultRandom(), userId: t - .uuid() + .text() .notNull() .references(() => usersTable.id, { onDelete: "cascade", diff --git a/src/db/types.ts b/src/db/types.ts index 8a448e4..edc3f4c 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -6,7 +6,6 @@ import { difficultyEnum, problemsTable, ratingEnum, - sessionsTable, studiesTable, topicEnum, usersTable, @@ -17,7 +16,6 @@ export type Topic = InferEnum; export type Rating = InferEnum; export type User = InferSelectModel; -export type Session = InferSelectModel; export type Collection = InferSelectModel; export type Problem = InferSelectModel; export type Study = InferSelectModel; diff --git a/src/env.ts b/src/env.ts index 02d9f55..74b109c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -9,12 +9,30 @@ export const env = createEnv({ .default("development"), DATABASE_URL: z.url(), DATABASE_POOLER_URL: z.url(), + CLERK_SECRET_KEY: z.string(), + CLERK_WEBHOOK_SIGNING_SECRET: z.string(), + }, + client: { + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), + NEXT_PUBLIC_CLERK_SIGN_IN_URL: z.string().default("/login"), + NEXT_PUBLIC_CLERK_SIGN_UP_URL: z.string().default("/register"), + NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL: z.string().default("/study"), + NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL: z.string().default("/study"), }, - client: {}, runtimeEnv: { NODE_ENV: process.env.NODE_ENV, DATABASE_URL: process.env.DATABASE_URL, DATABASE_POOLER_URL: process.env.DATABASE_POOLER_URL, + CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, + CLERK_WEBHOOK_SIGNING_SECRET: process.env.CLERK_WEBHOOK_SIGNING_SECRET, + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, + NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, + NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, + NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL: + process.env.NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL, + NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL: + process.env.NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, emptyStringAsUndefined: true, diff --git a/src/hooks/use-login.tsx b/src/hooks/use-login.tsx deleted file mode 100644 index 1c22599..0000000 --- a/src/hooks/use-login.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from "next/navigation"; -import { useTransition } from "react"; -import { toast } from "sonner"; -import { z } from "zod"; - -import { tc } from "@/lib/utils"; -import { loginFormSchema } from "@/lib/validation"; - -export async function login(values: z.infer) { - const [response] = await tc( - fetch("/api/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(values), - }), - ); - - if (response?.ok) { - return { - success: true as const, - error: null, - }; - } - - if (response?.status === 401) { - return { - success: false as const, - error: "Incorrect email or password.", - }; - } - - return { - success: false as const, - error: "Something went wrong! Try again later.", - }; -} - -export function useLogin() { - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - - const action = async (values: z.infer) => { - startTransition(async () => { - const { success, error } = await login(values); - if (success) { - router.push("/study"); - } else { - toast.error(error); - } - }); - }; - - return { login: action, isPending }; -} diff --git a/src/hooks/use-logout.tsx b/src/hooks/use-logout.tsx deleted file mode 100644 index 3ee0e3f..0000000 --- a/src/hooks/use-logout.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useRouter } from "next/navigation"; -import { useTransition } from "react"; -import { toast } from "sonner"; - -import { tc } from "@/lib/utils"; - -async function logout() { - const [response] = await tc( - fetch("/api/logout", { - method: "DELETE", - }), - ); - - if (response?.ok) { - return { - success: true as const, - error: null, - }; - } - - return { - success: false as const, - error: "Something went wrong! Try again later.", - }; -} - -export function useLogout() { - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - - const action = () => { - startTransition(async () => { - const { success, error } = await logout(); - if (success) { - router.refresh(); - } else { - toast.error(error); - } - }); - }; - - return { logout: action, isPending }; -} diff --git a/src/hooks/use-register.tsx b/src/hooks/use-register.tsx deleted file mode 100644 index 7935549..0000000 --- a/src/hooks/use-register.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from "next/navigation"; -import { useTransition } from "react"; -import { toast } from "sonner"; -import { z } from "zod"; - -import { tc } from "@/lib/utils"; -import { registerFormSchema } from "@/lib/validation"; - -export async function register(values: z.infer) { - const [response] = await tc( - fetch("/api/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(values), - }), - ); - - if (response?.ok) { - return { - success: true as const, - error: null, - }; - } - - if (response?.status === 409) { - return { - success: false as const, - error: "An account already exists with that email.", - }; - } - - return { - success: false as const, - error: "Something went wrong! Try again later.", - }; -} - -export function useRegister() { - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - - const action = async (values: z.infer) => { - startTransition(async () => { - const { success, error } = await register(values); - if (success) { - router.push("/study"); - } else { - toast.error(error); - } - }); - }; - - return { register: action, isPending }; -} diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts deleted file mode 100644 index 9f7f355..0000000 --- a/src/lib/auth/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { eq } from "drizzle-orm"; -import { cookies } from "next/headers"; -import { cache } from "react"; - -import { db } from "@/db"; -import { sessionsTable, usersTable } from "@/db/schema"; -import { Session, User } from "@/db/types"; -import { UTCDate } from "@date-fns/utc"; -import { sha256 } from "@oslojs/crypto/sha2"; -import { encodeHexLowerCase } from "@oslojs/encoding"; - -import { sessionDuration } from "./sessions"; - -type SanitizedUser = Omit & { __brand: "SanitizedUser" }; - -/** - * Retrieves the session and sanitized user from the database using the - * current session token. This function is cached by React. - * @returns The session and user objects, or `null` if nonexistent. - */ -export const auth = cache( - async (): Promise< - { session: Session; user: SanitizedUser } | { session: null; user: null } - > => { - const cookieStore = await cookies(); - const token = cookieStore.get("session")?.value; - - if (!token) return { session: null, user: null }; - - const sessionId = encodeHexLowerCase( - sha256(new TextEncoder().encode(token)), - ); - - return await db.transaction(async (tx) => { - const [data] = await tx - .select({ - user: { - id: usersTable.id, - email: usersTable.email, - createdAt: usersTable.createdAt, - }, - session: sessionsTable, - }) - .from(sessionsTable) - .innerJoin(usersTable, eq(sessionsTable.userId, usersTable.id)) - .where(eq(sessionsTable.id, sessionId)) - .limit(1); - - const user = data?.user as SanitizedUser; - const session = data?.session; - - if (!user || !session) return { session: null, user: null }; - - // Deletes session if expired. - if (UTCDate.now() >= session.expiresAt.getTime()) { - await tx.delete(sessionsTable).where(eq(sessionsTable.id, session.id)); - return { session: null, user: null }; - } - - // Extends the session expiration when it's near expiration (half of life). - if (UTCDate.now() >= session.expiresAt.getTime() - sessionDuration / 2) { - session.expiresAt = new UTCDate(UTCDate.now() + sessionDuration); - await tx - .update(sessionsTable) - .set({ expiresAt: session.expiresAt }) - .where(eq(sessionsTable.id, session.id)); - } - - return { session, user }; - }); - }, -); diff --git a/src/lib/auth/passwords.ts b/src/lib/auth/passwords.ts deleted file mode 100644 index 774da04..0000000 --- a/src/lib/auth/passwords.ts +++ /dev/null @@ -1,27 +0,0 @@ -import "server-only"; - -import { hash, verify } from "@node-rs/argon2"; - -/** - * Hashes a password using Argon2id. - * @param password The password. - * @returns The hashed password. - */ -export async function hashPassword(password: string) { - return await hash(password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1, - }); -} - -/** - * Verifies if a password matches an Argon2id password hash. - * @param hashed The password hash. - * @param password The password. - * @returns Whether or not the passwords match. - */ -export async function verifyPassword(hashed: string, password: string) { - return await verify(hashed, password); -} diff --git a/src/lib/auth/sessions.ts b/src/lib/auth/sessions.ts deleted file mode 100644 index 4440683..0000000 --- a/src/lib/auth/sessions.ts +++ /dev/null @@ -1,160 +0,0 @@ -import "server-only"; - -import { eq, lt } from "drizzle-orm"; -import { cookies } from "next/headers"; - -import { db } from "@/db"; -import { sessionsTable, usersTable } from "@/db/schema"; -import { Session, User } from "@/db/types"; -import { UTCDate } from "@date-fns/utc"; -import { sha256 } from "@oslojs/crypto/sha2"; -import { - encodeBase32LowerCaseNoPadding, - encodeHexLowerCase, -} from "@oslojs/encoding"; - -/** - * The duration of a user's session in milliseconds, by default 7 days. - */ -// NOTE: Should match the cookie age set in middleware. -export const sessionDuration = 1000 * 60 * 60 * 24 * 7; - -/** - * Generates and returns a new cryptographically secure session token. - * @returns A session token. - */ -export function generateSessionToken(): string { - const bytes = new Uint8Array(20); - crypto.getRandomValues(bytes); - const token = encodeBase32LowerCaseNoPadding(bytes); - return token; -} - -/** - * Creates a new session in the database. - * @param token A session token. - * @param userId The user's ID. - * @returns The session object. - */ -export async function createSession( - token: string, - userId: string, -): Promise { - const id = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const expiresAt = new UTCDate(UTCDate.now() + sessionDuration); - - const [session] = await db - .insert(sessionsTable) - .values({ id, userId, expiresAt }) - .returning(); - - return session!; -} - -/** - * Retrieves the session and user from the database using the current session - * token. **This function returns sensitive data, and is not safe to use on - * the frontend.** - * @returns The session and user objects, or `null` if nonexistent. - */ -export async function getCurrentSession(): Promise< - { session: Session; user: User } | { session: null; user: null } -> { - const cookieStore = await cookies(); - const token = cookieStore.get("session")?.value; - - if (!token) return { session: null, user: null }; - - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - - return await db.transaction(async (tx) => { - const [data] = await tx - .select({ - user: usersTable, - session: sessionsTable, - }) - .from(sessionsTable) - .innerJoin(usersTable, eq(sessionsTable.userId, usersTable.id)) - .where(eq(sessionsTable.id, sessionId)) - .limit(1); - - const user = data?.user; - const session = data?.session; - - if (!user || !session) return { session: null, user: null }; - - // Deletes session if expired. - if (UTCDate.now() >= session.expiresAt.getTime()) { - tx.delete(sessionsTable).where(eq(sessionsTable.id, session.id)); - return { session: null, user: null }; - } - - // Extends the session expiration when it's near expiration (half of life). - if (UTCDate.now() >= session.expiresAt.getTime() - sessionDuration / 2) { - session.expiresAt = new UTCDate(UTCDate.now() + sessionDuration); - tx.update(sessionsTable) - .set({ expiresAt: session.expiresAt }) - .where(eq(sessionsTable.id, session.id)); - } - - return { session, user }; - }); -} - -/** - * Deletes a specific session from the database. - * @param sessionId The session's ID. - */ -export async function invalidateSession(sessionId: string): Promise { - await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId)); -} - -/** - * Deletes all sessions from the database that belong to a specific user. - * @param userId The user's ID. - */ -export async function invalidateAllSessions(userId: string): Promise { - await db.delete(sessionsTable).where(eq(sessionsTable.userId, userId)); -} - -/** - * Deletes all expired sessions from the database. - */ -export async function invalidateExpiredSessions() { - await db - .delete(sessionsTable) - .where(lt(sessionsTable.expiresAt, new UTCDate())); -} - -/** - * Sets the session token cookie. - * @param token The session token. - * @param expiresAt The session token expiry date. - */ -export async function setSessionTokenCookie( - token: string, - expiresAt: Date, -): Promise { - const cookieStore = await cookies(); - cookieStore.set("session", token, { - httpOnly: true, - sameSite: "lax", - secure: process.env.NODE_ENV === "production", - expires: expiresAt, - path: "/", - }); -} - -/** - * Deletes the session token cookie. - */ -export async function deleteSessionTokenCookie(): Promise { - const cookieStore = await cookies(); - cookieStore.set("session", "", { - httpOnly: true, - sameSite: "lax", - secure: process.env.NODE_ENV === "production", - maxAge: 0, - path: "/", - }); -} diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 883574a..1a264f4 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -2,23 +2,6 @@ import { z } from "zod"; import { ratingEnum } from "@/db/schema"; -export const loginFormSchema = z - .object({ - email: z.email("Please enter a valid email address."), - password: z.string().min(1, "Please enter a valid password."), - }) - .strict(); - -export const registerFormSchema = z - .object({ - email: z.email("Please enter a valid email address."), - password: z - .string() - .min(6, "Your password must be at least 6 characters.") - .max(255, "Your password must not exceed 255 characters."), - }) - .strict(); - export const attemptSchema = z .object({ studyId: z.string(), diff --git a/src/middleware.ts b/src/middleware.ts index 8466288..7dac996 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,60 +1,15 @@ -import { NextRequest, NextResponse } from "next/server"; - -// NOTE: Make sure this matches the database session duration. -const cookieAge = 60 * 60 * 24 * 7; // 7 days in seconds - -export async function middleware(request: NextRequest): Promise { - // Since we can't extend set cookies inside server components due to a - // limitation with React, we continuously extend the cookie expiration inside - // middleware. However, we can't detect if a new cookie was set inside a - // server action or route handler from middleware. As such, we'll only extend - // the cookie expiration on GET requests. - if (request.method === "GET") { - const response = NextResponse.next(); - const token = request.cookies.get("session")?.value ?? null; - - if (token !== null) { - response.cookies.set("session", token, { - path: "/", - maxAge: cookieAge, - sameSite: "lax", - httpOnly: true, - secure: process.env.NODE_ENV === "production", - }); - } - - return response; - } - - // CSRF protection is a must when using cookies. While Next.js provides - // built-in CSRF protection for server actions, regular route handlers are - // not protected. As such, we implement CSRF protection globally via - // middleware as a precaution. - - const originHeader = request.headers.get("Origin"); - const hostHeader = request.headers.get("Host"); - const forwardedHostHeader = request.headers.get("X-Forwarded-Host"); - - if ( - originHeader === null || - hostHeader === null || - forwardedHostHeader === null - ) { - return new NextResponse(null, { status: 403 }); - } - - let origin: URL; - - try { - origin = new URL(originHeader); - } catch { - return new NextResponse(null, { status: 403 }); - } - - return origin.host !== hostHeader && origin.host !== forwardedHostHeader - ? new NextResponse(null, { status: 403 }) - : NextResponse.next(); -} +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; + +const isPublicRoute = createRouteMatcher([ + "/", + "/login(.*)", + "/register(.*)", + "/api/webhooks(.*)", +]); + +export default clerkMiddleware(async (auth, req) => { + if (!isPublicRoute(req)) await auth.protect(); +}); export const config = { matcher: [