bugfix, permission api rewrite
This commit is contained in:
parent
ee638914a8
commit
aa51380055
@ -12,6 +12,7 @@ namespace Core\API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function checkStaticPermission(): bool {
|
protected function checkStaticPermission(): bool {
|
||||||
|
// hardcoded permission checking
|
||||||
$user = $this->context->getUser();
|
$user = $this->context->getUser();
|
||||||
if (!$user || !$user->hasGroup(Group::ADMIN)) {
|
if (!$user || !$user->hasGroup(Group::ADMIN)) {
|
||||||
return $this->createError("Permission denied.");
|
return $this->createError("Permission denied.");
|
||||||
@ -19,18 +20,21 @@ namespace Core\API {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function isRestricted(string $method): bool {
|
||||||
|
return in_array(strtolower($method), ["permission/update", "permission/delete"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Core\API\Permission {
|
namespace Core\API\Permission {
|
||||||
|
|
||||||
|
use Core\API\Parameter\ArrayType;
|
||||||
use Core\API\Parameter\Parameter;
|
use Core\API\Parameter\Parameter;
|
||||||
use Core\API\Parameter\StringType;
|
use Core\API\Parameter\StringType;
|
||||||
use Core\API\PermissionAPI;
|
use Core\API\PermissionAPI;
|
||||||
use Core\Driver\SQL\Column\Column;
|
use Core\Driver\SQL\Column\Column;
|
||||||
use Core\Driver\SQL\Condition\CondIn;
|
|
||||||
use Core\Driver\SQL\Condition\CondLike;
|
use Core\Driver\SQL\Condition\CondLike;
|
||||||
use Core\Driver\SQL\Condition\CondNot;
|
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
@ -142,75 +146,51 @@ namespace Core\API\Permission {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Save extends PermissionAPI {
|
class Update extends PermissionAPI {
|
||||||
|
|
||||||
public function __construct(Context $context, bool $externalCall = false) {
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall, array(
|
parent::__construct($context, $externalCall, [
|
||||||
'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY)
|
"method" => new StringType("method", 32, false),
|
||||||
));
|
"groups" => new ArrayType("groups", Parameter::TYPE_INT, true, false),
|
||||||
|
"description" => new StringType("description", 128, true, null),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
protected function _execute(): bool {
|
||||||
|
|
||||||
if (!$this->checkStaticPermission()) {
|
if (!$this->checkStaticPermission()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$permissions = $this->getParam("permissions");
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$methodParam = new StringType('method', 32);
|
$method = $this->getParam("method");
|
||||||
$groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY);
|
$description = $this->getParam("description");
|
||||||
$descriptionParam = new StringType('method', 128);
|
if ($this->isRestricted($method)) {
|
||||||
|
return $this->createError("This method cannot be updated.");
|
||||||
|
}
|
||||||
|
|
||||||
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "description"])
|
$groups = $this->getParam("groups");
|
||||||
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], [
|
if (!empty($groups)) {
|
||||||
"groups" => new Column("groups"),
|
sort($groups);
|
||||||
"description" => new Column("description")
|
$availableGroups = Group::findAll($sql);
|
||||||
]));
|
foreach ($groups as $groupId) {
|
||||||
|
if (!isset($availableGroups[$groupId])) {
|
||||||
|
return $this->createError("Unknown group id: $groupId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$insertedMethods = array();
|
if ($description === null) {
|
||||||
|
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore"])
|
||||||
foreach ($permissions as $permission) {
|
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups]))
|
||||||
if (!is_array($permission)) {
|
->addRow($method, $groups, false);
|
||||||
return $this->createError("Invalid data type found in parameter: permissions, expected: object");
|
|
||||||
} else if (!isset($permission["method"]) || !isset($permission["description"]) || !array_key_exists("groups", $permission)) {
|
|
||||||
return $this->createError("Invalid object found in parameter: permissions, expected keys: 'method', 'groups', 'description'");
|
|
||||||
} else if (!$methodParam->parseParam($permission["method"])) {
|
|
||||||
$expectedType = $methodParam->getTypeName();
|
|
||||||
return $this->createError("Invalid data type found for attribute 'method', expected: $expectedType");
|
|
||||||
} else if (!$groupsParam->parseParam($permission["groups"])) {
|
|
||||||
$expectedType = $groupsParam->getTypeName();
|
|
||||||
return $this->createError("Invalid data type found for attribute 'groups', expected: $expectedType");
|
|
||||||
} else if (!$descriptionParam->parseParam($permission["description"])) {
|
|
||||||
$expectedType = $descriptionParam->getTypeName();
|
|
||||||
return $this->createError("Invalid data type found for attribute 'description', expected: $expectedType");
|
|
||||||
} else if (empty(trim($methodParam->value))) {
|
|
||||||
return $this->createError("Method cannot be empty.");
|
|
||||||
} else {
|
} else {
|
||||||
$method = $methodParam->value;
|
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore", "description"])
|
||||||
$groups = $groupsParam->value;
|
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups, "description" => $description]))
|
||||||
$description = $descriptionParam->value;
|
->addRow($method, $groups, false, $description);
|
||||||
$updateQuery->addRow($method, $groups, $description);
|
|
||||||
$insertedMethods[] = $method;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($permissions)) {
|
$this->success = $updateQuery->execute() !== false;
|
||||||
$res = $updateQuery->execute();
|
|
||||||
$this->success = ($res !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->success) {
|
|
||||||
$res = $sql->delete("ApiPermission")
|
|
||||||
->whereEq("description", "") // only delete non default permissions
|
|
||||||
->where(new CondNot(new CondIn(new Column("method"), $insertedMethods)))
|
|
||||||
->execute();
|
|
||||||
|
|
||||||
$this->success = ($res !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,4 +202,55 @@ namespace Core\API\Permission {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Delete extends PermissionAPI {
|
||||||
|
public function __construct(Context $context, bool $externalCall = false) {
|
||||||
|
parent::__construct($context, $externalCall, [
|
||||||
|
"method" => new StringType("method", 32, false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _execute(): bool {
|
||||||
|
|
||||||
|
if (!$this->checkStaticPermission()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = $this->context->getSQL();
|
||||||
|
$method = $this->getParam("method");
|
||||||
|
if ($this->isRestricted($method)) {
|
||||||
|
return $this->createError("This method cannot be deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $sql->select("method")
|
||||||
|
->from("ApiPermission")
|
||||||
|
->whereEq("method", $method)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$this->success = $res !== false;
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
if (!$res) {
|
||||||
|
return $this->createError("This method was not configured yet");
|
||||||
|
} else {
|
||||||
|
$res = $sql->delete("ApiPermission")
|
||||||
|
->whereEq("method", $method)
|
||||||
|
->execute();
|
||||||
|
$this->success = $res !== false;
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultACL(Insert $insert): void {
|
||||||
|
$insert->addRow(
|
||||||
|
self::getEndpoint(), [Group::ADMIN],
|
||||||
|
"Allows users to delete API permissions. This is restricted to the administrator and cannot be changed",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -244,7 +244,6 @@ abstract class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for permission
|
// Check for permission
|
||||||
if (!($this instanceof \Core\API\Permission\Save)) {
|
|
||||||
$req = new \Core\API\Permission\Check($this->context);
|
$req = new \Core\API\Permission\Check($this->context);
|
||||||
$this->success = $req->execute(array("method" => self::getEndpoint()));
|
$this->success = $req->execute(array("method" => self::getEndpoint()));
|
||||||
$this->lastError = $req->getLastError();
|
$this->lastError = $req->getLastError();
|
||||||
@ -252,7 +251,6 @@ abstract class Request {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->parseParams($values)) {
|
if (!$this->parseParams($values)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -41,11 +41,6 @@ class Swagger extends Request {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// special case: hardcoded permission
|
|
||||||
if ($request instanceof \Core\API\Permission\Save && (!$isLoggedIn || !$currentUser->hasGroup(Group::ADMIN))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($requiredGroups)) {
|
if (!empty($requiredGroups)) {
|
||||||
$userGroups = array_keys($currentUser?->getGroups() ?? []);
|
$userGroups = array_keys($currentUser?->getGroups() ?? []);
|
||||||
return !empty(array_intersect($requiredGroups, $userGroups));
|
return !empty(array_intersect($requiredGroups, $userGroups));
|
||||||
|
@ -52,7 +52,7 @@ class MySQL extends SQL {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@$this->connection = mysqli_connect(
|
$this->connection = @mysqli_connect(
|
||||||
$this->connectionData->getHost(),
|
$this->connectionData->getHost(),
|
||||||
$this->connectionData->getLogin(),
|
$this->connectionData->getLogin(),
|
||||||
$this->connectionData->getPassword(),
|
$this->connectionData->getPassword(),
|
||||||
|
15
cli.php
Normal file → Executable file
15
cli.php
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/php
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('WEBROOT', realpath("."));
|
define('WEBROOT', realpath("."));
|
||||||
@ -68,8 +69,7 @@ function connectSQL(): ?SQL {
|
|||||||
|
|
||||||
function printHelp(array $argv): void {
|
function printHelp(array $argv): void {
|
||||||
printLine("=== WebBase CLI tool ===");
|
printLine("=== WebBase CLI tool ===");
|
||||||
printLine("Usage: ");
|
printLine("Usage: " . $argv[0]);
|
||||||
var_dump($argv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool {
|
function applyPatch(\Core\Driver\SQL\SQL $sql, string $patchName): bool {
|
||||||
@ -186,7 +186,6 @@ function handleDatabase(array $argv): void {
|
|||||||
$command = array_merge(["docker", "exec", "-it", $containerName], $command);
|
$command = array_merge(["docker", "exec", "-it", $containerName], $command);
|
||||||
}
|
}
|
||||||
|
|
||||||
var_dump($command);
|
|
||||||
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
|
||||||
|
|
||||||
if (is_resource($process)) {
|
if (is_resource($process)) {
|
||||||
@ -655,7 +654,8 @@ function onImpersonate($argv): void {
|
|||||||
$session = new \Core\Objects\DatabaseEntity\Session($context, $user);
|
$session = new \Core\Objects\DatabaseEntity\Session($context, $user);
|
||||||
$session->setData(["2faAuthenticated" => true]);
|
$session->setData(["2faAuthenticated" => true]);
|
||||||
$session->update();
|
$session->update();
|
||||||
echo "session=" . $session->getUUID() . PHP_EOL;
|
echo "Cookie: session=" . $session->getUUID() . PHP_EOL .
|
||||||
|
"CSRF-Token: " . $session->getCsrfToken() . PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFrontend(array $argv): void {
|
function onFrontend(array $argv): void {
|
||||||
@ -809,7 +809,7 @@ $registeredCommands = [
|
|||||||
"test" => ["handler" => "onTest"],
|
"test" => ["handler" => "onTest"],
|
||||||
"mail" => ["handler" => "onMail"],
|
"mail" => ["handler" => "onMail"],
|
||||||
"settings" => ["handler" => "onSettings"],
|
"settings" => ["handler" => "onSettings"],
|
||||||
"impersonate" => ["handler" => "onImpersonate"],
|
"impersonate" => ["handler" => "onImpersonate", "requiresDocker" => true],
|
||||||
"frontend" => ["handler" => "onFrontend"],
|
"frontend" => ["handler" => "onFrontend"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -820,13 +820,12 @@ if (count($argv) < 2) {
|
|||||||
$command = $argv[1];
|
$command = $argv[1];
|
||||||
if (array_key_exists($command, $registeredCommands)) {
|
if (array_key_exists($command, $registeredCommands)) {
|
||||||
|
|
||||||
// TODO: do we need this?
|
|
||||||
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
||||||
$requiresDocker = in_array($argv[2] ?? null, $registeredCommands[$command]["requiresDocker"] ?? []);
|
$requiresDockerArgs = $registeredCommands[$command]["requiresDocker"] ?? [];
|
||||||
|
$requiresDocker = $requiresDockerArgs === true || in_array($argv[2] ?? null, $requiresDockerArgs);
|
||||||
if ($requiresDocker) {
|
if ($requiresDocker) {
|
||||||
$containerName = $dockerYaml["services"]["php"]["container_name"];
|
$containerName = $dockerYaml["services"]["php"]["container_name"];
|
||||||
printLine("Detected docker environment in config, running docker exec for container: $containerName");
|
printLine("Detected docker environment in config, running docker exec for container: $containerName");
|
||||||
var_dump($argv);
|
|
||||||
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
|
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
|
||||||
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
|
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
|
||||||
exit(proc_close($proc));
|
exit(proc_close($proc));
|
||||||
|
@ -54,6 +54,40 @@ export default function AccessControlList(props) {
|
|||||||
});
|
});
|
||||||
}, [currentLocale]);
|
}, [currentLocale]);
|
||||||
|
|
||||||
|
const onChangePermission = useCallback((methodIndex, groupId, selected) => {
|
||||||
|
let newGroups = null;
|
||||||
|
let currentGroups = acl[methodIndex].groups;
|
||||||
|
let groupIndex = currentGroups.indexOf(groupId);
|
||||||
|
if (!selected) {
|
||||||
|
if (currentGroups.length === 0) {
|
||||||
|
// it was an "everyone permission" before
|
||||||
|
newGroups = groups.filter(group => group.id !== groupId).map(group => group.id);
|
||||||
|
} else if (groupIndex !== -1 && currentGroups.length > 1) {
|
||||||
|
newGroups = [...currentGroups];
|
||||||
|
newGroups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
} else if (groupIndex === -1) {
|
||||||
|
newGroups = [...currentGroups];
|
||||||
|
newGroups.push(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newGroups !== null) {
|
||||||
|
props.api.updatePermission(acl[methodIndex].method, newGroups).then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
let newACL = [...acl];
|
||||||
|
newACL[methodIndex].groups = newGroups;
|
||||||
|
setACL(newACL);
|
||||||
|
} else {
|
||||||
|
props.showDialog("Error updating permission: " + data.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [acl]);
|
||||||
|
|
||||||
|
const isRestricted = (method) => {
|
||||||
|
return ["permissions/update", "permissions/delete"].includes(method.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const PermissionList = () => {
|
const PermissionList = () => {
|
||||||
let rows = [];
|
let rows = [];
|
||||||
|
|
||||||
@ -75,7 +109,8 @@ export default function AccessControlList(props) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
|
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
|
||||||
<Checkbox checked={!permission.groups.length || permission.groups.includes(group.id)}
|
<Checkbox checked={!permission.groups.length || permission.groups.includes(group.id)}
|
||||||
disabled={permission.method.toLowerCase() === "permission/save" || !props.api.hasGroup(USER_GROUP_ADMIN)}/>
|
onChange={(e) => onChangePermission(index, group.id, e.target.checked)}
|
||||||
|
disabled={isRestricted(permission.method) || !props.api.hasGroup(USER_GROUP_ADMIN)} />
|
||||||
</TableCell>)}
|
</TableCell>)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@ -132,7 +167,9 @@ export default function AccessControlList(props) {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{width: "auto"}}>{L("permission")}</TableCell>
|
<TableCell sx={{width: "auto"}}>{L("permission")}</TableCell>
|
||||||
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>{group.name}</TableCell>) }
|
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
|
||||||
|
{group.name}
|
||||||
|
</TableCell>) }
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
import {Link, Navigate, useNavigate} from "react-router-dom";
|
|
||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
|
||||||
import {LocaleContext} from "shared/locale";
|
|
||||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
|
||||||
import {Button, IconButton} from "@material-ui/core";
|
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
|
||||||
import {Chip} from "@mui/material";
|
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
|
||||||
import usePagination from "shared/hooks/pagination";
|
|
||||||
|
|
||||||
|
|
||||||
export default function UserListView(props) {
|
|
||||||
|
|
||||||
const api = props.api;
|
|
||||||
const showDialog = props.showDialog;
|
|
||||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const pagination = usePagination();
|
|
||||||
const [users, setUsers] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
|
||||||
if (!data.success) {
|
|
||||||
props.showDialog("Error fetching translations: " + data.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [currentLocale]);
|
|
||||||
|
|
||||||
const onFetchUsers = useCallback((page, count, orderBy, sortOrder) => {
|
|
||||||
api.fetchUsers(page, count, orderBy, sortOrder).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
setUsers(res.users);
|
|
||||||
pagination.update(res.pagination);
|
|
||||||
} else {
|
|
||||||
showDialog(res.msg, "Error fetching users");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [api, showDialog]);
|
|
||||||
|
|
||||||
const groupColumn = (() => {
|
|
||||||
let column = new DataColumn(L("account.groups"), "groups");
|
|
||||||
column.renderData = (L, entry) => {
|
|
||||||
return Object.values(entry.groups).map(group => <Chip key={"group-" + group.id} label={group.name}/>)
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const actionColumn = (() => {
|
|
||||||
let column = new DataColumn(L("general.actions"), null, false);
|
|
||||||
column.renderData = (L, entry) => <>
|
|
||||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/user/" + entry.id)}>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</>
|
|
||||||
return column;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const columnDefinitions = [
|
|
||||||
new NumericColumn(L("general.id"), "id"),
|
|
||||||
new StringColumn(L("account.username"), "name"),
|
|
||||||
new StringColumn(L("account.email"), "email"),
|
|
||||||
groupColumn,
|
|
||||||
new StringColumn(L("account.full_name"), "full_name"),
|
|
||||||
actionColumn,
|
|
||||||
];
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<div className={"content-header"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<div className={"row mb-2"}>
|
|
||||||
<div className={"col-sm-6"}>
|
|
||||||
<h1 className={"m-0 text-dark"}>Users</h1>
|
|
||||||
</div>
|
|
||||||
<div className={"col-sm-6"}>
|
|
||||||
<ol className={"breadcrumb float-sm-right"}>
|
|
||||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
|
||||||
<li className="breadcrumb-item active">Users</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={"content"}>
|
|
||||||
<div className={"container-fluid"}>
|
|
||||||
<Link to="/admin/user/new">
|
|
||||||
<Button variant={"outlined"} startIcon={<AddIcon />} size={"small"}>
|
|
||||||
{L("general.create_new")}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<DataTable
|
|
||||||
data={users}
|
|
||||||
pagination={pagination}
|
|
||||||
className={"table table-striped"}
|
|
||||||
fetchData={onFetchUsers}
|
|
||||||
placeholder={"No users to display"}
|
|
||||||
columns={columnDefinitions} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
@ -77,6 +77,8 @@ export default function UserListView(props) {
|
|||||||
<DataTable
|
<DataTable
|
||||||
data={users}
|
data={users}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
|
defaultSortOrder={"asc"}
|
||||||
|
defaultSortColumn={0}
|
||||||
className={"table table-striped"}
|
className={"table table-striped"}
|
||||||
fetchData={onFetchUsers}
|
fetchData={onFetchUsers}
|
||||||
placeholder={"No users to display"}
|
placeholder={"No users to display"}
|
||||||
|
@ -253,6 +253,14 @@ export default class API {
|
|||||||
return this.apiCall("permission/save", { permissions: permissions });
|
return this.apiCall("permission/save", { permissions: permissions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePermission(method, groups, description = null) {
|
||||||
|
return this.apiCall("permission/update", { method: method, groups: groups, description: description });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePermission(method) {
|
||||||
|
return this.apiCall("permission/delete", { method: method });
|
||||||
|
}
|
||||||
|
|
||||||
/** VisitorsAPI **/
|
/** VisitorsAPI **/
|
||||||
async getVisitors(type, date) {
|
async getVisitors(type, date) {
|
||||||
return this.apiCall("visitors/stats", { type: type, date: date });
|
return this.apiCall("visitors/stats", { type: type, date: date });
|
||||||
|
@ -8,6 +8,7 @@ import clsx from "clsx";
|
|||||||
import {Box, IconButton, Select, TextField} from "@mui/material";
|
import {Box, IconButton, Select, TextField} from "@mui/material";
|
||||||
import {formatDate, formatDateTime} from "../util";
|
import {formatDate, formatDateTime} from "../util";
|
||||||
import CachedIcon from "@material-ui/icons/Cached";
|
import CachedIcon from "@material-ui/icons/Cached";
|
||||||
|
import {isNumber} from "chart.js/helpers";
|
||||||
|
|
||||||
|
|
||||||
export function DataTable(props) {
|
export function DataTable(props) {
|
||||||
@ -22,8 +23,8 @@ export function DataTable(props) {
|
|||||||
const {translate: L} = useContext(LocaleContext);
|
const {translate: L} = useContext(LocaleContext);
|
||||||
|
|
||||||
const [doFetchData, setFetchData] = useState(false);
|
const [doFetchData, setFetchData] = useState(false);
|
||||||
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase));
|
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase()));
|
||||||
const [sortColumn, setSortColumn] = useState(defaultSortColumn || null);
|
const [sortColumn, setSortColumn] = useState(isNumber(defaultSortColumn) || null);
|
||||||
const sortable = !!fetchData && (props.hasOwnProperty("sortable") ? !!props.sortable : true);
|
const sortable = !!fetchData && (props.hasOwnProperty("sortable") ? !!props.sortable : true);
|
||||||
const onRowClick = onClick || (() => {});
|
const onRowClick = onClick || (() => {});
|
||||||
|
|
||||||
|
213
yarn.lock
Normal file
213
yarn.lock
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.23.5":
|
||||||
|
version "7.24.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae"
|
||||||
|
integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/highlight" "^7.24.2"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
|
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
|
||||||
|
integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin@^7.21.0":
|
||||||
|
version "7.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz#db58bf57137b623b916e24874ab7188d93d7f68f"
|
||||||
|
integrity sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-annotate-as-pure" "^7.22.5"
|
||||||
|
"@babel/helper-environment-visitor" "^7.22.20"
|
||||||
|
"@babel/helper-function-name" "^7.23.0"
|
||||||
|
"@babel/helper-member-expression-to-functions" "^7.23.0"
|
||||||
|
"@babel/helper-optimise-call-expression" "^7.22.5"
|
||||||
|
"@babel/helper-replace-supers" "^7.24.1"
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
|
||||||
|
"@babel/helper-split-export-declaration" "^7.22.6"
|
||||||
|
semver "^6.3.1"
|
||||||
|
|
||||||
|
"@babel/helper-environment-visitor@^7.22.20":
|
||||||
|
version "7.22.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
|
||||||
|
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
|
||||||
|
|
||||||
|
"@babel/helper-function-name@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
|
||||||
|
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/template" "^7.22.15"
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366"
|
||||||
|
integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
|
|
||||||
|
"@babel/helper-optimise-call-expression@^7.22.5":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e"
|
||||||
|
integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2":
|
||||||
|
version "7.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz#945681931a52f15ce879fd5b86ce2dae6d3d7f2a"
|
||||||
|
integrity sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers@^7.24.1":
|
||||||
|
version "7.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1"
|
||||||
|
integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-environment-visitor" "^7.22.20"
|
||||||
|
"@babel/helper-member-expression-to-functions" "^7.23.0"
|
||||||
|
"@babel/helper-optimise-call-expression" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers@^7.22.5":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847"
|
||||||
|
integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-split-export-declaration@^7.22.6":
|
||||||
|
version "7.22.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
|
||||||
|
integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.23.4":
|
||||||
|
version "7.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e"
|
||||||
|
integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.22.20":
|
||||||
|
version "7.22.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
||||||
|
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
||||||
|
|
||||||
|
"@babel/highlight@^7.24.2":
|
||||||
|
version "7.24.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26"
|
||||||
|
integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.22.20"
|
||||||
|
chalk "^2.4.2"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
|
"@babel/parser@^7.24.0":
|
||||||
|
version "7.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.1.tgz#1e416d3627393fab1cb5b0f2f1796a100ae9133a"
|
||||||
|
integrity sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==
|
||||||
|
|
||||||
|
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
|
||||||
|
version "7.21.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
|
||||||
|
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-annotate-as-pure" "^7.18.6"
|
||||||
|
"@babel/helper-create-class-features-plugin" "^7.21.0"
|
||||||
|
"@babel/helper-plugin-utils" "^7.20.2"
|
||||||
|
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-private-property-in-object@^7.14.5":
|
||||||
|
version "7.14.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad"
|
||||||
|
integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-plugin-utils" "^7.14.5"
|
||||||
|
|
||||||
|
"@babel/template@^7.22.15":
|
||||||
|
version "7.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50"
|
||||||
|
integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.23.5"
|
||||||
|
"@babel/parser" "^7.24.0"
|
||||||
|
"@babel/types" "^7.24.0"
|
||||||
|
|
||||||
|
"@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0":
|
||||||
|
version "7.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf"
|
||||||
|
integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.23.4"
|
||||||
|
"@babel/helper-validator-identifier" "^7.22.20"
|
||||||
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
ansi-styles@^3.2.1:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||||
|
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
|
chalk@^2.4.2:
|
||||||
|
version "2.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||||
|
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^3.2.1"
|
||||||
|
escape-string-regexp "^1.0.5"
|
||||||
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
|
color-convert@^1.9.0:
|
||||||
|
version "1.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||||
|
dependencies:
|
||||||
|
color-name "1.1.3"
|
||||||
|
|
||||||
|
color-name@1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
|
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||||
|
|
||||||
|
escape-string-regexp@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
|
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
|
||||||
|
|
||||||
|
has-flag@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||||
|
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
|
||||||
|
|
||||||
|
js-tokens@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
picocolors@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
semver@^6.3.1:
|
||||||
|
version "6.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
|
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||||
|
|
||||||
|
supports-color@^5.3.0:
|
||||||
|
version "5.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
|
to-fast-properties@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||||
|
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
|
Loading…
Reference in New Issue
Block a user