aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-05-04 18:02:01 -0700
committerAnton Tananaev <anton@traccar.org>2022-05-04 18:02:01 -0700
commit05623d59c14896da5ac1b2527e93d4af50ec87b6 (patch)
tree6e9dfb5d6f969b3b46b2725ca8bb264a86685111
parent53fd7f27b8a84b49ef7e4dafbc9e8ac985d7f3af (diff)
downloadtrackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.tar.gz
trackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.tar.bz2
trackermap-web-05623d59c14896da5ac1b2527e93d4af50ec87b6.zip
Handle user permissions
-rw-r--r--modern/src/EditCollectionView.js7
-rw-r--r--modern/src/MainPage.js16
-rw-r--r--modern/src/common/permissions.js11
-rw-r--r--modern/src/components/BottomMenu.js14
-rw-r--r--modern/src/components/PositionValue.js4
-rw-r--r--modern/src/components/registration/LoginForm.js6
-rw-r--r--modern/src/map/StatusCard.js10
-rw-r--r--modern/src/settings/ComputedAttributesPage.js19
-rw-r--r--modern/src/settings/OptionsLayout.js23
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));