bugfix, permission api rewrite

This commit is contained in:
Roman 2024-03-27 15:15:46 +01:00
parent ee638914a8
commit aa51380055
11 changed files with 365 additions and 182 deletions

@ -12,6 +12,7 @@ namespace Core\API {
}
protected function checkStaticPermission(): bool {
// hardcoded permission checking
$user = $this->context->getUser();
if (!$user || !$user->hasGroup(Group::ADMIN)) {
return $this->createError("Permission denied.");
@ -19,18 +20,21 @@ namespace Core\API {
return true;
}
protected function isRestricted(string $method): bool {
return in_array(strtolower($method), ["permission/update", "permission/delete"]);
}
}
}
namespace Core\API\Permission {
use Core\API\Parameter\ArrayType;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\API\PermissionAPI;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Query\Insert;
use Core\Driver\SQL\Strategy\UpdateStrategy;
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) {
parent::__construct($context, $externalCall, array(
'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY)
));
parent::__construct($context, $externalCall, [
"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()) {
return false;
}
$permissions = $this->getParam("permissions");
$sql = $this->context->getSQL();
$methodParam = new StringType('method', 32);
$groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY);
$descriptionParam = new StringType('method', 128);
$method = $this->getParam("method");
$description = $this->getParam("description");
if ($this->isRestricted($method)) {
return $this->createError("This method cannot be updated.");
}
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "description"])
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], [
"groups" => new Column("groups"),
"description" => new Column("description")
]));
$insertedMethods = array();
foreach ($permissions as $permission) {
if (!is_array($permission)) {
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 {
$method = $methodParam->value;
$groups = $groupsParam->value;
$description = $descriptionParam->value;
$updateQuery->addRow($method, $groups, $description);
$insertedMethods[] = $method;
$groups = $this->getParam("groups");
if (!empty($groups)) {
sort($groups);
$availableGroups = Group::findAll($sql);
foreach ($groups as $groupId) {
if (!isset($availableGroups[$groupId])) {
return $this->createError("Unknown group id: $groupId");
}
}
}
if (!empty($permissions)) {
$res = $updateQuery->execute();
$this->success = ($res !== FALSE);
$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();
if ($description === null) {
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore"])
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups]))
->addRow($method, $groups, false);
} else {
$updateQuery = $sql->insert("ApiPermission", ["method", "groups", "isCore", "description"])
->onDuplicateKeyStrategy(new UpdateStrategy(["method"], ["groups" => $groups, "description" => $description]))
->addRow($method, $groups, false, $description);
}
$this->success = $updateQuery->execute() !== false;
$this->lastError = $sql->getLastError();
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,13 +244,11 @@ abstract class Request {
}
// Check for permission
if (!($this instanceof \Core\API\Permission\Save)) {
$req = new \Core\API\Permission\Check($this->context);
$this->success = $req->execute(array("method" => self::getEndpoint()));
$this->lastError = $req->getLastError();
if (!$this->success) {
return false;
}
$req = new \Core\API\Permission\Check($this->context);
$this->success = $req->execute(array("method" => self::getEndpoint()));
$this->lastError = $req->getLastError();
if (!$this->success) {
return false;
}
}

@ -41,11 +41,6 @@ class Swagger extends Request {
return false;
}
// special case: hardcoded permission
if ($request instanceof \Core\API\Permission\Save && (!$isLoggedIn || !$currentUser->hasGroup(Group::ADMIN))) {
return false;
}
if (!empty($requiredGroups)) {
$userGroups = array_keys($currentUser?->getGroups() ?? []);
return !empty(array_intersect($requiredGroups, $userGroups));

@ -52,7 +52,7 @@ class MySQL extends SQL {
return true;
}
@$this->connection = mysqli_connect(
$this->connection = @mysqli_connect(
$this->connectionData->getHost(),
$this->connectionData->getLogin(),
$this->connectionData->getPassword(),

15
cli.php Normal file → Executable file

@ -1,3 +1,4 @@
#!/usr/bin/php
<?php
define('WEBROOT', realpath("."));
@ -68,8 +69,7 @@ function connectSQL(): ?SQL {
function printHelp(array $argv): void {
printLine("=== WebBase CLI tool ===");
printLine("Usage: ");
var_dump($argv);
printLine("Usage: " . $argv[0]);
}
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);
}
var_dump($command);
$process = proc_open($command, $descriptorSpec, $pipes, null, $env);
if (is_resource($process)) {
@ -655,7 +654,8 @@ function onImpersonate($argv): void {
$session = new \Core\Objects\DatabaseEntity\Session($context, $user);
$session->setData(["2faAuthenticated" => true]);
$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 {
@ -809,7 +809,7 @@ $registeredCommands = [
"test" => ["handler" => "onTest"],
"mail" => ["handler" => "onMail"],
"settings" => ["handler" => "onSettings"],
"impersonate" => ["handler" => "onImpersonate"],
"impersonate" => ["handler" => "onImpersonate", "requiresDocker" => true],
"frontend" => ["handler" => "onFrontend"],
];
@ -820,13 +820,12 @@ if (count($argv) < 2) {
$command = $argv[1];
if (array_key_exists($command, $registeredCommands)) {
// TODO: do we need this?
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) {
$containerName = $dockerYaml["services"]["php"]["container_name"];
printLine("Detected docker environment in config, running docker exec for container: $containerName");
var_dump($argv);
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
exit(proc_close($proc));

@ -54,6 +54,40 @@ export default function AccessControlList(props) {
});
}, [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 = () => {
let rows = [];
@ -75,7 +109,8 @@ export default function AccessControlList(props) {
</TableCell>
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
<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>)}
</TableRow>
);
@ -132,7 +167,9 @@ export default function AccessControlList(props) {
<TableHead>
<TableRow>
<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>
</TableHead>
<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
data={users}
pagination={pagination}
defaultSortOrder={"asc"}
defaultSortColumn={0}
className={"table table-striped"}
fetchData={onFetchUsers}
placeholder={"No users to display"}

@ -253,6 +253,14 @@ export default class API {
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 **/
async getVisitors(type, 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 {formatDate, formatDateTime} from "../util";
import CachedIcon from "@material-ui/icons/Cached";
import {isNumber} from "chart.js/helpers";
export function DataTable(props) {
@ -22,8 +23,8 @@ export function DataTable(props) {
const {translate: L} = useContext(LocaleContext);
const [doFetchData, setFetchData] = useState(false);
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase));
const [sortColumn, setSortColumn] = useState(defaultSortColumn || null);
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase()));
const [sortColumn, setSortColumn] = useState(isNumber(defaultSortColumn) || null);
const sortable = !!fetchData && (props.hasOwnProperty("sortable") ? !!props.sortable : true);
const onRowClick = onClick || (() => {});

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==