From 0fe899a39529bbcd1b16816b4fee67a3c4b05269 Mon Sep 17 00:00:00 2001 From: robin Date: Sat, 6 Jul 2024 22:55:42 +0900 Subject: [PATCH] chore/migrate_supabaseAuth --- .env | 3 + package-lock.json | 150 ++++++++++++++++++++++++++ package.json | 2 + src/app/api/auth/callback/route.tsx | 38 +++++++ src/app/api/auth/confirm/route.ts | 28 +++++ src/app/api/auth/providers.tsx | 11 -- src/app/api/auth/token/route.tsx | 7 -- src/app/login/actions.ts | 46 ++++++++ src/app/middleware.tsx | 18 +++- src/app/utils/suparbase/client.ts | 8 ++ src/app/utils/suparbase/middleware.ts | 65 +++++++++++ src/app/utils/suparbase/server.ts | 29 +++++ 12 files changed, 383 insertions(+), 22 deletions(-) create mode 100644 src/app/api/auth/callback/route.tsx create mode 100644 src/app/api/auth/confirm/route.ts delete mode 100644 src/app/api/auth/providers.tsx delete mode 100644 src/app/api/auth/token/route.tsx create mode 100644 src/app/login/actions.ts create mode 100644 src/app/utils/suparbase/client.ts create mode 100644 src/app/utils/suparbase/middleware.ts create mode 100644 src/app/utils/suparbase/server.ts diff --git a/.env b/.env index d26e5e1..d4714ad 100644 --- a/.env +++ b/.env @@ -10,6 +10,9 @@ DATABASE_URL="postgresql://postgres.ofytfuhgugaqsahbrrxs:janbul2024.@aws-0-ap-northeast-2.pooler.supabase.com:6543/postgres" DIRECT_URL="postgresql://postgres.ofytfuhgugaqsahbrrxs:janbul2024.@aws-0-ap-northeast-2.pooler.supabase.com:5432/postgres" +NEXT_PUBLIC_SUPABASE_URL="https://ofytfuhgugaqsahbrrxs.supabase.co" +NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9meXRmdWhndWdhcXNhaGJycnhzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTk5OTI2MjYsImV4cCI6MjAzNTU2ODYyNn0.uCgT44A55ijDC0PrhkNs-kFGwKBb3bfbSWu69kAayTc" + POSTGRES_URL="postgres://default:rp8vylxaqKO1@ep-crimson-haze-a1r4iuxg-pooler.ap-southeast-1.aws.neon.tech:5432/verceldb?sslmode=require" POSTGRES_PRISMA_URL="postgres://default:rp8vylxaqKO1@ep-crimson-haze-a1r4iuxg-pooler.ap-southeast-1.aws.neon.tech:5432/verceldb?sslmode=require&pgbouncer=true&connect_timeout=15" diff --git a/package-lock.json b/package-lock.json index f1341d9..bd0ea6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "hasInstallScript": true, "dependencies": { "@prisma/client": "^5.14.0", + "@supabase/ssr": "^0.4.0", + "@supabase/supabase-js": "^2.44.2", "@vercel/postgres": "^0.8.0", "next": "14.2.3", "next-auth": "^4.24.7", @@ -1573,6 +1575,19 @@ "@prisma/debug": "5.14.0" } }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.3", "dev": true, @@ -1605,6 +1620,104 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@supabase/auth-js": { + "version": "2.64.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz", + "integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz", + "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "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": "1.15.7", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.7.tgz", + "integrity": "sha512-TJztay5lcnnKuXjIO/X/aaajOsP8qNeW0k3MqIFoOtRolj5MEAIy8rixNakRk3o23eVCdsuP3iMLYPvOOruH6Q==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz", + "integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.0.tgz", + "integrity": "sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==", + "license": "MIT", + "dependencies": { + "cookie": "^0.6.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.9.5" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.43.4" + } + }, + "node_modules/@supabase/ssr/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.6.0.tgz", + "integrity": "sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.44.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.44.2.tgz", + "integrity": "sha512-fouCwL1OxqftOwLNgdDUPlNnFuCnt30nS4kLcnTpe6NYKn1PmjxRRBFmKscgHs6FjWyU+32ZG4uBJ29+/BWiDw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.64.2", + "@supabase/functions-js": "2.4.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.15.7", + "@supabase/realtime-js": "2.10.2", + "@supabase/storage-js": "2.6.0" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "license": "Apache-2.0" @@ -1720,6 +1833,12 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", + "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "dev": true, @@ -1749,6 +1868,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -7416,6 +7544,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/ts-api-utils": { "version": "1.3.0", "dev": true, @@ -7667,6 +7801,22 @@ "makeerror": "1.0.12" } }, + "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", "dev": true, diff --git a/package.json b/package.json index 7cf24ba..65a2ec4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@prisma/client": "^5.14.0", + "@supabase/ssr": "^0.4.0", + "@supabase/supabase-js": "^2.44.2", "@vercel/postgres": "^0.8.0", "next": "14.2.3", "next-auth": "^4.24.7", diff --git a/src/app/api/auth/callback/route.tsx b/src/app/api/auth/callback/route.tsx new file mode 100644 index 0000000..eae9b24 --- /dev/null +++ b/src/app/api/auth/callback/route.tsx @@ -0,0 +1,38 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { type CookieOptions, createServerClient } from "@supabase/ssr"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + // if "next" is in param, use it as the redirect URL + const next = searchParams.get("next") ?? "/"; + + if (code) { + const cookieStore = cookies(); + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + get(name: string) { + return cookieStore.get(name)?.value; + }, + set(name: string, value: string, options: CookieOptions) { + cookieStore.set({ name, value, ...options }); + }, + remove(name: string, options: CookieOptions) { + cookieStore.delete({ name, ...options }); + }, + }, + } + ); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + return NextResponse.redirect(`${origin}${next}`); + } + } + + // return the user to an error page with instructions + return NextResponse.redirect(`${origin}/auth/auth-code-error`); +} diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts new file mode 100644 index 0000000..6434371 --- /dev/null +++ b/src/app/api/auth/confirm/route.ts @@ -0,0 +1,28 @@ +import { type EmailOtpType } from "@supabase/supabase-js"; +import { type NextRequest, NextResponse } from "next/server"; + +import { createClient } from "@/app/utils/suparbase/server"; +import { redirect } from "next/navigation"; + +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 = searchParams.get("next") ?? "/"; + + if (token_hash && type) { + const supabase = createClient(); + + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }); + if (!error) { + // redirect user to specified redirect URL or root of app + redirect(next); + } + } + + // redirect the user to an error page with some instructions + redirect("/error"); +} diff --git a/src/app/api/auth/providers.tsx b/src/app/api/auth/providers.tsx deleted file mode 100644 index 77f25e4..0000000 --- a/src/app/api/auth/providers.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { SessionProvider } from "next-auth/react"; - -type Props = { - children: React.ReactNode; -}; - -export default function AuthContext({ children }: Props) { - return {children}; -} diff --git a/src/app/api/auth/token/route.tsx b/src/app/api/auth/token/route.tsx deleted file mode 100644 index 09b51a4..0000000 --- a/src/app/api/auth/token/route.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { getToken } from "next-auth/jwt"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const token = await getToken({ req: request }); - return NextResponse.json(token); -} diff --git a/src/app/login/actions.ts b/src/app/login/actions.ts new file mode 100644 index 0000000..fe42938 --- /dev/null +++ b/src/app/login/actions.ts @@ -0,0 +1,46 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { createClient } from "@/app/utils/suparbase/server"; + +export async function login(formData: FormData) { + const supabase = createClient(); + + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get("email") as string, + password: formData.get("password") as string, + }; + + const { error } = await supabase.auth.signInWithPassword(data); + + if (error) { + redirect("/error"); + } + + revalidatePath("/", "layout"); + redirect("/"); +} + +export async function signup(formData: FormData) { + const supabase = createClient(); + + // type-casting here for convenience + // in practice, you should validate your inputs + const data = { + email: formData.get("email") as string, + password: formData.get("password") as string, + }; + + const { error } = await supabase.auth.signUp(data); + + if (error) { + redirect("/error"); + } + + revalidatePath("/", "layout"); + redirect("/"); +} diff --git a/src/app/middleware.tsx b/src/app/middleware.tsx index da614f8..6d53e12 100644 --- a/src/app/middleware.tsx +++ b/src/app/middleware.tsx @@ -1,9 +1,19 @@ -import { NextRequest, NextResponse } from "next/server"; +import { type NextRequest } from "next/server"; +import { updateSession } from "@/app/utils/suparbase/middleware"; -export function middleware(request: NextRequest) { - return NextResponse.redirect(new URL("/login", request.url)); +export async function middleware(request: NextRequest) { + return await updateSession(request); } export const config = { - matcher: ["/mypage/:id*"], + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], }; diff --git a/src/app/utils/suparbase/client.ts b/src/app/utils/suparbase/client.ts new file mode 100644 index 0000000..9f2891b --- /dev/null +++ b/src/app/utils/suparbase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from "@supabase/ssr"; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); +} diff --git a/src/app/utils/suparbase/middleware.ts b/src/app/utils/suparbase/middleware.ts new file mode 100644 index 0000000..deba36d --- /dev/null +++ b/src/app/utils/suparbase/middleware.ts @@ -0,0 +1,65 @@ +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => + request.cookies.set(name, value) + ); + supabaseResponse = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ); + }, + }, + } + ); + + // IMPORTANT: Avoid writing any logic between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if ( + !user && + !request.nextUrl.pathname.startsWith("/login") && + !request.nextUrl.pathname.startsWith("/auth") + ) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone(); + url.pathname = "/login"; + return NextResponse.redirect(url); + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're + // creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse; +} diff --git a/src/app/utils/suparbase/server.ts b/src/app/utils/suparbase/server.ts new file mode 100644 index 0000000..bf0f8c9 --- /dev/null +++ b/src/app/utils/suparbase/server.ts @@ -0,0 +1,29 @@ +import { createServerClient, type CookieOptions } from "@supabase/ssr"; +import { cookies } from "next/headers"; + +export function createClient() { + const cookieStore = cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ); + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + } + ); +}