diff --git a/client/src/Components/v1/Alert/index.css b/client/src/Components/v1/Alert/index.css deleted file mode 100644 index 233b5c4666..0000000000 --- a/client/src/Components/v1/Alert/index.css +++ /dev/null @@ -1,9 +0,0 @@ -.alert { - margin: 0; - width: fit-content; -} -.alert, -.alert button, -.alert .MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx deleted file mode 100644 index dd3b2f9f2b..0000000000 --- a/client/src/Components/v1/Alert/index.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; -import Icon from "../Icon"; -import "./index.css"; - -/** - * Icons mapping for different alert variants. - * @type {Object} - */ - -const icons = { - info: ( - - ), - error: ( - - ), - warning: ( - - ), -}; - -/** - * @param {Object} props - * @param {'info' | 'error' | 'warning'} props.variant - The type of alert. - * @param {string} [props.title] - The title of the alert. - * @param {string} [props.body] - The body text of the alert. - * @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification. - * @param {boolean} [props.hasIcon] - Whether to display an icon in the alert. - * @param {function} props.onClick - Toast dismiss function. - * @returns {JSX.Element} - */ - -const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { - const theme = useTheme(); - /* TODO - Do we need other variants for alert? - */ - - const text = theme.palette.secondary.contrastText; - const border = theme.palette.alert.contrastText; - const bg = theme.palette.alert.main; - const icon = icons[variant]; - - return ( - - {hasIcon && {icon}} - - {title && ( - {title} - )} - {body && ( - {body} - )} - {hasIcon && isToast && ( - - )} - - {isToast && ( - - - - )} - - ); -}; - -Alert.propTypes = { - variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, - title: PropTypes.string, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - isToast: PropTypes.bool, - hasIcon: PropTypes.bool, - onClick: function (props, propName, componentName) { - if (props.isToast && !props[propName]) { - return new Error( - `Prop '${propName}' is required when 'isToast' is true in '${componentName}'.` - ); - } - return null; - }, -}; - -export default Alert; diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx deleted file mode 100644 index 6343b2e4ee..0000000000 --- a/client/src/Components/v1/HOC/withAdminCheck.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { logger } from "../../../Utils/Logger.js"; -import { useLazyGet } from "@/Hooks/UseApi"; - -const withAdminCheck = (WrappedComponent) => { - const WithAdminCheck = (props) => { - const navigate = useNavigate(); - const [superAdminExists, setSuperAdminExists] = useState(false); - const [hasChecked, setHasChecked] = useState(false); - const { get: checkSuperAdmin, loading: isChecking } = useLazyGet(); - - useEffect(() => { - checkSuperAdmin("/auth/users/superadmin") - .then((response) => { - if (response?.data === true) { - navigate("/login"); - } else { - setSuperAdminExists(false); - } - }) - .catch((error) => { - logger.error(error); - }) - .finally(() => { - setHasChecked(true); - }); - }, [navigate, checkSuperAdmin]); - - if (!hasChecked || isChecking) { - return null; - } - - return ( - - ); - }; - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || "Component"; - WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`; - - return WithAdminCheck; -}; - -export default withAdminCheck; diff --git a/client/src/Components/v1/ProtectedRoute/index.jsx b/client/src/Components/v1/ProtectedRoute/index.jsx deleted file mode 100644 index 3135003387..0000000000 --- a/client/src/Components/v1/ProtectedRoute/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const ProtectedRoute = ({ children }) => { - const authState = useSelector((state) => state.auth); - - return authState.authToken ? ( - children - ) : ( - - ); -}; - -ProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, -}; - -export default ProtectedRoute; diff --git a/client/src/Components/v1/RoleProtectedRoute/index.jsx b/client/src/Components/v1/RoleProtectedRoute/index.jsx deleted file mode 100644 index c7b7dfc127..0000000000 --- a/client/src/Components/v1/RoleProtectedRoute/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const RoleProtectedRoute = ({ roles, children }) => { - const authState = useSelector((state) => state.auth); - const userRoles = authState?.user?.role || []; - const canAccess = userRoles.some((role) => roles.includes(role)); - - return canAccess ? ( - children - ) : ( - - ); -}; - -RoleProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, - roles: PropTypes.array, -}; - -export default RoleProtectedRoute; diff --git a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx b/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx deleted file mode 100644 index 21256e1146..0000000000 --- a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useTheme } from "@mui/material"; -import "./index.css"; - -const SunAndMoonIcon = () => { - const theme = useTheme(); - - return ( - - ); -}; - -export default SunAndMoonIcon; diff --git a/client/src/Components/v1/ThemeSwitch/index.css b/client/src/Components/v1/ThemeSwitch/index.css deleted file mode 100644 index d5db1c1caf..0000000000 --- a/client/src/Components/v1/ThemeSwitch/index.css +++ /dev/null @@ -1,64 +0,0 @@ -.sun-and-moon > :is(.moon, .sun, .sun-beams) { - transform-origin: center; -} - -.theme-toggle .sun-and-moon > .sun-beams { - stroke-width: 2px; -} - -.theme-dark .sun-and-moon > .sun { - transform: scale(1.75); -} - -.theme-dark .sun-and-moon > .sun-beams { - opacity: 0; -} - -.theme-dark .sun-and-moon > .moon > circle { - transform: translateX(-7px); -} - -@supports (cx: 1) { - .theme-dark .sun-and-moon > .moon > circle { - cx: 17; - transform: translateX(0); - } -} - -@media (prefers-reduced-motion: no-preference) { - .sun-and-moon > .sun { - transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); - } - - .sun-and-moon > .sun-beams { - transition: - transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), - opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1); - } - - .sun-and-moon .moon > circle { - transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - - @supports (cx: 1) { - .sun-and-moon .moon > circle { - transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - } - - .theme-dark .sun-and-moon > .sun { - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); - transition-duration: 0.25s; - transform: scale(1.75); - } - - .theme-dark .sun-and-moon > .sun-beams { - transition-duration: 0.15s; - transform: rotateZ(-25deg); - } - - .theme-dark .sun-and-moon > .moon > circle { - transition-duration: 0.5s; - transition-delay: 0.25s; - } -} diff --git a/client/src/Components/v1/ThemeSwitch/index.jsx b/client/src/Components/v1/ThemeSwitch/index.jsx deleted file mode 100644 index 60763da907..0000000000 --- a/client/src/Components/v1/ThemeSwitch/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * ThemeSwitch Component - * Dark and Light Theme Switch - * Original Code: https://web.dev/patterns/theming/theme-switch - * License: Apache License 2.0 - * Copyright © Google LLC - * - * This code has been adapted for use in this project. - * Apache License: https://www.apache.org/licenses/LICENSE-2.0 - */ - -import { IconButton } from "@mui/material"; -import SunAndMoonIcon from "./SunAndMoonIcon.jsx"; -import { useDispatch, useSelector } from "react-redux"; -import { setMode } from "../../../Features/UI/uiSlice.js"; -import "./index.css"; -import { useTranslation } from "react-i18next"; - -const ThemeSwitch = ({ width = 48, height = 48, color }) => { - const mode = useSelector((state) => state.ui.mode); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const toggleTheme = () => { - dispatch(setMode(mode === "light" ? "dark" : "light")); - }; - - return ( - :is(circle, g)": { - fill: color, - stroke: color, - }, - }} - > - - - ); -}; - -export default ThemeSwitch; diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx index 74c06f8f30..2dd1160adc 100644 --- a/client/src/Components/v2/design-elements/Avatar.tsx +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -31,6 +31,7 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { alt={`${user?.firstName} ${user?.lastName}`} src={src ? src : user?.avatarImage ? image : undefined} sx={{ + color: theme.palette.primary.contrastText, fontSize: small ? "16px" : "22px", fontWeight: 400, backgroundColor: theme.palette.primary.main, diff --git a/client/src/Components/v2/routing/RouteProtected.tsx b/client/src/Components/v2/routing/RouteProtected.tsx new file mode 100644 index 0000000000..7064c26a41 --- /dev/null +++ b/client/src/Components/v2/routing/RouteProtected.tsx @@ -0,0 +1,41 @@ +import { Navigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import type { UserRole } from "@/Types/User"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + + return authState.authToken ? ( + children + ) : ( + + ); +}; + +interface RoleProtectedRouteProps { + roles: UserRole[]; + children: React.ReactNode; +} + +export const RoleProtectedRoute = ({ roles, children }: RoleProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + const userRoles = authState?.user?.role || []; + const canAccess = userRoles.some((role) => roles.includes(role)); + + return canAccess ? ( + children + ) : ( + + ); +}; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 62ea5610be..4724ed97d4 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -76,7 +76,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => alignItems="center" py={theme.spacing(4)} px={theme.spacing(8)} - gap={theme.spacing(2)} + gap={theme.spacing(4)} > { }; return ( - t.zIndex.drawer : "auto", - }} - > - + dispatch(setCollapsed({ collapsed: true }))} + sx={{ zIndex: 999 }} + /> + - - {menu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - - - {bottomMenu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - + + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + - - + + + ); }; diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx index 75d27649fc..3c8a4e4bc8 100644 --- a/client/src/Pages/Auth/Register/index.tsx +++ b/client/src/Pages/Auth/Register/index.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; import { useRegisterForm } from "@/Hooks/useRegisterForm"; import type { RegisterFormData } from "@/Validation/register"; import { useTranslation } from "react-i18next"; -import { usePost } from "@/Hooks/UseApi"; +import { usePost, useGet } from "@/Hooks/UseApi"; import { setAuthState } from "@/Features/Auth/authSlice"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; @@ -32,11 +32,21 @@ const RegisterPage = () => { const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>(); const hasVerified = useRef(false); + const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet( + token ? null : "/auth/users/superadmin" + ); + const { control, handleSubmit, setError, reset } = useForm({ resolver: zodResolver(schema), defaultValues: defaults, }); + useEffect(() => { + if (superAdminExists === true) { + navigate("/login", { replace: true }); + } + }, [superAdminExists, navigate]); + useEffect(() => { if (!token || hasVerified.current) return; hasVerified.current = true; @@ -53,6 +63,8 @@ const RegisterPage = () => { }); }, [token]); + if (isCheckingAdmin) return null; + const onSubmit = async (data: RegisterFormData) => { if (loading) return; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 7d9163c439..3b0b76fc67 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -11,7 +11,7 @@ import NotFound from "@/Pages/NotFound"; import AuthLogin from "@/Pages/Auth/Login"; import AuthRegister from "@/Pages/Auth/Register"; import AuthForgotPassword from "@/Pages/Auth/Recovery"; -import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; +import AuthSetNewPassword from "@/Pages/Auth/SetNewPassword"; // Uptime import Uptime from "@/Pages/Uptime/Monitors"; @@ -23,40 +23,43 @@ import PageSpeedDetails from "@/Pages/PageSpeed/Details/"; // Infrastructure import Infrastructure from "@/Pages/Infrastructure/Monitors"; -import InfrastructureDetails from "@/Pages/Infrastructure/Details/index"; +import InfrastructureDetails from "@/Pages/Infrastructure/Details"; // Checks -import Checks from "../Pages/Checks/index"; +import Checks from "@/Pages/Checks"; // Incidents -import Incidents from "../Pages/Incidents/"; +import Incidents from "@/Pages/Incidents"; // Status pages -import CreateStatus from "../Pages/StatusPage/Create/"; -import StatusPages from "../Pages/StatusPage/StatusPages"; -import Status from "../Pages/StatusPage/Status"; +import CreateStatus from "@/Pages/StatusPage/Create/"; +import StatusPages from "@/Pages/StatusPage/StatusPages"; +import Status from "@/Pages/StatusPage/Status"; -import Notifications from "../Pages/Notifications"; -import CreateNotifications from "../Pages/Notifications/create"; +import Notifications from "@/Pages/Notifications"; +import CreateNotifications from "@/Pages/Notifications/create"; // Settings import Account from "@/Pages/Account"; -import EditUser from "../Pages/Account/EditUser"; -import Settings from "../Pages/Settings"; +import EditUser from "@/Pages/Account/EditUser"; +import Settings from "@/Pages/Settings"; -import Maintenance from "../Pages/Maintenance"; +import Maintenance from "@/Pages/Maintenance"; import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create"; -import ProtectedRoute from "../Components/v1/ProtectedRoute"; -import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; -import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; -import Logs from "../Pages/Logs"; +// Logs & Diagnostics +import Logs from "@/Pages/Logs"; + +// Routing +import { + ProtectedRoute, + RoleProtectedRoute, +} from "@/Components/v2/routing/RouteProtected"; import CreateMonitor from "@/Pages/CreateMonitor"; const Routes = () => { const mode = useSelector((state) => state.ui.mode); - const AdminCheckedRegister = withAdminCheck(AuthRegister); const v2theme = mode === "light" ? lightTheme : darkTheme; return ( @@ -380,7 +383,7 @@ const Routes = () => { element={ <> - + }