From 0411a501801b11facb09acde38b5c1dc8d83e4ee Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Thu, 13 Aug 2026 16:18:48 +0530 Subject: [PATCH 1/2] login feature changes --- src/components/layout/Header.tsx | 107 +++++++++- src/lib/supabase/actions/auth.actions.ts | 5 +- src/lib/useAuthRole.ts | 5 + src/lib/utils.ts | 1 - src/pages/admin/page.tsx | 7 +- src/pages/login/callback.tsx | 19 +- src/pages/login/index.tsx | 254 +++++++++++++++++------ 7 files changed, 315 insertions(+), 83 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 06537ff..af4b139 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,22 +1,61 @@ "use client"; -import { useEffect, useState } from "react"; +import { useMemo, useState } from "react"; import Image from "next/image"; import { Button } from "@/components/ui/button"; -import { GitFork, Menu } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { GitFork, LogOut, Menu } from "lucide-react"; import Link from "next/link"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { useRouter } from "next/router"; import { useAuthRole } from "@/lib/useAuthRole"; +import { handleLogout } from "@/lib/utils"; + +const getInitials = (value: string) => { + const tokens = value.trim().split(/\s+/).filter(Boolean); + + if (tokens.length === 0) { + return "U"; + } + + if (tokens.length === 1) { + return tokens[0].slice(0, 2).toUpperCase(); + } + + return `${tokens[0][0]}${tokens[tokens.length - 1][0]}`.toUpperCase(); +}; const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => { - const { role } = useAuthRole(); + const { role, user } = useAuthRole(); + const router = useRouter(); + const [profileOpen, setProfileOpen] = useState(false); const isPanelist = role?.slug === "admin" || role?.slug?.includes("panel"); + const displayName = user?.user_metadata?.full_name ?? user?.user_metadata?.name ?? null; + const displayEmail = user?.email ?? null; + const avatarUrl = + user?.user_metadata?.avatar_url ?? + user?.user_metadata?.picture ?? + user?.user_metadata?.avatarUrl ?? + null; + const fallbackInitials = useMemo(() => { + const baseLabel = displayName ?? displayEmail ?? "User"; + return getInitials(baseLabel); + }, [displayEmail, displayName]); const handleGithubClick = () => { window.open("https://github.com/Robotics-PEC", "_blank"); }; + const loginHref = `/login?redirect=${encodeURIComponent(router.asPath)}`; + + const handleProfileLogout = async () => { + setProfileOpen(false); + await handleLogout(); + router.replace("/login"); + }; + const navigation = [ { name: "Home", path: "/" }, { name: "Projects", path: "/project" }, @@ -69,9 +108,6 @@ const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => { View Website - - Logout - ) } @@ -79,7 +115,64 @@ const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => { {/* Buttons and Mobile Menu */} -
+
+ {!user ? ( + + ) : ( + + + + + +
+
+ + + + {fallbackInitials} + + +
+ {displayName && ( +

+ {displayName} +

+ )} + {displayEmail && ( +

+ {displayEmail} +

+ )} +
+
+ + +
+
+
+ )} + + +

+ By continuing, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +

+
+
+ +
+
+
+
+ + {currentImage ? ( + + Robotics society gallery image +
- {/* Login Card */} -
- - -

- By continuing, you agree to our{" "} - - Terms of Service - {" "} - and{" "} - - Privacy Policy - - . -

+ + ) : ( + +
+
+
+
+
+
+
+ + )} + +
+
+
From cc47b413c5acf54c75030c3f47950a24325bac1f Mon Sep 17 00:00:00 2001 From: ShreyasVij Date: Thu, 13 Aug 2026 16:27:54 +0530 Subject: [PATCH 2/2] Tightened the redirect handling so protocol-relative values like //evil.com are rejected, and login-related routes now fall back to / instead of looping back into the auth flow. The navbar Login button now sanitizes the current path before sending it into /login, and both the login page and callback use the same guard. I also fixed the admin auth listener so it unsubscribes on cleanup, preventing duplicate redirects after remounts or HMR. The shared sanitization lives in src/lib/utils.ts, and the route handling updates are in src/components/layout/Header.tsx, src/pages/login/index.tsx, src/pages/login/callback.tsx, and src/pages/admin/page.tsx. --- src/components/layout/Header.tsx | 4 ++-- src/lib/supabase/actions/auth.actions.ts | 3 ++- src/lib/utils.ts | 14 ++++++++++++++ src/pages/admin/page.tsx | 8 ++++++-- src/pages/login/callback.tsx | 5 +++-- src/pages/login/index.tsx | 5 +++-- 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index af4b139..139291c 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -11,7 +11,7 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { useRouter } from "next/router"; import { useAuthRole } from "@/lib/useAuthRole"; -import { handleLogout } from "@/lib/utils"; +import { handleLogout, sanitizeRedirectPath } from "@/lib/utils"; const getInitials = (value: string) => { const tokens = value.trim().split(/\s+/).filter(Boolean); @@ -48,7 +48,7 @@ const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => { window.open("https://github.com/Robotics-PEC", "_blank"); }; - const loginHref = `/login?redirect=${encodeURIComponent(router.asPath)}`; + const loginHref = `/login?redirect=${encodeURIComponent(sanitizeRedirectPath(router.asPath))}`; const handleProfileLogout = async () => { setProfileOpen(false); diff --git a/src/lib/supabase/actions/auth.actions.ts b/src/lib/supabase/actions/auth.actions.ts index c31a82d..7c0e236 100644 --- a/src/lib/supabase/actions/auth.actions.ts +++ b/src/lib/supabase/actions/auth.actions.ts @@ -1,6 +1,7 @@ import { toast } from "@/components/ui/use-toast"; import { client } from "../supabase"; import crypto from "crypto"; +import { sanitizeRedirectPath } from "@/lib/utils"; export const loginUser = async (email: string, password: string) => { @@ -34,7 +35,7 @@ export const loginUser = async (email: string, password: string) => { } export const loginWithGoogle = async (redirectPath = "/") => { - const safeRedirectPath = redirectPath.startsWith("/") ? redirectPath : "/"; + const safeRedirectPath = sanitizeRedirectPath(redirectPath); const { data, error } = await client.auth.signInWithOAuth({ provider: "google", options: { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0b105a6..62b2f05 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -45,6 +45,20 @@ export const handleLogout = async () => { await client.auth.signOut(); }; +export const sanitizeRedirectPath = (value?: string | null) => { + if (!value || !value.startsWith("/") || value.startsWith("//")) { + return "/"; + } + + const pathname = value.split("?")[0].split("#")[0]; + + if (pathname === "/login" || pathname === "/login/callback") { + return "/"; + } + + return value; +}; + export const base64ToBlob = (base64Data: string, contentType = 'image/png') => { const byteCharacters = atob(base64Data) const byteArrays = [] diff --git a/src/pages/admin/page.tsx b/src/pages/admin/page.tsx index f57ff5e..e62dae4 100644 --- a/src/pages/admin/page.tsx +++ b/src/pages/admin/page.tsx @@ -19,13 +19,17 @@ const AdminPage = () => { } return; } - client.auth.onAuthStateChange((event, session) => { + const { data: subscription } = client.auth.onAuthStateChange((event, session) => { if (event === "SIGNED_OUT") { setValidUser(false); router.replace("/login"); } - }) + }); checkSession(); + + return () => { + subscription.subscription.unsubscribe(); + }; }, [router]); return ( diff --git a/src/pages/login/callback.tsx b/src/pages/login/callback.tsx index ede9525..a75dae3 100644 --- a/src/pages/login/callback.tsx +++ b/src/pages/login/callback.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { useRouter } from "next/router"; import { client } from "@/lib/supabase/supabase"; import { toast } from "@/components/ui/use-toast"; +import { sanitizeRedirectPath } from "@/lib/utils"; const PEC_DOMAIN = "@pec.edu.in"; // ← replace with your actual college domain @@ -15,8 +16,8 @@ export default function AuthCallback() { } const query = router.asPath.split("?")[1] ?? ""; - const redirectValue = new URLSearchParams(query).get("redirect") ?? "/"; - return redirectValue.startsWith("/") ? redirectValue : "/"; + const redirectValue = new URLSearchParams(query).get("redirect"); + return sanitizeRedirectPath(redirectValue); }, [router.asPath, router.isReady]); useEffect(() => { diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 9bb9f79..b0f294f 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -8,6 +8,7 @@ import { loginWithGoogle } from "@/lib/supabase/actions/auth.actions"; import { getImagesFromFolder } from "@/lib/supabase/actions/storage.actions"; import { client } from "@/lib/supabase/supabase"; import { useRouter } from "next/router"; +import { sanitizeRedirectPath } from "@/lib/utils"; const Login: NextPage = () => { const [heroImages, setHeroImages] = useState([]); @@ -21,8 +22,8 @@ const Login: NextPage = () => { } const query = router.asPath.split("?")[1] ?? ""; - const redirectValue = new URLSearchParams(query).get("redirect") ?? "/"; - return redirectValue.startsWith("/") ? redirectValue : "/"; + const redirectValue = new URLSearchParams(query).get("redirect"); + return sanitizeRedirectPath(redirectValue); }, [router.asPath, router.isReady]); useEffect(() => {