369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import type React from "react";
|
|
import { useState } from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Share2,
|
|
Palette,
|
|
GitFork,
|
|
Building2,
|
|
Calendar,
|
|
Target,
|
|
Mail,
|
|
HardDrive,
|
|
GraduationCap,
|
|
MessageSquare,
|
|
FileText,
|
|
Calculator,
|
|
Kanban,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
BookOpen,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { useSession } from "next-auth/react";
|
|
import { CalendarNav } from "@/components/sidebar/calendar-nav";
|
|
|
|
interface SidebarProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface MenuItem {
|
|
title: string;
|
|
icon: any;
|
|
href: string;
|
|
iframe?: string;
|
|
external?: boolean;
|
|
requiredRole?: string | string[];
|
|
}
|
|
|
|
export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|
const { data: session, status } = useSession();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
|
|
// Debug session data
|
|
console.log('Session state:', {
|
|
status,
|
|
hasSession: !!session,
|
|
user: session?.user,
|
|
roles: session?.user?.role,
|
|
rawRoles: session?.user?.role,
|
|
pathname
|
|
});
|
|
|
|
// Show loading state while session is being checked
|
|
if (status === 'loading') {
|
|
return null;
|
|
}
|
|
|
|
// Show nothing if not authenticated (middleware will handle redirect)
|
|
if (status === 'unauthenticated') {
|
|
return null;
|
|
}
|
|
|
|
// Function to check if user has a specific role
|
|
const hasRole = (requiredRole: string | string[] | undefined) => {
|
|
// If no role is required, allow access
|
|
if (!requiredRole) {
|
|
return true;
|
|
}
|
|
|
|
// If no session or user roles, deny access
|
|
if (!session?.user?.role) {
|
|
console.log('No user roles found in session');
|
|
return false;
|
|
}
|
|
|
|
// Get user roles and normalize them properly
|
|
const userRoles = Array.isArray(session.user.role) ? session.user.role : [session.user.role];
|
|
|
|
// Filter out technical/system roles that shouldn't count for permissions
|
|
const ignoredRoles = ['offline_access', 'uma_authorization', 'default-roles-cercle'];
|
|
|
|
const cleanUserRoles = userRoles
|
|
.filter(Boolean) // Remove any null/undefined values
|
|
.filter(role => !ignoredRoles.includes(String(role))) // Filter out system roles
|
|
.map(role => {
|
|
if (typeof role !== 'string') return '';
|
|
return role
|
|
.replace(/^\//, '') // Remove leading slash
|
|
.replace(/^ROLE_/i, '') // Remove ROLE_ prefix, case insensitive
|
|
.replace(/^default-roles-[^/]*\//i, '') // Remove realm prefix like default-roles-cercle/
|
|
.toLowerCase();
|
|
})
|
|
.filter(role => role !== ''); // Remove empty strings
|
|
|
|
// For debugging only
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`Role check for: ${JSON.stringify(requiredRole)}`, {
|
|
userRoles,
|
|
ignoredRoles,
|
|
cleanUserRoles,
|
|
});
|
|
}
|
|
|
|
// Check against array of required roles
|
|
if (Array.isArray(requiredRole)) {
|
|
const cleanRequiredRoles = requiredRole
|
|
.filter(Boolean)
|
|
.map(role => typeof role === 'string' ? role.toLowerCase() : '')
|
|
.filter(role => role !== '');
|
|
|
|
const hasRequiredRole = cleanRequiredRoles.some(role => cleanUserRoles.includes(role));
|
|
console.log(`Array role check: Required ${JSON.stringify(cleanRequiredRoles)}, Has any: ${hasRequiredRole}`);
|
|
return hasRequiredRole;
|
|
}
|
|
|
|
// Check against single required role
|
|
if (typeof requiredRole === 'string') {
|
|
const cleanRequiredRole = requiredRole.toLowerCase();
|
|
const hasRequiredRole = cleanUserRoles.includes(cleanRequiredRole);
|
|
console.log(`Single role check: Required "${cleanRequiredRole}", Has: ${hasRequiredRole}`);
|
|
return hasRequiredRole;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Base menu items (available for everyone)
|
|
const baseMenuItems: MenuItem[] = [
|
|
{
|
|
title: "Pages",
|
|
icon: BookOpen,
|
|
href: "/pages",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_CARNET_URL,
|
|
},
|
|
{
|
|
title: "Courrier",
|
|
icon: Mail,
|
|
href: "/courrier",
|
|
},
|
|
{
|
|
title: "Dossiers",
|
|
icon: HardDrive,
|
|
href: "/dossiers",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_DRIVE_URL,
|
|
},
|
|
{
|
|
title: "Apprendre",
|
|
icon: GraduationCap,
|
|
href: "/apprendre",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_LEARN_URL,
|
|
},
|
|
{
|
|
title: "Parole",
|
|
icon: MessageSquare,
|
|
href: "/parole",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL,
|
|
},
|
|
{
|
|
title: "Missions",
|
|
icon: Kanban,
|
|
href: "/missions",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_MISSIONSBOARD_URL,
|
|
},
|
|
{
|
|
title: "Chapitre",
|
|
icon: FileText,
|
|
href: "/chapitre",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_CHAPTER_URL,
|
|
},
|
|
{
|
|
title: "Agilité",
|
|
icon: Share2,
|
|
href: "/agilite",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_AGILITY_URL,
|
|
},
|
|
];
|
|
|
|
// Role-specific menu items
|
|
const roleSpecificItems: MenuItem[] = [
|
|
{
|
|
title: "Artlab",
|
|
icon: Palette,
|
|
href: "/design",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_ARTLAB_URL,
|
|
requiredRole: "expression",
|
|
},
|
|
{
|
|
title: "Gite",
|
|
icon: GitFork,
|
|
href: "/gite",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_GITE_URL,
|
|
requiredRole: ["coding", "dataintelligence"],
|
|
},
|
|
{
|
|
title: "Calcul",
|
|
icon: Calculator,
|
|
href: "/calcul",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_CALCULATION_URL,
|
|
requiredRole: "dataintelligence",
|
|
},
|
|
{
|
|
title: "Médiation",
|
|
icon: Building2,
|
|
href: "/mediation",
|
|
iframe: process.env.NEXT_PUBLIC_IFRAME_MEDIATIONS_URL,
|
|
requiredRole: "mediation",
|
|
},
|
|
];
|
|
|
|
// Combine base items with role-specific items based on user roles
|
|
const visibleMenuItems = [
|
|
...baseMenuItems,
|
|
...roleSpecificItems.filter(item => {
|
|
const isVisible = hasRole(item.requiredRole);
|
|
console.log(`Item ${item.title} with requiredRole ${JSON.stringify(item.requiredRole)} is ${isVisible ? 'visible' : 'hidden'}`);
|
|
return isVisible;
|
|
})
|
|
];
|
|
|
|
const handleNavigation = (href: string, external?: boolean) => {
|
|
if (external && href) {
|
|
window.open(href, "_blank");
|
|
} else {
|
|
router.push(href);
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
{isOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<div
|
|
className={cn(
|
|
"fixed top-0 left-0 z-50 h-full w-64 transform bg-panel transition-all duration-200 ease-in-out",
|
|
isOpen ? "translate-x-0" : "-translate-x-full"
|
|
)}
|
|
>
|
|
<ScrollArea className="h-full w-full relative">
|
|
{/* Hide Button */}
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute -right-3 top-1/2 transform -translate-y-1/2 w-6 h-12 bg-black text-white rounded-r-md flex items-center justify-center hover:bg-gray-800 transition-colors z-[60]"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</button>
|
|
|
|
{/* Logo */}
|
|
<div className="flex justify-center p-6 border-b">
|
|
<Image
|
|
src="/Neahv3 logo.png"
|
|
alt="Neah Logo"
|
|
width={50}
|
|
height={16.5}
|
|
className="text-black"
|
|
/>
|
|
</div>
|
|
|
|
{/* Menu Items */}
|
|
<div className="space-y-1 p-4">
|
|
{visibleMenuItems.map((item) => (
|
|
<Button
|
|
key={item.title}
|
|
variant="ghost"
|
|
className={cn(
|
|
"w-full justify-start gap-2 text-black hover:bg-gray-100",
|
|
pathname === item.href && !item.external && "bg-gray-100"
|
|
)}
|
|
onClick={() => handleNavigation(item.href, item.external)}
|
|
>
|
|
<item.icon className="h-5 w-5" />
|
|
<span>{item.title}</span>
|
|
</Button>
|
|
))}
|
|
|
|
{/* Debug display only in development */}
|
|
{process.env.NODE_ENV === 'development' && (
|
|
<div className="p-2 mt-4 border border-gray-200 rounded bg-blue-50 text-xs">
|
|
<p className="font-bold">Debug Info:</p>
|
|
<div className="mt-1">
|
|
<p>User: {session?.user?.name}</p>
|
|
<p>Email: {session?.user?.email}</p>
|
|
<details>
|
|
<summary className="cursor-pointer">User Roles</summary>
|
|
<pre className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
|
{JSON.stringify(session?.user?.role, null, 2)}
|
|
</pre>
|
|
</details>
|
|
<details>
|
|
<summary className="cursor-pointer">Normalized Roles</summary>
|
|
<pre className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
|
{JSON.stringify(
|
|
Array.isArray(session?.user?.role)
|
|
? session.user.role
|
|
.filter(role => typeof role === 'string')
|
|
.filter(role => !['offline_access', 'uma_authorization', 'default-roles-cercle'].includes(role))
|
|
.map(role =>
|
|
role
|
|
.replace(/^\//, '')
|
|
.replace(/^ROLE_/i, '')
|
|
.replace(/^default-roles-[^/]*\//i, '')
|
|
.toLowerCase()
|
|
)
|
|
: []
|
|
, null, 2)}
|
|
</pre>
|
|
</details>
|
|
<details>
|
|
<summary className="cursor-pointer">Role-Based Items</summary>
|
|
<div className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
|
{roleSpecificItems.map(item => {
|
|
const isVisible = hasRole(item.requiredRole);
|
|
return (
|
|
<div key={item.title} className="mb-1 pb-1 border-b">
|
|
<p><strong>Item:</strong> {item.title}</p>
|
|
<p><strong>Required Role:</strong> {JSON.stringify(item.requiredRole)}</p>
|
|
<p><strong>Visible:</strong> {isVisible ? '✅' : '❌'}</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary className="cursor-pointer">Visible Menu Items</summary>
|
|
<div className="mt-1 p-1 bg-white text-[10px] overflow-x-auto">
|
|
<h3 className="font-bold">Base Items:</h3>
|
|
<ul className="list-disc pl-3">
|
|
{baseMenuItems.map(item => (
|
|
<li key={item.title}>{item.title}</li>
|
|
))}
|
|
</ul>
|
|
<h3 className="font-bold mt-2">Role-Specific Items (After Filtering):</h3>
|
|
<ul className="list-disc pl-3">
|
|
{roleSpecificItems
|
|
.filter(item => hasRole(item.requiredRole))
|
|
.map(item => (
|
|
<li key={item.title}>{item.title}</li>
|
|
))
|
|
}
|
|
</ul>
|
|
<h3 className="font-bold mt-2">Total Visible Items: {visibleMenuItems.length}</h3>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|