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
107 changes: 100 additions & 7 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -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, sanitizeRedirectPath } 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(sanitizeRedirectPath(router.asPath))}`;

const handleProfileLogout = async () => {
setProfileOpen(false);
await handleLogout();
router.replace("/login");
};

const navigation = [
{ name: "Home", path: "/" },
{ name: "Projects", path: "/project" },
Expand Down Expand Up @@ -69,17 +108,71 @@ const Header = ({ isAdmin = false }: { isAdmin?: boolean }) => {
<Link href="/" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
View Website
</Link>
<Link href="/" className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors">
Logout
</Link>
</>
)
}
</nav>
</div>

{/* Buttons and Mobile Menu */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 sm:gap-4">
{!user ? (
<Button asChild variant="outline" className="border-border bg-background/80 hover:bg-accent">
<Link href={loginHref}>Login</Link>
</Button>
) : (
<Popover open={profileOpen} onOpenChange={setProfileOpen}>
<PopoverTrigger asChild>
<button
type="button"
aria-label="Open profile menu"
className="relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-full border border-border bg-background shadow-sm ring-offset-background transition hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Avatar className="h-10 w-10">
<AvatarImage src={avatarUrl ?? undefined} alt={displayName ?? displayEmail ?? "User profile"} />
<AvatarFallback className="bg-slate-900 text-xs font-semibold text-white">
{fallbackInitials}
</AvatarFallback>
</Avatar>
</button>
</PopoverTrigger>
<PopoverContent align="end" sideOffset={12} className="w-72 rounded-2xl border border-border/60 bg-background p-3 shadow-xl">
<div className="space-y-3">
<div className="flex items-center gap-3 rounded-xl bg-muted/40 px-3 py-3">
<Avatar className="h-11 w-11">
<AvatarImage src={avatarUrl ?? undefined} alt={displayName ?? displayEmail ?? "User profile"} />
<AvatarFallback className="bg-slate-900 text-xs font-semibold text-white">
{fallbackInitials}
</AvatarFallback>
</Avatar>
<div className="min-w-0 space-y-0.5">
{displayName && (
<p className="truncate text-sm font-semibold text-foreground">
{displayName}
</p>
)}
{displayEmail && (
<p className="truncate text-xs text-muted-foreground">
{displayEmail}
</p>
)}
</div>
</div>

<Button
type="button"
variant="outline"
className="w-full justify-start gap-2 border-destructive/20 bg-destructive/5 text-destructive hover:bg-destructive hover:text-destructive-foreground"
onClick={handleProfileLogout}
>
<LogOut className="h-4 w-4" />
Logout
</Button>
</div>
</PopoverContent>
</Popover>
)}

<Button
variant="outline"
onClick={handleGithubClick}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/supabase/actions/auth.actions.ts
Original file line number Diff line number Diff line change
@@ -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) => {

Expand Down Expand Up @@ -33,11 +34,12 @@ export const loginUser = async (email: string, password: string) => {

}

export const loginWithGoogle = async () => {
export const loginWithGoogle = async (redirectPath = "/") => {
const safeRedirectPath = sanitizeRedirectPath(redirectPath);
const { data, error } = await client.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/login/callback`,
redirectTo: `${window.location.origin}/login/callback?redirect=${encodeURIComponent(safeRedirectPath)}`,
Comment thread
ShreyasVij marked this conversation as resolved.
queryParams: {
hd: "pec.edu.in",
},
Expand Down
5 changes: 5 additions & 0 deletions src/lib/useAuthRole.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useState } from "react";
import type { User } from "@supabase/supabase-js";
import { fetchMyRole, isAllowed, type Role } from "./roles";
import { client } from "./supabase/supabase";

export type AuthRoleState = {
loading: boolean;
userId: string | null;
user: User | null;
role: Role | null;
routes: string[];
isAdmin: boolean;
Expand All @@ -19,13 +21,15 @@ export type AuthRoleState = {
export function useAuthRole(): AuthRoleState {
const [loading, setLoading] = useState(true);
const [userId, setUserId] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
const [role, setRole] = useState<Role | null>(null);
const [routes, setRoutes] = useState<string[]>([]);

const load = useCallback(async () => {
setLoading(true);
try {
const { data } = await client.auth.getUser();
setUser(data.user ?? null);
setUserId(data.user?.id ?? null);
if (!data.user) {
setRole(null);
Expand Down Expand Up @@ -53,6 +57,7 @@ export function useAuthRole(): AuthRoleState {
return {
loading,
userId,
user,
role,
routes,
isAdmin: role?.slug === "admin",
Expand Down
15 changes: 14 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,20 @@ export const markdownToHTML = async (markdown: string) => {

export const handleLogout = async () => {
await client.auth.signOut();
localStorage.clear();
};

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') => {
Expand Down
15 changes: 11 additions & 4 deletions src/pages/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import PageHead from '@/components/layout/PageHead';
import NotFound from '@/pages/404';
import { client } from '@/lib/supabase/supabase';
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router';

const AdminPage = () => {
const [validUser, setValidUser] = useState(false);
const router = useRouter();

useEffect(() => {
const checkSession = async () => {
Expand All @@ -17,13 +19,18 @@ const AdminPage = () => {
}
return;
}
client.auth.onAuthStateChange((event, session) => {
const { data: subscription } = client.auth.onAuthStateChange((event, session) => {
if (event === "SIGNED_OUT") {
localStorage.clear();
setValidUser(false);
router.replace("/login");
}
})
});
checkSession();
}, []);

return () => {
subscription.subscription.unsubscribe();
};
}, [router]);
Comment on lines 12 to +33

return (
<>
Expand Down
20 changes: 18 additions & 2 deletions src/pages/login/callback.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { useEffect } from "react";
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

export default function AuthCallback() {
const router = useRouter();

const redirectTarget = useMemo(() => {
if (!router.isReady) {
return "/";
}

const query = router.asPath.split("?")[1] ?? "";
const redirectValue = new URLSearchParams(query).get("redirect");
return sanitizeRedirectPath(redirectValue);
}, [router.asPath, router.isReady]);

useEffect(() => {
if (!router.isReady) {
return;
}

const { data: listener } = client.auth.onAuthStateChange(async (event, session) => {

if (!session) {
Expand Down Expand Up @@ -36,13 +52,13 @@ export default function AuthCallback() {
return;
}

router.replace("/");
router.replace(redirectTarget);
});

return () => {
listener.subscription.unsubscribe();
};
}, [router]);
}, [redirectTarget, router]);

return <p>Signing you in...</p>;
}
Loading
Loading