diff --git a/package-lock.json b/package-lock.json index 96d9f5a..05d2bcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1325,9 +1325,9 @@ } }, "@flare/tuitio-client-react": { - "version": "1.2.5", - "resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client-react/-/tuitio-client-react-1.2.5.tgz", - "integrity": "sha512-Qx2EFe8WhPhP53Fpai+fhWa7QGG18vwyPvBp8CkWqf+ivHFzz+Rfa9Rrkck3gstQW7b02sYWBFwHMHLmGj8YuA==", + "version": "1.2.6", + "resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client-react/-/tuitio-client-react-1.2.6.tgz", + "integrity": "sha512-T2uo9r9fQKTsbC25EaQnSifoDKrw3tgwy6fT/O1BdO4RPCUiRDr0YHiIssgPat+sB1G7iiUtV9CS8UNTJkacXw==", "requires": { "@flare/js-utils": "^1.1.0", "@flare/tuitio-client": "^1.2.5" diff --git a/package.json b/package.json index 31e5e91..d8e9f74 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "private": true, "dependencies": { "@flare/js-utils": "^1.1.0", - "@flare/tuitio-client-react": "^1.2.5", + "@flare/tuitio-client-react": "^1.2.6", "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.61", diff --git a/src/api/useApi.js b/src/api/useApi.js index ddad59b..388cfd8 100644 --- a/src/api/useApi.js +++ b/src/api/useApi.js @@ -1,115 +1,107 @@ -import { useToast } from "../hooks"; import { get, post } from "../utils/axios"; +import { toast } from "react-toastify"; const networkRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/network`; const systemRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/system`; const powerActionsRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/resurrector`; +const securityRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/security`; + +const handleError = err => { + let message; + switch (err?.status) { + case 500: + message = err.title; + break; + + case 404: + message = err.message; + break; + + default: + message = err.title; + } + + toast.error(message); +}; + +const defaultOptions = { + onCompleted: () => {}, + onError: handleError +}; + +const call = async (request, options) => { + const internalOptions = { ...defaultOptions, ...options }; + const { onCompleted, onError } = internalOptions; + + try { + const result = await request(); + onCompleted(result); + } catch (error) { + onError(error); + } +}; + +const getPermissions = (options = defaultOptions) => { + const promise = call(() => get(`${securityRoute}/permissions`), options); + return promise; +}; + +const getSystemVersion = (options = defaultOptions) => { + const releaseNotesPromise = call( + () => get(`${systemRoute}/version`), + options + ); + return releaseNotesPromise; +}; + +const readReleaseNotes = (options = defaultOptions) => { + const releaseNotesPromise = call( + () => get(`${systemRoute}/release-notes`), + options + ); + return releaseNotesPromise; +}; + +const readMachines = (options = defaultOptions) => { + const machinesPromise = call(() => get(`${networkRoute}/machines`), options); + return machinesPromise; +}; + +const wakeMachine = (machineId, options = defaultOptions) => { + const promise = call( + () => post(`${powerActionsRoute}/wake`, { machineId }), + options + ); + return promise; +}; + +const pingMachine = (machineId, options = defaultOptions) => { + const promise = call( + () => post(`${powerActionsRoute}/ping`, { machineId }), + options + ); + return promise; +}; + +const shutdownMachine = (machineId, delay, force, options = defaultOptions) => { + const promise = call( + () => post(`${powerActionsRoute}/shutdown`, { machineId, delay, force }), + options + ); + return promise; +}; + +const restartMachine = (machineId, delay, force, options = defaultOptions) => { + const promise = call( + () => post(`${powerActionsRoute}/restart`, { machineId, delay, force }), + options + ); + return promise; +}; const useApi = () => { - const { error } = useToast(); - - const handleError = err => { - let message; - switch (err?.status) { - case 500: - message = err.title; - break; - - case 404: - message = err.message; - break; - - default: - message = err.title; - } - - error(message); - }; - - const defaultOptions = { - onCompleted: () => {}, - onError: handleError - }; - - const call = async (request, options) => { - const internalOptions = { ...defaultOptions, ...options }; - const { onCompleted, onError } = internalOptions; - - try { - const result = await request(); - onCompleted(result); - } catch (error) { - onError(error); - } - }; - - const getSystemVersion = (options = defaultOptions) => { - const releaseNotesPromise = call( - () => get(`${systemRoute}/version`), - options - ); - return releaseNotesPromise; - }; - - const readReleaseNotes = (options = defaultOptions) => { - const releaseNotesPromise = call( - () => get(`${systemRoute}/release-notes`), - options - ); - return releaseNotesPromise; - }; - - const readMachines = (options = defaultOptions) => { - const machinesPromise = call( - () => get(`${networkRoute}/machines`), - options - ); - return machinesPromise; - }; - - const wakeMachine = (machineId, options = defaultOptions) => { - const promise = call( - () => post(`${powerActionsRoute}/wake`, { machineId }), - options - ); - return promise; - }; - - const pingMachine = (machineId, options = defaultOptions) => { - const promise = call( - () => post(`${powerActionsRoute}/ping`, { machineId }), - options - ); - return promise; - }; - - const shutdownMachine = ( - machineId, - delay, - force, - options = defaultOptions - ) => { - const promise = call( - () => post(`${powerActionsRoute}/shutdown`, { machineId, delay, force }), - options - ); - return promise; - }; - - const restartMachine = ( - machineId, - delay, - force, - options = defaultOptions - ) => { - const promise = call( - () => post(`${powerActionsRoute}/restart`, { machineId, delay, force }), - options - ); - return promise; - }; - return { + getPermissions, getSystemVersion, readReleaseNotes, readMachines, diff --git a/src/components/App.js b/src/components/App.js index dad60bc..3c826be 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,72 +1,14 @@ import React from "react"; -import PropTypes from "prop-types"; +import { UserPermissionsProvider, SensitiveInfoProvider } from "../providers"; import AppLayout from "./layout/AppLayout"; -import { BrowserRouter, Switch, Redirect, Route } from "react-router-dom"; -import { useTuitioToken } from "@flare/tuitio-client-react"; -import LoginContainer from "../features/login/components/LoginContainer"; - -const PrivateRoute = ({ component, ...rest }) => { - const { valid } = useTuitioToken(); - - return ( - - valid ? ( - React.createElement(component, props) - ) : ( - - ) - } - /> - ); -}; - -PrivateRoute.propTypes = { - component: PropTypes.func.isRequired, - location: PropTypes.object -}; - -const PublicRoute = ({ component, ...rest }) => { - const { valid } = useTuitioToken(); - return ( - - valid ? ( - - ) : ( - React.createElement(component, props) - ) - } - /> - ); -}; - -PublicRoute.propTypes = { - component: PropTypes.func.isRequired -}; const App = () => { return ( - - - } /> - - - - + + + + + ); }; diff --git a/src/components/AppRouter.js b/src/components/AppRouter.js new file mode 100644 index 0000000..1208f28 --- /dev/null +++ b/src/components/AppRouter.js @@ -0,0 +1,73 @@ +import React from "react"; +import PropTypes from "prop-types"; +import App from "./App"; +import { BrowserRouter, Switch, Redirect, Route } from "react-router-dom"; +import { useTuitioToken } from "@flare/tuitio-client-react"; +import LoginContainer from "../features/login/components/LoginContainer"; + +const PrivateRoute = ({ component, ...rest }) => { + const { valid } = useTuitioToken(); + + return ( + + valid ? ( + React.createElement(component, props) + ) : ( + + ) + } + /> + ); +}; + +PrivateRoute.propTypes = { + component: PropTypes.func.isRequired, + location: PropTypes.object +}; + +const PublicRoute = ({ component, ...rest }) => { + const { valid } = useTuitioToken(); + return ( + + valid ? ( + + ) : ( + React.createElement(component, props) + ) + } + /> + ); +}; + +PublicRoute.propTypes = { + component: PropTypes.func.isRequired +}; + +const AppRouter = () => { + return ( + + + } /> + + + + + ); +}; + +export default AppRouter; diff --git a/src/features/dashboard/announcements/AnnouncementsSection.js b/src/features/dashboard/announcements/AnnouncementsSection.js index 03a2ef7..a55146a 100644 --- a/src/features/dashboard/announcements/AnnouncementsSection.js +++ b/src/features/dashboard/announcements/AnnouncementsSection.js @@ -1,17 +1,15 @@ import React from "react"; import GuestAnnouncement from "./GuestAnnouncement"; import UserAnnouncement from "./UserAnnouncement"; -import { useTuitioUserInfo } from "@flare/tuitio-client-react"; +import { usePermissions } from "../../../hooks"; const AnnouncementsSection = () => { - const { userInfo, isGuest } = useTuitioUserInfo(); - const loading = !userInfo; - + const { loading, isGuest, isUser } = usePermissions(); if (loading) return ""; return ( <> + {isUser && } {isGuest && } - {!isGuest && } ); }; diff --git a/src/features/dashboard/announcements/UserAnnouncement.js b/src/features/dashboard/announcements/UserAnnouncement.js index 9ef80d6..1b6c0ac 100644 --- a/src/features/dashboard/announcements/UserAnnouncement.js +++ b/src/features/dashboard/announcements/UserAnnouncement.js @@ -1,22 +1,23 @@ import React from "react"; -import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; import { Alert, AlertTitle } from "@material-ui/lab"; import styles from "../styles"; import { useTranslation } from "react-i18next"; +import { useTuitioUser } from "@flare/tuitio-client-react"; const useStyles = makeStyles(styles); -export default function UserAnnouncement({ userData }) { +export default function UserAnnouncement() { const classes = useStyles(); const { t } = useTranslation(); + const { userName } = useTuitioUser(); return (
{t("Dashboard.Announcements.User.Title", { - userName: userData.firstName + userName })} {t("Dashboard.Announcements.User.Message")} @@ -24,7 +25,3 @@ export default function UserAnnouncement({ userData }) {
); } - -UserAnnouncement.propTypes = { - userData: PropTypes.object.isRequired -}; diff --git a/src/features/machines/components/common/ActionsGroup.js b/src/features/machines/components/common/ActionsGroup.js index 0f12835..6394ffb 100644 --- a/src/features/machines/components/common/ActionsGroup.js +++ b/src/features/machines/components/common/ActionsGroup.js @@ -5,13 +5,13 @@ import ActionButton from "./ActionButton"; import { Menu } from "@material-ui/core"; import { MoreHoriz } from "@material-ui/icons"; import { useTranslation } from "react-i18next"; -import { useTuitioUserInfo } from "@flare/tuitio-client-react"; +import { usePermissions } from "../../../../hooks"; const ActionsGroup = ({ machine, actions, addLog }) => { const [menuAnchor, setMenuAnchor] = useState(null); const { t } = useTranslation(); - const { isSysAdmin } = useTuitioUserInfo(); + const { operateMachines: canOperateMachines } = usePermissions(); const mainActions = useMemo( () => actions.filter(a => a.main === true), @@ -33,13 +33,16 @@ const ActionsGroup = ({ machine, actions, addLog }) => { return ( <> - + {mainActions.map(action => ( ))} { action={action} machine={machine} callback={handleMenuClose} - disabled={!isSysAdmin} + disabled={!canOperateMachines} /> ))} diff --git a/src/hooks/index.js b/src/hooks/index.js index 973e94d..44ec796 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,5 +1,6 @@ import { useToast } from "./useToast"; import { useSensitiveInfo } from "../providers/SensitiveInfoProvider"; +import { usePermissions } from "../providers/UserPermissionsProvider"; import { useClipboard } from "./useClipboard"; -export { useToast, useSensitiveInfo, useClipboard }; +export { useToast, useSensitiveInfo, usePermissions, useClipboard }; diff --git a/src/hooks/useToast.js b/src/hooks/useToast.js index 22305c7..8fdd808 100644 --- a/src/hooks/useToast.js +++ b/src/hooks/useToast.js @@ -1,11 +1,11 @@ import { toast } from "react-toastify"; -export const useToast = () => { - const info = message => toast.info(message); - const success = message => toast.success(message); - const warning = message => toast.warning(message); - const error = message => toast.error(message); - const dark = message => toast.dark(message); +const info = message => toast.info(message); +const success = message => toast.success(message); +const warning = message => toast.warning(message); +const error = message => toast.error(message); +const dark = message => toast.dark(message); +export const useToast = () => { return { info, success, warning, error, dark }; }; diff --git a/src/index.js b/src/index.js index 1797fb6..9d88f5f 100644 --- a/src/index.js +++ b/src/index.js @@ -2,23 +2,20 @@ import React, { Suspense } from "react"; import ReactDOM from "react-dom"; import ThemeProvider from "./providers/ThemeProvider"; import CssBaseline from "@material-ui/core/CssBaseline"; -import App from "./components/App"; +import AppRouter from "./components/AppRouter"; import { TuitioProvider } from "@flare/tuitio-client-react"; -import ToastProvider from "./providers/ToastProvider"; -import SensitiveInfoProvider from "./providers/SensitiveInfoProvider"; +import { ToastProvider } from "./providers"; import "./utils/i18n"; ReactDOM.render( - - Loading...}> - - - - - + Loading...}> + + + + , document.getElementById("root") diff --git a/src/providers/UserPermissionsProvider.js b/src/providers/UserPermissionsProvider.js new file mode 100644 index 0000000..297d674 --- /dev/null +++ b/src/providers/UserPermissionsProvider.js @@ -0,0 +1,87 @@ +import React, { useState, useEffect, useContext, useMemo } from "react"; +import PropTypes from "prop-types"; +import useApi from "../api"; + +const permissionCodes = { + VIEW_DASHBOARD: "VIEW_DASHBOARD", + MANAGE_USERS: "MANAGE_USERS", + MANAGE_SETTINGS: "MANAGE_SETTINGS", + VIEW_MACHINES: "VIEW_MACHINES", + MANAGE_MACHINES: "MANAGE_MACHINES", + OPERATE_MACHINES: "OPERATE_MACHINES", + GUEST_ACCESS: "GUEST_ACCESS" +}; + +const initialState = { + permissions: [], + loading: true +}; + +const getPermissionFlags = permissions => { + const viewDashboard = + permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false; + const manageUsers = + permissions.includes(permissionCodes.MANAGE_USERS) ?? false; + const manageSettings = + permissions.includes(permissionCodes.MANAGE_SETTINGS) ?? false; + const viewMachines = + permissions.includes(permissionCodes.VIEW_MACHINES) ?? false; + const manageMachines = + permissions.includes(permissionCodes.MANAGE_MACHINES) ?? false; + const operateMachines = + permissions.includes(permissionCodes.OPERATE_MACHINES) ?? false; + const guestAccess = + permissions.includes(permissionCodes.GUEST_ACCESS) ?? false; + + const flags = { + viewDashboard, + manageUsers, + manageSettings, + viewMachines, + manageMachines, + operateMachines, + guestAccess + }; + + const isGuest = guestAccess === true; + const isUser = Object.values(flags).includes(true) && !flags.guestAccess; + + return { + ...flags, + isGuest, + isUser + }; +}; + +const UserPermissionsContext = React.createContext(initialState); + +const usePermissions = () => { + const { permissions, loading } = useContext(UserPermissionsContext); + const flags = useMemo(() => getPermissionFlags(permissions), [permissions]); + return { loading, ...flags }; +}; + +const UserPermissionsProvider = ({ children }) => { + const [permissions, setPermissions] = useState(initialState); + + const { getPermissions } = useApi(); + + useEffect(() => { + getPermissions({ + onCompleted: data => setPermissions({ ...data, loading: false }) + }); + }, [getPermissions]); + + return ( + + {children} + + ); +}; + +UserPermissionsProvider.propTypes = { + children: PropTypes.node.isRequired +}; + +export { UserPermissionsProvider, usePermissions }; +export default UserPermissionsProvider; diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 0000000..118ffbc --- /dev/null +++ b/src/providers/index.js @@ -0,0 +1,11 @@ +import ThemeProvider from "./ThemeProvider"; +import ToastProvider from "./ToastProvider"; +import SensitiveInfoProvider from "./SensitiveInfoProvider"; +import UserPermissionsProvider from "./UserPermissionsProvider"; + +export { + ThemeProvider, + ToastProvider, + SensitiveInfoProvider, + UserPermissionsProvider +};