diff --git a/package-lock.json b/package-lock.json index 4b97cce..6dd4612 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", + "react-icons": "^5.5.0", "react-youtube": "^10.1.0" }, "devDependencies": { @@ -6347,9 +6348,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7549,6 +7550,15 @@ "react": "^19.1.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8354,11 +8364,11 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/package.json b/package.json index c80c556..7876773 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", + "react-icons": "^5.5.0", "react-youtube": "^10.1.0" }, "devDependencies": { diff --git a/src/app/auth/auth-code-error/page.tsx b/src/app/auth/auth-code-error/page.tsx new file mode 100644 index 0000000..5a1c40e --- /dev/null +++ b/src/app/auth/auth-code-error/page.tsx @@ -0,0 +1,24 @@ +import Link from "next/link"; + +export default function AuthCodeError() { + return ( +
+
+

⚠️ Authentication Failed

+

+ Something went wrong while signing you in. This could happen if the login was cancelled or + took too long. +

+ + Try Again + +
+
+ ); +} diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts new file mode 100644 index 0000000..861316d --- /dev/null +++ b/src/app/auth/callback/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; +// The client you created from the Server-Side Auth instructions +import { createClient } from "@/lib/supabase/server"; +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 + let next = searchParams.get("next") ?? "/"; + if (!next.startsWith("/")) { + // if "next" is not a relative URL, use the default + next = "/"; + } + + if (code) { + const supabase = await createClient(); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer + const isLocalEnv = process.env.NODE_ENV === "development"; + if (isLocalEnv) { + // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host + return NextResponse.redirect(`${origin}${next}`); + } else if (forwardedHost) { + return NextResponse.redirect(`https://${forwardedHost}${next}`); + } else { + 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/login/page.tsx b/src/app/login/page.tsx index 5e886a3..21f34eb 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { createClient } from "../../lib/supabase/client"; +import handleOAuthLogin from "@/lib/supabase/handle-oauth-login"; +import { FaGoogle, FaGithub } from "react-icons/fa"; export default function AuthPage() { const router = useRouter(); @@ -43,7 +45,7 @@ export default function AuthPage() { className="min-h-dvh flex items-center justify-center bg-cover bg-center bg-no-repeat p-6 font-cinzel text-white" style={{ backgroundImage: "url('/geminiblurred.png')" }} > -
+

{mode === "login" ? "Welcome Back" : "Create Your Account"}

@@ -82,7 +84,7 @@ export default function AuthPage() { +
+ + {/* Github */} +
+ +
+ +
diff --git a/src/lib/supabase/handle-oauth-login.ts b/src/lib/supabase/handle-oauth-login.ts new file mode 100644 index 0000000..8dc9feb --- /dev/null +++ b/src/lib/supabase/handle-oauth-login.ts @@ -0,0 +1,19 @@ +"use server"; + +import { redirect } from "next/navigation"; +import { createClient } from "./server"; +import { headers } from "next/headers"; +export default async function handleOAuthLogin(provider: "github" | "google") { + const supabase = await createClient(); + const headersList = await headers(); + const origin = headersList.get("origin") ?? `https://${headersList.get("host")}`;; + const { error, data } = await supabase.auth.signInWithOAuth({ + provider: provider, + options: { redirectTo: `${origin}/auth/callback?next=/dashboard` }, + }); + + if (error) throw error; + else { + return redirect(data.url); + } +} diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts index 93f6021..fbd8b8a 100644 --- a/src/lib/supabase/middleware.ts +++ b/src/lib/supabase/middleware.ts @@ -1,10 +1,10 @@ -import { createServerClient } from '@supabase/ssr' -import { NextResponse, type NextRequest } from 'next/server' +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!, @@ -12,35 +12,36 @@ export async function updateSession(request: NextRequest) { { cookies: { getAll() { - return request.cookies.getAll() + return request.cookies.getAll(); }, setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) + 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) - ) + ); }, }, } - ) + ); const { data: { user }, - } = await supabase.auth.getUser() + } = await supabase.auth.getUser(); if ( !user && - !request.nextUrl.pathname.startsWith('/login') && - !request.nextUrl.pathname.startsWith('/error') + !request.nextUrl.pathname.startsWith("/login") && + !request.nextUrl.pathname.startsWith("/error") && + !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) + const url = request.nextUrl.clone(); + url.pathname = "/login"; + return NextResponse.redirect(url); } - return supabaseResponse -} \ No newline at end of file + return supabaseResponse; +}