Updated authentication handling in controllers, added JWT support, and improved error logging. Introduced centralized API calls with customFetch for better token management. Added Grafana's Faro SDK for monitoring and tracing. Refactored project files for improved structure and maintainability.
361 lines
16 KiB
TypeScript
361 lines
16 KiB
TypeScript
// src/components/layouts/Layout.tsx
|
|
|
|
import { useTitle } from "@/context/TitleContext";
|
|
import { routeRoleRequirements } from '@/components/auth/ProtectedPageWrapper';
|
|
import { useAuth } from '@/components/auth/AuthContext';
|
|
|
|
import React, { ReactNode, useEffect } from 'react';
|
|
import { useTheme, useMediaQuery } from '@mui/material';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { styled, useColorScheme } from '@mui/material/styles';
|
|
|
|
import Box from '@mui/material/Box';
|
|
import Drawer from '@mui/material/Drawer';
|
|
import AppBar from '@mui/material/AppBar';
|
|
import Toolbar from '@mui/material/Toolbar';
|
|
import List from '@mui/material/List';
|
|
import Typography from '@mui/material/Typography';
|
|
import Divider from '@mui/material/Divider';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import MenuIcon from '@mui/icons-material/Menu';
|
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
import ListItem from '@mui/material/ListItem';
|
|
import ListItemButton from '@mui/material/ListItemButton';
|
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
|
import ListItemText from '@mui/material/ListItemText';
|
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
import HttpIcon from '@mui/icons-material/Http';
|
|
import AccountBoxIcon from '@mui/icons-material/AccountBox';
|
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
|
|
import DnsIcon from '@mui/icons-material/Dns';
|
|
import TargetIcon from '@mui/icons-material/TrackChanges';
|
|
import MarkEmailReadIcon from '@mui/icons-material/MarkEmailRead';
|
|
import BlockIcon from '@mui/icons-material/Block';
|
|
//import LinkOffIcon from '@mui/icons-material/LinkOff';
|
|
import EmailIcon from '@mui/icons-material/Email';
|
|
import SendIcon from '@mui/icons-material/Send';
|
|
import ScheduleSendIcon from '@mui/icons-material/ScheduleSend';
|
|
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
|
|
import { Link as RouterLink } from 'react-router-dom';
|
|
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
|
import Menu from '@mui/material/Menu';
|
|
import MenuItem from '@mui/material/MenuItem';
|
|
import FormControl from '@mui/material/FormControl';
|
|
import InputLabel from '@mui/material/InputLabel';
|
|
import { useCustomFetch } from "@/utils/customFetch";
|
|
|
|
|
|
// Constants
|
|
const drawerWidth = 240;
|
|
|
|
// Styled components
|
|
const DrawerHeader = styled('div')(({ theme }) => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: theme.spacing(0, 1),
|
|
...theme.mixins.toolbar,
|
|
justifyContent: 'flex-end',
|
|
}));
|
|
|
|
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{
|
|
open?: boolean;
|
|
}>(({ theme, open }) => ({
|
|
flexGrow: 1,
|
|
padding: theme.spacing(3),
|
|
transition: theme.transitions.create(['margin', 'width', 'padding'], {
|
|
easing: theme.transitions.easing.sharp,
|
|
duration: theme.transitions.duration.enteringScreen,
|
|
}),
|
|
marginLeft: "0px !important", // Force remove any margin on the left
|
|
marginRight: "0px !important", // Force remove any margin on the left
|
|
...(open && {/*Opened specific types go here*/}),
|
|
...(!open && {/*closed specific styles go here*/})
|
|
}));
|
|
|
|
interface LayoutProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
const Layout = ({ children }: LayoutProps) => {
|
|
const customFetch = useCustomFetch();
|
|
const theme = useTheme();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
|
|
const [open, setOpen] = React.useState(!isMobile);
|
|
const { mode, setMode } = useColorScheme(); // MUI v6 hook for theme switching
|
|
const iconButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
const { title } = useTitle();
|
|
const navigate = useNavigate();
|
|
|
|
const menuItems = [
|
|
{ text: 'Home', icon: <DashboardIcon />, path: '/home' },
|
|
{ text: 'Servers', icon: <DnsIcon />, path: '/servers' },
|
|
{ text: 'Targets', icon: <TargetIcon />, path: '/targets' },
|
|
{ text: 'Test Lists', icon: <MarkEmailReadIcon />, path: '/testEmailLists' },
|
|
{ text: 'Blocked Emails', icon: <BlockIcon />, path: '/blockedEmails' },
|
|
{ text: 'Email Domains', icon: <HttpIcon />, path: '/emailDomains' },
|
|
{ text: 'Templates', icon: <EmailIcon />, path: '/templates' },
|
|
{ text: 'New Mailings', icon: <SendIcon />, path: '/newMailings' },
|
|
{ text: 'Scheduled Mailings', icon: <ScheduleSendIcon />, path: '/scheduledMailings' },
|
|
{ text: 'Active Mailings', icon: <AutorenewIcon />, path: '/activeMailings' },
|
|
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
|
|
{ text: 'Cancelled Mailings', icon: <CancelIcon />, path: '/cancelledMailings' },
|
|
];
|
|
const { userRoles, setAuth } = useAuth(); // Use context
|
|
const [profileMenuAnchorEl, setProfileMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
const profileMenuOpen = Boolean(profileMenuAnchorEl);
|
|
const handleOpenProfileMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
setProfileMenuAnchorEl(event.currentTarget);
|
|
};
|
|
const handleCloseProfileMenu = () => {
|
|
setProfileMenuAnchorEl(null);
|
|
};
|
|
|
|
const visibleMenuItems = menuItems.filter(item => {
|
|
const requiredRoles = routeRoleRequirements[item.path] || [];
|
|
return requiredRoles.length == 0 || requiredRoles.some(role => userRoles.includes(role));
|
|
});
|
|
const handleRefreshUser = async () => {
|
|
handleCloseProfileMenu();
|
|
try {
|
|
const response = await customFetch('/api/authentication/refreshtoken', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setAuth(data.accessToken); // Update context
|
|
} else {
|
|
setAuth(null); // Clear context on failure
|
|
navigate('/login');
|
|
}
|
|
} catch {
|
|
setAuth(null); // Clear context on failure
|
|
navigate('/login');
|
|
}
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
setAuth(null); // Clear context
|
|
await customFetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
|
|
navigate('/login');
|
|
};
|
|
|
|
const handleDrawerOpen = () => {
|
|
setOpen(true);
|
|
};
|
|
|
|
const handleDrawerClose = () => {
|
|
setOpen(false);
|
|
};
|
|
useEffect(() => {
|
|
if (isMobile) {
|
|
setOpen(false);
|
|
}
|
|
}, [isMobile]);
|
|
|
|
const handleThemeChange = (event: SelectChangeEvent) => {
|
|
setMode(event.target.value as 'light' | 'dark');
|
|
if (iconButtonRef.current) {
|
|
const selectElement = iconButtonRef.current;
|
|
if (selectElement) {
|
|
if (selectElement instanceof HTMLElement) {
|
|
setTimeout(() => {
|
|
selectElement.focus(); // Blur the focusable input
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex' }}>
|
|
{/* App Bar */}
|
|
<AppBar
|
|
position="fixed"
|
|
sx={(theme) => ({
|
|
zIndex: theme.zIndex.drawer + 1,
|
|
transition: theme.transitions.create(['width', 'margin', 'padding'], {
|
|
easing: theme.transitions.easing.easeInOut,
|
|
duration: theme.transitions.duration.leavingScreen,
|
|
}),
|
|
...(open && {
|
|
width: isMobile ? "100%" : `calc(100% - ${drawerWidth}px)`,
|
|
ml: `${drawerWidth}px`,
|
|
transition: theme.transitions.create(['width', 'margin', 'padding'], {
|
|
easing: theme.transitions.easing.easeInOut,
|
|
duration: theme.transitions.duration.enteringScreen,
|
|
}),
|
|
}),
|
|
})}
|
|
>
|
|
<Toolbar>
|
|
{isMobile && open ?
|
|
<IconButton
|
|
color="inherit"
|
|
aria-label="close drawer"
|
|
onClick={handleDrawerClose}
|
|
edge="start"
|
|
ref={iconButtonRef}
|
|
sx={{ mr: 2 }}
|
|
>
|
|
<ChevronLeftIcon />
|
|
</IconButton> :
|
|
<IconButton
|
|
color="inherit"
|
|
aria-label="open drawer"
|
|
onClick={handleDrawerOpen}
|
|
edge="start"
|
|
ref={iconButtonRef}
|
|
sx={{ mr: 2, ...(open && { display: 'none' }) }}
|
|
>
|
|
<MenuIcon />
|
|
</IconButton>
|
|
}
|
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
|
{title}
|
|
</Typography>
|
|
<FormControl sx={{ minWidth: 120 }} size="small">
|
|
<InputLabel
|
|
id="theme-select-label"
|
|
sx={{
|
|
color: 'white', // White in both modes
|
|
'&.Mui-focused': { color: 'white' }, // Keep white when focused
|
|
}}
|
|
>
|
|
Theme
|
|
</InputLabel>
|
|
<Select
|
|
labelId="theme-select-label"
|
|
id="theme-select"
|
|
value={mode || 'light'}
|
|
label="Theme"
|
|
onChange={handleThemeChange}
|
|
sx={{
|
|
color: 'white', // White text
|
|
'& .MuiSvgIcon-root': { color: 'white' }, // White dropdown arrow
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'white',
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'white',
|
|
borderWidth: 2
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'white',
|
|
borderWidth: 2
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="light">Light</MenuItem>
|
|
<MenuItem value="dark">Dark</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small">
|
|
<IconButton
|
|
color="inherit"
|
|
id="profile-menu-button"
|
|
aria-label="Profile Menu"
|
|
aria-controls={open ? 'profile-menu' : undefined}
|
|
aria-haspopup="true"
|
|
aria-expanded={open ? 'true' : undefined}
|
|
onClick={handleOpenProfileMenu}
|
|
sx={{ mr: 2 }}
|
|
>
|
|
<AccountBoxIcon />
|
|
</IconButton>
|
|
<Menu
|
|
id="profile-menu"
|
|
anchorEl={profileMenuAnchorEl}
|
|
open={profileMenuOpen}
|
|
onClose={handleCloseProfileMenu}
|
|
>
|
|
<MenuItem onClick={handleRefreshUser}>Refresh User</MenuItem>
|
|
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
|
</Menu>
|
|
</FormControl>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
{/* Sidebar */}
|
|
<Drawer
|
|
variant={isMobile ? "temporary" : "persistent"}
|
|
anchor="left"
|
|
open={open}
|
|
onClose={handleDrawerClose}
|
|
ModalProps={{
|
|
keepMounted: true, // Keep mounted to avoid re-render issues
|
|
hideBackdrop: isMobile && !open, // Explicitly hide backdrop when closed in mobile
|
|
}}
|
|
sx={{
|
|
width: isMobile ? "100%" : open ? `var(--mui-drawer-width, ${drawerWidth}px)` : 0,
|
|
flexShrink: 0,
|
|
'& .MuiDrawer-paper': {
|
|
width: isMobile ? "100%" : open ? `var(--mui-drawer-width, ${drawerWidth}px)` : 0,
|
|
boxSizing: 'border-box',
|
|
},
|
|
}}
|
|
>
|
|
|
|
<DrawerHeader>
|
|
<IconButton onClick={handleDrawerClose}>
|
|
<ChevronLeftIcon />
|
|
</IconButton>
|
|
</DrawerHeader>
|
|
<Divider />
|
|
<List>
|
|
{/*{[*/}
|
|
{/* { text: 'Home', icon: <DashboardIcon />, path: '/home' },*/}
|
|
{/* { text: 'Servers', icon: <DnsIcon />, path: '/servers' },*/}
|
|
{/* { text: 'Targets', icon: <TargetIcon />, path: '/targets' },*/}
|
|
{/* { text: 'Test Lists', icon: <MarkEmailReadIcon />, path: '/testEmailLists' },*/}
|
|
{/* { text: 'Blocked Emails', icon: <BlockIcon />, path: '/blockedEmails' },*/}
|
|
{/* { text: 'Email Domains', icon: <HttpIcon />, path: '/emailDomains' },*/}
|
|
{/* //{ text: 'Unsubscribe Urls', icon: <LinkOffIcon />, path: '/unsubscribeUrls' },*/}
|
|
{/* { text: 'Templates', icon: <EmailIcon />, path: '/templates' },*/}
|
|
{/* { text: 'New Mailings', icon: <SendIcon />, path: '/newMailings' }, //TODO: Maybe move all mailings to same page? Mailing stats on dashboard?*/}
|
|
{/* { text: 'Scheduled Mailings', icon: <ScheduleSendIcon />, path: '/scheduledMailings' }, //*/}
|
|
{/* { text: 'Active Mailings', icon: <AutorenewIcon />, path: '/activeMailings' },*/}
|
|
{/* { text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },*/}
|
|
{/*].map((item) => (}*/}
|
|
{visibleMenuItems.map((item) => (
|
|
<ListItem key={item.text} disablePadding>
|
|
<ListItemButton
|
|
component={RouterLink}
|
|
to={item.path}
|
|
selected={location.pathname === item.path}
|
|
onClick={() => isMobile && handleDrawerClose()}
|
|
sx={{
|
|
'&.Mui-selected': {
|
|
backgroundColor: 'primary.main',
|
|
color: 'primary.contrastText',
|
|
'& .MuiListItemIcon-root': {
|
|
color: 'primary.contrastText',
|
|
},
|
|
},
|
|
'&.Mui-selected:hover': {
|
|
backgroundColor: 'primary.dark',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
|
<ListItemText primary={item.text} />
|
|
</ListItemButton>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Drawer>
|
|
|
|
{/* Main Content */}
|
|
<Main open={open}>
|
|
<DrawerHeader />
|
|
{children}
|
|
</Main>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Layout; |