diff options
author | Anton Tananaev <anton@traccar.org> | 2022-05-04 18:02:01 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2022-05-04 18:02:01 -0700 |
commit | 05623d59c14896da5ac1b2527e93d4af50ec87b6 (patch) | |
tree | 6e9dfb5d6f969b3b46b2725ca8bb264a86685111 | |
parent | 53fd7f27b8a84b49ef7e4dafbc9e8ac985d7f3af (diff) | |
download | trackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.tar.gz trackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.tar.bz2 trackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.zip |
Handle user permissions
-rw-r--r-- | modern/src/EditCollectionView.js | 7 | ||||
-rw-r--r-- | modern/src/MainPage.js | 16 | ||||
-rw-r--r-- | modern/src/common/permissions.js | 11 | ||||
-rw-r--r-- | modern/src/components/BottomMenu.js | 14 | ||||
-rw-r--r-- | modern/src/components/PositionValue.js | 4 | ||||
-rw-r--r-- | modern/src/components/registration/LoginForm.js | 6 | ||||
-rw-r--r-- | modern/src/map/StatusCard.js | 10 | ||||
-rw-r--r-- | modern/src/settings/ComputedAttributesPage.js | 19 | ||||
-rw-r--r-- | modern/src/settings/OptionsLayout.js | 23 |
9 files changed, 72 insertions, 38 deletions
diff --git a/modern/src/EditCollectionView.js b/modern/src/EditCollectionView.js index 59a91c60..c167a53c 100644 --- a/modern/src/EditCollectionView.js +++ b/modern/src/EditCollectionView.js @@ -5,11 +5,11 @@ import Menu from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; -import { useSelector } from 'react-redux'; import RemoveDialog from './RemoveDialog'; import { useTranslation } from './LocalizationProvider'; import dimensions from './theme/dimensions'; +import { useEditable } from './common/permissions'; const useStyles = makeStyles((theme) => ({ fab: { @@ -29,11 +29,12 @@ const EditCollectionView = ({ const history = useHistory(); const t = useTranslation(); + const editable = useEditable(); + const [selectedId, setSelectedId] = useState(null); const [selectedAnchorEl, setSelectedAnchorEl] = useState(null); const [removeDialogShown, setRemoveDialogShown] = useState(false); const [updateTimestamp, setUpdateTimestamp] = useState(Date.now()); - const adminEnabled = useSelector((state) => state.session.user && state.session.user.administrator); const menuShow = (anchorId, itemId) => { setSelectedAnchorEl(anchorId); @@ -69,7 +70,7 @@ const EditCollectionView = ({ return ( <> <Content updateTimestamp={updateTimestamp} onMenuClick={menuShow} filter={filter} /> - {adminEnabled && !disableAdd && ( + {editable && !disableAdd && ( <Fab size="medium" color="primary" className={classes.fab} onClick={handleAdd}> <AddIcon /> </Fab> diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index ccb3f83f..2798bcf5 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -28,6 +28,7 @@ import { devicesActions } from './store'; import DefaultCameraMap from './map/DefaultCameraMap'; import usePersistedState from './common/usePersistedState'; import LiveRoutesMap from './map/LiveRoutesMap'; +import { useDeviceReadonly } from './common/permissions'; const useStyles = makeStyles((theme) => ({ root: { @@ -119,6 +120,7 @@ const MainPage = () => { const theme = useTheme(); const t = useTranslation(); + const deviceReadonly = useDeviceReadonly(); const isTablet = useMediaQuery(theme.breakpoints.down('md')); const isPhone = useMediaQuery(theme.breakpoints.down('xs')); @@ -163,9 +165,9 @@ const MainPage = () => { <Paper square elevation={3}> <Toolbar className={classes.toolbar} disableGutters> {isTablet && ( - <IconButton onClick={handleClose}> - <ArrowBackIcon /> - </IconButton> + <IconButton onClick={handleClose}> + <ArrowBackIcon /> + </IconButton> )} <TextField fullWidth @@ -177,13 +179,13 @@ const MainPage = () => { placeholder={t('sharedSearchDevices')} variant="filled" /> - <IconButton onClick={() => history.push('/device')}> + <IconButton onClick={() => history.push('/device')} disabled={deviceReadonly}> <AddIcon /> </IconButton> {!isTablet && ( - <IconButton onClick={handleClose}> - <CloseIcon /> - </IconButton> + <IconButton onClick={handleClose}> + <CloseIcon /> + </IconButton> )} </Toolbar> </Paper> diff --git a/modern/src/common/permissions.js b/modern/src/common/permissions.js new file mode 100644 index 00000000..72ca0b08 --- /dev/null +++ b/modern/src/common/permissions.js @@ -0,0 +1,11 @@ +import { useSelector } from 'react-redux'; + +export const useAdministrator = () => useSelector((state) => state.session.user?.administrator); + +export const useReadonly = () => useSelector((state) => state.session.server?.readonly || state.session.user?.readonly); + +export const useDeviceReadonly = () => useSelector((state) => state.session.server?.readonly || state.session.user?.readonly + || state.session.server?.deviceReadonly || state.session.user?.deviceReadonly); + +export const useEditable = () => useSelector((state) => state.session.user?.administrator + || (!state.session.server?.readonly && !state.session.user?.readonly)); diff --git a/modern/src/components/BottomMenu.js b/modern/src/components/BottomMenu.js index 087d241d..920622ae 100644 --- a/modern/src/components/BottomMenu.js +++ b/modern/src/components/BottomMenu.js @@ -9,9 +9,11 @@ import DescriptionIcon from '@material-ui/icons/Description'; import SettingsIcon from '@material-ui/icons/Settings'; import MapIcon from '@material-ui/icons/Map'; import PersonIcon from '@material-ui/icons/Person'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; import { sessionActions } from '../store'; import { useTranslation } from '../LocalizationProvider'; +import { useReadonly } from '../common/permissions'; const BottomMenu = () => { const history = useHistory(); @@ -19,6 +21,7 @@ const BottomMenu = () => { const dispatch = useDispatch(); const t = useTranslation(); + const readonly = useReadonly(); const userId = useSelector((state) => state.session.user?.id); const [anchorEl, setAnchorEl] = useState(null); @@ -48,7 +51,11 @@ const BottomMenu = () => { history.push('/settings/preferences'); break; case 3: - setAnchorEl(event.currentTarget); + if (readonly) { + handleLogout(); + } else { + setAnchorEl(event.currentTarget); + } break; default: break; @@ -73,7 +80,10 @@ const BottomMenu = () => { <BottomNavigationAction label={t('mapTitle')} icon={<MapIcon />} /> <BottomNavigationAction label={t('reportTitle')} icon={<DescriptionIcon />} /> <BottomNavigationAction label={t('settingsTitle')} icon={<SettingsIcon />} /> - <BottomNavigationAction label={t('settingsUser')} icon={<PersonIcon />} /> + {readonly + ? (<BottomNavigationAction label={t('loginLogout')} icon={<ExitToAppIcon />} />) + : (<BottomNavigationAction label={t('settingsUser')} icon={<PersonIcon />} />) + } </BottomNavigation> <Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}> <MenuItem onClick={handleAccount}> diff --git a/modern/src/components/PositionValue.js b/modern/src/components/PositionValue.js index fd331a7d..5a07959d 100644 --- a/modern/src/components/PositionValue.js +++ b/modern/src/components/PositionValue.js @@ -1,17 +1,17 @@ import React, { useEffect, useState } from 'react'; import { Link } from '@material-ui/core'; import { Link as RouterLink } from 'react-router-dom'; -import { useSelector } from 'react-redux'; import { formatAlarm, formatBoolean, formatCoordinate, formatCourse, formatDistance, formatNumber, formatPercentage, formatSpeed, formatTime, } from '../common/formatter'; import { useAttributePreference, usePreference } from '../common/preferences'; import { useTranslation } from '../LocalizationProvider'; +import { useAdministrator } from '../common/permissions'; const PositionValue = ({ position, property, attribute }) => { const t = useTranslation(); - const admin = useSelector((state) => state.session.user?.administrator); + const admin = useAdministrator(); const key = property || attribute; const value = property ? position[property] : position.attributes[attribute]; diff --git a/modern/src/components/registration/LoginForm.js b/modern/src/components/registration/LoginForm.js index 58c50df8..0c28e00b 100644 --- a/modern/src/components/registration/LoginForm.js +++ b/modern/src/components/registration/LoginForm.js @@ -36,11 +36,11 @@ const LoginForm = () => { const [email, setEmail] = usePersistedState('loginEmail', ''); const [password, setPassword] = useState(''); - const registrationEnabled = useSelector((state) => (state.session.server ? state.session.server.registration : false)); - const emailEnabled = useSelector((state) => (state.session.server ? state.session.server.emailEnabled : false)); + const registrationEnabled = useSelector((state) => state.session.server?.registration); + const emailEnabled = useSelector((state) => state.session.server?.emailEnabled); const [announcementShown, setAnnouncementShown] = useState(false); - const announcement = useSelector((state) => (state.session.server && state.session.server.announcement)); + const announcement = useSelector((state) => state.session.server?.announcement); const handleSubmit = async (event) => { event.preventDefault(); diff --git a/modern/src/map/StatusCard.js b/modern/src/map/StatusCard.js index 73cd3b0f..5fc3edd0 100644 --- a/modern/src/map/StatusCard.js +++ b/modern/src/map/StatusCard.js @@ -15,6 +15,7 @@ import { formatStatus } from '../common/formatter'; import RemoveDialog from '../RemoveDialog'; import PositionValue from '../components/PositionValue'; import dimensions from '../theme/dimensions'; +import { useDeviceReadonly, useReadonly } from '../common/permissions'; const useStyles = makeStyles((theme) => ({ card: { @@ -59,6 +60,9 @@ const StatusCard = ({ deviceId, onClose }) => { const history = useHistory(); const t = useTranslation(); + const readonly = useReadonly(); + const deviceReadonly = useDeviceReadonly(); + const device = useSelector((state) => state.devices.items[deviceId]); const position = useSelector((state) => state.positions.items[deviceId]); @@ -105,13 +109,13 @@ const StatusCard = ({ deviceId, onClose }) => { <IconButton onClick={() => history.push('/replay')} disabled={!position}> <ReplayIcon /> </IconButton> - <IconButton onClick={() => history.push(`/command/${deviceId}`)}> + <IconButton onClick={() => history.push(`/command/${deviceId}`)} disabled={readonly}> <PublishIcon /> </IconButton> - <IconButton onClick={() => history.push(`/device/${deviceId}`)}> + <IconButton onClick={() => history.push(`/device/${deviceId}`)} disabled={deviceReadonly}> <EditIcon /> </IconButton> - <IconButton onClick={() => setRemoveDialogShown(true)} className={classes.negative}> + <IconButton onClick={() => setRemoveDialogShown(true)} disabled={deviceReadonly} className={classes.negative}> <DeleteIcon /> </IconButton> </CardActions> diff --git a/modern/src/settings/ComputedAttributesPage.js b/modern/src/settings/ComputedAttributesPage.js index b989b43e..78842e33 100644 --- a/modern/src/settings/ComputedAttributesPage.js +++ b/modern/src/settings/ComputedAttributesPage.js @@ -3,11 +3,11 @@ import { TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton, } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; -import { useSelector } from 'react-redux'; import { useEffectAsync } from '../reactHelper'; import EditCollectionView from '../EditCollectionView'; import OptionsLayout from './OptionsLayout'; import { useTranslation } from '../LocalizationProvider'; +import { useAdministrator } from '../common/permissions'; const useStyles = makeStyles((theme) => ({ columnAction: { @@ -21,7 +21,7 @@ const ComputedAttributeView = ({ updateTimestamp, onMenuClick }) => { const t = useTranslation(); const [items, setItems] = useState([]); - const adminEnabled = useSelector((state) => state.session.user && state.session.user.administrator); + const administrator = useAdministrator(); useEffectAsync(async () => { const response = await fetch('/api/attributes/computed'); @@ -35,7 +35,7 @@ const ComputedAttributeView = ({ updateTimestamp, onMenuClick }) => { <Table> <TableHead> <TableRow> - {adminEnabled && <TableCell className={classes.columnAction} />} + {administrator && <TableCell className={classes.columnAction} />} <TableCell>{t('sharedDescription')}</TableCell> <TableCell>{t('sharedAttribute')}</TableCell> <TableCell>{t('sharedExpression')}</TableCell> @@ -45,13 +45,12 @@ const ComputedAttributeView = ({ updateTimestamp, onMenuClick }) => { <TableBody> {items.map((item) => ( <TableRow key={item.id}> - {adminEnabled - && ( - <TableCell className={classes.columnAction} padding="none"> - <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> - <MoreVertIcon /> - </IconButton> - </TableCell> + {administrator && ( + <TableCell className={classes.columnAction} padding="none"> + <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> + <MoreVertIcon /> + </IconButton> + </TableCell> )} <TableCell>{item.description}</TableCell> <TableCell>{item.attribute}</TableCell> diff --git a/modern/src/settings/OptionsLayout.js b/modern/src/settings/OptionsLayout.js index 6ba636c2..3a78929b 100644 --- a/modern/src/settings/OptionsLayout.js +++ b/modern/src/settings/OptionsLayout.js @@ -26,6 +26,7 @@ import ExitToAppIcon from '@material-ui/icons/ExitToApp'; import SideNav from '../components/SideNav'; import NavBar from '../components/NavBar'; import { useTranslation } from '../LocalizationProvider'; +import { useAdministrator, useReadonly } from '../common/permissions'; const useStyles = makeStyles((theme) => ({ root: { @@ -70,18 +71,15 @@ const OptionsLayout = ({ children }) => { const [openDrawer, setOpenDrawer] = useState(false); const [optionTitle, setOptionTitle] = useState(); - const admin = useSelector((state) => state.session.user?.administrator); + const readonly = useReadonly(); + const admin = useAdministrator(); const userId = useSelector((state) => state.session.user?.id); - const adminRoutes = useMemo(() => [ - { subheader: t('userAdmin') }, - { name: t('settingsServer'), href: '/admin/server', icon: <StorageIcon /> }, - { name: t('settingsUsers'), href: '/admin/users', icon: <PeopleIcon /> }, - { name: t('statisticsTitle'), href: '/admin/statistics', icon: <BarChartIcon /> }, + const readonlyRoutes = useMemo(() => [ + { name: t('sharedPreferences'), href: '/settings/preferences', icon: <SettingsIcon /> }, ], [t]); const mainRoutes = useMemo(() => [ - { name: t('sharedPreferences'), href: '/settings/preferences', icon: <SettingsIcon /> }, { name: t('sharedNotifications'), href: '/settings/notifications', icon: <NotificationsIcon /> }, { name: t('settingsUser'), href: `/user/${userId}`, icon: <PersonIcon /> }, { name: t('sharedGeofences'), href: '/geofences', icon: <CreateIcon /> }, @@ -93,7 +91,16 @@ const OptionsLayout = ({ children }) => { { name: t('sharedSavedCommands'), href: '/settings/commands', icon: <ExitToAppIcon /> }, ], [t, userId]); - const routes = useMemo(() => [...mainRoutes, ...(admin ? adminRoutes : [])], [mainRoutes, admin, adminRoutes]); + const adminRoutes = useMemo(() => [ + { subheader: t('userAdmin') }, + { name: t('settingsServer'), href: '/admin/server', icon: <StorageIcon /> }, + { name: t('settingsUsers'), href: '/admin/users', icon: <PeopleIcon /> }, + { name: t('statisticsTitle'), href: '/admin/statistics', icon: <BarChartIcon /> }, + ], [t]); + + const routes = useMemo(() => ( + [ ...readonlyRoutes, ...(!readonly ? mainRoutes : []), ...(admin ? adminRoutes : [])] + ), [readonlyRoutes, readonly, mainRoutes, admin, adminRoutes]); useEffect(() => { const activeRoute = routes.find((route) => route.href && location.pathname.includes(route.href)); |