- Add user_group migration and created_by column migration - Replace requireRole middleware with requireGroup - Update all backend routes to use group-based authorization - Add Standard_User conditional delete with ownership, state, and compliance checks - Add cascade impact check for CVE deletes - Update AuthContext with group-based permission helpers - Update all frontend components for group-based rendering - Update UserManagement UI with group dropdown, confirmation dialogs, self-demotion prevention
120 lines
4.5 KiB
JavaScript
120 lines
4.5 KiB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { User, LogOut, ChevronDown, Shield, Clock } from 'lucide-react';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
export default function UserMenu({ onManageUsers, onAuditLog }) {
|
|
const { user, logout, isAdmin } = useAuth();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const menuRef = useRef(null);
|
|
|
|
// Close menu when clicking outside
|
|
useEffect(() => {
|
|
function handleClickOutside(event) {
|
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const getGroupBadgeColor = (group) => {
|
|
switch (group) {
|
|
case 'Admin':
|
|
return 'bg-red-100 text-red-800';
|
|
case 'Standard_User':
|
|
return 'bg-blue-100 text-blue-800';
|
|
case 'Leadership':
|
|
return 'bg-purple-100 text-purple-800';
|
|
case 'Read_Only':
|
|
return 'bg-gray-100 text-gray-800';
|
|
default:
|
|
return 'bg-gray-100 text-gray-800';
|
|
}
|
|
};
|
|
|
|
const formatGroupName = (group) => {
|
|
if (!group) return '';
|
|
return group.replace(/_/g, ' ');
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
setIsOpen(false);
|
|
await logout();
|
|
};
|
|
|
|
const handleManageUsers = () => {
|
|
setIsOpen(false);
|
|
if (onManageUsers) {
|
|
onManageUsers();
|
|
}
|
|
};
|
|
|
|
const handleAuditLog = () => {
|
|
setIsOpen(false);
|
|
if (onAuditLog) {
|
|
onAuditLog();
|
|
}
|
|
};
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="relative" ref={menuRef}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="w-8 h-8 bg-[#0476D9] rounded-full flex items-center justify-center">
|
|
<User className="w-4 h-4 text-white" />
|
|
</div>
|
|
<div className="text-left hidden sm:block">
|
|
<p className="text-sm font-medium text-gray-900">{user.username}</p>
|
|
<p className="text-xs text-gray-500">{formatGroupName(user.group)}</p>
|
|
</div>
|
|
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
|
|
<div className="px-4 py-3 border-b border-gray-100">
|
|
<p className="text-sm font-medium text-gray-900">{user.username}</p>
|
|
<p className="text-sm text-gray-500">{user.email}</p>
|
|
<span className={`inline-block mt-2 px-2 py-1 rounded text-xs font-medium ${getGroupBadgeColor(user.group)}`}>
|
|
{formatGroupName(user.group)}
|
|
</span>
|
|
</div>
|
|
|
|
{isAdmin() && (
|
|
<>
|
|
<button
|
|
onClick={handleManageUsers}
|
|
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3"
|
|
>
|
|
<Shield className="w-4 h-4" />
|
|
Manage Users
|
|
</button>
|
|
<button
|
|
onClick={handleAuditLog}
|
|
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3"
|
|
>
|
|
<Clock className="w-4 h-4" />
|
|
Audit Log
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleLogout}
|
|
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-3"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|