implemented permissions

This commit is contained in:
Anton 2023-09-05 16:45:01 +02:00
parent 42fe4fae4e
commit 0138f20b97
15 changed files with 118 additions and 62 deletions

View File

@ -3,7 +3,7 @@ import NavBar from './NavBar'
import { BrowserRouter } from 'react-router-dom'
import { ToastContainer } from 'react-toastify'
export function AppShell({ appName, useAuth, children }) {
export function AppShell({ appName, children }) {
return (
<BrowserRouter>
<ToastContainer position="top-right"
@ -16,7 +16,7 @@ export function AppShell({ appName, useAuth, children }) {
draggable
pauseOnHover
theme="light" />
<NavBar appName={appName} useAuth={useAuth}></NavBar>
<NavBar appName={appName}></NavBar>
<div id="app-content" className="tw-flex tw-!pl-[77px]">
{children}
</div>

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
//import { useAuth } from "../api/auth";
import { useAuth } from "../Auth"
import { Link } from "react-router-dom";
import { toast } from "react-toastify";
import QuestionMarkIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon'
@ -8,7 +8,7 @@ import * as React from "react";
import DialogModal from "./DialogModal";
export default function NavBar({ appName, useAuth }: { appName: string, useAuth: any }) {
export default function NavBar({ appName}: { appName: string }) {
const [signupOpen, setSignupOpen] = useState(false);
@ -110,7 +110,7 @@ export default function NavBar({ appName, useAuth }: { appName: string, useAuth:
{isAuthenticated ?
<div className="tw-flex-none">
{user.avatar ? <div className="tw-avatar">
{user?.avatar ? <div className="tw-avatar">
<div className="tw-w-10 tw-rounded-full">
<img src={"https://api.utopia-lab.org/assets/" + user?.avatar + "?access_token=" + token} />
</div>
@ -122,7 +122,7 @@ export default function NavBar({ appName, useAuth }: { appName: string, useAuth:
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</label>
<ul tabIndex={0} className="tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[1500]">
<ul tabIndex={0} className="tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[10000]">
<li><Link to={"/settings"}>Settings</Link></li>
<li><a onClick={() => { onLogout() }}>Logout</a></li>
</ul>
@ -148,7 +148,7 @@ export default function NavBar({ appName, useAuth }: { appName: string, useAuth:
</label>
<ul tabIndex={1} className="tw-menu tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[1500]">
<ul tabIndex={1} className="tw-menu tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[10000]">
<li><a onClick={() => {setLoginOpen(true)}}>Login</a></li>
<li><a onClick={() => setSignupOpen(true)}>Sign Up</a></li>
</ul>

View File

@ -1,4 +1,4 @@
import {useState, useRef} from 'react'
import {useState} from 'react'
import {Link} from 'react-router-dom'
import ErrorText from '../Typography/ErrorText'
import {TextInput} from '../Input/TextInput'

View File

@ -1,4 +1,4 @@
import { useState, useRef } from 'react'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import ErrorText from '../Typography/ErrorText'
import {TextInput} from '../Input/TextInput'

View File

@ -22,7 +22,7 @@ type AuthContextProps = {
login: (credentials: AuthCredentials) => Promise<UserItem | undefined>,
register: (credentials: AuthCredentials, userName: string) => Promise<UserItem | undefined>,
loading: Boolean,
logout: () => void,
logout: () => Promise<any>,
updateUser: (user: UserItem) => any,
token: String | null
}
@ -33,7 +33,7 @@ const AuthContext = createContext<AuthContextProps>({
login: () => Promise.reject(),
register: () => Promise.reject(),
loading: false,
logout: () => { },
logout: () => Promise.reject(),
updateUser: () => Promise.reject(),
token: ""
});

View File

@ -23,7 +23,6 @@ export const Layer = (props: LayerProps) => {
const searchPhrase = useSearchPhrase();
useEffect(() => {
resetItems(props);
props.data && setItemsData(props);

View File

@ -1,16 +1,22 @@
import * as React from 'react'
import { useEffect } from 'react';
import { ItemsApi, Permission } from '../../types';
import { useSetPermissionData, useSetPermissionApi } from './hooks/usePermissions'
import { useSetPermissionData, useSetPermissionApi, useSetAdminRole } from './hooks/usePermissions'
import { useAuth } from '../Auth';
export function Permissions({data, api} : {data?: Permission[], api?: ItemsApi<Permission>}) {
export function Permissions({data, api, adminRole} : {data?: Permission[], api?: ItemsApi<Permission>, adminRole?: string}) {
const setPermissionData = useSetPermissionData();
const setPermissionApi = useSetPermissionApi();
const setAdminRole = useSetAdminRole();
const {user} = useAuth();
useEffect(() => {
console.log(adminRole);
adminRole && setAdminRole(adminRole);
data && setPermissionData(data);
api && setPermissionApi(api);
}, [api, data])
}, [api, data, adminRole, user])
return (
<></>

View File

@ -1,11 +1,14 @@
import * as React from 'react'
import DynamicHeroIcon from '../../../Utils/DynamicHeroIcon'
import { useLayers } from '../hooks/useLayers'
import { useHasUserPermission } from '../hooks/usePermissions';
export default function AddButton({ setSelectNewItemPosition }: { setSelectNewItemPosition: React.Dispatch<React.SetStateAction<any>> }) {
const layers = useLayers();
const hasUserPermission = useHasUserPermission();
return (
<div className="tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-5 tw-bottom-5" >
@ -16,7 +19,7 @@ export default function AddButton({ setSelectNewItemPosition }: { setSelectNewIt
</label>
<ul tabIndex={0} className="tw-dropdown-content tw-pr-1 tw-list-none">
{layers.map((layer) => (
layer.api?.createItem && (
layer.api?.createItem && hasUserPermission(layer.api.collectionName!,"create") && (
<li key={layer.name} >
<a>
<div className="tw-tooltip tw-tooltip-left" data-tip={layer.menuText}>

View File

@ -5,6 +5,7 @@ import { ItemFormPopupProps } from "../ItemFormPopup";
import { LatLng } from "leaflet";
import { Item } from "../../../../types";
import { toast } from "react-toastify";
import { useHasUserPermission, usePermissions } from "../../hooks/usePermissions";
@ -17,12 +18,15 @@ export function HeaderView({ item, setItemFormPopup }: {
const removeItem = useRemoveItem();
const map = useMap();
const hasUserPermission = useHasUserPermission();
const permissions = usePermissions();
const removeItemFromMap = async (event: React.MouseEvent<HTMLElement>) => {
setLoading(true);
let success = false;
try {
await item.layer.api?.deleteItem!(item.id)
await item.layer?.api?.deleteItem!(item.id)
success = true;
} catch (error) {
toast.error(error.toString());
@ -41,16 +45,22 @@ export function HeaderView({ item, setItemFormPopup }: {
event.stopPropagation();
map.closePopup();
if (setItemFormPopup)
setItemFormPopup({ position: new LatLng(item.position.coordinates[1], item.position.coordinates[0]), layer: item.layer, item: item, setItemFormPopup: setItemFormPopup })
setItemFormPopup({ position: new LatLng(item.position.coordinates[1], item.position.coordinates[0]), layer: item.layer!, item: item, setItemFormPopup: setItemFormPopup })
}
console.log(item.layer.api.collectionName);
console.log(permissions);
console.log( hasUserPermission(item.api?.collectionName!,"update") );
return (
<div className='tw-grid tw-grid-cols-6 tw-pb-2'>
<div className='tw-col-span-5'>
<b className="tw-text-xl tw-font-bold">{item.name}</b>
</div>
<div className='tw-col-span-1'>
{item.layer.api &&
{item.layer?.api &&
<div className="tw-dropdown tw-dropdown-bottom">
<label tabIndex={0} className="tw-bg-base-100 tw-btn tw-m-1 tw-leading-3 tw-border-none tw-min-h-0 tw-h-6">
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
@ -58,7 +68,7 @@ export function HeaderView({ item, setItemFormPopup }: {
</svg>
</label>
<ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box">
{item.layer.api.updateItem && <li>
{item.layer.api.updateItem && hasUserPermission(item.layer.api?.collectionName!,"update") && <li>
<a className="!tw-text-base-content" onClick={openEditPopup}>
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
@ -66,7 +76,7 @@ export function HeaderView({ item, setItemFormPopup }: {
</a>
</li>}
{item.layer.api.deleteItem && <li>
{item.layer.api.deleteItem && hasUserPermission(item.layer.api?.collectionName!,"delete") && <li>
<a className=' !tw-text-error' onClick={removeItemFromMap}>
{loading ? <span className="tw-loading tw-loading-spinner tw-loading-sm"></span>
:

View File

@ -13,6 +13,7 @@ import { TagsProvider } from "./hooks/useTags";
import { LayersProvider } from "./hooks/useLayers";
import { FilterProvider } from "./hooks/useFilter";
import { FilterControl } from "./Subcomponents/FilterControl";
import { PermissionsProvider } from "./hooks/usePermissions";
export interface MapEventListenerProps {
@ -57,37 +58,39 @@ function UtopiaMap({
return (
<LayersProvider initialLayers={[]}>
<TagsProvider initialTags={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}>
<FilterControl></FilterControl>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
<MarkerClusterGroup showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{
React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null }>(child) ?
React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup }) : child
)
}
</MarkerClusterGroup>
<MapEventListener setSelectNewItemPosition={setSelectNewItemPosition} selectNewItemPosition={selectNewItemPosition} setItemFormPopup={setItemFormPopup} />
</MapContainer>
<AddButton setSelectNewItemPosition={setSelectNewItemPosition}></AddButton>
{selectNewItemPosition != null &&
<div className="tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
<div className="tw-alert tw-bg-base-100 tw-text-base-content">
<div>
<span>Select {selectNewItemPosition.name} position!</span>
<PermissionsProvider initialPermissions={[]}>
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}>
<FilterControl></FilterControl>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
<MarkerClusterGroup showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
{
React.Children.toArray(children).map((child) =>
React.isValidElement<{ setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>, itemFormPopup: ItemFormPopupProps | null }>(child) ?
React.cloneElement(child, { setItemFormPopup: setItemFormPopup, itemFormPopup: itemFormPopup }) : child
)
}
</MarkerClusterGroup>
<MapEventListener setSelectNewItemPosition={setSelectNewItemPosition} selectNewItemPosition={selectNewItemPosition} setItemFormPopup={setItemFormPopup} />
</MapContainer>
<AddButton setSelectNewItemPosition={setSelectNewItemPosition}></AddButton>
{selectNewItemPosition != null &&
<div className="tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
<div className="tw-alert tw-bg-base-100 tw-text-base-content">
<div>
<span>Select {selectNewItemPosition.name} position!</span>
</div>
</div>
</div>
</div>
}
</div>
</ItemsProvider>
</FilterProvider>
}
</div>
</ItemsProvider>
</FilterProvider>
</PermissionsProvider>
</TagsProvider>
</LayersProvider>
);

View File

@ -1,6 +1,7 @@
import { useCallback, useReducer, createContext, useContext } from "react";
import * as React from "react";
import { ItemsApi, Permission } from "../../../types";
import { ItemsApi, LayerProps, Permission, PermissionAction } from "../../../types";
import { useAuth } from "../../Auth";
type ActionType =
| { type: "ADD"; permission: Permission }
@ -11,13 +12,17 @@ type UsePermissionManagerResult = ReturnType<typeof usePermissionsManager>;
const PermissionContext = createContext<UsePermissionManagerResult>({
permissions: [],
setPermissionApi: () => { },
setPermissionData: () => { }
setPermissionData: () => { },
setAdminRole: () => { },
hasUserPermission: () => true
});
function usePermissionsManager(initialPermissions: Permission[]): {
permissions: Permission[];
setPermissionApi: (api: ItemsApi<Permission>) => void;
setPermissionApi: (api: ItemsApi<any>) => void;
setPermissionData: (data: Permission[]) => void;
setAdminRole: (adminRole: string) => void;
hasUserPermission: (collectionName: string, action: PermissionAction) => boolean;
} {
const [permissions, dispatch] = useReducer((state: Permission[], action: ActionType) => {
switch (action.type) {
@ -38,10 +43,13 @@ function usePermissionsManager(initialPermissions: Permission[]): {
}
}, initialPermissions);
const [api, setApi] = React.useState<ItemsApi<Permission>>({} as ItemsApi<Permission>)
const [adminRole, setAdminRole] = React.useState<string | null>(null);
const { user } = useAuth();
const setPermissionApi = useCallback(async (api: ItemsApi<Permission>) => {
setApi(api);
console.log("check");
const result = await api.getItems();
if (result) {
result.map(permission => {
@ -56,8 +64,17 @@ function usePermissionsManager(initialPermissions: Permission[]): {
})
}, []);
const hasUserPermission = useCallback((collectionName: string, action: PermissionAction) => {
console.log(permissions);
if (permissions.length == 0) return true;
else if (user && user.role == adminRole) return true;
else return permissions.some(p => p.action === action && p.collection === collectionName && p.role == user?.role)
}, [permissions, user]);
return { permissions, setPermissionApi, setPermissionData };
return { permissions, setPermissionApi, setPermissionData, setAdminRole, hasUserPermission };
}
export const PermissionsProvider: React.FunctionComponent<{
@ -82,4 +99,14 @@ export const useSetPermissionApi = (): UsePermissionManagerResult["setPermission
export const useSetPermissionData = (): UsePermissionManagerResult["setPermissionData"] => {
const { setPermissionData } = useContext(PermissionContext);
return setPermissionData;
}
export const useHasUserPermission = (): UsePermissionManagerResult["hasUserPermission"] => {
const { hasUserPermission } = useContext(PermissionContext);
return hasUserPermission;
}
export const useSetAdminRole = (): UsePermissionManagerResult["setAdminRole"] => {
const { setAdminRole } = useContext(PermissionContext);
return setAdminRole;
}

View File

@ -1,6 +1,7 @@
export { UtopiaMap } from './UtopiaMap';
export { Layer } from './Layer';
export { Tags } from "./Tags";
export { Permissions } from "./Permissions";
export {ItemForm} from './ItemForm';
export {ItemView} from './ItemView';
export {PopupTextAreaInput} from './Subcomponents/ItemPopupComponents/PopupTextAreaInput';

View File

@ -4,11 +4,13 @@ import {TextInput} from '../Input/TextInput'
import {TextAreaInput} from '../Input/TextAreaInput'
import { toast } from 'react-toastify';
import {useNavigate} from 'react-router-dom'
import { useAuth } from '../Auth';
import * as React from 'react'
import 'react-toastify/dist/ReactToastify.css';
import { UserItem } from '../../types';
export function Settings({useAuth}) {
export function Settings() {
const { user, updateUser, loading } = useAuth();
@ -35,7 +37,7 @@ export function Settings({useAuth}) {
const onUpdateUser = () => {
let changedUser = {};
let changedUser = {} as UserItem;
if(passwordChanged) {
changedUser = { id: id, first_name: name, description: text, email: email, password: password };

View File

@ -1,4 +1,4 @@
export { UtopiaMap, Layer, Tags, ItemForm, ItemView, PopupTextAreaInput, PopupStartEndInput, TextView, StartEndView } from './Components/Map/index';
export { UtopiaMap, Layer, Tags, Permissions, ItemForm, ItemView, PopupTextAreaInput, PopupStartEndInput, TextView, StartEndView } from './Components/Map';
export {AppShell, Content, SideBar} from "./Components/AppShell"
export {AuthProvider, useAuth, LoginPage, SignupPage} from "./Components/Auth"
export {Settings} from './Components/Profile'

View File

@ -66,6 +66,7 @@ export interface ItemsApi<T> {
createItem?(item : T): Promise<any>,
updateItem?(item : T): Promise<any>,
deleteItem?(id : number | string): Promise<any>,
collectionName?: string
}
export interface UserApi {
@ -79,7 +80,8 @@ export interface UserApi {
export type UserItem = {
id?: string;
avatar: string;
avatar?: string;
role?: string;
first_name: string;
description: string;
email: string;
@ -90,5 +92,8 @@ export type Permission = {
id?: string;
role: string;
collection: string;
action: string;
}
action: PermissionAction
}
export type PermissionAction = "create"|"read"|"update"|"delete";