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 (
-
-
- );
-}
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 (
-
-
- );
-}
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 (
);
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: [