From 210a987a450dea7a7cf3bcc72740c42c8f664a1d Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Wed, 8 Oct 2025 16:04:55 -1000 Subject: [PATCH] Implement Supabase Auth with auth pages --- .vscode/extensions.json | 3 + .vscode/settings.json | 24 + package-lock.json | 157 ++- package.json | 6 +- src/app/account/page.tsx | 22 + src/app/auth/confirm/route.ts | 35 + src/app/auth/signout/route.ts | 21 + src/app/layout.tsx | 4 +- src/app/login/actions.ts | 42 + src/app/login/page.tsx | 18 + src/app/signup/actions.ts | 59 + src/app/signup/page.tsx | 18 + src/components/Account/AccountForm.tsx | 174 +++ src/components/Account/Avatar.tsx | 152 +++ src/components/Auth/LoginForm.tsx | 97 ++ src/components/Auth/SignupForm.tsx | 136 +++ src/middleware.ts | 20 + src/utils/auth.ts | 16 + src/utils/supabase/client.ts | 9 + src/utils/supabase/middleware.ts | 34 + src/utils/supabase/server.ts | 31 + supabase/.gitignore | 8 + supabase/config.toml | 347 ++++++ .../20251008081035_remote_schema.sql | 1036 +++++++++++++++++ 24 files changed, 2463 insertions(+), 6 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 src/app/account/page.tsx create mode 100644 src/app/auth/confirm/route.ts create mode 100644 src/app/auth/signout/route.ts create mode 100644 src/app/login/actions.ts create mode 100644 src/app/login/page.tsx create mode 100644 src/app/signup/actions.ts create mode 100644 src/app/signup/page.tsx create mode 100644 src/components/Account/AccountForm.tsx create mode 100644 src/components/Account/Avatar.tsx create mode 100644 src/components/Auth/LoginForm.tsx create mode 100644 src/components/Auth/SignupForm.tsx create mode 100644 src/middleware.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/supabase/client.ts create mode 100644 src/utils/supabase/middleware.ts create mode 100644 src/utils/supabase/server.ts create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/migrations/20251008081035_remote_schema.sql diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..74baffc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["denoland.vscode-deno"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..af62c23 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "deno.enablePaths": [ + "supabase/functions" + ], + "deno.lint": true, + "deno.unstable": [ + "bare-node-builtins", + "byonm", + "sloppy-imports", + "unsafe-proto", + "webgpu", + "broadcast-channel", + "worker-options", + "cron", + "kv", + "ffi", + "fs", + "http", + "net" + ], + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } +} diff --git a/package-lock.json b/package-lock.json index f4ac9b7..9160875 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", + "@supabase/ssr": "^0.7.0", + "@supabase/supabase-js": "^2.74.0", "embla-carousel-react": "8.6.0", "next": "15.5.4", "react": "19.1.0", @@ -733,6 +735,92 @@ "node": ">= 10" } }, + "node_modules/@supabase/auth-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.74.0.tgz", + "integrity": "sha512-EJYDxYhBCOS40VJvfQ5zSjo8Ku7JbTICLTcmXt4xHMQZt4IumpRfHg11exXI9uZ6G7fhsQlNgbzDhFN4Ni9NnA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.74.0.tgz", + "integrity": "sha512-VqWYa981t7xtIFVf7LRb9meklHckbH/tqwaML5P3LgvlaZHpoSPjMCNLcquuLYiJLxnh2rio7IxLh+VlvRvSWw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.74.0.tgz", + "integrity": "sha512-9Ypa2eS0Ib/YQClE+BhDSjx7OKjYEF6LAGjTB8X4HucdboGEwR0LZKctNfw6V0PPIAVjjzZxIlNBXGv0ypIkHw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.74.0.tgz", + "integrity": "sha512-K5VqpA4/7RO1u1nyD5ICFKzWKu58bIDcPxHY0aFA7MyWkFd0pzi/XYXeoSsAifnD9p72gPIpgxVXCQZKJg1ktQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz", + "integrity": "sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.74.0.tgz", + "integrity": "sha512-o0cTQdMqHh4ERDLtjUp1/KGPbQoNwKRxUh6f8+KQyjC5DSmiw/r+jgFe/WHh067aW+WU8nA9Ytw9ag7OhzxEkQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", + "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.74.0", + "@supabase/functions-js": "2.74.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.74.0", + "@supabase/realtime-js": "2.74.0", + "@supabase/storage-js": "2.74.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1083,12 +1171,17 @@ "version": "20.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", @@ -1114,6 +1207,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -1370,6 +1472,15 @@ "node": ">=16" } }, + "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", @@ -3807,6 +3918,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -3851,7 +3968,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -3986,6 +4102,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4002,6 +4134,27 @@ "node": ">= 8" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 830861e..7b99710 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", + "@supabase/ssr": "^0.7.0", + "@supabase/supabase-js": "^2.74.0", "embla-carousel-react": "8.6.0", "next": "15.5.4", "react": "19.1.0", @@ -27,8 +29,8 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "patch-package": "^8.0.0", "tailwindcss": "^4", - "typescript": "^5", - "patch-package": "^8.0.0" + "typescript": "^5" } } diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..d8a3b56 --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,22 @@ +import AccountForm from "@/components/Account/AccountForm"; +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export default async function AccountPage() { + const supabase = await createClient() + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + redirect('/login') + } + + return ( +
+

Your account

+

Manage profile details and avatar.

+
+ +
+
+ ); +} diff --git a/src/app/auth/confirm/route.ts b/src/app/auth/confirm/route.ts new file mode 100644 index 0000000..bb218e4 --- /dev/null +++ b/src/app/auth/confirm/route.ts @@ -0,0 +1,35 @@ +import { type EmailOtpType } from '@supabase/supabase-js' +import { type NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/utils/supabase/server' + +// Creating a handler to a GET request to route /auth/confirm +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const token_hash = searchParams.get('token_hash') + const type = searchParams.get('type') as EmailOtpType | null + const next = '/account' + + // Create redirect link without the secret token + const redirectTo = request.nextUrl.clone() + redirectTo.pathname = next + redirectTo.searchParams.delete('token_hash') + redirectTo.searchParams.delete('type') + + if (token_hash && type) { + const supabase = await createClient() + + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }) + if (!error) { + redirectTo.searchParams.delete('next') + return NextResponse.redirect(redirectTo) + } + } + + // return the user to login with an error message + redirectTo.pathname = '/login' + redirectTo.searchParams.set('error', 'EMAIL_CONFIRMATION_ERROR') + return NextResponse.redirect(redirectTo) +} diff --git a/src/app/auth/signout/route.ts b/src/app/auth/signout/route.ts new file mode 100644 index 0000000..99ee3bf --- /dev/null +++ b/src/app/auth/signout/route.ts @@ -0,0 +1,21 @@ +import { createClient } from "@/utils/supabase/server"; +import { revalidatePath } from "next/cache"; +import { type NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const supabase = await createClient(); + + // Check if a user's logged in + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (user) { + await supabase.auth.signOut(); + } + + revalidatePath("/", "layout"); + return NextResponse.redirect(new URL("/login", req.url), { + status: 302, + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cd41353..bf3045b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -28,14 +28,14 @@ export default function RootLayout({ return (
-
{children}
+
{children}