Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
24 changes: 24 additions & 0 deletions src/app/auth/auth-code-error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Link from "next/link";

export default function AuthCodeError() {
return (
<main
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')" }}
>
<div className="w-full max-w-md bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl shadow-[0_0_30px_rgba(255,255,255,0.25)] p-8 text-center">
<h1 className="text-3xl font-bold mb-4">⚠️ Authentication Failed</h1>
<p className="text-white/70 mb-6">
Something went wrong while signing you in. This could happen if the login was cancelled or
took too long.
</p>
<Link
href="/login"
className="inline-block px-6 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-md font-semibold transition shadow-[0_0_20px_rgba(140,120,255,0.7)]"
>
Try Again
</Link>
</div>
</main>
);
}
33 changes: 33 additions & 0 deletions src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
40 changes: 37 additions & 3 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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')" }}
>
<div className="w-full max-w-md bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl shadow-[0_0_30px_rgba(255,255,255,0.25)] p-8 transition-all">
<div className="w-full max-w-md bg-white/10 backdrop-blur-lg border border-white/20 rounded-2xl shadow-[0_0_30px_rgba(255,255,255,0.25)] p-8 transition-all flex flex-col">
<h1 className="text-4xl text-center mb-6 font-bold drop-shadow-lg">
{mode === "login" ? "Welcome Back" : "Create Your Account"}
</h1>
Expand Down Expand Up @@ -82,7 +84,7 @@ export default function AuthPage() {
<button
type="submit"
disabled={busy}
className={`w-full py-2 rounded-md font-semibold mt-4 transition ${
className={`w-full py-2 rounded-md font-semibold mt-4 transition hover:cursor-pointer ${
busy
? "bg-indigo-700/60 cursor-not-allowed"
: "bg-indigo-600 hover:bg-indigo-500 shadow-[0_0_20px_rgba(140,120,255,0.7)]"
Expand All @@ -92,11 +94,43 @@ export default function AuthPage() {
</button>
</form>

<section className="flex flex-col gap-4 text-center">
<div className="mt-5">
<p className="text-center">Or continue with</p>
</div>

{/* Google */}
<div className="">
<button
className="w-full p-2 rounded-md bg-transparent border border-white/40 hover:border-indigo-400 hover:shadow-[0_0_12px_rgba(140,120,255,0.8)] outline-none transition hover:cursor-pointer"
onClick={() => handleOAuthLogin("google")}
>
<div className="flex flex-row items-center justify-center gap-2 font-semibold">
<FaGoogle></FaGoogle>
<span>Google</span>
</div>
</button>
</div>

{/* Github */}
<div>
<button
className="w-full p-2 rounded-md bg-transparent border border-white/40 hover:border-indigo-400 hover:shadow-[0_0_12px_rgba(140,120,255,0.8)] outline-none transition hover:cursor-pointer"
onClick={() => handleOAuthLogin("github")}
>
<div className="flex flex-row items-center justify-center gap-2 font-semibold">
<FaGithub></FaGithub>
<span>Github</span>
</div>
</button>
</div>
</section>

<div className="text-center mt-6">
<button
type="button"
onClick={() => setMode(mode === "login" ? "signup" : "login")}
className="text-sm text-indigo-300 hover:text-indigo-200 transition mt-2 underline"
className="text-sm text-indigo-300 hover:text-indigo-200 transition mt-2 underline hover: cursor-pointer"
>
{mode === "login" ? "Join the Realm" : "Return to Login"}
</button>
Expand Down
19 changes: 19 additions & 0 deletions src/lib/supabase/handle-oauth-login.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
33 changes: 17 additions & 16 deletions src/lib/supabase/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
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!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
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
}
return supabaseResponse;
}