From a8f4c84f60364de4499bcc2c96fbd0a378e30f57 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 27 Mar 2024 16:27:26 +0100 Subject: [PATCH] acl view done --- Core/API/Parameter/ArrayType.class.php | 2 +- Core/API/Request.class.php | 2 +- .../src/views/access-control-list.js | 111 ++++++++++++++++-- react/shared/elements/dialog.jsx | 10 +- react/shared/hooks/async-search.js | 11 +- 5 files changed, 119 insertions(+), 17 deletions(-) diff --git a/Core/API/Parameter/ArrayType.class.php b/Core/API/Parameter/ArrayType.class.php index 133ca4f..d55b154 100644 --- a/Core/API/Parameter/ArrayType.class.php +++ b/Core/API/Parameter/ArrayType.class.php @@ -24,7 +24,7 @@ class ArrayType extends Parameter { } public function parseParam($value): bool { - if(!is_array($value)) { + if (!is_array($value)) { if (!$this->canBeOne) { return false; } else { diff --git a/Core/API/Request.class.php b/Core/API/Request.class.php index a4ba499..2f1b0a6 100644 --- a/Core/API/Request.class.php +++ b/Core/API/Request.class.php @@ -93,7 +93,7 @@ abstract class Request { foreach ($structure as $name => $param) { $value = $values[$name] ?? NULL; - $isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value)); + $isEmpty = is_string($value) && strlen($value) === 0; if (!$param->optional && (is_null($value) || $isEmpty)) { return $this->createError("Missing parameter: $name"); } diff --git a/react/admin-panel/src/views/access-control-list.js b/react/admin-panel/src/views/access-control-list.js index 4205004..bf20f64 100644 --- a/react/admin-panel/src/views/access-control-list.js +++ b/react/admin-panel/src/views/access-control-list.js @@ -1,9 +1,23 @@ import {useCallback, useContext, useEffect, useState} from "react"; import {LocaleContext} from "shared/locale"; import {Link, useNavigate} from "react-router-dom"; -import {Button, Checkbox, TextField, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; -import {Add, Refresh} from "@material-ui/icons"; +import { + Button, + Checkbox, + TextField, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + IconButton +} from "@material-ui/core"; +import {Add, Delete, Edit, Refresh} from "@material-ui/icons"; import {USER_GROUP_ADMIN} from "shared/constants"; +import Dialog from "shared/elements/dialog"; +import {TableFooter} from "@mui/material"; export default function AccessControlList(props) { @@ -20,6 +34,9 @@ export default function AccessControlList(props) { // filters const [query, setQuery] = useState(""); + // view + const [dialogData, setDialogData] = useState({open: false}); + const onFetchACL = useCallback((force = false) => { if (force || fetchACL) { setFetchACL(false); @@ -84,8 +101,33 @@ export default function AccessControlList(props) { } }, [acl]); + const onDeletePermission = useCallback(method => { + props.api.deletePermission(method).then(data => { + if (data.success) { + let newACL = acl.filter(acl => acl.method.toLowerCase() !== method.toLowerCase()); + setACL(newACL); + } else { + props.showDialog("Error deleting permission: " + data.msg); + } + }) + }, [acl]); + + const onUpdatePermission = useCallback((inputData, groups) => { + props.api.updatePermission(inputData.method, groups, inputData.description).then(data => { + if (data.success) { + let newACL = acl.filter(acl => acl.method.toLowerCase() !== inputData.method.toLowerCase()); + newACL.push({method: inputData.method, groups: groups, description: inputData.description}); + newACL = newACL.sort((a, b) => a.method.localeCompare(b.method)) + setACL(newACL); + } else { + props.showDialog("Error updating permission: " + data.msg); + } + }) + }, [acl]); + const isRestricted = (method) => { - return ["permissions/update", "permissions/delete"].includes(method.toLowerCase()); + return ["permissions/update", "permissions/delete"].includes(method.toLowerCase()) && + !props.api.hasGroup(USER_GROUP_ADMIN); } const PermissionList = () => { @@ -104,13 +146,44 @@ export default function AccessControlList(props) { rows.push( - {permission.method}
- {permission.description} +
+
+ setDialogData({ + open: true, + title: L("Edit permission"), + inputs: [ + { type: "label", value: L("general.method") + ":" }, + { type: "text", name: "method", value: permission.method, disabled: true }, + { type: "label", value: L("general.description") + ":" }, + { type: "text", name: "description", value: permission.description, maxLength: 128 } + ], + onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, permission.groups) + })} > + + + setDialogData({ + open: true, + title: L("Do you really want to delete this permission?"), + message: "Method: " + permission.method, + onOption: (option) => option === 0 && onDeletePermission(permission.method) + })} > + + +
+
+ {permission.method}
+ {permission.description} +
+
{groups.map(group => onChangePermission(index, group.id, e.target.checked)} - disabled={isRestricted(permission.method) || !props.api.hasGroup(USER_GROUP_ADMIN)} /> + disabled={isRestricted(permission.method)} /> )}
); @@ -155,18 +228,28 @@ export default function AccessControlList(props) { {L("general.reload")}
- - + +
- {L("permission")} + {L("permission")} { groups.map(group => {group.name} ) } @@ -178,5 +261,13 @@ export default function AccessControlList(props) {
+ setDialogData({open: false})} + title={dialogData.title} + message={dialogData.message} + onOption={dialogData.onOption} + inputs={dialogData.inputs} + options={[L("general.ok"), L("general.cancel")]} /> + } \ No newline at end of file diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index 0998663..c415f0d 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -7,7 +7,7 @@ import { DialogContent, DialogContentText, DialogTitle, - Input, List, ListItem, TextField + List, ListItem, TextField } from "@mui/material"; export default function Dialog(props) { @@ -24,7 +24,9 @@ export default function Dialog(props) { if (props.inputs) { let initialData = {}; for (const input of props.inputs) { - initialData[input.name] = input.value || ""; + if (input.type !== "label") { + initialData[input.name] = input.value || ""; + } } setInputData(initialData); } @@ -47,6 +49,9 @@ export default function Dialog(props) { delete inputProps.type; switch (input.type) { + case 'label': + inputElements.push({input.value}); + break; case 'text': case 'password': inputElements.push( setInputData({ ...inputData, [input.name]: e.target.value })} />) break; diff --git a/react/shared/hooks/async-search.js b/react/shared/hooks/async-search.js index b1584a9..fff51e7 100644 --- a/react/shared/hooks/async-search.js +++ b/react/shared/hooks/async-search.js @@ -5,6 +5,7 @@ export default function useAsyncSearch(callback, minLength = 1) { const [searchString, setSearchString] = useState(""); const [results, setResults] = useState(null); + const [isSearching, setSearching] = useState(false); useEffect(() => { if (minLength > 0 && (!searchString || searchString.length < minLength)) { @@ -12,9 +13,13 @@ export default function useAsyncSearch(callback, minLength = 1) { return; } - callback(searchString).then(results => { - setResults(results || null); - }); + if (!isSearching) { + setSearching(true); + callback(searchString).then(results => { + setResults(results || null); + setSearching(false); + }); + } }, [searchString]); return [searchString, setSearchString, results];