This commit removes the `AuthenticationController.cs` and related DTOs, indicating a shift in authentication handling. The `BaseController.cs` has been updated to remove the authorization attribute, affecting access control. Multiple controllers have been restructured to reference `Surge365.Core.Controllers`. Significant changes in `Program.cs` enhance security and service management with new middleware and JWT configurations. The project file now includes references to `Surge365.Core`, and the `IAuthService` interface has been updated accordingly. React components have been modified to support the new authentication flow, including token refresh handling. The `customFetch.ts` utility has been improved for better session management. Mapping classes have been introduced or updated for improved entity mapping. Overall, these changes enhance the application's architecture, security, and data handling processes.
356 lines
16 KiB
TypeScript
356 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 getSystemTheme = (): 'light' | 'dark' => {
|
|
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
? 'dark'
|
|
: 'light';
|
|
};
|
|
|
|
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, refreshAuthToken } = 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();
|
|
const success = await refreshAuthToken();
|
|
if (!success) {
|
|
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 effectiveMode = (mode === 'system' ? getSystemTheme() : mode) || "light";
|
|
|
|
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={effectiveMode || '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; |