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