From dd12e644c7416ce898ef7f9586cf400a317a5bcb Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Tue, 17 Sep 2024 07:48:35 +0000
Subject: [PATCH] feat: adds resources and users policy

---
 .../dbConnectionSelector.tsx                  |  40 ++-
 apps/web/src/components/navbar/navbar.tsx     |  58 ++++-
 libs/shared/lib/components/inputs/index.tsx   |  51 +++-
 .../inputs/toggleSwitch.stories.tsx           |  22 ++
 .../components/tableUI/TableUI.stories.tsx    |  50 ++++
 .../shared/lib/components/tableUI/TableUI.tsx | 142 +++++++++++
 .../UserManagementContent.tsx                 |  96 +++++++
 .../lib/data-access/authorization/index.ts    |   1 +
 .../authorization/useResourcesPolicy.tsx      |  35 +++
 .../store/authorizationResourcesSlice.ts      |  27 ++
 .../store/authorizationUsersSlice.ts          |  35 +++
 libs/shared/lib/data-access/store/hooks.ts    |   8 +
 libs/shared/lib/data-access/store/store.ts    |   4 +
 .../lib/querybuilder/panel/QueryBuilder.tsx   |  29 +++
 .../entitypill/QueryEntityPill.tsx            |  31 ++-
 .../vis/components/VisualizationTabBar.tsx    |  31 ++-
 libs/shared/lib/vis/views/Recommender.tsx     |  34 ++-
 package.json                                  |   1 +
 pnpm-lock.yaml                                | 241 +++++++++++++++++-
 19 files changed, 908 insertions(+), 28 deletions(-)
 create mode 100644 libs/shared/lib/components/inputs/toggleSwitch.stories.tsx
 create mode 100644 libs/shared/lib/components/tableUI/TableUI.stories.tsx
 create mode 100644 libs/shared/lib/components/tableUI/TableUI.tsx
 create mode 100644 libs/shared/lib/components/userManagementContent/UserManagementContent.tsx
 create mode 100644 libs/shared/lib/data-access/authorization/useResourcesPolicy.tsx
 create mode 100644 libs/shared/lib/data-access/store/authorizationResourcesSlice.ts
 create mode 100644 libs/shared/lib/data-access/store/authorizationUsersSlice.ts

diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
index 1ce362fea..659f03e83 100644
--- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
+++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx
@@ -1,5 +1,11 @@
-import React, { useEffect, useState } from 'react';
-import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access';
+import React, { useEffect, useState, useCallback } from 'react';
+import {
+  useAppDispatch,
+  useSchemaGraph,
+  useSessionCache,
+  useAuthorizationCache,
+  useCheckPermissionPolicy,
+} from '@graphpolaris/shared/lib/data-access';
 import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice';
 import { SettingsForm } from './forms/settings';
 import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner';
@@ -56,6 +62,29 @@ export default function DatabaseSelector({}) {
     };
   }, [connecting]);
 
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'database';
+
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
+
   return (
     <div className="menu-walkthrough">
       <TooltipProvider delayDuration={1000}>
@@ -85,11 +114,14 @@ export default function DatabaseSelector({}) {
         >
           <DropdownTrigger
             onClick={() => {
-              setDbSelectionMenuOpen(!dbSelectionMenuOpen);
+              if (connecting || authCache.authorized === false || !!authCache.roomID || writeAllowed) {
+                console.debug('User blocked from editing query due to being a viewer');
+                setDbSelectionMenuOpen(!dbSelectionMenuOpen);
+              }
             }}
             className="w-[18rem]"
             size="md"
-            disabled={connecting || authCache.authorized === false || !!authCache.roomID}
+            disabled={connecting || authCache.authorized === false || !!authCache.roomID || !writeAllowed}
             title={
               <div className="flex items-center">
                 {connecting && session.currentSaveState && session.currentSaveState in session.saveStates ? (
diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx
index f1c9a060d..190fccd27 100644
--- a/apps/web/src/components/navbar/navbar.tsx
+++ b/apps/web/src/components/navbar/navbar.tsx
@@ -8,18 +8,24 @@
 /* The comment above was added so the code coverage wouldn't count this file towards code coverage.
  * We do not test components/renderfunctions/styling files.
  * See testing plan for more details.*/
-import React, { useState, useRef, useEffect } from 'react';
-import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access';
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+import { useAuthorizationCache, useAuth, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
 import DatabaseSelector from './DatabaseManagement/dbConnectionSelector';
 import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
 import GpLogo from './gp-logo';
 import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover';
+import { useDispatch } from 'react-redux';
+import { Dialog, DialogContent, DialogTrigger } from '@graphpolaris/shared/lib/components/layout/Dialog';
+import { UserManagementContent } from '@graphpolaris/shared/lib/components/userManagementContent/UserManagementContent';
+import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 
 export const Navbar = () => {
   const dropdownRef = useRef<HTMLDivElement>(null);
   const auth = useAuth();
   const authCache = useAuthorizationCache();
   const [menuOpen, setMenuOpen] = useState(false);
+  const dispatch = useDispatch();
+  const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
 
   useEffect(() => {
     const handleClickOutside = (event: MouseEvent) => {
@@ -33,8 +39,37 @@ export const Navbar = () => {
     };
   }, [menuOpen]);
 
-  const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'policy';
+
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
+
+  const handleConfirmUsers = (users: { name: string; email: string; type: string }[]) => {
+    //TODO !FIXME: when the user clicks on confirm, users state is ready to be sent to backend
+  };
 
+  const handleClickShareLink = () => {
+    //TODO !FIXME: add copy link to clipoard functionality
+    dispatch(addInfo('Link copied to clipboard'));
+  };
   return (
     <nav className="w-full px-4 h-12 flex flex-row items-center gap-2 md:gap-3 lg:gap-4">
       <a href="https://graphpolaris.com/" target="_blank" className="shrink-0 text-dark">
@@ -57,8 +92,8 @@ export const Navbar = () => {
               <div className="p-2 text-sm border-b">
                 <h2 className="font-bold">user: {authCache.username}</h2>
                 <h3 className="text-xs break-words">session: {authCache.sessionID}</h3>
+                <h3 className="text-xs break-words">license: Creator</h3>
               </div>
-
               {authCache.authorized ? (
                 <>
                   <DropdownItem
@@ -75,7 +110,6 @@ export const Navbar = () => {
                   <DropdownItem value="Login" onClick={() => {}} />
                 </>
               )}
-
               {authCache?.roomID && (
                 <div className="p-2 border-b">
                   <h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3>
@@ -84,6 +118,20 @@ export const Navbar = () => {
               <div className="p-2 border-t">
                 <h3 className="text-xs">Version: {buildInfo}</h3>
               </div>
+              {writeAllowed && (
+                <>
+                  <Dialog>
+                    <DialogTrigger className="ml-2 text-sm hover:bg-secondary-200">Manage Viewers Permission</DialogTrigger>
+                    <DialogContent>
+                      <UserManagementContent
+                        sessionId={authCache.sessionID ?? ''}
+                        onConfirm={handleConfirmUsers}
+                        onClickShareLink={handleClickShareLink}
+                      />
+                    </DialogContent>
+                  </Dialog>
+                </>
+              )}
             </PopoverContent>
           </Popover>
         </div>
diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx
index 9485ddfc8..4f89fed34 100644
--- a/libs/shared/lib/components/inputs/index.tsx
+++ b/libs/shared/lib/components/inputs/index.tsx
@@ -106,7 +106,15 @@ type DropdownProps = {
   onChange?: (value: number | string) => void;
 };
 
-export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps | RadioProps | BooleanProps | NumberProps;
+export type InputProps =
+  | TextProps
+  | SliderProps
+  | CheckboxProps
+  | DropdownProps
+  | RadioProps
+  | BooleanProps
+  | NumberProps
+  | ToggleSwitchProps;
 
 export const Input = (props: InputProps) => {
   switch (props.type) {
@@ -124,6 +132,8 @@ export const Input = (props: InputProps) => {
       return <BooleanInput {...(props as BooleanProps)} />;
     case 'number':
       return <NumberInput {...(props as NumberProps)} />;
+    case 'toggle':
+      return <ToggleSwitchInput {...(props as ToggleSwitchProps)} />;
     default:
       return null;
   }
@@ -523,3 +533,42 @@ export const DropdownInput = ({
     </Tooltip>
   );
 };
+
+type ToggleSwitchProps = {
+  label: string;
+  type: 'toggle';
+  value: boolean;
+  classText?: string;
+  tooltip?: string;
+  onChange?: (value: boolean) => void;
+};
+
+export const ToggleSwitchInput = ({ label, classText, value, tooltip, onChange }: ToggleSwitchProps) => {
+  const [isSelected, setIsSelected] = useState(value);
+
+  const handleClick = () => {
+    const newValue = !isSelected;
+    setIsSelected(newValue);
+    if (onChange) {
+      onChange(newValue);
+    }
+  };
+  return (
+    <Tooltip>
+      <TooltipTrigger>
+        <div className="flex items-center space-x-2">
+          <div
+            className={`relative  flex w-10 h-5 rounded-full cursor-pointer transition-all duration-500 ${isSelected ? 'bg-secondary-800' : 'bg-secondary-300'}`}
+            onClick={handleClick}
+          >
+            <div
+              className={`absolute top-0 left-0 w-5 h-5 rounded-full items-center bg-white transition-all duration-500 shadow-lg border-1 border-gray ${isSelected ? 'translate-x-full' : 'translate-x-0'} `}
+            ></div>
+          </div>
+          <span className={classText}>{label}</span>
+        </div>
+      </TooltipTrigger>
+      {tooltip && <TooltipContent>{tooltip}</TooltipContent>}
+    </Tooltip>
+  );
+};
diff --git a/libs/shared/lib/components/inputs/toggleSwitch.stories.tsx b/libs/shared/lib/components/inputs/toggleSwitch.stories.tsx
new file mode 100644
index 000000000..a02fd23c6
--- /dev/null
+++ b/libs/shared/lib/components/inputs/toggleSwitch.stories.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { ToggleSwitchInput } from '.';
+
+const Component: Meta<typeof ToggleSwitchInput> = {
+  title: 'Components/Inputs',
+  component: ToggleSwitchInput,
+  argTypes: { onChange: {} },
+  decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>],
+};
+
+export default Component;
+type Story = StoryObj<typeof Component>;
+
+export const ToggleSwitchInputStory: Story = {
+  args: {
+    type: 'toggle',
+    label: 'Toggle Switch component',
+    value: false,
+    onChange: (value) => {},
+  },
+};
diff --git a/libs/shared/lib/components/tableUI/TableUI.stories.tsx b/libs/shared/lib/components/tableUI/TableUI.stories.tsx
new file mode 100644
index 000000000..223d53ed7
--- /dev/null
+++ b/libs/shared/lib/components/tableUI/TableUI.stories.tsx
@@ -0,0 +1,50 @@
+import React, { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { TableUI } from './TableUI'; // Import the TableUI component from the correct path
+
+const metaTableUI: Meta<typeof TableUI> = {
+  component: TableUI,
+  title: 'Components/TableUI',
+};
+
+export default metaTableUI;
+
+type Story = StoryObj<typeof TableUI>;
+
+interface SampleData {
+  name: string;
+  age: string;
+  email: string;
+  role: string;
+}
+
+const fieldConfigs = [
+  { key: 'name', label: 'Name', type: 'text' as const, required: true },
+  { key: 'age', label: 'Age', type: 'text' as const, required: true },
+  { key: 'email', label: 'Email', type: 'text' as const, required: true },
+  { key: 'role', label: 'Role', type: 'dropdown' as const, options: ['Admin', 'User', 'Guest'] },
+];
+
+const dropdownOptions = {
+  role: ['Admin', 'User', 'Guest'],
+};
+
+export const MainStory: Story = {
+  render: (args) => {
+    // eslint-disable-next-line react-hooks/rules-of-hooks
+    const [tableData, setTableData] = useState<SampleData[]>([
+      { name: 'John Doe', age: '28', email: 'j.doe@email.com', role: 'Admin' },
+      { name: 'Jane Smith', age: '34', email: 'j.smith@email.com', role: 'User' },
+    ]);
+
+    const handleDataChange = (updatedData: SampleData[]) => {
+      setTableData(updatedData);
+    };
+
+    return <TableUI {...args} data={tableData} onDataChange={handleDataChange} />;
+  },
+  args: {
+    fieldConfigs: fieldConfigs,
+    dropdownOptions: dropdownOptions,
+  },
+};
diff --git a/libs/shared/lib/components/tableUI/TableUI.tsx b/libs/shared/lib/components/tableUI/TableUI.tsx
new file mode 100644
index 000000000..8138d147c
--- /dev/null
+++ b/libs/shared/lib/components/tableUI/TableUI.tsx
@@ -0,0 +1,142 @@
+import React, { useState } from 'react';
+import { Button } from '@graphpolaris/shared/lib/components/buttons';
+import { Input } from '@graphpolaris/shared/lib/components/inputs';
+
+interface FieldConfig<T> {
+  key: keyof T;
+  label: string;
+  type: 'text' | 'dropdown';
+  options?: string[];
+  required?: boolean;
+}
+
+interface TableUIProps<T> {
+  data: T[];
+  onDataChange: (updatedData: T[]) => void;
+  fieldConfigs: FieldConfig<T>[];
+  dropdownOptions: Record<string, string[]>;
+}
+
+export const TableUI = <T extends Record<string, any>>({ data, fieldConfigs, dropdownOptions, onDataChange }: TableUIProps<T>) => {
+  const [editingIndex, setEditingIndex] = useState<number | null>(null);
+  const [editItem, setEditItem] = useState<T | null>(null);
+
+  const handleAddRow = () => {
+    const newItem = {} as T;
+    fieldConfigs.forEach((config) => {
+      newItem[config.key] = config.type === 'dropdown' ? ('' as T[keyof T]) : ('' as T[keyof T]);
+    });
+    onDataChange([...data, newItem]);
+    setEditingIndex(data.length);
+    setEditItem(newItem);
+  };
+
+  const handleDeleteRow = (index: number) => {
+    const updatedData = data.filter((_, i) => i !== index);
+    onDataChange(updatedData);
+  };
+
+  const handleEditRow = (index: number) => {
+    setEditingIndex(index);
+    setEditItem(data[index]);
+  };
+
+  const handleSaveEdit = () => {
+    if (editingIndex !== null && editItem) {
+      const updatedData = data.map((item, index) => (index === editingIndex ? editItem : item));
+      onDataChange(updatedData);
+      setEditingIndex(null);
+      setEditItem(null);
+    }
+  };
+
+  const handleCancelEdit = () => {
+    if (editingIndex === data.length - 1) {
+      onDataChange(data.slice(0, -1));
+    }
+    setEditingIndex(null);
+    setEditItem(null);
+  };
+
+  const handleInputChange = (key: keyof T, value: string) => {
+    setEditItem((prev) => (prev ? { ...prev, [key]: value } : null));
+  };
+
+  return (
+    <div className="mt-2 w-full overflow-x-auto">
+      <div className="flex justify-end mb-4">
+        <Button variant="solid" size="md" label="Add Row" onClick={handleAddRow} />
+      </div>
+      <table className="min-w-full bg-white border border-gray-300 rounded-md">
+        <thead>
+          <tr className="bg-gray-100 border-b">
+            {fieldConfigs.map((field) => (
+              <th key={field.key.toString()} className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
+                {field.label}
+              </th>
+            ))}
+            <th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Actions</th>
+          </tr>
+        </thead>
+        <tbody>
+          {data.map((item, index) => (
+            <tr key={index} className="border-b rounded-md">
+              {fieldConfigs.map((field) => (
+                <td key={field.key.toString()} className="px-6 py-4 text-sm text-gray-700">
+                  {editingIndex === index ? (
+                    field.type === 'dropdown' ? (
+                      <Input
+                        type="dropdown"
+                        label=""
+                        options={dropdownOptions[field.key.toString()] || []}
+                        value={editItem ? editItem[field.key] : ''}
+                        onChange={(value) => handleInputChange(field.key, value.toString())}
+                      />
+                    ) : (
+                      <Input
+                        type="text"
+                        size="xs"
+                        value={editItem ? editItem[field.key] : ''}
+                        required={field.required}
+                        onChange={(e) => handleInputChange(field.key, e)}
+                      />
+                    )
+                  ) : (
+                    item[field.key]
+                  )}
+                </td>
+              ))}
+              <td className="px-6 py-4 text-sm text-gray-700">
+                <div className="flex space-x-2">
+                  {editingIndex === index ? (
+                    <>
+                      <Button variant="solid" size="sm" label="Save" onClick={handleSaveEdit} />
+                      <Button variant="ghost" size="sm" label="Cancel" onClick={handleCancelEdit} />
+                    </>
+                  ) : (
+                    <>
+                      <Button
+                        variant="ghost"
+                        size="lg"
+                        iconComponent={'icon-[ic--baseline-delete-outline]'}
+                        className="text-danger-500"
+                        onClick={() => handleDeleteRow(index)}
+                      />
+                      <Button
+                        variantType="secondary"
+                        variant="ghost"
+                        size="lg"
+                        iconComponent={'icon-[ic--baseline-mode-edit]'}
+                        onClick={() => handleEditRow(index)}
+                      />
+                    </>
+                  )}
+                </div>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+};
diff --git a/libs/shared/lib/components/userManagementContent/UserManagementContent.tsx b/libs/shared/lib/components/userManagementContent/UserManagementContent.tsx
new file mode 100644
index 000000000..7357f76f1
--- /dev/null
+++ b/libs/shared/lib/components/userManagementContent/UserManagementContent.tsx
@@ -0,0 +1,96 @@
+import React, { useState } from 'react';
+import { Button } from '@graphpolaris/shared/lib/components/buttons';
+import { useDialogContext } from '@graphpolaris/shared/lib/components/layout/Dialog';
+import { Input } from '@graphpolaris/shared/lib/components/inputs';
+import { TableUI } from '@graphpolaris/shared/lib/components/tableUI/TableUI';
+import { useUsersPolicy } from '@graphpolaris/shared/lib/data-access/store';
+import { useDispatch } from 'react-redux';
+import { setUsersPolicy, UserPolicy } from '@graphpolaris/shared/lib/data-access/store/authorizationUsersSlice';
+interface UserManagementContentProps {
+  sessionId: string;
+  onConfirm: (users: { name: string; email: string; type: string }[]) => void;
+  onClickShareLink: () => void;
+}
+
+interface FieldConfig<T> {
+  key: keyof T;
+  label: string;
+  type: 'text' | 'dropdown';
+  required?: boolean;
+}
+
+export const UserManagementContent: React.FC<UserManagementContentProps> = ({ sessionId, onConfirm, onClickShareLink }) => {
+  const { setOpen } = useDialogContext();
+  const dispatch = useDispatch();
+
+  // !FIXME: This should definited at high level
+  const optionsTypeUser = ['', 'Creator', 'Viewer'];
+  const [copiedAccessOption, setCopiedAccessOption] = useState<string>(optionsTypeUser[2]);
+
+  const usersPolicy = useUsersPolicy();
+  // !FIXME: This should be populated from the store
+  const userFields = [
+    { key: 'name', label: 'Name', type: 'text', required: true },
+    { key: 'email', label: 'Email', type: 'text', required: true },
+    { key: 'type', label: 'Type', type: 'dropdown', required: true },
+  ] as FieldConfig<UserPolicy>[];
+
+  const options = {
+    type: optionsTypeUser,
+  };
+
+  const handleUserChange = (newUsers: UserPolicy[]) => {
+    dispatch(setUsersPolicy({ users: newUsers }));
+  };
+
+  const handleCancel = () => {
+    setOpen(false);
+  };
+
+  const handleClickShare = () => {
+    setOpen(false);
+    onClickShareLink();
+  };
+
+  return (
+    <div className="flex flex-col w-[50rem] h-[40rem]">
+      <div className="flex-grow justify-center p-2 text-sm overflow-hidden text-ellipsis whitespace-nowrap">
+        <h1 className="flex justify-center font-bold gap-x-1">
+          <span>Manage Users Sharing of</span> <span className="text-secondary-500">{sessionId}</span>
+        </h1>
+      </div>
+
+      <div className="flex flex-row items-center justify-center gap-2 mt-4 w-full">
+        <span>By sharing this link recipient will have </span>
+        <div>
+          <Input
+            type="dropdown"
+            label=""
+            value={copiedAccessOption}
+            options={optionsTypeUser}
+            onChange={(value) => setCopiedAccessOption(value.toString())}
+          />
+        </div>
+        <span>access</span>
+        <Button variant="solid" size="md" label="Copy link" onClick={handleClickShare} />
+      </div>
+
+      <div className="flex items-center my-4">
+        <hr className="flex-grow border-t border-gray-300" />
+        <span className="mx-4 text-gray-500">or</span>
+        <hr className="flex-grow border-t border-gray-300" />
+      </div>
+
+      <div className="flex flex-col items-center flex-grow mt-4">
+        <TableUI data={usersPolicy.users} fieldConfigs={userFields} dropdownOptions={options} onDataChange={handleUserChange} />
+      </div>
+
+      <div className="flex justify-center p-2 mt-auto">
+        <div className="flex space-x-4">
+          <Button variant="outline" size="md" label="Cancel" onClick={handleCancel} />
+          <Button variantType="primary" size="md" label="Confirm" onClick={() => onConfirm(usersPolicy.users)} />
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/libs/shared/lib/data-access/authorization/index.ts b/libs/shared/lib/data-access/authorization/index.ts
index d9ae7204e..a9af457e9 100644
--- a/libs/shared/lib/data-access/authorization/index.ts
+++ b/libs/shared/lib/data-access/authorization/index.ts
@@ -1 +1,2 @@
 export * from './useAuth';
+export * from './useResourcesPolicy';
diff --git a/libs/shared/lib/data-access/authorization/useResourcesPolicy.tsx b/libs/shared/lib/data-access/authorization/useResourcesPolicy.tsx
new file mode 100644
index 000000000..d7390ba3a
--- /dev/null
+++ b/libs/shared/lib/data-access/authorization/useResourcesPolicy.tsx
@@ -0,0 +1,35 @@
+import { useResourcesPolicy } from '@graphpolaris/shared/lib/data-access';
+import { useMemo } from 'react';
+//import casbinjs from 'casbin.js';
+import { Authorizer } from 'casbin.js';
+export const useCheckPermissionPolicy = () => {
+  const policyPermissions = useResourcesPolicy();
+
+  const authorizer = useMemo(() => {
+    // docs tell to go this way, but it doesn't work
+    //const auth = new casbinjs.Authorizer('manual');
+    const auth = new Authorizer('manual');
+
+    const permission = {
+      read: policyPermissions.read,
+      write: policyPermissions.write,
+    };
+
+    auth.setPermission(permission);
+
+    return auth;
+  }, [policyPermissions]);
+
+  const canRead = async (resource: string) => {
+    return await authorizer.can('read', resource);
+  };
+
+  const canWrite = async (resource: string) => {
+    return await authorizer.can('write', resource);
+  };
+
+  return {
+    canRead,
+    canWrite,
+  };
+};
diff --git a/libs/shared/lib/data-access/store/authorizationResourcesSlice.ts b/libs/shared/lib/data-access/store/authorizationResourcesSlice.ts
new file mode 100644
index 000000000..efbe39f83
--- /dev/null
+++ b/libs/shared/lib/data-access/store/authorizationResourcesSlice.ts
@@ -0,0 +1,27 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import type { RootState } from './store';
+
+export interface PolicyResourcesState {
+  read: string[];
+  write: string[];
+}
+
+//TODO !FIXME: add middleware to fetch resources from backend
+const initialState: PolicyResourcesState = {
+  read: ['database', 'query', 'visualization', 'policy'],
+  write: ['database', 'query', 'visualization', 'policy'],
+};
+
+export const policyResourcesSlice = createSlice({
+  name: 'policyResources',
+  initialState,
+  reducers: {
+    getResourcesPolicy: (state, action: PayloadAction<PolicyResourcesState>) => {
+      return action.payload;
+    },
+  },
+});
+
+export const { getResourcesPolicy } = policyResourcesSlice.actions;
+export default policyResourcesSlice.reducer;
+export const selectResourcesPolicyState = (state: RootState) => state.policyResources;
diff --git a/libs/shared/lib/data-access/store/authorizationUsersSlice.ts b/libs/shared/lib/data-access/store/authorizationUsersSlice.ts
new file mode 100644
index 000000000..d6398b9e0
--- /dev/null
+++ b/libs/shared/lib/data-access/store/authorizationUsersSlice.ts
@@ -0,0 +1,35 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import type { RootState } from './store';
+
+export interface UserPolicy {
+  name: string;
+  email: string;
+  type: string;
+}
+
+export interface PolicyUsersState {
+  users: UserPolicy[];
+}
+
+//TODO !FIXME: add middleware to fetch users from backend
+const initialState: PolicyUsersState = {
+  users: [
+    { name: 'Scheper. W', email: 'user1@companyA.com', type: 'creator' },
+    { name: 'Smit. S', email: 'user2@companyB.com', type: 'viewer' },
+    { name: 'De Jong. B', email: 'user3@companyC.com', type: 'creator' },
+  ],
+};
+
+export const policyUsersSlice = createSlice({
+  name: 'policyUsers',
+  initialState,
+  reducers: {
+    setUsersPolicy: (state, action: PayloadAction<PolicyUsersState>) => {
+      return action.payload;
+    },
+  },
+});
+
+export const { setUsersPolicy } = policyUsersSlice.actions;
+export default policyUsersSlice.reducer;
+export const selectPolicyState = (state: RootState) => state.policyUsers;
diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts
index a9e0dd94f..9680186ed 100644
--- a/libs/shared/lib/data-access/store/hooks.ts
+++ b/libs/shared/lib/data-access/store/hooks.ts
@@ -38,6 +38,8 @@ import { SchemaGraph, SchemaGraphInference, SchemaGraphStats } from '../../schem
 import { GraphMetadata } from '../statistics';
 import { SelectionStateI, FocusStateI, focusState, selectionState } from './interactionSlice';
 import { VisualizationSettingsType } from '../../vis/common';
+import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice';
+import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice';
 
 // Use throughout your app instead of plain `useDispatch` and `useSelector`
 export const useAppDispatch: () => AppDispatch = useDispatch;
@@ -85,3 +87,9 @@ export const useActiveVisualization: () => VisualizationSettingsType | undefined
 // Interaction Slices
 export const useSelection: () => SelectionStateI | undefined = () => useAppSelector(selectionState);
 export const useFocus: () => FocusStateI | undefined = () => useAppSelector(focusState);
+
+// Authorization Users Slice
+export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selectPolicyState);
+
+// Authorization Resources Slice
+export const useResourcesPolicy: () => PolicyResourcesState = () => useAppSelector(selectResourcesPolicyState);
diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts
index ec6bce793..c1aea4606 100644
--- a/libs/shared/lib/data-access/store/store.ts
+++ b/libs/shared/lib/data-access/store/store.ts
@@ -9,6 +9,8 @@ import mlSlice from './mlSlice';
 import searchResultSlice from './searchResultSlice';
 import visualizationSlice from './visualizationSlice';
 import interactionSlice from './interactionSlice';
+import policyUsersSlice from './authorizationUsersSlice';
+import policyPermissionSlice from './authorizationResourcesSlice';
 
 export const store = configureStore({
   reducer: {
@@ -22,6 +24,8 @@ export const store = configureStore({
     searchResults: searchResultSlice,
     interaction: interactionSlice,
     visualize: visualizationSlice,
+    policyUsers: policyUsersSlice,
+    policyResources: policyPermissionSlice,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware({
diff --git a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
index 2c725687a..645bea816 100644
--- a/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
+++ b/libs/shared/lib/querybuilder/panel/QueryBuilder.tsx
@@ -8,6 +8,7 @@ import {
   useSchemaInference,
   useSearchResultQB,
 } from '@graphpolaris/shared/lib/data-access/store';
+import { useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
 import { clearQB, setQuerybuilderGraphology, toQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useDispatch } from 'react-redux';
@@ -83,6 +84,28 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
   const searchResults = useSearchResultQB();
   const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
   const [allowZoom, setAllowZoom] = useState(true);
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'query';
+
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
 
   useEffect(() => {
     const searchResultKeys = new Set([...searchResults.nodes.map((node) => node.key), ...searchResults.edges.map((edge) => edge.key)]);
@@ -223,6 +246,12 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
    * @param event Drag event.
    */
   const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
+    if (!writeAllowed) {
+      console.debug('User blocked from editing query due to being a viewer');
+      event.preventDefault();
+      return;
+    }
+
     event.preventDefault();
 
     // The dropped element should be a valid reactflow element
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
index 993108329..7d4c3c8b6 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
@@ -1,5 +1,5 @@
-import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access';
-import React, { useMemo, useRef, useState } from 'react';
+import { useQuerybuilderGraph, useCheckPermissionPolicy } from '@graphpolaris/shared/lib/data-access';
+import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
 import { Handle, Position, useUpdateNodeInternals } from 'reactflow';
 import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model';
 import { PillDropdown } from '../../pilldropdown/PillDropdown';
@@ -24,7 +24,28 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
   );
 
   const [openDropdown, setOpenDropdown] = useState(false);
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'query';
 
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
   return (
     <div className="w-fit h-fit nowheel" ref={ref} id="asd">
       <EntityPill
@@ -52,6 +73,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
             position={Position.Left}
             className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
             type="target"
+            style={{
+              pointerEvents: writeAllowed ? 'auto' : 'none',
+            }}
           ></Handle>
         }
         handleRight={
@@ -61,6 +85,9 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
             position={Position.Right}
             className={'!rounded-none !bg-transparent !w-full !h-full !border-0 !right-0 !left-0'}
             type="source"
+            style={{
+              pointerEvents: writeAllowed ? 'auto' : 'none',
+            }}
           ></Handle>
         }
       >
diff --git a/libs/shared/lib/vis/components/VisualizationTabBar.tsx b/libs/shared/lib/vis/components/VisualizationTabBar.tsx
index 4af49ca41..35a69a8ad 100644
--- a/libs/shared/lib/vis/components/VisualizationTabBar.tsx
+++ b/libs/shared/lib/vis/components/VisualizationTabBar.tsx
@@ -1,9 +1,9 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { Button, DropdownContainer, DropdownItem, DropdownItemContainer, DropdownTrigger } from '../../components';
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip';
 import { ControlContainer } from '../../components/controls';
 import { Tabs, Tab } from '../../components/tabs';
-import { useAppDispatch, useVisualization } from '../../data-access';
+import { useAppDispatch, useVisualization, useCheckPermissionPolicy } from '../../data-access';
 import { addVisualization, removeVisualization, reorderVisState, setActiveVisualization } from '../../data-access/store/visualizationSlice';
 import { Visualizations } from './VisualizationPanel';
 
@@ -12,6 +12,29 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
   const [open, setOpen] = useState(false);
   const dispatch = useAppDispatch();
 
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'visualization';
+
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
+
   const handleDragStart = (e: React.DragEvent<HTMLDivElement>, i: number) => {
     e.dataTransfer.setData('text/plain', i.toString());
   };
@@ -90,14 +113,16 @@ export default function VisualizationTabBar(props: { fullSize: () => void; expor
           <Tooltip>
             <TooltipTrigger>
               <DropdownContainer open={open} onOpenChange={setOpen}>
-                <DropdownTrigger onClick={() => setOpen((v) => !v)}>
+                <DropdownTrigger disabled={!writeAllowed} onClick={() => setOpen((v) => !v)}>
                   <Button
                     as={'a'}
                     variantType="secondary"
                     variant="ghost"
                     size="xs"
+                    disabled={true}
                     iconComponent="icon-[ic--baseline-add]"
                     onClick={() => {}}
+                    className={`${writeAllowed ? 'cursor-pointer' : 'cursor-not-allowed'}`}
                   />
                 </DropdownTrigger>
                 <DropdownItemContainer>
diff --git a/libs/shared/lib/vis/views/Recommender.tsx b/libs/shared/lib/vis/views/Recommender.tsx
index 74c730298..9fb611c29 100644
--- a/libs/shared/lib/vis/views/Recommender.tsx
+++ b/libs/shared/lib/vis/views/Recommender.tsx
@@ -1,7 +1,7 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useCallback } from 'react';
 import Info from '../../components/info';
 import { addVisualization } from '../../data-access/store/visualizationSlice';
-import { useAppDispatch } from '../../data-access';
+import { useAppDispatch, useCheckPermissionPolicy } from '../../data-access';
 import { Visualizations } from '../components/VisualizationPanel';
 
 type VisualizationDescription = {
@@ -14,6 +14,29 @@ export function Recommender() {
   const dispatch = useAppDispatch();
   const [visualizationDescriptions, setVisualizationDescriptions] = useState<VisualizationDescription[]>([]);
 
+  const { canRead, canWrite } = useCheckPermissionPolicy();
+  const [readAllowed, setReadAllowed] = useState(false);
+  const [writeAllowed, setWriteAllowed] = useState(false);
+  const resource = 'visualization';
+
+  const checkReadPermission = useCallback(async () => {
+    const result = await canRead(resource);
+    setReadAllowed(result);
+  }, [canRead]);
+
+  const checkWritePermission = useCallback(async () => {
+    const result = await canWrite(resource);
+    setWriteAllowed(result);
+  }, [canWrite]);
+
+  useEffect(() => {
+    checkReadPermission();
+  }, [checkReadPermission]);
+
+  useEffect(() => {
+    checkWritePermission();
+  }, [checkWritePermission]);
+
   useEffect(() => {
     const loadVisualizations = async () => {
       const descriptions = await Promise.all(
@@ -40,9 +63,14 @@ export function Recommender() {
         {visualizationDescriptions.map(({ name, displayName, description }) => (
           <div
             key={name}
-            className="p-4 cursor-pointer border hover:bg-secondary-100"
+            className={`p-4 border ${writeAllowed ? 'cursor-pointer hover:bg-secondary-100' : 'cursor-not-allowed opacity-50'}`}
             onClick={async (e) => {
               e.preventDefault();
+              if (!writeAllowed) {
+                console.debug('User blocked from editing query due to being a viewer');
+                return;
+              }
+
               const component = await Visualizations[name]();
               dispatch(addVisualization({ ...component.default.settings, name: name, id: name }));
             }}
diff --git a/package.json b/package.json
index 8191ed01a..8e93d8295 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
     "node": ">=14.0.0"
   },
   "dependencies": {
+    "casbin.js": "^0.5.1",
     "html2canvas": "^1.4.1"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 58f63ae1a..df12bd0d0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
 
   .:
     dependencies:
+      casbin.js:
+        specifier: ^0.5.1
+        version: 0.5.1(webpack@5.90.3)
       html2canvas:
         specifier: ^1.4.1
         version: 1.4.1
@@ -3815,11 +3818,24 @@ packages:
     resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
     engines: {node: '>=8'}
 
+  ajv-formats@2.1.1:
+    resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
+    peerDependencies:
+      ajv: ^8.0.0
+    peerDependenciesMeta:
+      ajv:
+        optional: true
+
   ajv-keywords@3.5.2:
     resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
     peerDependencies:
       ajv: ^6.9.1
 
+  ajv-keywords@5.1.0:
+    resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==}
+    peerDependencies:
+      ajv: ^8.8.2
+
   ajv@6.12.6:
     resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
 
@@ -3974,10 +3990,16 @@ packages:
   avsdf-base@1.0.0:
     resolution: {integrity: sha512-APhZNUFJwIwrLsSfE95QjobEntdUhFQgfNtC/BrYmjUpwHh5Y2fbRv8lxAlMr1hdf/CuQYsqJxK3dRzcCL77qw==}
 
+  await-lock@2.2.2:
+    resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
+
   axe-core@4.7.0:
     resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==}
     engines: {node: '>=4'}
 
+  axios@0.21.4:
+    resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
+
   axobject-query@3.2.1:
     resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
 
@@ -3986,6 +4008,18 @@ packages:
     peerDependencies:
       '@babel/core': ^7.0.0-0
 
+<<<<<<< HEAD
+  babel-loader@9.2.1:
+    resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==}
+=======
+  babel-loader@9.1.3:
+    resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==}
+>>>>>>> 136e84467234d1cabea8a266f0577ecda55aabaa
+    engines: {node: '>= 14.15.0'}
+    peerDependencies:
+      '@babel/core': ^7.12.0
+      webpack: '>=5'
+
   babel-plugin-macros@3.1.0:
     resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
     engines: {node: '>=10', npm: '>=6'}
@@ -4083,6 +4117,9 @@ packages:
   buffer@5.7.1:
     resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
 
+  buffer@6.0.3:
+    resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
   busboy@1.6.0:
     resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
     engines: {node: '>=10.16.0'}
@@ -4127,6 +4164,12 @@ packages:
   cartocolor@5.0.2:
     resolution: {integrity: sha512-Ihb/wU5V6BVbHwapd8l/zg7bnhZ4YPFVfa7quSpL86lfkPJSf4YuNBT+EvesPRP5vSqhl6vZVsQJwCR8alBooQ==}
 
+  casbin-core@0.0.0-beta.2:
+    resolution: {integrity: sha512-yHkpGOPvGsjbfm4xtmnbuMzwuLjhz5tvTKptDAQmSl/UbGJPa+/+wBwSD+rQQ9AJ93cyyMSapweesWfqSKljPQ==}
+
+  casbin.js@0.5.1:
+    resolution: {integrity: sha512-CfNMe3hxNwQhgAGCT9SV1kvzD7GrtB49ECIJBPLqdCHrGoEg+noH7K10WjRUa/JUuC7Fgp16wYt138LY71wtVg==}
+
   chai@4.4.1:
     resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
     engines: {node: '>=4'}
@@ -4282,6 +4325,9 @@ packages:
     resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
     engines: {node: ^12.20.0 || >=14}
 
+  common-path-prefix@3.0.0:
+    resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==}
+
   commondir@1.0.1:
     resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
 
@@ -5145,6 +5191,10 @@ packages:
     resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
     engines: {node: '>= 0.10.0'}
 
+  expression-eval@4.0.0:
+    resolution: {integrity: sha512-YHSnLTyIb9IKaho2IdQbvlei/pElxnGm48UgaXJ1Fe5au95Ck0R9ftm6rHJQuKw3FguZZ4eXVllJFFFc7LX0WQ==}
+    deprecated: The expression-eval npm package is no longer maintained. The package was originally published as part of a now-completed personal project, and I do not have incentives to continue maintenance.
+
   expression-eval@5.0.1:
     resolution: {integrity: sha512-7SL4miKp19lI834/F6y156xlNg+i9Q41tteuGNCq9C06S78f1bm3BXuvf0+QpQxv369Pv/P2R7Hb17hzxLpbDA==}
     deprecated: The expression-eval npm package is no longer maintained. The package was originally published as part of a now-completed personal project, and I do not have incentives to continue maintenance.
@@ -5222,6 +5272,10 @@ packages:
     resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
     engines: {node: '>=8'}
 
+  find-cache-dir@4.0.0:
+    resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==}
+    engines: {node: '>=14.16'}
+
   find-root@1.1.0:
     resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
 
@@ -5237,6 +5291,10 @@ packages:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
 
+  find-up@6.3.0:
+    resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
   find-up@7.0.0:
     resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==}
     engines: {node: '>=18'}
@@ -5260,6 +5318,15 @@ packages:
   focus-trap@7.5.4:
     resolution: {integrity: sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==}
 
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
   for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
 
@@ -5948,6 +6015,9 @@ packages:
   jju@1.4.0:
     resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
 
+  js-cookie@2.2.1:
+    resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
+
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -6838,6 +6908,10 @@ packages:
     resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
     engines: {node: '>=10'}
 
+  pkg-dir@7.0.0:
+    resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==}
+    engines: {node: '>=14.16'}
+
   pkg-types@1.0.3:
     resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
 
@@ -7465,6 +7539,10 @@ packages:
     resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
     engines: {node: '>= 10.13.0'}
 
+  schema-utils@4.2.0:
+    resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==}
+    engines: {node: '>= 12.13.0'}
+
   scroll@3.0.1:
     resolution: {integrity: sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==}
 
@@ -7877,9 +7955,12 @@ packages:
   tinybench@2.6.0:
     resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
 
+<<<<<<< HEAD
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
+=======
+>>>>>>> 136e84467234d1cabea8a266f0577ecda55aabaa
   tinypool@0.8.2:
     resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
     engines: {node: '>=14.0.0'}
@@ -8722,7 +8803,7 @@ snapshots:
       '@babel/core': 7.24.0
       '@babel/helper-compilation-targets': 7.23.6
       '@babel/helper-plugin-utils': 7.24.0
-      debug: 4.3.4
+      debug: 4.3.7
       lodash.debounce: 4.0.8
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -12787,10 +12868,19 @@ snapshots:
       clean-stack: 2.2.0
       indent-string: 4.0.0
 
+  ajv-formats@2.1.1(ajv@8.12.0):
+    optionalDependencies:
+      ajv: 8.12.0
+
   ajv-keywords@3.5.2(ajv@6.12.6):
     dependencies:
       ajv: 6.12.6
 
+  ajv-keywords@5.1.0(ajv@8.12.0):
+    dependencies:
+      ajv: 8.12.0
+      fast-deep-equal: 3.1.3
+
   ajv@6.12.6:
     dependencies:
       fast-deep-equal: 3.1.3
@@ -12979,8 +13069,16 @@ snapshots:
     dependencies:
       layout-base: 1.0.2
 
+  await-lock@2.2.2: {}
+
   axe-core@4.7.0: {}
 
+  axios@0.21.4:
+    dependencies:
+      follow-redirects: 1.15.9
+    transitivePeerDependencies:
+      - debug
+
   axobject-query@3.2.1:
     dependencies:
       dequal: 2.0.3
@@ -12989,6 +13087,17 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.0
 
+<<<<<<< HEAD
+  babel-loader@9.2.1(@babel/core@7.24.0)(webpack@5.90.3):
+=======
+  babel-loader@9.1.3(@babel/core@7.24.0)(webpack@5.90.3):
+>>>>>>> 136e84467234d1cabea8a266f0577ecda55aabaa
+    dependencies:
+      '@babel/core': 7.24.0
+      find-cache-dir: 4.0.0
+      schema-utils: 4.2.0
+      webpack: 5.90.3
+
   babel-plugin-macros@3.1.0:
     dependencies:
       '@babel/runtime': 7.24.0
@@ -13106,6 +13215,11 @@ snapshots:
       base64-js: 1.5.1
       ieee754: 1.2.1
 
+  buffer@6.0.3:
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+
   busboy@1.6.0:
     dependencies:
       streamsearch: 1.1.0
@@ -13153,6 +13267,30 @@ snapshots:
     dependencies:
       colorbrewer: 1.5.6
 
+  casbin-core@0.0.0-beta.2:
+    dependencies:
+      await-lock: 2.2.2
+      buffer: 6.0.3
+      expression-eval: 4.0.0
+      minimatch: 5.1.6
+
+  casbin.js@0.5.1(webpack@5.90.3):
+    dependencies:
+      '@babel/core': 7.24.0
+      '@babel/preset-env': 7.24.0(@babel/core@7.24.0)
+      axios: 0.21.4
+<<<<<<< HEAD
+      babel-loader: 9.2.1(@babel/core@7.24.0)(webpack@5.90.3)
+=======
+      babel-loader: 9.1.3(@babel/core@7.24.0)(webpack@5.90.3)
+>>>>>>> 136e84467234d1cabea8a266f0577ecda55aabaa
+      casbin-core: 0.0.0-beta.2
+      js-cookie: 2.2.1
+    transitivePeerDependencies:
+      - debug
+      - supports-color
+      - webpack
+
   chai@4.4.1:
     dependencies:
       assertion-error: 1.1.0
@@ -13302,6 +13440,8 @@ snapshots:
   commander@9.5.0:
     optional: true
 
+  common-path-prefix@3.0.0: {}
+
   commondir@1.0.1: {}
 
   compare-func@2.0.0:
@@ -14147,8 +14287,8 @@ snapshots:
       '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+      eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
       eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
       eslint-plugin-react: 7.34.0(eslint@8.57.0)
       eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -14175,13 +14315,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0):
+  eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0):
     dependencies:
       debug: 4.3.4
       enhanced-resolve: 5.16.0
       eslint: 8.57.0
-      eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+      eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)
       fast-glob: 3.3.2
       get-tsconfig: 4.7.3
       is-core-module: 2.13.1
@@ -14192,14 +14332,14 @@ snapshots:
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
+  eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
     dependencies:
       debug: 3.2.7
     optionalDependencies:
       '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
+      eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -14213,7 +14353,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
+  eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
     dependencies:
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.4
@@ -14223,7 +14363,7 @@ snapshots:
       doctrine: 2.1.0
       eslint: 8.57.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+      eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
       hasown: 2.0.1
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -14471,6 +14611,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  expression-eval@4.0.0:
+    dependencies:
+      jsep: 0.3.5
+
   expression-eval@5.0.1:
     dependencies:
       jsep: 0.3.5
@@ -14570,6 +14714,11 @@ snapshots:
       make-dir: 3.1.0
       pkg-dir: 4.2.0
 
+  find-cache-dir@4.0.0:
+    dependencies:
+      common-path-prefix: 3.0.0
+      pkg-dir: 7.0.0
+
   find-root@1.1.0: {}
 
   find-up@3.0.0:
@@ -14586,6 +14735,11 @@ snapshots:
       locate-path: 6.0.0
       path-exists: 4.0.0
 
+  find-up@6.3.0:
+    dependencies:
+      locate-path: 7.2.0
+      path-exists: 5.0.0
+
   find-up@7.0.0:
     dependencies:
       locate-path: 7.2.0
@@ -14614,6 +14768,8 @@ snapshots:
     dependencies:
       tabbable: 6.2.0
 
+  follow-redirects@1.15.9: {}
+
   for-each@0.3.3:
     dependencies:
       is-callable: 1.2.7
@@ -15281,6 +15437,8 @@ snapshots:
 
   jju@1.4.0: {}
 
+  js-cookie@2.2.1: {}
+
   js-tokens@4.0.0: {}
 
   js-tokens@8.0.3: {}
@@ -16094,6 +16252,10 @@ snapshots:
     dependencies:
       find-up: 5.0.0
 
+  pkg-dir@7.0.0:
+    dependencies:
+      find-up: 6.3.0
+
   pkg-types@1.0.3:
     dependencies:
       jsonc-parser: 3.2.1
@@ -16815,6 +16977,13 @@ snapshots:
       ajv: 6.12.6
       ajv-keywords: 3.5.2(ajv@6.12.6)
 
+  schema-utils@4.2.0:
+    dependencies:
+      '@types/json-schema': 7.0.15
+      ajv: 8.12.0
+      ajv-formats: 2.1.1(ajv@8.12.0)
+      ajv-keywords: 5.1.0(ajv@8.12.0)
+
   scroll@3.0.1: {}
 
   scrollparent@2.1.0: {}
@@ -17244,6 +17413,24 @@ snapshots:
     optionalDependencies:
       '@swc/core': 1.4.2(@swc/helpers@0.5.2)
 
+  terser-webpack-plugin@5.3.10(webpack@5.90.3):
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.25
+      jest-worker: 27.5.1
+      schema-utils: 3.3.0
+      serialize-javascript: 6.0.2
+      terser: 5.29.2
+      webpack: 5.90.3
+
+  terser-webpack-plugin@5.3.10(webpack@5.90.3):
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.25
+      jest-worker: 27.5.1
+      schema-utils: 3.3.0
+      serialize-javascript: 6.0.2
+      terser: 5.29.2
+      webpack: 5.90.3
+
   terser@5.29.2:
     dependencies:
       '@jridgewell/source-map': 0.3.6
@@ -17287,8 +17474,11 @@ snapshots:
 
   tinybench@2.6.0: {}
 
+<<<<<<< HEAD
   tinybench@2.9.0: {}
 
+=======
+>>>>>>> 136e84467234d1cabea8a266f0577ecda55aabaa
   tinypool@0.8.2: {}
 
   tinypool@1.0.1: {}
@@ -17837,6 +18027,37 @@ snapshots:
 
   webpack-virtual-modules@0.6.1: {}
 
+  webpack@5.90.3:
+    dependencies:
+      '@types/eslint-scope': 3.7.7
+      '@types/estree': 1.0.5
+      '@webassemblyjs/ast': 1.12.1
+      '@webassemblyjs/wasm-edit': 1.12.1
+      '@webassemblyjs/wasm-parser': 1.12.1
+      acorn: 8.11.3
+      acorn-import-assertions: 1.9.0(acorn@8.11.3)
+      browserslist: 4.23.0
+      chrome-trace-event: 1.0.3
+      enhanced-resolve: 5.16.0
+      es-module-lexer: 1.4.1
+      eslint-scope: 5.1.1
+      events: 3.3.0
+      glob-to-regexp: 0.4.1
+      graceful-fs: 4.2.11
+      json-parse-even-better-errors: 2.3.1
+      loader-runner: 4.3.0
+      mime-types: 2.1.35
+      neo-async: 2.6.2
+      schema-utils: 3.3.0
+      tapable: 2.2.1
+      terser-webpack-plugin: 5.3.10(webpack@5.90.3)
+      watchpack: 2.4.1
+      webpack-sources: 3.2.3
+    transitivePeerDependencies:
+      - '@swc/core'
+      - esbuild
+      - uglify-js
+
   webpack@5.90.3(@swc/core@1.4.2(@swc/helpers@0.5.2)):
     dependencies:
       '@types/eslint-scope': 3.7.7
-- 
GitLab