From df86ace4727334261f014d2321653ee46129b768 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Mon, 16 Dec 2024 12:01:57 +0100 Subject: [PATCH 1/2] [Requests] added error toasts display [Header] Removed useless imports --- src/actions/user.js | 16 ++++++------ src/components/Header/Header.js | 2 +- src/context/InSylvaGatekeeperClient.js | 2 +- src/pages/requests/Requests.js | 34 ++++++++++++++++++-------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/actions/user.js b/src/actions/user.js index fb7f9d4..180a086 100644 --- a/src/actions/user.js +++ b/src/actions/user.js @@ -148,25 +148,25 @@ export async function fetchPendingRequests(store, request = igClient) { } } -export async function processUserRequest(store, requestId, request = igClient) { +export const processUserRequest = async (store, requestId) => { store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); + igClient.token = sessionStorage.getItem('access_token'); try { - await request.processUserRequest(requestId); + return await igClient.processUserRequest(requestId); } catch (error) { console.error(error); } -} +}; -export async function deleteUserRequest(store, requestId, request = igClient) { +export const deleteUserRequest = async (store, requestId) => { store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); + igClient.token = sessionStorage.getItem('access_token'); try { - await request.deleteUserRequest(requestId); + return await igClient.deleteUserRequest(requestId); } catch (error) { console.error(error); } -} +}; export const createRole = async (store, name, description, request = igClient) => { try { diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 3064767..a534bad 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { AppBar, Toolbar, IconButton, Menu, MenuItem } from '@material-ui/core'; +import { AppBar, Toolbar, IconButton, Menu } from '@material-ui/core'; import { Menu as MenuIcon, Person as AccountIcon, diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js index d1b8568..f976cd7 100644 --- a/src/context/InSylvaGatekeeperClient.js +++ b/src/context/InSylvaGatekeeperClient.js @@ -80,7 +80,7 @@ class InSylvaGatekeeperClient { async deleteUserRequest(id) { const path = `/user/delete-request`; - await this.post('DELETE', `${path}`, { + return await this.post('DELETE', `${path}`, { id, }); } diff --git a/src/pages/requests/Requests.js b/src/pages/requests/Requests.js index 97bec19..04d3fcd 100644 --- a/src/pages/requests/Requests.js +++ b/src/pages/requests/Requests.js @@ -65,25 +65,39 @@ const Requests = () => { }, [loadRequests, loadPendingRequests]); const onDeleteRequest = async (request) => { - if (request) { - await globalActions.user.deleteUserRequest(request.id); - setOpen(true); + if (!request) { + return; + } + const result = await globalActions.user.deleteUserRequest(request.id); + if (result && !result.error) { setAlertMessage('Request has been deleted.'); setSeverity('success'); - await loadRequests(); - await loadPendingRequests(); + setOpen(true); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); + setOpen(true); } + await loadRequests(); + await loadPendingRequests(); }; const onProcessRequest = async (request) => { - if (request) { - await globalActions.user.processUserRequest(request.id); - setOpen(true); + if (!request) { + return; + } + const result = await globalActions.user.processUserRequest(request.id); + if (result && !result.error) { setAlertMessage('Request has been processed.'); setSeverity('success'); - await loadRequests(); - await loadPendingRequests(); + setOpen(true); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); + setOpen(true); } + await loadRequests(); + await loadPendingRequests(); }; const requestActions = [ -- GitLab From ec0ad1cd2a64c226b32db371556cb183bfe2dac4 Mon Sep 17 00:00:00 2001 From: Brett Choquet <brett.choquet@inra.fr> Date: Fri, 17 Jan 2025 00:30:00 +0100 Subject: [PATCH 2/2] Authentication and major rewriting --- .dockerignore | 22 + .gitignore | 3 + .gitlab-ci.yml | 15 + Dockerfile | 25 + env.sh | 121 +++ nginx/gzip.conf | 44 ++ nginx/nginx.conf | 60 ++ package.json | 17 +- public/index.html | 29 +- src/actions/group.js | 149 ---- src/actions/index.js | 8 - src/actions/policy.js | 262 ------- src/actions/source.js | 336 -------- src/actions/user.js | 322 -------- src/components/App.js | 12 +- src/components/Header/Header.js | 31 +- src/components/Layout/Layout.js | 128 ++- src/components/Sidebar/Sidebar.js | 10 +- src/context/InSylvaGatekeeperClient.js | 307 -------- src/context/InSylvaSourceManager.js | 155 ---- src/context/KeycloakClient.js | 84 -- src/context/UserContext.js | 101 --- src/images/logo.png | Bin 0 -> 23893 bytes src/images/mongo_logo.svg | 5 + src/index.js | 52 +- src/pages/dashboard/Dashboard.js | 69 +- src/pages/error/403.js | 44 ++ src/pages/error/{Error.js => 404.js} | 18 +- src/pages/fields/Fields.js | 256 +++--- src/pages/groups/Groups.js | 121 +-- src/pages/home/Home.js | 48 ++ src/pages/home/package.json | 7 + src/pages/home/styles.js | 25 + src/pages/policies/AssignedPolicies.js | 131 ++++ src/pages/policies/NewPolicyForm.js | 59 ++ src/pages/policies/Policies.js | 786 +++++-------------- src/pages/policies/PolicyAssignment.js | 68 ++ src/pages/policies/PolicyGroupAssignment.js | 62 ++ src/pages/policies/PolicySourceAssignment.js | 69 ++ src/pages/requests/Requests.js | 74 +- src/pages/roles/Roles.js | 311 ++++---- src/pages/sources/Sources.js | 240 +++--- src/pages/users/Users.js | 253 ++---- src/services/GatekeeperService.js | 374 +++++++++ src/store/CustomConnector.js | 53 -- src/store/asyncEnhancer.js | 16 - src/store/index.js | 17 - src/store/useStore.js | 60 -- src/utils.js | 106 +-- 49 files changed, 2173 insertions(+), 3392 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100755 env.sh create mode 100644 nginx/gzip.conf create mode 100644 nginx/nginx.conf delete mode 100644 src/actions/group.js delete mode 100644 src/actions/index.js delete mode 100644 src/actions/policy.js delete mode 100644 src/actions/source.js delete mode 100644 src/actions/user.js delete mode 100644 src/context/InSylvaGatekeeperClient.js delete mode 100644 src/context/InSylvaSourceManager.js delete mode 100644 src/context/KeycloakClient.js delete mode 100644 src/context/UserContext.js create mode 100644 src/images/logo.png create mode 100644 src/images/mongo_logo.svg create mode 100644 src/pages/error/403.js rename src/pages/error/{Error.js => 404.js} (77%) create mode 100644 src/pages/home/Home.js create mode 100644 src/pages/home/package.json create mode 100644 src/pages/home/styles.js create mode 100644 src/pages/policies/AssignedPolicies.js create mode 100644 src/pages/policies/NewPolicyForm.js create mode 100644 src/pages/policies/PolicyAssignment.js create mode 100644 src/pages/policies/PolicyGroupAssignment.js create mode 100644 src/pages/policies/PolicySourceAssignment.js create mode 100644 src/services/GatekeeperService.js delete mode 100644 src/store/CustomConnector.js delete mode 100644 src/store/asyncEnhancer.js delete mode 100644 src/store/index.js delete mode 100644 src/store/useStore.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7dc10ba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# Ignore node_modules directory +node_modules/ + +# Ignore npm debug log +npm-debug.log + +# Ignore build artifacts +dist/ +build/ +out/ + +# Ignore development files +*.log +*.pid +*.lock + +# Ignore version control files +.git/ +.gitignore + +# Ignore certificates +ssl/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index f5d62d3..de47805 100755 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ yarn-error.log* # Misc .DS_Store + +ssl/* +public/env-config.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0591f3c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +stages: + - build + +build: + stage: build + image: docker:latest + services: + - docker:dind + script: + - apk add jq + - version=$(jq -r .version package.json) + - docker build -t registry.forgemia.inra.fr/in-sylva-development/in-sylva.portal:$version . + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker push registry.forgemia.inra.fr/in-sylva-development/in-sylva.portal:$version + when: manual diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4e5eee9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:18.20.5 as portal + +WORKDIR /app/ +COPY package.json . +RUN yarn install +COPY . . +RUN yarn build + +FROM nginx:1.24-bullseye +COPY --from=portal /app/build /usr/share/nginx/html + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx/gzip.conf /etc/nginx/conf.d/gzip.conf + +WORKDIR /usr/share/nginx/html +RUN chown -R :www-data /usr/share/nginx/html + +COPY ./env.sh . +RUN chmod +x env.sh + +COPY .env.production .env +RUN ./env.sh -e .env -o ./ + +CMD ["nginx", "-g", "daemon off;"] diff --git a/env.sh b/env.sh new file mode 100755 index 0000000..5c94083 --- /dev/null +++ b/env.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +usage() { + BASENAME=$(basename "$0") + echo "Usage: $BASENAME -e <env-file> -o <output-file>" + echo " -e, --env-file Path to .env file" + echo " -o, --output-file Path to output '.env-config.js' file" + exit 1 +} + +while getopts "e:o:" opt; do + case $opt in + e) + ENV_FILE=$OPTARG + ;; + o) + OUT_DIRECTORY=$OPTARG + ;; + \?) + # Invalid option + echo "Invalid option: -$OPTARG" + usage + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." + usage + exit 1 + ;; + esac +done + +# Check if required options are provided +if [ -z "$OUT_DIRECTORY" ]; then + echo "Error: -o (output directory) is required." + echo "" + usage + exit 1 +fi + +if [ -z "$ENV_FILE" ]; then + echo "Error: -e (environment file) is required." + echo "" + usage + exit 1 +fi + +ENV_FILE_PATH=$(realpath $ENV_FILE) + +# Check if the environment file exists +if [[ ! -f $ENV_FILE_PATH ]]; then + echo "Environment file does not exist" + echo "" + usage + exit 1 +fi + +# Check if the environment file is readable +if [[ ! -r $ENV_FILE_PATH ]]; then + echo "Environment file is not readable" + echo "" + usage + exit 1 +fi + +# Check if the environment file is empty +if [[ ! -s $ENV_FILE_PATH ]]; then + echo "Environment file is empty" + echo "" + usage + exit 1 +fi + +# Check if the environment file has the correct format +BAD_LINES=$(grep -v -P '^[^=\s]+=[^=\s]+$' "$ENV_FILE_PATH") +if [ -n "$BAD_LINES" ]; then + echo "Environment file has incorrect format or contains empty values:" + echo "$BAD_LINES" + echo "" + usage + exit 1 +fi + +FULL_PATH_OUTPUT_DIRECTORY=$(realpath $OUT_DIRECTORY) + +# Check if the output directory is a directory, exists, and can be written +if [[ ! -d $FULL_PATH_OUTPUT_DIRECTORY ]]; then + echo "Output directory does not exist or is not a directory" + echo "" + usage + exit 1 +fi + +if [[ ! -w $FULL_PATH_OUTPUT_DIRECTORY ]]; then + echo "Output directory is not writable" + echo "" + usage + exit 1 +fi + +OUTPUT_FILE="env-config.js" +FULL_OUTPUT_PATH="$FULL_PATH_OUTPUT_DIRECTORY/$OUTPUT_FILE" + +# Remove the output file if it exists +if [[ -f $FULL_OUTPUT_PATH ]]; then + rm $FULL_OUTPUT_PATH +fi + +# Add assignment +echo "window._env_ = {" >>$FULL_OUTPUT_PATH + +while read -r line || [[ -n "$line" ]]; do + # Split env variables by '=' + varname=$(echo $line | cut -d'=' -f1) + value=$(echo $line | cut -d'=' -f2) + + # Append configuration property to JS file + echo " $varname: \"$value\"," >>$FULL_OUTPUT_PATH +done <$ENV_FILE_PATH + +echo "}" >>$FULL_OUTPUT_PATH diff --git a/nginx/gzip.conf b/nginx/gzip.conf new file mode 100644 index 0000000..2f54ae1 --- /dev/null +++ b/nginx/gzip.conf @@ -0,0 +1,44 @@ +# Enable Gzip compressed. +# gzip on; + + # Enable compression both for HTTP/1.0 and HTTP/1.1 (required for CloudFront). + gzip_http_version 1.0; + + # Compression level (1-9). + # 5 is a perfect compromise between size and cpu usage, offering about + # 75% reduction for most ascii files (almost identical to level 9). + gzip_comp_level 5; + + # Don't compress anything that's already small and unlikely to shrink much + # if at all (the default is 20 bytes, which is bad as that usually leads to + # larger files after gzipping). + gzip_min_length 256; + + # Compress data even for clients that are connecting to us via proxies, + # identified by the "Via" header (required for CloudFront). + gzip_proxied any; + + # Tell proxies to cache both the gzipped and regular version of a resource + # whenever the client's Accept-Encoding capabilities header varies; + # Avoids the issue where a non-gzip capable client (which is extremely rare + # today) would display gibberish if their proxy gave them the gzipped version. + gzip_vary on; + + # Compress all output labeled with one of the following MIME-types. + gzip_types + application/atom+xml + application/javascript + application/json + application/rss+xml + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/svg+xml + image/x-icon + text/css + text/plain + text/x-component; + # text/html is always compressed by HttpGzipModule \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..e3c6319 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,60 @@ +include /etc/nginx/mime.types; + +server { + + listen 80; + server_name -; + + ssl_certificate /etc/ssl/fullchain.pem; + ssl_certificate_key /etc/ssl/fullchain.pem.key; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + + access_log /var/log/nginx/host.access.log; + error_log /var/log/nginx/host.error.log; + + root /usr/share/nginx/html; + index index.html index.htm; + + location /static/media/ { + try_files $uri /usr/share/nginx/html/static/media; + } + + location / { + + root /usr/share/nginx/html; + index index.html; + autoindex on; + set $fallback_file /index.html; + if ($http_accept !~ text/html) { + set $fallback_file /null; + } + if ($uri ~ /$) { + set $fallback_file /null; + } + try_files $uri $fallback_file; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/package.json b/package.json index b3e53d7..e3f8cc4 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,32 @@ { "name": "in-sylva.portal", - "version": "1.0.0", + "version": "1.1.1-alpha", "private": true, "homepage": ".", "dependencies": { "@elastic/datemath": "^5.0.3", "@elastic/eui": "^27.4.0", - "@in-sylva/json-view": "git+ssh://git@forgemia.inra.fr:in-sylva-development/json-view.git", - "@in-sylva/react-use-storage": "git+ssh://git@forgemia.inra.fr:in-sylva-development/react-use-storage.git", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.48", "@material-ui/styles": "^4.10.0", + "@microlink/react-json-view": "^1.23.1", "moment": "^2.27.0", "mui-datatables": "^3.4.0", + "oidc-client-ts": "^3.1.0", "papaparse": "5.3.0", "react": "^16.13.1", "react-apexcharts": "^1.3.3", "react-dom": "^16.13.1", + "react-oidc-context": "^3.2.0", "react-router-dom": "^5.2.0", "react-scripts": "^3.3.0", "recharts": "^2.0.0-beta.1", "tinycolor2": "^1.4.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "start": "./env.sh -e .env.development -o ./public && BROWSER=none NODE_OPTIONS=--openssl-legacy-provider react-scripts start", + "build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint ./src", @@ -47,10 +48,10 @@ "devDependencies": { "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "lint-staged": "14.0.1", "husky": "8.0.3", - "prettier": "^3.2.5", - "jscs": "^3.0.7" + "jscs": "^3.0.7", + "lint-staged": "14.0.1", + "prettier": "^3.2.5" }, "husky": { "hooks": { diff --git a/public/index.html b/public/index.html index e4c2254..8661142 100755 --- a/public/index.html +++ b/public/index.html @@ -1,12 +1,13 @@ <!DOCTYPE html> <html lang="en"> -<head> - <meta charset="utf-8" /> - <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <meta name="theme-color" content="#000000" /> - <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> - <!-- + <head> + <meta charset="utf-8" /> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" /> + <meta name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <meta name="theme-color" content="#000000" /> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. @@ -15,11 +16,11 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>IN-SYLVA Portal</title> - <script src="%PUBLIC_URL%/env-config.js"></script> -</head> -<body style="font-family: 'Roboto', sans-serif;"> - <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root"></div> -</body> + <title>IN-SYLVA Portal</title> + <script src="%PUBLIC_URL%/env-config.js"></script> + </head> + <body style="font-family: 'Roboto', sans-serif;"> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> + </body> </html> diff --git a/src/actions/group.js b/src/actions/group.js deleted file mode 100644 index 9aaf645..0000000 --- a/src/actions/group.js +++ /dev/null @@ -1,149 +0,0 @@ -import { InSylvaGatekeeperClient } from '../context/InSylvaGatekeeperClient'; -import { getGatekeeperBaseUrl } from '../utils'; - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = getGatekeeperBaseUrl(); - -export const createGroup = async (store, name, description, kcId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const group = await request.createGroup({ name, description, kcId }); - if (group) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return group; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getGroups = async (store, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groups = await request.getGroups(); - if (groups) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groups; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createGroupUser = async (store, groupId, kcId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupUser = await request.createGroupUser({ groupId, kcId }); - if (groupUser) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupUser; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getGroupUsers = async (store, groupId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupUser = await request.getGroupUsers({ groupId }); - if (groupUser) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupUser; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createGroupPolicy = async (store, groupId, policyId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupPolicy = await request.createGroupPolicy({ groupId, policyId }); - if (groupPolicy) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupPolicy; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteGroup = async (store, groupId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupUser = await request.deleteGroup({ id: groupId }); - if (groupUser) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupUser; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteGroupPolicy = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupPolicy = await request.deleteGroupPolicy({ id }); - if (groupPolicy) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupPolicy; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteGroupUser = async (store, groupId, userId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const groupPolicy = await request.deleteGroupUser({ groupId, userId }); - if (groupPolicy) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return groupPolicy; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 65fb44f..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as user from './user'; -import * as source from './source'; -import * as policy from './policy'; -import * as group from './group'; -export { user }; -export { source }; -export { policy }; -export { group }; diff --git a/src/actions/policy.js b/src/actions/policy.js deleted file mode 100644 index 1cd9e24..0000000 --- a/src/actions/policy.js +++ /dev/null @@ -1,262 +0,0 @@ -import { InSylvaGatekeeperClient } from '../context/InSylvaGatekeeperClient'; -import { getGatekeeperBaseUrl } from '../utils'; - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = getGatekeeperBaseUrl(); - -export const createPolicy = async (store, name, kcId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.createPolicy({ name, kcId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createPolicyField = async ( - store, - policyId, - stdFieldId, - request = igClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.createPolicyField({ policyId, stdFieldId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const updatePolicy = async ( - store, - id, - name, - sourceId, - isDefault, - request = igClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.updatePolicy({ id, name, sourceId, isDefault }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const updatePolicyField = async ( - store, - id, - policyId, - stdFieldId, - request = igClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.updatePolicyField({ id, policyId, stdFieldId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deletePolicyField = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.deletePolicyField({ id }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deletePolicy = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.deletePolicy({ id }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getPolicies = async (store, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getPolicies({}); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getPoliciesByUser = async (store, kc_id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getPoliciesByUser(kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getPoliciesWithSourcesByUser = async (store, kc_id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getPoliciesWithSourcesByUser(kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getAssignedPolicies = async (store, policyId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getAssignedPolicies({ policyId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getPoliciesWithSources = async (store, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getPoliciesWithSources(); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getGroupDetailsByPolicy = async (store, policyId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.getGroupDetailsByPolicy({ policyId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createPolicySource = async ( - store, - policyId, - sourceId, - request = igClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.createPolicySource({ policyId, sourceId }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; diff --git a/src/actions/source.js b/src/actions/source.js deleted file mode 100644 index cf41d7d..0000000 --- a/src/actions/source.js +++ /dev/null @@ -1,336 +0,0 @@ -import { InSylvaSourceManager } from '../context/InSylvaSourceManager'; - -const smClient = new InSylvaSourceManager(); -smClient.baseUrl = process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_PORT - ? `${process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_HOST}:${process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_SOURCE_MANAGER_HOST}`; - -export const createSource = async ( - store, - metaUrfms, - name, - description, - kc_id, - request = smClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.createSource(metaUrfms, name, description, kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const sources = async (store, kc_id, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.sources(kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const indexedSources = async (store, kc_id, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.indexedSources(kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createStdField = async ( - store, - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - request = smClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.addStdField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const createAddtlField = async ( - store, - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - request = smClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.addAddtlField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const updateStdField = async ( - store, - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - request = smClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.updateStdField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const updateSource = async (store, kc_id, source_id, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.updateSource(kc_id, source_id); - - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const publicFields = async (store, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const stdFields = await request.publicFields(); - - if (stdFields) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return stdFields; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const privateFields = async (store, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const stdFields = await request.privateFields(); - - if (stdFields) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return stdFields; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const allSources = async (store, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const result = await request.allSource(); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const allIndexedSources = async (store, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const result = await request.allIndexedSource(); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const truncateStdField = async (store, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const result = await request.truncateStdField(); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteSource = async (store, sourceId, kc_id, request = smClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const result = await request.deleteSource(sourceId, kc_id); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const mergeAndSendSource = async ( - store, - kc_id, - name, - description, - sources, - request = smClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const result = await request.mergeAndSendSource({ - kc_id, - name, - description, - sources, - }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return result; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; diff --git a/src/actions/user.js b/src/actions/user.js deleted file mode 100644 index 180a086..0000000 --- a/src/actions/user.js +++ /dev/null @@ -1,322 +0,0 @@ -import { InSylvaGatekeeperClient } from '../context/InSylvaGatekeeperClient'; -import { getGatekeeperBaseUrl } from '../utils'; - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = getGatekeeperBaseUrl(); - -export const createUser = async ( - store, - username, - email, - password, - roleId, - request = igClient -) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const nUser = await request.createUser({ username, email, password, roleId }); - if (nUser) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const findUser = async (store, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const users = await request.findUser(); - if (users) { - const status = 'SUCCESS'; - store.setState({ users, status, isLoading: false }); - return users; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const updateUser = async (store, user, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.updateUser({ - id: user.id, - firstName: user.firstName, - lastName: user.lastName, - }); - if (result) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const findOneUser = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const user = await request.findOneUser(id); - - if (user) { - const status = 'SUCCESS'; - store.setState({ user: user, status, isLoading: false }); - return user; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const findOneUserWithGroupAndRole = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const user = await request.findOneUserWithGroupAndRole(id); - - if (user) { - const status = 'SUCCESS'; - store.setState({ user: user, status, isLoading: false }); - return user; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteUser = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const user = await request.deleteUser({ id: id }); - if (user) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export async function fetchRequests(store, request = igClient) { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - try { - const requests = await request.getRequests(); - if (requests) { - return requests; - } - } catch (error) { - console.error(error); - } -} - -export async function fetchPendingRequests(store, request = igClient) { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - try { - const requests = await request.getPendingRequests(); - if (requests) { - return requests; - } - } catch (error) { - console.error(error); - } -} - -export const processUserRequest = async (store, requestId) => { - store.setState({ isLoading: true }); - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.processUserRequest(requestId); - } catch (error) { - console.error(error); - } -}; - -export const deleteUserRequest = async (store, requestId) => { - store.setState({ isLoading: true }); - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.deleteUserRequest(requestId); - } catch (error) { - console.error(error); - } -}; - -export const createRole = async (store, name, description, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const role = await request.createRole({ name: name, description: description }); - if (role) { - const status = 'SUCCESS'; - store.setState({ role, status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const findRole = async (store, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const roles = await request.findRole(); - if (roles) { - const status = 'SUCCESS'; - store.setState({ roles, status, isLoading: false }); - return roles; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const allocateRoles = async (store, roles, users, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.allocateRoles({ roles, users }); - if (result === true) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const allocateRolesToUser = async (store, userId, roleId, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const result = await request.allocateRolesToUser({ userId, roleId }); - if (result === true) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const allocatedRoles = async (store, kc_id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - - const allocatedRoles = await request.allocatedRoles(kc_id); - if (allocatedRoles) { - const status = 'SUCCESS'; - store.setState({ allocatedRoles, status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const setKcId = async (store, email, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const { kc_id } = await request.kcId(email); - - store.setState({ kcId: kc_id }); - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const deleteRole = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const role = await request.deleteRole({ id }); - - if (role) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const getAssignedUserByRole = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const assignedUserByRole = await request.getAssignedUserByRole({ id }); - - if (assignedUserByRole) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return assignedUserByRole; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; - -export const usersWithGroupAndRole = async (store, id, request = igClient) => { - try { - store.setState({ isLoading: true }); - request.token = sessionStorage.getItem('access_token'); - const usersWithGroupAndRole = await request.usersWithGroupAndRole(); - - if (usersWithGroupAndRole) { - const status = 'SUCCESS'; - store.setState({ status, isLoading: false }); - return usersWithGroupAndRole; - } - } catch (error) { - const isError404 = error.response && error.response.status === 404; - const status = isError404 ? 'NOT_FOUND' : 'ERROR'; - store.setState({ status, isLoading: false }); - } -}; diff --git a/src/components/App.js b/src/components/App.js index 1476783..9812ec8 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,16 +1,18 @@ import React from 'react'; import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'; import Layout from './Layout'; -import Error from '../pages/error'; +import Error404 from '../pages/error/404'; +import Error403 from '../pages/error/403'; export default function App() { return ( <HashRouter> <Switch> - <Route exact path="/" render={() => <Redirect to="/app/dashboard" />} /> - <Route exact path="/app" render={() => <Redirect to="/app/dashboard" />} /> - <Route path="/app" component={Layout} /> - <Route path="/Error" component={Error} /> + <Route exact path="/" render={() => <Redirect to="/home" />} /> + <Route path="/not-found" component={Error404} /> + <Route path="/forbidden" component={Error403} /> + <Route path="/" component={Layout} /> + <Route render={() => <Redirect to="/not-found" />} /> </Switch> </HashRouter> ); diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index a534bad..6c09d23 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState } from 'react'; import { AppBar, Toolbar, IconButton, Menu } from '@material-ui/core'; import { Menu as MenuIcon, @@ -13,30 +13,15 @@ import { useLayoutDispatch, toggleSidebar, } from '../../context/LayoutContext'; -import { useUserDispatch, signOut } from '../../context/UserContext'; -import store from '../../store/index'; +import { useAuth } from 'react-oidc-context'; -export default function Header(props) { +export default function Header({ email }) { const classes = useStyles(); - let [, globalActions] = store(); const layoutState = useLayoutState(); const layoutDispatch = useLayoutDispatch(); - const userDispatch = useUserDispatch(); - let [user, setUser] = useState({}); + const auth = useAuth(); let [profileMenu, setProfileMenu] = useState(null); - const loadUser = useCallback(async () => { - if (sessionStorage.getItem('userId')) { - const userId = sessionStorage.getItem('userId'); - const user = await globalActions.user.findOneUser(userId); - setUser(user); - } - }, [globalActions]); - - useEffect(() => { - loadUser(); - }, [loadUser]); - return ( <AppBar position="fixed" className={classes.appBar}> <Toolbar className={classes.toolbar}> @@ -87,14 +72,18 @@ export default function Header(props) { <div className={classes.profileMenuUser}> <div className={classes.profileMenuItem}> <Typography variant="h4" weight="medium"> - {user.username} + {email} </Typography> </div> <div className={classes.profileMenuItem}> <Typography className={classes.profileMenuLink} color="primary" - onClick={() => signOut(userDispatch, props.history)} + onClick={async () => { + await auth.signoutRedirect({ + post_logout_redirect_uri: `${process.env.REACT_APP_BASE_URL}`, + }); + }} > Sign out </Typography> diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index f2240a9..239296a 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -1,9 +1,10 @@ -import React from 'react'; -import { Route, Switch, withRouter } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { Route, Switch, withRouter, Redirect } from 'react-router-dom'; import classnames from 'classnames'; import useStyles from './styles'; import Header from '../Header'; import Sidebar from '../Sidebar'; +import Home from '../../pages/home'; import Dashboard from '../../pages/dashboard'; import Users from '../../pages/users'; import Roles from '../../pages/roles/Roles'; @@ -13,35 +14,112 @@ import Fields from '../../pages/fields'; import Policies from '../../pages/policies'; import Groups from '../../pages/groups/Groups'; import { useLayoutState } from '../../context/LayoutContext'; +import { useAuth } from 'react-oidc-context'; +import { createUser, findUserBySub } from '../../services/GatekeeperService'; + +const ProtectedRoute = ({ isAllowed, redirectPath = '/forbidden', path, component }) => { + if (!isAllowed) { + return <Redirect to={redirectPath} />; + } + return <Route path={path} component={component} />; +}; function Layout(props) { const classes = useStyles(); const layoutState = useLayoutState(); + const auth = useAuth(); + const [user, setUser] = useState(null); + const [roles, setRoles] = useState([]); + + useEffect(() => { + const fetchUser = async (sub) => { + let user = await findUserBySub(sub); + if (!user.id) { + // User registered on Keycloak but not in the database + if (auth.user?.profile?.email) { + const result = await createUser(sub, auth.user?.profile?.email); + + if (result) { + user = await findUserBySub(sub); + } else { + auth.signoutRedirect(); + } + } + } + setUser(user); + setRoles(user.roles.map((r) => r.role_id)); + }; + if (!auth || auth.isLoading) { + return; + } + if (auth.isAuthenticated) { + fetchUser(auth.user.profile.sub); + } else { + auth.signinRedirect(); + } + }, [auth]); return ( - <div className={classes.root}> - <> - <Header history={props.history} /> - <Sidebar /> - <div - className={classnames(classes.content, { - [classes.contentShift]: layoutState.isSidebarOpened, - })} - > - <div className={classes.fakeToolbar} /> - <Switch> - <Route path="/app/dashboard" component={Dashboard} /> - <Route path="/app/users" component={Users} /> - <Route path="/app/roles" component={Roles} /> - <Route path="/app/groups" component={Groups} /> - <Route path="/app/requests" component={Requests} /> - <Route path="/app/policies" component={Policies} /> - <Route path="/app/sources" component={Sources} /> - <Route path="/app/fields" component={Fields} /> - </Switch> - </div> - </> - </div> + user && + roles.length > 0 && ( + <div className={classes.root}> + <> + <Header email={user.email} usehistory={props.history} /> + <Sidebar roles={roles} /> + <div + className={classnames(classes.content, { + [classes.contentShift]: layoutState.isSidebarOpened, + })} + > + <div className={classes.fakeToolbar} /> + <Switch> + <Route path="/home" component={Home} /> + <ProtectedRoute + path="/dashboard" + isAllowed={roles.includes(1)} + component={Dashboard} + /> + <ProtectedRoute + path="/users" + isAllowed={roles.includes(1)} + component={Users} + /> + <ProtectedRoute + isAllowed={roles.includes(1)} + path="/roles" + component={Roles} + /> + <ProtectedRoute + isAllowed={roles.includes(1)} + path="/groups" + component={Groups} + /> + <ProtectedRoute + isAllowed={roles.includes(1)} + path="/requests" + component={Requests} + /> + <ProtectedRoute + isAllowed={roles.includes(1)} + path="/policies" + component={Policies} + /> + <ProtectedRoute + isAllowed={roles.includes(1) || roles.includes(2)} + path="/sources" + component={Sources} + /> + <ProtectedRoute + isAllowed={roles.includes(1) || roles.includes(2)} + path="/fields" + component={Fields} + /> + <Route render={() => <Redirect to="/not-found" />} /> + </Switch> + </div> + </> + </div> + ) ); } diff --git a/src/components/Sidebar/Sidebar.js b/src/components/Sidebar/Sidebar.js index dc3b4e6..e6a2006 100644 --- a/src/components/Sidebar/Sidebar.js +++ b/src/components/Sidebar/Sidebar.js @@ -11,20 +11,18 @@ import { useLayoutDispatch, toggleSidebar, } from '../../context/LayoutContext'; -import { getSideBarItems, getUserRoleId } from '../../utils'; +import { getSideBarItems } from '../../utils'; -const Sidebar = ({ location }) => { +const Sidebar = ({ location, roles }) => { const classes = useStyles(); const theme = useTheme(); let { isSidebarOpened } = useLayoutState(); const layoutDispatch = useLayoutDispatch(); let [isPermanent, setPermanent] = useState(true); const [sidebarItems, setSidebarItems] = useState([]); - const [userRoleId, setUserRoleId] = useState(0); useEffect(() => { setSidebarItems(getSideBarItems()); - setUserRoleId(getUserRoleId()); const handleWindowWidthChange = () => { const windowWidth = window.innerWidth; const breakpointWidth = theme.breakpoints.values.md; @@ -41,11 +39,11 @@ const Sidebar = ({ location }) => { return function cleanup() { window.removeEventListener('resize', handleWindowWidthChange); }; - }, [setSidebarItems, setUserRoleId, isPermanent, theme]); + }, [setSidebarItems, isPermanent, theme]); const buildSidebarItems = () => { return sidebarItems.map((item) => { - if (item.roles.some((authorizedRoleId) => authorizedRoleId === userRoleId)) { + if (item.roles.some((authorizedRoleId) => roles.includes(authorizedRoleId))) { return ( <SidebarLink key={item.id} diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js deleted file mode 100644 index f976cd7..0000000 --- a/src/context/InSylvaGatekeeperClient.js +++ /dev/null @@ -1,307 +0,0 @@ -class InSylvaGatekeeperClient { - async post(method, path, requestContent) { - const headers = { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - Authorization: `Bearer ${this.token}`, - }; - - const response = await fetch(`${this.baseUrl}${path}`, { - method: method, - headers, - body: JSON.stringify(requestContent), - mode: 'cors', - }); - return await response.json(); - } - - async createUser({ username, email, password, roleId }) { - const path = `/user`; - return await this.post('POST', `${path}`, { - username, - email, - password, - roleId, - }); - } - - async kcId({ email }) { - const path = `/user/kcid`; - - return await this.post('POST', `${path}`, { - email, - }); - } - - async updateUser({ id, firstName, lastName }) { - const path = `/user/update`; - return await this.post('PUT', `${path}`, { - id, - firstName, - lastName, - }); - } - - async findUser() { - const path = `/user/find`; - return await this.post('GET', `${path}`); - } - - async findOneUser(id) { - const path = `/user/findOne`; - return await this.post('POST', `${path}`, { - id, - }); - } - - async deleteUser({ id }) { - const path = `/user/delete`; - return await this.post('DELETE', `${path}`, { - userId: id, - }); - } - - async getRequests() { - const path = `/user/list-requests`; - return await this.post('GET', `${path}`); - } - - async getPendingRequests() { - const path = `/user/list-pending-requests`; - return await this.post('GET', `${path}`); - } - - async processUserRequest(id) { - const path = `/user/process-request`; - return await this.post('POST', `${path}`, { - id, - }); - } - - async deleteUserRequest(id) { - const path = `/user/delete-request`; - return await this.post('DELETE', `${path}`, { - id, - }); - } - - async createRole({ name, description }) { - const path = `/role`; - return await this.post('POST', `${path}`, { - name, - description, - }); - } - - async findRole() { - const path = `/role/find`; - return await this.post('GET', `${path}`); - } - - async allocateRoles({ roles, users }) { - try { - const path = `/allocate-role-to-user`; - for (let x = 0; x < users.length; x++) { - for (let y = 0; y < roles.length; y++) { - await this.post('POST', `${path}`, { - kc_id: users[x], - role_id: roles[y], - }); - } - } - return true; - } catch (error) { - return false; - } - } - - async allocateRolesToUser({ userId, roleId }) { - const path = `/allocate-role-to-user`; - return await this.post('POST', `${path}`, { - kc_id: userId, - role_id: roleId, - }); - } - async allocatedRoles(kc_id) { - const path = `/allocatedRoles`; - return await this.post('POST', `${path}`, { - kc_id, - }); - } - - async createPolicy({ name, kcId }) { - const path = `/policy/add`; - return await this.post('POST', `${path}`, { - name, - kcId, - }); - } - - async createPolicyField({ policyId, stdFieldId }) { - const path = `/policyField/add`; - return await this.post('POST', `${path}`, { - policyId, - stdFieldId, - }); - } - - async updatePolicy({ id, name, sourceId, isDefault }) { - const path = `/policy/update`; - return await this.post('PUT', `${path}`, { - id, - name, - sourceId, - isDefault, - }); - } - - async updatePolicyField({ id, policyId, stdFieldId }) { - const path = `/policyField/update`; - return this.post('PUT', `${path}`, { - id, - policyId, - stdFieldId, - }); - } - - async deletePolicyField({ id }) { - const path = `/policyField/delete`; - return await this.post('DELETE', `${path}`, { - id, - }); - } - - async deletePolicy({ id }) { - const path = `/policy/delete`; - return await this.post('DELETE', `${path}`, { - id, - }); - } - - async getPolicies() { - const path = `/policy/list`; - return await this.post('GET', `${path}`); - } - - async getPoliciesByUser(kcId) { - const path = `/policy/list-by-user`; - return await this.post('POST', `${path}`, { kcId }); - } - - async getPoliciesWithSourcesByUser(kcId) { - const path = `/policy/policies-with-sources-by-user`; - return await this.post('POST', `${path}`, { kcId }); - } - - async getPoliciesWithSources() { - const path = `/policy/policies-with-sources`; - return await this.post('GET', `${path}`); - } - - async getPolicyFields() { - const path = `/policyField/list`; - return await this.post('GET', `${path}`, {}); - } - - async getGroups() { - const path = `/user/groups`; - return await this.post('POST', `${path}`, {}); - } - - async getGroupPolicies() { - const path = `/user/group-policies`; - return await this.post('GET', `${path}`, {}); - } - - async getGroupUsers({ groupId }) { - const path = `/user/group-users`; - return await this.post('POST', `${path}`, { groupId }); - } - - async createGroup({ name, description, kcId }) { - const path = `/user/add-group`; - return await this.post('POST', `${path}`, { name, description, kcId }); - } - - async createGroupPolicy({ groupId, policyId }) { - const path = `/user/add-group-policy`; - return await this.post('POST', `${path}`, { groupId, policyId }); - } - - async createGroupUser({ groupId, kcId }) { - const path = `/user/add-group-user`; - return await this.post('POST', `${path}`, { groupId, kcId }); - } - - async updateGroup({ id, userId, name, description }) { - const path = `/user/update-group`; - return await this.post('PUT', `${path}`, { name, description, id, userId }); - } - - async updateGroupPolicy({ id, groupId, policyId }) { - const path = `/user/update-group-policy`; - return await this.post('PUT', `${path}`, { id, groupId, policyId }); - } - - async updateGroupUser({ id, kcId, groupId }) { - const path = `/user/update-group-user`; - return await this.post('PUT', `${path}`, { id, kcId, groupId }); - } - - async deleteGroup({ id }) { - const path = `/user/delete-group`; - return await this.post('DELETE', `${path}`, { id }); - } - - async deleteGroupPolicy({ id }) { - const path = `/user/delete-group-policy`; - return await this.post('DELETE', `${path}`, { id }); - } - - async deleteGroupUser({ groupId, userId }) { - const path = `/user/delete-group-user`; - return await this.post('DELETE', `${path}`, { groupId, userId }); - } - - async getAssignedPolicies({ policyId }) { - const path = `/policy/assigned-fields`; - return await this.post('POST', `${path}`, { policyId }); - } - - async deleteRole({ id }) { - const path = `/role/delete`; - return await this.post('DELETE', `${path}`, { id }); - } - - async getAssignedUserByRole({ id }) { - const path = `/user/assigned-by-role`; - return await this.post('POST', `${path}`, { id }); - } - - async getGroupDetailsByPolicy({ policyId }) { - const path = `/policy/policies-with-groups`; - return await this.post('POST', `${path}`, { policyId }); - } - - async usersWithGroupAndRole() { - const path = `/user/with-groups-and-roles`; - return await this.post('GET', `${path}`); - } - - async findOneUserWithGroupAndRole(id) { - const path = `/user/one-with-groups-and-roles`; - return await this.post('POST', `${path}`, { - id, - }); - } - - async createPolicySource({ policyId, sourceId }) { - const path = `/policySource/add`; - return await this.post('POST', `${path}`, { policyId, sourceId }); - } -} - -InSylvaGatekeeperClient.prototype.baseUrl = null; -InSylvaGatekeeperClient.prototype.token = null; - -export { InSylvaGatekeeperClient }; diff --git a/src/context/InSylvaSourceManager.js b/src/context/InSylvaSourceManager.js deleted file mode 100644 index a02e89c..0000000 --- a/src/context/InSylvaSourceManager.js +++ /dev/null @@ -1,155 +0,0 @@ -class InSylvaSourceManager { - async post(method, path, requestContent) { - const headers = { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - Authorization: `Bearer ${this.token}`, - }; - - const response = await fetch(`${this.baseUrl}${path}`, { - method: method, - headers, - body: JSON.stringify(requestContent), - mode: 'cors', - }); - return await response.json(); - } - - async createSource(metaUrfms, name, description, kc_id) { - const path = `/source`; - return await this.post('POST', `${path}`, { - metaUrfms, - name, - description, - kc_id, - }); - } - - async sources(kc_id) { - const path = '/sources'; - return await this.post('POST', `${path}`, { kc_id }); - } - - async indexedSources(kc_id) { - const path = '/indexed-sources'; - return await this.post('POST', `${path}`, { kc_id }); - } - - async allSource() { - const path = '/allSource'; - return await this.post('GET', `${path}`); - } - - async allIndexedSource() { - const path = '/allIndexedSource'; - return await this.post('GET', `${path}`); - } - - async addStdField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ) { - const path = `/addStdField`; - return await this.post('POST', `${path}`, { - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - }); - } - - async addAddtlField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ) { - const path = `/addAddtlField`; - return await this.post('POST', `${path}`, { - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - }); - } - - async updateStdField( - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional - ) { - const path = `/updateStdField`; - return await this.post('POST', `${path}`, { - category, - field_name, - definition_and_comment, - obligation_or_condition, - field_type, - cardinality, - values, - isPublic, - isOptional, - }); - } - - async updateSource(kc_id, source_id) { - const path = '/update_source'; - return await this.post('POST', `${path}`, { kc_id, source_id }); - } - - async publicFields() { - const path = `/stdFields`; - return this.post('GET', `${path}`); - } - async privateFields() { - const path = `/privateFieldList`; - return this.post('GET', `${path}`); - } - - async truncateStdField() { - const path = `/stdFields/truncate`; - return this.post('DELETE', `${path}`); - } - - async deleteSource(sourceId, kc_id) { - const path = `/source/delete`; - return this.post('POST', `${path}`, { sourceId, kc_id }); - } - - async mergeAndSendSource({ kc_id, name, description, sources }) { - const path = `/source/merge-and-send`; - return this.post('POST', `${path}`, { kc_id, name, description, sources }); - } -} - -InSylvaSourceManager.prototype.baseUrl = null; -InSylvaSourceManager.prototype.token = null; -export { InSylvaSourceManager }; diff --git a/src/context/KeycloakClient.js b/src/context/KeycloakClient.js deleted file mode 100644 index 1dcd055..0000000 --- a/src/context/KeycloakClient.js +++ /dev/null @@ -1,84 +0,0 @@ -class KeycloakClient { - async post(path, requestContent) { - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - // "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", - // "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization" - }; - let formBody = []; - for (const property in requestContent) { - const encodedKey = encodeURIComponent(property); - const encodedValue = encodeURIComponent(requestContent[property]); - formBody.push(encodedKey + '=' + encodedValue); - } - formBody = formBody.join('&'); - - const response = await fetch(`${this.baseUrl}${path}`, { - method: 'POST', - headers, - body: formBody, - mode: 'cors', - }); - if (response.ok === true) { - // ok - } else { - // throw new Error(response.status); - return await response.json(); - } - if (response.statusText === 'No Content') { - // ok - } else { - return await response.json(); - } - } - - async login({ - realm = this.realm, - client_id = this.client_id, - username, - password, - grant_type = this.grant_type, - }) { - const path = `/auth/realms/${realm}/protocol/openid-connect/token`; - const token = await this.post(`${path}`, { - client_id, - username, - password, - grant_type, - }); - return { token }; - } - - async refreshToken({ - realm = this.realm, - client_id = this.client_id, - // client_secret : 'optional depending on the type of client', - grant_type = 'refresh_token', - refresh_token, - }) { - const path = `/auth/realms/${realm}/protocol/openid-connect/token`; - const token = await this.post(`${path}`, { - client_id, - grant_type, - refresh_token, - }); - return { token }; - } - - async logout({ realm = this.realm, client_id = this.client_id }) { - const refresh_token = sessionStorage.getItem('refresh_token'); - const path = `/auth/realms/${realm}/protocol/openid-connect/logout`; - if (refresh_token) { - await this.post(`${path}`, { - client_id, - refresh_token, - }); - } - } -} - -KeycloakClient.prototype.baseUrl = null; -KeycloakClient.prototype.client_id = null; -KeycloakClient.prototype.grant_type = null; -KeycloakClient.prototype.realm = null; -export { KeycloakClient }; diff --git a/src/context/UserContext.js b/src/context/UserContext.js deleted file mode 100644 index 5c209e8..0000000 --- a/src/context/UserContext.js +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react'; -import { KeycloakClient } from './KeycloakClient'; -import { InSylvaGatekeeperClient } from './InSylvaGatekeeperClient'; -import { getGatekeeperBaseUrl, getLoginUrl, redirect } from '../utils.js'; - -const UserStateContext = React.createContext(); -const UserDispatchContext = React.createContext(); - -const kcClient = new KeycloakClient(); -kcClient.baseUrl = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_KEYCLOAK_HOST}:${process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_KEYCLOAK_HOST}`; -kcClient.realm = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_REALM}` - : `${window._env_.REACT_APP_IN_SYLVA_REALM}`; -kcClient.client_id = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_CLIENT_ID}` - : `${window._env_.REACT_APP_IN_SYLVA_CLIENT_ID}`; -kcClient.grant_type = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_GRANT_TYPE}` - : `${window._env_.REACT_APP_IN_SYLVA_GRANT_TYPE}`; - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = getGatekeeperBaseUrl(); - -function userReducer(state, action) { - switch (action.type) { - case 'LOGIN_SUCCESS': - return { ...state, isAuthenticated: true }; - case 'SIGN_OUT_SUCCESS': - return { ...state, isAuthenticated: false }; - case 'LOGIN_FAILURE': - return { ...state, isAuthenticated: false }; - case 'USER_CREATED': - return { ...state }; - default: { - throw new Error(`Unhandled action type: ${action.type} `); - } - } -} - -function UserProvider({ children }) { - const [state, dispatch] = React.useReducer(userReducer, { - isAuthenticated: !!sessionStorage.getItem('access_token'), - }); - - return ( - <UserStateContext.Provider value={state}> - <UserDispatchContext.Provider value={dispatch}> - {children} - </UserDispatchContext.Provider> - </UserStateContext.Provider> - ); -} - -function useUserState() { - const context = React.useContext(UserStateContext); - if (context === undefined) { - throw new Error('useUserState must be used within a UserProvider'); - } - return context; -} - -function useUserDispatch() { - const context = React.useContext(UserDispatchContext); - if (context === undefined) { - throw new Error('useUserDispatch must be used within a UserProvider'); - } - return context; -} - -async function checkUserLogin(userId, accessToken, refreshToken, roleId) { - if (!!userId && !!accessToken && !!refreshToken && !!roleId) { - sessionStorage.setItem('userId', userId); - sessionStorage.setItem('access_token', accessToken); - sessionStorage.setItem('refresh_token', refreshToken); - sessionStorage.setItem('roleId', roleId); - //To Do: - // Load the users histories from UserHistory(userId) endpoint - // Load the users result filters from Result_Filter(userId) endpoints - // Load the users policies from Policies(userId) endpoint - if (!sessionStorage.getItem('token_refresh_time')) { - sessionStorage.setItem('token_refresh_time', Date.now()); - } - } else { - console.error('users not logged in'); - } -} - -async function signOut(dispatch) { - await kcClient.logout({}); - dispatch({ type: 'SIGN_OUT_SUCCESS' }); - sessionStorage.removeItem('access_token'); - sessionStorage.removeItem('refresh_token'); - sessionStorage.removeItem('portal'); - sessionStorage.removeItem('userId'); - sessionStorage.removeItem('roleId'); - redirect(getLoginUrl() + '?requestType=portal'); -} - -export { UserProvider, useUserState, useUserDispatch, signOut, checkUserLogin }; diff --git a/src/images/logo.png b/src/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4fabf38732fa03638d007d456bf3b3dffab5f3ea GIT binary patch literal 23893 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 MmJzsK<9W>g0hikdyZ`_I literal 0 HcmV?d00001 diff --git a/src/images/mongo_logo.svg b/src/images/mongo_logo.svg new file mode 100644 index 0000000..65c4a12 --- /dev/null +++ b/src/images/mongo_logo.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"> + <circle cx="512" cy="512" r="512" style="fill:#13aa52"/> + <path d="M648.86 449.44c-32.34-142.73-108.77-189.66-117-207.59-9-12.65-18.12-35.15-18.12-35.15-.15-.38-.39-1.05-.67-1.7-.93 12.65-1.41 17.53-13.37 30.29-18.52 14.48-113.54 94.21-121.27 256.37-7.21 151.24 109.25 241.36 125 252.85l1.79 1.27v-.11c.1.76 5 36 8.44 73.34H526a726.68 726.68 0 0 1 13-78.53l1-.65a204.48 204.48 0 0 0 20.11-16.45l.72-.65c33.48-30.93 93.67-102.47 93.08-216.53a347.07 347.07 0 0 0-5.05-56.76zM512.35 659.12s0-212.12 7-212.08c5.46 0 12.53 273.61 12.53 273.61-9.72-1.17-19.53-45.03-19.53-61.53z" style="fill:#fff"/> +</svg> \ No newline at end of file diff --git a/src/index.js b/src/index.js index c7ada2c..3d4a198 100755 --- a/src/index.js +++ b/src/index.js @@ -6,36 +6,26 @@ import Themes from './themes'; import '@elastic/eui/dist/eui_theme_light.css'; import App from './components/App'; import { LayoutProvider } from './context/LayoutContext'; -import { UserProvider, checkUserLogin } from './context/UserContext'; -import { getLoginUrl, getUrlParam, redirect } from './utils.js'; +import { AuthProvider } from 'react-oidc-context'; +import { userManager } from './services/GatekeeperService'; -const userId = getUrlParam('kcId', ''); -const accessToken = getUrlParam('accessToken', ''); -const roleId = getUrlParam('roleId', ''); - -let refreshToken = getUrlParam('refreshToken', ''); -if (refreshToken.includes('#/app/portal')) { - refreshToken = refreshToken.substring(0, refreshToken.indexOf('#')); -} - -checkUserLogin(userId, accessToken, refreshToken, roleId); - -//To-Do -// * delete previous tokens from session storage -// * refresh tokens - -if (sessionStorage.getItem('access_token')) { - ReactDOM.render( +ReactDOM.render( + <AuthProvider + userManager={userManager} + onSigninCallback={() => { + window.history.replaceState( + {}, + document.title, + window.location.pathname + '#/home' + ); + }} + > <LayoutProvider> - <UserProvider> - <ThemeProvider theme={Themes.default}> - <CssBaseline /> - <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> - </ThemeProvider> - </UserProvider> - </LayoutProvider>, - document.getElementById('root') - ); -} else { - redirect(getLoginUrl() + '?requestType=portal'); -} + <ThemeProvider theme={Themes.default}> + <CssBaseline /> + <App /> + </ThemeProvider> + </LayoutProvider> + </AuthProvider>, + document.getElementById('root') +); diff --git a/src/pages/dashboard/Dashboard.js b/src/pages/dashboard/Dashboard.js index 1932fe6..5807df6 100644 --- a/src/pages/dashboard/Dashboard.js +++ b/src/pages/dashboard/Dashboard.js @@ -2,6 +2,7 @@ import React from 'react'; import { Grid, LinearProgress } from '@material-ui/core'; import { useTheme } from '@material-ui/styles'; import keycloakLogo from '../../images/Keycloak_Logo.svg'; +import mongoLogo from '../../images/mongo_logo.svg'; import { ResponsiveContainer, AreaChart, @@ -70,6 +71,13 @@ urlMaps.set( export default function Dashboard() { const classes = useStyles(); const theme = useTheme(); + const { + REACT_APP_KIBANA_URL, + REACT_APP_PGADMIN_URL, + REACT_APP_KEYCLOAK_URL, + REACT_APP_PORTAINER_URL, + REACT_APP_MONGO_EXPRESS_URL, + } = process.env; return ( <> @@ -83,9 +91,8 @@ export default function Dashboard() { <div> <EuiButton aria-label="Go to Kibana" - onClick={() => { - window.open(urlMaps.get('kibana'), '_blank'); - }} + href={`${REACT_APP_KIBANA_URL}`} + target="_blank" > Go for it </EuiButton> @@ -96,16 +103,15 @@ export default function Dashboard() { </EuiFlexItem> <EuiFlexItem> <EuiCard - icon={<EuiIcon size="xxl" type="logoPostgres" />} - title="PgAdmin" - description="Access to Postgresql" + icon={<EuiIcon size="xxl" type={mongoLogo} />} + title="MongoDB" + description="Access to MongoDB" footer={ <div> <EuiButton - aria-label="Go to Postgresql" - onClick={() => { - window.open(urlMaps.get('postgresql'), '_blank'); - }} + aria-label="Go to MongoDB" + href={`${REACT_APP_MONGO_EXPRESS_URL}`} + target="_blank" > Go for it </EuiButton> @@ -116,36 +122,15 @@ export default function Dashboard() { </EuiFlexItem> <EuiFlexItem> <EuiCard - icon={<EuiIcon size="xxl" type="logoMongodb" />} - title="MongoDb" - description="Access to MongoDb" - footer={ - <div> - <EuiButton - aria-label="Go to MongoDb" - onClick={() => { - window.open(urlMaps.get('mongoDb'), '_blank'); - }} - > - Go for it - </EuiButton> - <EuiSpacer size="xs" /> - </div> - } - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiCard - icon={<EuiIcon size="xxl" type="logoElastic" />} - title="Elasticsearch" - description="Access to Elasticsearch" + icon={<EuiIcon size="xxl" type="logoPostgres" />} + title="PgAdmin" + description="Access to PgAdmin" footer={ <div> <EuiButton - aria-label="Go to Elasticsearch" - onClick={() => { - window.open(urlMaps.get('elasticsearch'), '_blank'); - }} + aria-label="Go to PgAdmin" + href={`${REACT_APP_PGADMIN_URL}`} + target="_blank" > Go for it </EuiButton> @@ -163,9 +148,8 @@ export default function Dashboard() { <div> <EuiButton aria-label="Go to Keycloak" - onClick={() => { - window.open(urlMaps.get('keycloak'), '_blank'); - }} + href={`${REACT_APP_KEYCLOAK_URL}`} + target="_blank" > Go for it </EuiButton> @@ -183,9 +167,8 @@ export default function Dashboard() { <div> <EuiButton aria-label="Go to Portainer" - onClick={() => { - window.open(urlMaps.get('portainer'), '_blank'); - }} + href={`${REACT_APP_PORTAINER_URL}`} + target="_blank" > Go for it </EuiButton> diff --git a/src/pages/error/403.js b/src/pages/error/403.js new file mode 100644 index 0000000..1cace73 --- /dev/null +++ b/src/pages/error/403.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { Grid, Paper, Typography, Button } from '@material-ui/core'; +import classnames from 'classnames'; +import useStyles from './styles'; +import logo from './logo.png'; + +export default function Error403() { + const classes = useStyles(); + + return ( + <Grid container className={classes.container}> + <div className={classes.logotype}> + <img className={classes.logotypeIcon} src={logo} alt="logo" /> + <Typography variant="h3" color="white" className={classes.logotypeText}> + In-Sylva Project + </Typography> + </div> + <Paper classes={{ root: classes.paperRoot }}> + <Typography + variant="h1" + color="primary" + className={classnames(classes.textRow, classes.errorCode)} + > + 403 + </Typography> + <Typography variant="h5" color="primary" className={classes.textRow}> + Forbidden + </Typography> + <Typography variant="h5" color="primary" className={classes.textRow}> + Oops. Looks like you don't have the right permissions to access this page + </Typography> + <Button + href="#/home" + variant="contained" + color="primary" + size="large" + className={classes.backButton} + > + Back to Home + </Button> + </Paper> + </Grid> + ); +} diff --git a/src/pages/error/Error.js b/src/pages/error/404.js similarity index 77% rename from src/pages/error/Error.js rename to src/pages/error/404.js index e466863..ccf5ebe 100644 --- a/src/pages/error/Error.js +++ b/src/pages/error/404.js @@ -1,13 +1,11 @@ import React from 'react'; import { Grid, Paper, Typography, Button } from '@material-ui/core'; -import { Link } from 'react-router-dom'; import classnames from 'classnames'; import useStyles from './styles'; import logo from './logo.png'; -export default function Error() { +export default function Error404() { const classes = useStyles(); - return ( <Grid container className={classes.container}> <div className={classes.logotype}> @@ -25,21 +23,15 @@ export default function Error() { 404 </Typography> <Typography variant="h5" color="primary" className={classes.textRow}> - Oops. Looks like the page you're looking for no longer exists + Not Found </Typography> - <Typography - variant="h6" - color="text" - colorBrightness="secondary" - className={classnames(classes.textRow, classes.safetyText)} - > - But we're here to bring you back to safety + <Typography variant="h5" color="primary" className={classes.textRow}> + Oops. Looks like the page you're looking for no longer exists </Typography> <Button + href="#/home" variant="contained" color="primary" - component={Link} - to="/" size="large" className={classes.backButton} > diff --git a/src/pages/fields/Fields.js b/src/pages/fields/Fields.js index 1ec424c..46f766a 100644 --- a/src/pages/fields/Fields.js +++ b/src/pages/fields/Fields.js @@ -1,5 +1,4 @@ -import React, { useState, useCallback, useEffect, memo, useRef } from 'react'; -import store from '../../store/index'; +import React, { useState, useCallback, useEffect, memo } from 'react'; import { EuiForm, EuiPageContent, @@ -16,6 +15,11 @@ import { } from '@elastic/eui'; import { ShowAlert } from '../../components/Common'; import Papa from 'papaparse'; +import { + getPublicFields, + deleteAllStdFields, + createStdField, +} from '../../services/GatekeeperService'; const renderFiles = (files) => { if (files && files.length > 0) { @@ -33,7 +37,7 @@ const renderFiles = (files) => { } }; -const NewFieldsForm = memo(({ files, globalState, onFilePickerChange, onSaveField }) => { +const NewFieldsForm = memo(({ files, onFilePickerChange, onSaveField }) => { return ( <> <EuiForm component="form"> @@ -56,7 +60,7 @@ const NewFieldsForm = memo(({ files, globalState, onFilePickerChange, onSaveFiel </EuiFormRow> <EuiFormRow label=""> { - <EuiButton fill onClick={onSaveField} isLoading={globalState.isLoading}> + <EuiButton fill onClick={onSaveField}> Load </EuiButton> } @@ -66,69 +70,47 @@ const NewFieldsForm = memo(({ files, globalState, onFilePickerChange, onSaveFiel ); }); -const StdFields = memo( - ({ - stdFields, - stdFieldColumns, - tableref, - pagination, - sorting, - onTableChange, - onSelection, - search, - isLoading, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFormRow label="Uploaded standard fields" fullWidth> - <EuiInMemoryTable - // ref={tableref} - // itemId="id" - // isSelectable={true} - items={stdFields} - loading={isLoading} - columns={stdFieldColumns} - search={search} - pagination={true} - sorting={true} - // onChange={onTableChange} - // rowheader={"field_name"} - // selection={onSelection} - /> - </EuiFormRow> - </EuiForm> - </> - ); - } -); +const StdFields = memo(({ stdFields, stdFieldColumns }) => { + return ( + <> + <EuiForm component="form"> + <EuiFormRow label="Uploaded standard fields" fullWidth> + <EuiInMemoryTable + items={stdFields} + columns={stdFieldColumns} + search={{ + box: { + incremental: true, + }, + }} + pagination={true} + sorting={true} + /> + </EuiFormRow> + </EuiForm> + </> + ); +}); let debounceTimeoutId; let requestTimeoutId; const Fields = (props) => { - const [globalState, globalActions] = store(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [open, setOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(''); const [severity, setSeverity] = useState('info'); const [files, setFiles] = useState(); - const [fields, setFields] = useState([]); + const [loadedFields, setLoadedFields] = useState([]); const [stdFields, setStdFields] = useState([]); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortDirection, setSortDirection] = useState('asc'); - const [sortField, setSortField] = useState('field_name'); - const [totalItemCount, setTotalItemCount] = useState(0); - const tableref = useRef(); - const [isLoading, setIsLoading] = useState(false); + const [filteredFields, setFilteredFields] = useState([]); const loadStdFields = useCallback(async () => { - const fields = await globalActions.source.publicFields(); + const fields = await getPublicFields(); if (fields) { - setStdFields((prevFields) => [...prevFields, ...fields]); - setTotalItemCount(typeof fields === typeof undefined ? 0 : fields.length); + setStdFields(fields); + setFilteredFields(fields); } - }, [globalActions.source]); + }, []); useEffect(() => { // clean up controller @@ -138,69 +120,25 @@ const Fields = (props) => { } // cancel subscription to useEffect return () => (isSubscribed = false); - }, [totalItemCount, loadStdFields]); + }, [loadStdFields]); const onQueryChange = ({ query }) => { clearTimeout(debounceTimeoutId); - clearTimeout(requestTimeoutId); - debounceTimeoutId = setTimeout(() => { - setIsLoading(true); - requestTimeoutId = setTimeout(() => { - const items = stdFields.filter((field) => { - const normalizedFieldName = - `${field.field_name} ${field.Definition_and_comment}`.toLowerCase(); - const normalizedQuery = query.text.toLowerCase(); - return normalizedFieldName.indexOf(normalizedQuery) !== -1; - }); - if (query.text !== '') { - setIsLoading(false); - setStdFields(items); - } else { - loadStdFields(); - } - }, 1000); + const items = stdFields.filter((field) => { + const normalizedFieldName = + `${field.field_name} ${field.Definition_and_comment}`.toLowerCase(); + const normalizedQuery = query.text.toLowerCase(); + return normalizedFieldName.indexOf(normalizedQuery) !== -1; + }); + if (query.text !== '') { + setFilteredFields(items); + } else { + setFilteredFields([]); + } }, 300); }; - const search = { - onChange: onQueryChange, - box: { - incremental: true, - }, - }; - - /* - const onSelection = { - selectable: source => !source.is_send, - onSelectionChange: onSelectionChange, - initialSelected: unsentSources, - }; */ - - const onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - - setPageIndex(pageIndex); - setPageSize(pageSize); - setSortField(sortField); - setSortDirection(sortDirection); - }; - - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItemCount, - pageSizeOptions: [3, 5, 8], - }; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const onFilePickerChange = async (files) => { setFiles(files); await handleSelectedFile(files); @@ -209,8 +147,8 @@ const Fields = (props) => { const handleSelectedFile = async (files) => { for (const file of files) { const reader = new FileReader(); - await reader.readAsText(file); reader.onload = () => handleData(reader.result); + reader.readAsText(file); } }; @@ -230,38 +168,70 @@ const Fields = (props) => { }); result.data.forEach((item) => { + // lowercase the key + Object.keys(item).forEach((key) => { + const lowercasedKey = key.toLowerCase(); + if (key !== lowercasedKey) { + item[lowercasedKey] = item[key]; + delete item[key]; + } + }); rows.push(item); }); if (columns && rows) { - // (preColumns => ([...preColumns, ...columns])) - setFields((preRows) => [...preRows, ...rows]); + setLoadedFields((preRows) => [...preRows, ...rows]); } } }; const onSaveField = async () => { - if (fields) { - await globalActions.source.truncateStdField(); + if (loadedFields) { + await deleteAllStdFields(); + try { + for (const field of loadedFields) { + const { + cardinality, + category, + definition_and_comment, + field_name, + field_type, + isoptional, + is_optional, + ispublic, + is_public, + obligation_or_condition, + values, + list_url, + default_display_fields + } = field; - await fields.forEach((item) => { - globalActions.source.createStdField( - item.Category, - item.Field_name, - item.Definition_and_comment, - item.Obligation_or_condition, - item.Field_type, - item.Cardinality, - item.Values, - item.is_public, - false - ); - }); + console.log("fields: ", field); + const isOptional = isoptional || is_optional; + const isPublic = ispublic || is_public; + const response = await createStdField( + cardinality, + category, + definition_and_comment, + field_name, + field_type, + isOptional?.toString().toLowerCase() === 'true', + isPublic?.toString().toLowerCase() === 'true', + obligation_or_condition, + values, + list_url, + default_display_fields + ); + } - setOpen(true); - setAlertMessage('Standard fields added.'); - setSeverity('success'); - loadStdFields(); + // Reload fields after processing + loadStdFields(); + } catch (error) { + console.error('Unexpected error during field saving:', error); + setOpen(true); + setAlertMessage('An unexpected error occurred.'); + setSeverity('error'); + } } }; @@ -285,6 +255,18 @@ const Fields = (props) => { field: 'Obligation_or_condition', name: 'Obligation or condition', }, + { + field: 'cardinality', + name: 'Cardinality', + }, + { + field: 'default_display_fields', + name: 'Default display fields', + }, + { + field: 'list_url', + name: 'List URL', + }, { field: 'values', name: 'Values', @@ -303,7 +285,6 @@ const Fields = (props) => { <br /> <NewFieldsForm files={files} - globalState={globalState} onFilePickerChange={onFilePickerChange} onSaveField={onSaveField} /> @@ -316,16 +297,7 @@ const Fields = (props) => { content: ( <> <br /> - <StdFields - stdFields={stdFields} - stdFieldColumns={stdFieldColumns} - pagination={pagination} - sorting={sorting} - tableref={tableref} - onTableChange={onTableChange} - search={search} - loading={isLoading} - /> + <StdFields stdFields={stdFields} stdFieldColumns={stdFieldColumns} /> </> ), }, diff --git a/src/pages/groups/Groups.js b/src/pages/groups/Groups.js index eeb1655..8e4441d 100644 --- a/src/pages/groups/Groups.js +++ b/src/pages/groups/Groups.js @@ -1,5 +1,4 @@ import React, { useState, Fragment, useEffect, memo, useCallback } from 'react'; -import store from '../../store/index'; import { ShowAlert } from '../../components/Common'; import { EuiForm, @@ -19,10 +18,17 @@ import { EuiSelectable, EuiComboBox, } from '@elastic/eui'; +import { + createGroup, + getGroups, + getUsers, + deleteGroup, + addUserToGroup, + removeUserFromGroup, +} from '../../services/GatekeeperService'; const NewGroupForm = memo( ({ - globalState, groupNameValue, setGroupNameValue, groupDescriptionValue, @@ -50,7 +56,7 @@ const NewGroupForm = memo( </EuiFormRow> <EuiSpacer /> { - <EuiButton fill onClick={onSaveGroup} isLoading={globalState.isLoading}> + <EuiButton fill onClick={onSaveGroup}> Save </EuiButton> } @@ -65,7 +71,7 @@ const NewGroupForm = memo( ); const GroupAssignment = memo( - ({ groups, setGroups, users, setUsers, onGroupAssignment, globalState }) => { + ({ groups, setGroups, users, setUsers, onGroupAssignment }) => { return ( <> <EuiForm component="form"> @@ -106,12 +112,7 @@ const GroupAssignment = memo( </EuiFlexGroup> <EuiSpacer /> { - <EuiButton - type="submit" - onClick={onGroupAssignment} - isLoading={globalState.isLoading} - fill - > + <EuiButton type="submit" onClick={onGroupAssignment} fill> Save </EuiButton> } @@ -135,7 +136,7 @@ const AssignedGroups = memo( <EuiFormRow label="Select specific group"> <EuiComboBox placeholder="Select a group" - singleSelection={{ asPlainText: true }} + singleSelection={true} options={groups} selectedOptions={selectedGroup} onChange={(e) => { @@ -154,20 +155,22 @@ const AssignedGroups = memo( const Groups = () => { const [selectedTabNumber, setSelectedTabNumber] = useState(0); - const [globalState, globalActions] = store(); const [groupNameValue, setGroupNameValue] = useState(''); const [groupDescriptionValue, setGroupDescriptionValue] = useState(''); const [groups, setGroups] = useState([]); + const [open, setOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(''); const [severity, setSeverity] = useState('info'); + const [users, setUsers] = useState([]); const [tblGroups, setTblGroups] = useState([]); const [selectedGroup, setSelectedGroup] = useState([]); const [groupedUsers, setGroupedUsers] = useState([]); + const [refresh, setRefresh] = useState(true); const loadGroups = useCallback(async () => { - const _groups = await globalActions.group.getGroups(); + const _groups = await getGroups(); if (_groups) { const grouplist = []; for (const group of _groups) { @@ -178,55 +181,62 @@ const Groups = () => { } setGroups(_groups); } - }, [globalActions.group]); + }, []); const loadUsers = useCallback(async () => { - const _users = await globalActions.user.findUser(); + const _users = await getUsers(); const usersList = []; if (_users) { for (const user of _users) { - usersList.push({ value: user.id, label: user.username }); + usersList.push({ value: user.id, label: user.email, sub: user.kc_id }); } if (usersList) { setUsers(usersList); } } - }, [globalActions.user]); + }, []); useEffect(() => { - let isSubscribed = true; - if (isSubscribed) { + if (refresh) { + setRefresh(false); loadGroups(); loadUsers(); + onSelectedGroup([]); } - return () => (isSubscribed = false); - }, [loadGroups, loadUsers]); + }, [loadGroups, loadUsers, refresh, setRefresh]); const onDeleteGroup = async (group) => { if (group) { - await globalActions.group.deleteGroup(group.id); + const result = await deleteGroup(group.id); + if (result) { + setAlertMessage('Group has been deleted.'); + setSeverity('success'); + setRefresh(true); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); + } + setOpen(true); } - loadGroups(); }; const onSaveGroup = async () => { - if (groupNameValue && sessionStorage.getItem('userId')) { - await globalActions.group.createGroup( - groupNameValue, - groupDescriptionValue, - sessionStorage.getItem('userId') - ); - - if (globalState.status === 'SUCCESS') { - setOpen(true); + if (groupNameValue && groupDescriptionValue) { + const result = await createGroup(groupNameValue, groupDescriptionValue); + if (result?.id) { setAlertMessage('Group has been created.'); setSeverity('success'); + setRefresh(true); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); } - await loadGroups(); + setOpen(true); } }; - const onGroupAssignment = async () => { + const onGroupAssignment = async (e) => { + e.preventDefault(); const checkedGroups = tblGroups.filter((e) => { return e.checked === 'on'; }); @@ -237,30 +247,37 @@ const Groups = () => { for (const user of checkedUsers) { for (const group of checkedGroups) { - await globalActions.group.createGroupUser(group.value, user.value); + const response = await addUserToGroup(user.sub, group.value); } - } - - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Users-group assignment has been completed.'); - setSeverity('success'); + setRefresh(true); } }; const onRemoveUserFromGroup = async (e) => { - await globalActions.group.deleteGroupUser(e.groupid, e.userid); - const groupedUsers = await globalActions.group.getGroupUsers(e.groupid); - setGroupedUsers(groupedUsers); + await removeUserFromGroup(e.sub, e.groupId); + onSelectedGroup([]); + setRefresh(true); }; const onSelectedGroup = async (e) => { if (e.length > 0) { - const groupedUsers = await globalActions.group.getGroupUsers(e[0].value); - setGroupedUsers(groupedUsers); + const selectedGroup = groups.filter((group) => { + return group.id === e[0].value; + }); + const groupUsers = selectedGroup.map((group) => { + return group.users.map((user) => ({ + email: user.user.email, + name: group.name, + groupId: group.id, + description: group.description, + sub: user.user.kc_id, + })); + }); + setGroupedUsers(groupUsers.flatMap((x) => x)); setSelectedGroup(e); } else { - setSelectedGroup(e); + setGroupedUsers([]); + setSelectedGroup([]); } }; @@ -271,7 +288,9 @@ const Groups = () => { icon: 'trash', type: 'icon', color: 'danger', - onClick: onDeleteGroup, + onClick: async (group) => { + await onDeleteGroup(group); + }, }, ]; @@ -289,11 +308,11 @@ const Groups = () => { const groupColumns = [ { field: 'name', name: 'Group name' }, { field: 'description', name: 'Group description' }, - { name: 'Actions', actions: groupActions }, + { name: 'Actions', actions: groupActions, truncateText: true }, ]; const assignedGroupedUserColumns = [ - { field: 'username', name: 'Username' }, + { field: 'email', name: 'Email' }, { field: 'name', name: 'Group name' }, { field: 'description', name: 'Group description' }, { name: 'Actions', actions: assignedGroupedUserActions }, @@ -306,7 +325,6 @@ const Groups = () => { content: ( <> <NewGroupForm - globalState={globalState} groupNameValue={groupNameValue} setGroupNameValue={setGroupNameValue} groupDescriptionValue={groupDescriptionValue} @@ -329,7 +347,6 @@ const Groups = () => { users={users} setUsers={setUsers} onGroupAssignment={onGroupAssignment} - globalState={globalState} /> </> ), diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js new file mode 100644 index 0000000..76e97bc --- /dev/null +++ b/src/pages/home/Home.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { + EuiButton, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import logo from '../../images/logo.png'; +import useStyles from './styles'; + +export default function Home() { + const classes = useStyles(); + const { REACT_APP_VALIDATOR_URL } = process.env; + + return ( + <EuiFlexGroup + direction="column" + justifyContent="center" + alignItems="center" + style={{ minHeight: 'calc(90vh)', textAlign: 'center' }} + > + {/* Logo at the top */} + <EuiFlexItem grow={false}> + <EuiIcon type={logo} size="original" /> + </EuiFlexItem> + + {/* Main content */} + <EuiFlexItem> + <div className={classes.heroContainer}> + <EuiText> + <h1>Welcome to the Portal</h1> + <p> + This is the central hub for managing users, groups, sources, policies, + fields, and relationships between them. + </p> + </EuiText> + <EuiSpacer size="l" /> + <EuiButton color="primary" size="m" href={`${REACT_APP_VALIDATOR_URL}`} target="_blank"> + <EuiIcon type="checkInCircleFilled" size="l" /> + Validate your JSON sources + </EuiButton> + </div> + </EuiFlexItem> + </EuiFlexGroup> + ); +} diff --git a/src/pages/home/package.json b/src/pages/home/package.json new file mode 100644 index 0000000..2ecfad4 --- /dev/null +++ b/src/pages/home/package.json @@ -0,0 +1,7 @@ +{ + "name": "Home", + "version": "1.0.0", + "private": true, + "main": "Home.js" + } + \ No newline at end of file diff --git a/src/pages/home/styles.js b/src/pages/home/styles.js new file mode 100644 index 0000000..05e123e --- /dev/null +++ b/src/pages/home/styles.js @@ -0,0 +1,25 @@ +import { makeStyles } from '@material-ui/styles'; + +export default makeStyles((theme) => ({ + root: { + display: 'flex', + maxWidth: '100vw', + overflowX: 'hidden', + }, + content: { + flexGrow: 1, + padding: theme.spacing(3), + width: `calc(100vw - 240px)`, + minHeight: '100vh', + }, + contentShift: { + width: `calc(100vw - ${240 + theme.spacing(6)}px)`, + transition: theme.transitions.create(['width', 'margin'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + }, + fakeToolbar: { + ...theme.mixins.toolbar, + }, +})); diff --git a/src/pages/policies/AssignedPolicies.js b/src/pages/policies/AssignedPolicies.js new file mode 100644 index 0000000..53ca15c --- /dev/null +++ b/src/pages/policies/AssignedPolicies.js @@ -0,0 +1,131 @@ +import React, { memo } from 'react'; +import { EuiBasicTable, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; + +const AssignedPolicies = memo( + ({ + policies, + selectedPolicy, + onSelectedPolicy, + assignedPolicySources, + assignedPolicyFields, + assignedPolicyGroups, + onDeletePolicySource, + onDeletePolicyField, + onDeletePolicyGroup, + }) => { + const assignedPolicyColumns = [ + { + field: 'field_name', + name: 'Field name', + sortable: true, + }, + { + field: 'definition_and_comment', + name: 'Definition and comment', + truncateText: true, + mobileOptions: { + show: false, + }, + }, + { + field: 'field_type', + name: 'Field type', + }, + { + name: 'Actions', + actions: [ + { + name: 'Delete', + description: 'Delete this policies-field', + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: async (e) => { + await onDeletePolicyField(e); + }, + }, + ], + }, + ]; + + const assignedPolicyGroupColumns = [ + { field: 'group_name', name: 'Group name' }, + { field: 'group_description', name: 'Group description' }, + { + name: 'Actions', + actions: [ + { + name: 'Delete', + description: 'Delete this policies-group', + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: async (e) => { + await onDeletePolicyGroup(e); + }, + }, + ], + }, + ]; + + const assignedPolicySourceColumns = [ + { + field: 'source_name', + name: 'Source name', + }, + { + field: 'source_description', + name: 'Source description', + }, + { + name: 'Actions', + actions: [ + { + name: 'Delete', + description: 'Delete this policy-source', + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: async (e) => { + await onDeletePolicySource(e); + }, + }, + ], + }, + ]; + return ( + <> + <EuiForm component="form"> + <EuiFormRow label="Select a specific policy"> + <EuiComboBox + placeholder="Select a policy" + singleSelection={true} + options={policies} + selectedOptions={selectedPolicy} + onChange={(e) => { + onSelectedPolicy(e); + }} + /> + </EuiFormRow> + <EuiFormRow label="Assigned sources" fullWidth> + <EuiBasicTable + items={assignedPolicySources} + columns={assignedPolicySourceColumns} + /> + </EuiFormRow> + <EuiFormRow label="Assigned standard fields" fullWidth> + <EuiBasicTable items={assignedPolicyFields} columns={assignedPolicyColumns} /> + </EuiFormRow> + <EuiFormRow label="Assigned groups" fullWidth> + <EuiBasicTable + items={assignedPolicyGroups} + columns={assignedPolicyGroupColumns} + /> + </EuiFormRow> + </EuiForm> + </> + ); + } +); + +export default AssignedPolicies; diff --git a/src/pages/policies/NewPolicyForm.js b/src/pages/policies/NewPolicyForm.js new file mode 100644 index 0000000..6350ba1 --- /dev/null +++ b/src/pages/policies/NewPolicyForm.js @@ -0,0 +1,59 @@ +import React, { memo } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiBasicTable, +} from '@elastic/eui'; + +const NewPolicyForm = memo( + ({ policyName, setPolicyName, onSaveNewPolicy, onDeletePolicy, policies }) => { + const policyColumns = [ + { + field: 'policyname', + name: 'Policy name', + }, + { + field: 'sourcename', + name: 'Source name', + }, + { + name: 'Actions', + actions: [ + { + name: 'Delete', + description: 'Delete this policy', + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: onDeletePolicy, + }, + ], + }, + ]; + return ( + <> + <EuiForm component="form"> + <EuiFormRow label="Policy name"> + <EuiFieldText + value={policyName} + onChange={(e) => setPolicyName(e.target.value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton type="submit" onClick={onSaveNewPolicy} fill> + Save + </EuiButton> + <EuiSpacer /> + <EuiFormRow label="" fullWidth> + <EuiBasicTable items={policies} rowheader="Name" columns={policyColumns} /> + </EuiFormRow> + </EuiForm> + </> + ); + } +); + +export default NewPolicyForm; diff --git a/src/pages/policies/Policies.js b/src/pages/policies/Policies.js index ab3c622..e53c0d5 100644 --- a/src/pages/policies/Policies.js +++ b/src/pages/policies/Policies.js @@ -1,5 +1,4 @@ -import React, { useState, Fragment, useCallback, useEffect, memo } from 'react'; -import store from '../../store/index'; +import React, { useState, useCallback, useEffect } from 'react'; import { EuiForm, EuiPageContent, @@ -8,306 +7,45 @@ import { EuiTitle, EuiPageContentBody, EuiTabbedContent, - EuiFormRow, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiButton, - EuiBasicTable, - EuiSelectable, - EuiComboBox, } from '@elastic/eui'; +import PolicyAssignment from './PolicyAssignment'; +import PolicyGroupAssignment from './PolicyGroupAssignment'; +import PolicySourceAssignment from './PolicySourceAssignment'; +import NewPolicyForm from './NewPolicyForm'; +import AssignedPolicies from './AssignedPolicies'; import { ShowAlert } from '../../components/Common'; - -const NewPolicyForm = memo( - ({ - globalState, - policyName, - setPolicyName, - sources, - isSwitchChecked, - onSwitchChange, - selectedSource, - setSelectedSource, - onSaveNewPolicy, - policies, - policyColumns, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFormRow label="Policy name"> - <EuiFieldText - value={policyName} - onChange={(e) => setPolicyName(e.target.value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton - type="submit" - onClick={onSaveNewPolicy} - isLoading={globalState.isLoading} - fill - > - Save - </EuiButton> - <EuiSpacer /> - <EuiFormRow label="" fullWidth> - <EuiBasicTable items={policies} rowheader="Name" columns={policyColumns} /> - </EuiFormRow> - </EuiForm> - </> - ); - } -); - -const PolicyAssignment = memo( - ({ - globalState, - optPolicies, - setOptPolicies, - optStdFields, - setOptStdFields, - onPolicyAssignment, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFlexGroup component="span"> - <EuiFlexItem component="span"> - <EuiFormRow label="Policies" fullWidth> - <EuiSelectable - searchable - singleSelection={true} - options={optPolicies} - onChange={(newOptions) => setOptPolicies(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem component="span"> - <EuiFormRow label="Standard fields" fullWidth> - <EuiSelectable - searchable - options={optStdFields} - onChange={(newOptions) => setOptStdFields(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer /> - <EuiButton - type="submit" - onClick={onPolicyAssignment} - isLoading={globalState.isLoading} - fill - > - Save - </EuiButton> - </EuiForm> - </> - ); - } -); - -const PolicyGroupAssignment = memo( - ({ - globalState, - optPolicies, - setOptPolicies, - optGroups, - setOPtGroups, - onPolicyGroupAssignment, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFlexGroup component="span"> - <EuiFlexItem component="span"> - <EuiFormRow label="Policies" fullWidth> - <EuiSelectable - searchable - singleSelection={true} - options={optPolicies} - onChange={(newOptions) => setOptPolicies(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem component="span"> - <EuiFormRow label="Groups" fullWidth> - <EuiSelectable - searchable - options={optGroups} - onChange={(newOptions) => setOPtGroups(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer /> - <EuiButton - type="submit" - onClick={onPolicyGroupAssignment} - isLoading={globalState.isLoading} - fill - > - Save - </EuiButton> - </EuiForm> - </> - ); - } -); - -const PolicySourceAssignment = memo( - ({ - globalState, - optPolicies, - setOptPolicies, - optSources, - setOptSources, - onPolicySourceAssignment, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFlexGroup component="span"> - <EuiFlexItem component="span"> - <EuiFormRow label="Policies" fullWidth> - <EuiSelectable - searchable - singleSelection={true} - options={optPolicies} - onChange={(newOptions) => setOptPolicies(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem component="span"> - <EuiFormRow label="Sources" fullWidth> - <EuiSelectable - searchable - singleSelection={true} - options={optSources} - onChange={(newOptions) => setOptSources(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer /> - <EuiButton - type="submit" - onClick={onPolicySourceAssignment} - isLoading={globalState.isLoading} - fill - > - Save - </EuiButton> - </EuiForm> - </> - ); - } -); - -const AssignedPolicies = memo( - ({ - policies, - selectedPolicy, - fields, - assignedPolicyColumns, - getRowProps, - getCellProps, - onSelectedPolicy, - assignedPolicyGroups, - assignedPolicyGroupColumns, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFormRow label="Select a specific policy"> - <EuiComboBox - placeholder="Select a policy" - singleSelection={{ asPlainText: true }} - options={policies} - selectedOptions={selectedPolicy} - onChange={(e) => { - onSelectedPolicy(e); - }} - /> - </EuiFormRow> - <EuiFormRow label="Assigned standard fields" fullWidth> - <EuiBasicTable - items={fields} - columns={assignedPolicyColumns} - rowProps={getRowProps} - cellProps={getCellProps} - /> - </EuiFormRow> - <EuiFormRow label="Assigned groups" fullWidth> - <EuiBasicTable - items={assignedPolicyGroups} - columns={assignedPolicyGroupColumns} - /> - </EuiFormRow> - </EuiForm> - </> - ); - } -); +import { + findUserBySub, + getSources, + getPolicies, + getStdFields, + getGroups, + createPolicy, + deletePolicy, + addSourceToPolicy, + addFieldToPolicy, + addPolicyToGroup, + removeSourceFromPolicy, + removeFieldFromPolicy, + removePolicyFromGroup, + getUser, +} from '../../services/GatekeeperService'; const Policies = () => { - const [globalState, globalActions] = store(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [isSwitchChecked, setIsSwitchChecked] = useState(false); const [policyName, setPolicyName] = useState(''); const [selectedSource, setSelectedSource] = useState(); + const [open, setOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(''); const [severity, setSeverity] = useState('info'); + const [optPolicies, setOptPolicies] = useState([]); const [selectedPolicy, setSelectedPolicy] = useState(); const [optStdFields, setOptStdFields] = useState([]); const [assignedPolicyItems, setAssignedPolicyItems] = useState([]); + const [assignedSources, setAssignedSources] = useState([]); const [policies, setPolicies] = useState([]); const [optGroupPolicies, setOptGroupPolicies] = useState([]); const [optGroups, setOPtGroups] = useState([]); @@ -316,125 +54,124 @@ const Policies = () => { const [optSources, setOptSources] = useState([]); const loadSources = useCallback(async () => { - if (sessionStorage.getItem('userId')) { - const user = await globalActions.user.findOneUserWithGroupAndRole( - sessionStorage.getItem('userId') - ); - const role = user[0].roleid; - let result; - if (role === 1) { - result = await globalActions.source.allIndexedSources(); - } else { - result = await globalActions.source.indexedSources( - sessionStorage.getItem('userId') - ); - } - if (result) { - const _sources = []; - for (const source of result) { - _sources.push({ value: source.id, label: source.name }); - } - setOptSources(_sources); + const user = await findUserBySub(getUser().profile.sub); + const role = user.roles[0].roleid; + let result; + if (role === 1) { + result = await getSources(); + } else { + result = await getSources(); // TODO load sources for user + } + if (result) { + const _sources = []; + for (const source of result) { + _sources.push({ value: source.id, label: source.name }); } + setOptSources(_sources); } - }, [globalActions.source, globalActions.user]); + }, [findUserBySub]); const loadPolicies = useCallback(async () => { - if (sessionStorage.getItem('userId')) { - const user = await globalActions.user.findOneUserWithGroupAndRole( - sessionStorage.getItem('userId') - ); - const role = user[0].roleid; - let result; - if (role === 1) { - result = await globalActions.policy.getPolicies(); - } else { - result = await globalActions.policy.getPoliciesByUser( - sessionStorage.getItem('userId') - ); - } - if (result) { - const _policies = []; - for (const policy of result) { - _policies.push({ value: policy.id, label: policy.name }); - } - setOptPolicies(_policies); - setOptGroupPolicies(_policies); - setOptSourcePolicies(_policies); - } + const user = await findUserBySub(getUser().profile.sub); + const role = user.roles[0].roleid; + let result; + if (role === 1) { + result = await getPolicies(); + } else { + result = await getPolicies(); // TODO load policies for user } - }, [globalActions.policy, globalActions.user]); - - const loadPoliciesWithSources = useCallback(async () => { - if (sessionStorage.getItem('userId')) { - const user = await globalActions.user.findOneUserWithGroupAndRole( - sessionStorage.getItem('userId') - ); - const role = user[0].roleid; - let result; - if (role === 1) { - result = await globalActions.policy.getPoliciesWithSources(); - } else { - result = await globalActions.policy.getPoliciesWithSourcesByUser( - sessionStorage.getItem('userId') - ); - } - if (result) { - const policyList = []; - const policyNames = []; - result.forEach((policy) => { - if (policyNames.includes(policy.policyname)) { - policyList[policyNames.indexOf(policy.policyname)].sourcename = - `${policyList[policyNames.indexOf(policy.policyname)].sourcename}, ${policy.sourcename}`; - } else { - policyNames.push(policy.policyname); - policyList.push(policy); - } - }); - setPolicies(policyList); + if (result) { + const _policies = []; + for (const policy of result) { + _policies.push({ value: policy.id, label: policy.name }); } + result = result.map((policy) => { + return { + id: policy.id, + policyname: policy.name, + sourcename: policy.sources.map((source) => source.source.name).join(', '), + }; + }); + setPolicies(result); + setOptPolicies(_policies); + setOptGroupPolicies(_policies); + setOptSourcePolicies(_policies); } - }, [globalActions.policy, globalActions.user]); + }, [getPolicies]); const loadStdFields = useCallback(async () => { - const result = await globalActions.source.privateFields(); + let result = await getStdFields(); if (result) { + result = result.filter((field) => !field.ispublic); const _fields = []; for (const field of result) { _fields.push({ value: field.id, label: field.field_name }); } setOptStdFields(_fields); } - }, [globalActions.source]); + }, [getStdFields]); const loadGroups = useCallback(async () => { - const _groups = await globalActions.group.getGroups(); + const _groups = await getGroups(); if (_groups) { const groups = []; for (const group of _groups) { - groups.push({ value: group.id, label: group.name }); + groups.push({ + value: group.id, + label: group.name, + description: group.description, + }); } setOPtGroups(groups); } - }, [globalActions.group]); + }, [getGroups]); useEffect(() => { - // clean up controller - let isSubscribed = true; + loadSources(); + loadPolicies(); + loadStdFields(); + loadGroups(); + onSelectedPolicy([]); + setPolicyName(''); + }, [loadGroups, loadPolicies, loadSources, loadStdFields, selectedTabNumber]); - if (isSubscribed) { - loadSources(); - loadPolicies(); - loadStdFields(); - loadPoliciesWithSources(); - loadGroups(); + const onSwitchChange = () => { + setIsSwitchChecked(!isSwitchChecked); + }; + + const onSaveNewPolicy = async (e) => { + e.preventDefault(); + if (policyName) { + const result = await createPolicy(policyName); + if (result.id) { + setAlertMessage('Policy created.'); + setSeverity('success'); + setPolicyName(''); + loadPolicies(); + } else { + setAlertMessage('Policy not created.'); + setSeverity('error'); + } + setOpen(true); } + }; - // cancel subscription to useEffect - return () => (isSubscribed = false); - }, [loadGroups, loadPolicies, loadPoliciesWithSources, loadSources, loadStdFields]); + const onDeletePolicy = async (e) => { + if (!e.id) return; + const response = await deletePolicy(e.id); + if (response && response.success) { + setAlertMessage('Policy deleted.'); + setSeverity('success'); + loadPolicies(); + } else { + setAlertMessage('Policy not deleted.'); + setSeverity('error'); + } + setOpen(true); + }; - const onPolicySourceAssignment = async () => { + const onPolicySourceAssignment = async (e) => { + e.preventDefault(); const checkedPolicies = optSourcePolicies.filter((e) => { return e.checked === 'on'; }); @@ -446,41 +183,17 @@ const Policies = () => { if (checkedPolicies && checkedSources) { for (const policy of checkedPolicies) { for (const source of checkedSources) { - await globalActions.policy.createPolicySource(policy.value, source.value); + await addSourceToPolicy(source.value, policy.value); } } - - if (globalState.status === 'SUCCESS') { - loadPoliciesWithSources(); - setOpen(true); - setAlertMessage('Policies-Source assignment completed successfully !'); - setSeverity('success'); - } - } - }; - - const onSwitchChange = () => { - setIsSwitchChecked(!isSwitchChecked); - }; - - const onSaveNewPolicy = async () => { - if (policyName && sessionStorage.getItem('userId')) { - await globalActions.policy.createPolicy( - policyName, - sessionStorage.getItem('userId') - ); - - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Policy created.'); - setSeverity('success'); - loadPolicies(); - loadPoliciesWithSources(); - } } + setOptSourcePolicies(optSourcePolicies.map((e) => ({ ...e, checked: null }))); + setOptSources(optSources.map((e) => ({ ...e, checked: null }))); + loadPolicies(); }; - const onPolicyAssignment = async () => { + const onPolicyAssignment = async (e) => { + e.preventDefault(); const checkedPolicies = optPolicies.filter((e) => { return e.checked === 'on'; }); @@ -488,22 +201,19 @@ const Policies = () => { const checkedStdFields = optStdFields.filter((e) => { return e.checked === 'on'; }); - for (const policy of checkedPolicies) { for (const stdField of checkedStdFields) { - //to-do: if policies is already defined, do not let insert them into table. - await globalActions.policy.createPolicyField(policy.value, stdField.value); + await addFieldToPolicy(stdField.value, policy.value); } } - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Policies-Assignment completed.'); - setSeverity('success'); - } + setOptPolicies(optPolicies.map((e) => ({ ...e, checked: null }))); + setOptStdFields(optStdFields.map((e) => ({ ...e, checked: null }))); + loadPolicies(); }; - const onPolicyGroupAssignment = async () => { + const onPolicyGroupAssignment = async (e) => { + e.preventDefault(); const checkedPolicies = optGroupPolicies.filter((e) => { return e.checked === 'on'; }); @@ -515,184 +225,110 @@ const Policies = () => { if (checkedPolicies && checkedGroups) { for (const group of checkedGroups) { for (const policy of checkedPolicies) { - await globalActions.group.createGroupPolicy(group.value, policy.value); + await addPolicyToGroup(policy.value, group.value); } } - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Policies-Group assignment completed.'); - setSeverity('success'); - } + + setOptGroupPolicies(optGroupPolicies.map((e) => ({ ...e, checked: null }))); + setOPtGroups(optGroups.map((e) => ({ ...e, checked: null }))); + loadPolicies(); } }; const onSelectedPolicy = async (e) => { if (e.length > 0) { - const aPolicies = await globalActions.policy.getAssignedPolicies(e[0].value); - const groupDetailsByPolicy = await globalActions.policy.getGroupDetailsByPolicy( - e[0].value - ); - - if (aPolicies) { - const policyItemsList = []; - const fieldNames = []; - aPolicies.forEach((aPolicy) => { - if (!fieldNames.includes(aPolicy.field_name)) { - fieldNames.push(aPolicy.field_name); - policyItemsList.push(aPolicy); - } - }); - setAssignedPolicyItems(policyItemsList); + const policies = await getPolicies(); + const selected = policies.find((policy) => { + return policy.id === e[0].value; + }); + + const policyItems = selected.std_fields.map((field) => { + return { + id: field.std_field.id, + field_name: field.std_field.field_name, + definition_and_comment: field.std_field.definition_and_comment, + field_type: field.std_field.field_type, + }; + }); + setAssignedPolicyItems(policyItems); + + let assignedSources = []; + for (const source of selected.sources) { + let assignedSource = {}; + assignedSource.id = source.source.id; + assignedSource.source_name = source.source.name; + assignedSource.source_description = source.source.description; + assignedSources.push(assignedSource); } - - if (groupDetailsByPolicy) { - const policyGroupList = []; - const groupIds = []; - groupDetailsByPolicy.forEach((groupDetail) => { - if (groupIds.includes(groupDetail.groupid)) { - policyGroupList[groupIds.indexOf(groupDetail.groupid)].source_name = - `${policyGroupList[groupIds.indexOf(groupDetail.groupid)].source_name}, ${groupDetail.source_name}`; - } else { - groupIds.push(groupDetail.groupid); - policyGroupList.push(groupDetail); - } - }); - setAssignedPolicyGroups(policyGroupList); + setAssignedSources(assignedSources); + + let assignedGroups = []; + for (const group of selected.groups) { + //console.log(group); + let assignedGroup = {}; + assignedGroup.id = group.group.id; + assignedGroup.group_name = group.group.name; + assignedGroup.group_description = group.group.description; + assignedGroups.push(assignedGroup); } - + setAssignedPolicyGroups(assignedGroups); setSelectedPolicy(e); } else { - setSelectedPolicy(e); + setSelectedPolicy([]); + setAssignedPolicyItems([]); + setAssignedSources([]); + setAssignedPolicyGroups([]); } }; - const onDeletePolicyField = async (e) => { - if (e.id) { - await globalActions.policy.deletePolicyField(e.id); - + const onDeletePolicySource = async (e) => { + if (e.id && selectedPolicy.length > 0) { + const result = await removeSourceFromPolicy(e.id, selectedPolicy[0].value); + if (result && result.success) { + setAlertMessage('Policies-Source deleted.'); + setSeverity('success'); + onSelectedPolicy(selectedPolicy); + loadPolicies(); + } else { + setAlertMessage('Policies-Source not deleted.'); + setSeverity('error'); + } setOpen(true); - setAlertMessage('Policies-Field deleted.'); - setSeverity('success'); } }; - const assignedPolicyActions = [ - { - name: 'Delete', - description: 'Delete this policies-field', - icon: 'trash', - type: 'icon', - color: 'danger', - onClick: onDeletePolicyField, - }, - ]; - - const assignedPolicyColumns = [ - { - field: 'field_name', - name: 'Field name', - sortable: true, - }, - { - field: 'definition_and_comment', - name: 'Definition and comment', - truncateText: true, - mobileOptions: { - show: false, - }, - }, - { - field: 'field_type', - name: 'Field type', - }, - { - name: 'Actions', - actions: assignedPolicyActions, - }, - ]; - - const onAssignedPolicyGroup = async (e) => { - if (e.grouppolicyid) { - await globalActions.group.deleteGroupPolicy(e.grouppolicyid); - + const onDeletePolicyField = async (e) => { + if (e.id && selectedPolicy.length > 0) { + const result = await removeFieldFromPolicy(e.id, selectedPolicy[0].value); + if (result && result.success) { + setAlertMessage('Policies-Field deleted.'); + setSeverity('success'); + onSelectedPolicy(selectedPolicy); + loadPolicies(); + } else { + setAlertMessage('Policies-Field not deleted.'); + setSeverity('error'); + } setOpen(true); - setAlertMessage('Policies-groups deleted.'); - setSeverity('success'); } }; - const assignedPolicyGroupActions = [ - { - name: 'Delete', - description: 'Delete this policies-group', - icon: 'trash', - type: 'icon', - color: 'danger', - onClick: onAssignedPolicyGroup, - }, - ]; - - const assignedPolicyGroupColumns = [ - { field: 'group_name', name: 'Group name' }, - { field: 'source_name', name: 'Source name' }, - { field: 'source_description', name: 'Source description' }, - { name: 'Actions', actions: assignedPolicyGroupActions }, - ]; - - const onDeletePolicy = async (e) => { - await globalActions.policy.deletePolicy(e.id); - if (globalState.status === 'SUCCESS') { + const onDeletePolicyGroup = async (e) => { + if (e.id && selectedPolicy.length > 0) { + const result = await removePolicyFromGroup(selectedPolicy[0].value, e.id); + if (result && result.success) { + setAlertMessage('Policies-Group deleted.'); + setSeverity('success'); + onSelectedPolicy(selectedPolicy); + loadPolicies(); + } else { + setAlertMessage('Policies-Group not deleted.'); + setSeverity('error'); + } setOpen(true); - setAlertMessage('Policy deleted.'); - setSeverity('success'); - loadPoliciesWithSources(); - loadPolicies(); } }; - const policyColumnAction = [ - { - name: 'Delete', - description: 'Delete this policy', - icon: 'trash', - type: 'icon', - color: 'danger', - onClick: onDeletePolicy, - }, - ]; - const policyColumns = [ - { - field: 'policyname', - name: 'Policy name', - }, - { - field: 'sourcename', - name: 'Source name', - }, - { - name: 'Actions', - actions: policyColumnAction, - }, - ]; - - const getRowProps = (item) => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - className: 'customRowClass', - }; - }; - - const getCellProps = (item, column) => { - const { id } = item; - const { field } = column; - return { - className: 'customCellClass', - 'data-test-subj': `cell-${id}-${field}`, - textOnly: true, - }; - }; - const handleClose = (event, reason) => { if (reason === 'clickaway') { return; @@ -708,7 +344,6 @@ const Policies = () => { <> <br /> <NewPolicyForm - globalState={globalState} policyName={policyName} setPolicyName={setPolicyName} isSwitchChecked={isSwitchChecked} @@ -717,8 +352,8 @@ const Policies = () => { selectedSource={selectedSource} setSelectedSource={setSelectedSource} onSaveNewPolicy={onSaveNewPolicy} + onDeletePolicy={onDeletePolicy} policies={policies} - policyColumns={policyColumns} /> </> ), @@ -730,7 +365,6 @@ const Policies = () => { <> <br /> <PolicySourceAssignment - globalState={globalState} optPolicies={optSourcePolicies} setOptPolicies={setOptSourcePolicies} optSources={optSources} @@ -747,7 +381,6 @@ const Policies = () => { <> <br /> <PolicyAssignment - globalState={globalState} optPolicies={optPolicies} setOptPolicies={setOptPolicies} optStdFields={optStdFields} @@ -764,7 +397,6 @@ const Policies = () => { <> <br /> <PolicyGroupAssignment - globalState={globalState} optPolicies={optGroupPolicies} setOptPolicies={setOptGroupPolicies} optGroups={optGroups} @@ -783,13 +415,13 @@ const Policies = () => { <AssignedPolicies policies={optPolicies} selectedPolicy={selectedPolicy} - fields={assignedPolicyItems} - assignedPolicyColumns={assignedPolicyColumns} - getRowProps={getRowProps} - getCellProps={getCellProps} onSelectedPolicy={onSelectedPolicy} + assignedPolicySources={assignedSources} + assignedPolicyFields={assignedPolicyItems} assignedPolicyGroups={assignedPolicyGroups} - assignedPolicyGroupColumns={assignedPolicyGroupColumns} + onDeletePolicySource={onDeletePolicySource} + onDeletePolicyField={onDeletePolicyField} + onDeletePolicyGroup={onDeletePolicyGroup} /> </> ), diff --git a/src/pages/policies/PolicyAssignment.js b/src/pages/policies/PolicyAssignment.js new file mode 100644 index 0000000..44cfbc0 --- /dev/null +++ b/src/pages/policies/PolicyAssignment.js @@ -0,0 +1,68 @@ +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSelectable, + EuiSpacer, +} from '@elastic/eui'; +import React, { Fragment, memo } from 'react'; + +const PolicyAssignment = memo( + ({ + optPolicies, + setOptPolicies, + optStdFields, + setOptStdFields, + onPolicyAssignment, + }) => { + return ( + <> + <EuiForm component="form"> + <EuiFlexGroup component="span"> + <EuiFlexItem component="span"> + <EuiFormRow label="Policies" fullWidth> + <EuiSelectable + searchable + singleSelection={true} + options={optPolicies} + onChange={(newOptions) => setOptPolicies(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem component="span"> + <EuiFormRow label="Standard fields" fullWidth> + <EuiSelectable + searchable + options={optStdFields} + onChange={(newOptions) => setOptStdFields(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiButton type="submit" onClick={onPolicyAssignment} fill> + Save + </EuiButton> + </EuiForm> + </> + ); + } +); + +export default PolicyAssignment; diff --git a/src/pages/policies/PolicyGroupAssignment.js b/src/pages/policies/PolicyGroupAssignment.js new file mode 100644 index 0000000..7233a2b --- /dev/null +++ b/src/pages/policies/PolicyGroupAssignment.js @@ -0,0 +1,62 @@ +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSelectable, + EuiSpacer, +} from '@elastic/eui'; +import React, { Fragment, memo } from 'react'; + +const PolicyGroupAssignment = memo( + ({ optPolicies, setOptPolicies, optGroups, setOPtGroups, onPolicyGroupAssignment }) => { + return ( + <> + <EuiForm component="form"> + <EuiFlexGroup component="span"> + <EuiFlexItem component="span"> + <EuiFormRow label="Policies" fullWidth> + <EuiSelectable + searchable + singleSelection={true} + options={optPolicies} + onChange={(newOptions) => setOptPolicies(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem component="span"> + <EuiFormRow label="Groups" fullWidth> + <EuiSelectable + searchable + options={optGroups} + onChange={(newOptions) => setOPtGroups(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiButton type="submit" onClick={onPolicyGroupAssignment} fill> + Save + </EuiButton> + </EuiForm> + </> + ); + } +); + +export default PolicyGroupAssignment; diff --git a/src/pages/policies/PolicySourceAssignment.js b/src/pages/policies/PolicySourceAssignment.js new file mode 100644 index 0000000..4db353b --- /dev/null +++ b/src/pages/policies/PolicySourceAssignment.js @@ -0,0 +1,69 @@ +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSelectable, + EuiSpacer, +} from '@elastic/eui'; +import React, { Fragment, memo } from 'react'; + +const PolicySourceAssignment = memo( + ({ + optPolicies, + setOptPolicies, + optSources, + setOptSources, + onPolicySourceAssignment, + }) => { + return ( + <> + <EuiForm component="form"> + <EuiFlexGroup component="span"> + <EuiFlexItem component="span"> + <EuiFormRow label="Policies" fullWidth> + <EuiSelectable + searchable + singleSelection={true} + options={optPolicies} + onChange={(newOptions) => setOptPolicies(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem component="span"> + <EuiFormRow label="Sources" fullWidth> + <EuiSelectable + searchable + singleSelection={false} + options={optSources} + onChange={(newOptions) => setOptSources(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiButton type="submit" onClick={onPolicySourceAssignment} fill> + Save + </EuiButton> + </EuiForm> + </> + ); + } +); + +export default PolicySourceAssignment; diff --git a/src/pages/requests/Requests.js b/src/pages/requests/Requests.js index 04d3fcd..5d2387b 100644 --- a/src/pages/requests/Requests.js +++ b/src/pages/requests/Requests.js @@ -1,5 +1,4 @@ import React, { useState, useCallback, useEffect, memo } from 'react'; -import store from '../../store/index'; import { ShowAlert } from '../../components/Common/Common'; import { EuiForm, @@ -12,6 +11,12 @@ import { EuiFormRow, EuiBasicTable, } from '@elastic/eui'; +import { + getRequests, + getPendingRequests, + deleteUserRequest, + updateUserRequest, +} from '../../services/GatekeeperService'; const RequestList = memo(({ requests, requestColumns }) => { return ( @@ -31,7 +36,6 @@ const RequestList = memo(({ requests, requestColumns }) => { }); const Requests = () => { - const [, globalActions] = store(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [open, setOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(''); @@ -40,64 +44,54 @@ const Requests = () => { const [pendingRequests, setPendingRequests] = useState([]); const loadRequests = useCallback(async () => { - const requestList = await globalActions.user.fetchRequests(); + const requestList = await getRequests(); if (requestList) { setRequests(requestList); } - }, [globalActions.user]); + }, [getRequests]); const loadPendingRequests = useCallback(async () => { - const requestList = await globalActions.user.fetchPendingRequests(); + const requestList = await getPendingRequests(); if (requestList) { setPendingRequests(requestList); } - }, [globalActions.user]); + }, [getPendingRequests]); useEffect(() => { - // clean up controller - let isSubscribed = true; - if (isSubscribed) { - loadRequests(); - loadPendingRequests(); - } - // cancel subscription to useEffect - return () => (isSubscribed = false); + loadRequests(); + loadPendingRequests(); }, [loadRequests, loadPendingRequests]); const onDeleteRequest = async (request) => { - if (!request) { - return; - } - const result = await globalActions.user.deleteUserRequest(request.id); - if (result && !result.error) { - setAlertMessage('Request has been deleted.'); - setSeverity('success'); - setOpen(true); - } else { - setAlertMessage(`Error: ${result.error}`); - setSeverity('error'); + if (request) { + const result = await deleteUserRequest(request.id); + if (result) { + setAlertMessage('Request has been deleted.'); + setSeverity('success'); + await loadRequests(); + await loadPendingRequests(); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); + } setOpen(true); } - await loadRequests(); - await loadPendingRequests(); }; const onProcessRequest = async (request) => { - if (!request) { - return; - } - const result = await globalActions.user.processUserRequest(request.id); - if (result && !result.error) { - setAlertMessage('Request has been processed.'); - setSeverity('success'); - setOpen(true); - } else { - setAlertMessage(`Error: ${result.error}`); - setSeverity('error'); + if (request) { + const result = await updateUserRequest(request.id, true); + if (result) { + setAlertMessage('Request has been processed.'); + setSeverity('success'); + await loadRequests(); + await loadPendingRequests(); + } else { + setAlertMessage(`Error: ${result.error}`); + setSeverity('error'); + } setOpen(true); } - await loadRequests(); - await loadPendingRequests(); }; const requestActions = [ diff --git a/src/pages/roles/Roles.js b/src/pages/roles/Roles.js index 4706c0f..4f59de7 100644 --- a/src/pages/roles/Roles.js +++ b/src/pages/roles/Roles.js @@ -1,5 +1,4 @@ import React, { useState, Fragment, useCallback, useEffect } from 'react'; -import store from '../../store/index'; import { ShowAlert } from '../../components/Common'; import { EuiForm, @@ -19,104 +18,107 @@ import { EuiSelectable, EuiComboBox, } from '@elastic/eui'; +import { + getRoles, + getUsers, + createRole, + addUserToRole, + deleteRole, +} from '../../services/GatekeeperService'; const newRoleForm = ({ roleNameValue, setRoleNameValue, roleDescriptionValue, setRoleDescriptionValue, - globalState, + roles, onSaveRole, roleColumns, }) => { return ( - <> - <EuiForm component="form"> - <EuiFormRow label="Name"> - <EuiFieldText - value={roleNameValue} - onChange={(e) => setRoleNameValue(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow label="Description"> - <EuiFieldText - value={roleDescriptionValue} - onChange={(e) => setRoleDescriptionValue(e.target.value)} - /> - </EuiFormRow> - <EuiSpacer /> - { - <EuiButton fill onClick={onSaveRole} isLoading={globalState.isLoading}> - Save - </EuiButton> - } - <EuiSpacer /> - <EuiFormRow label="" fullWidth> - <EuiBasicTable - items={globalState.roles} - rowheader="Name" - columns={roleColumns} - // rowProps={getRowProps} - // cellProps={getCellProps} - /> - </EuiFormRow> - </EuiForm> - </> + roles && + roles.length > 0 && ( + <> + <EuiForm component="form"> + <EuiFormRow label="Name"> + <EuiFieldText + value={roleNameValue || ''} + onChange={(e) => setRoleNameValue(e.target.value)} + /> + </EuiFormRow> + <EuiFormRow label="Description"> + <EuiFieldText + value={roleDescriptionValue || ''} + onChange={(e) => setRoleDescriptionValue(e.target.value)} + /> + </EuiFormRow> + <EuiSpacer /> + { + <EuiButton fill onClick={onSaveRole}> + Save + </EuiButton> + } + <EuiSpacer /> + <EuiFormRow label="" fullWidth> + <EuiBasicTable items={roles} rowheader="Name" columns={roleColumns} /> + </EuiFormRow> + </EuiForm> + </> + ) ); }; -const roleAssignment = ( - globalState, - roles, - setRoles, - users, - setUsers, - onRoleAssignment -) => { + +const roleAssignment = (roles, setRoles, users, setUsers, onRoleAssignment) => { return ( - <> - <EuiForm component="form"> - <EuiFlexGroup component="span"> - <EuiFlexItem component="span"> - <EuiFormRow label="Roles" fullWidth> - <EuiSelectable - searchable - singleSelection={true} - options={roles} - onChange={(newOptions) => setRoles(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem component="span"> - <EuiFormRow label="Users" fullWidth> - <EuiSelectable - searchable - options={users} - onChange={(newOptions) => setUsers(newOptions)} - > - {(list, search) => ( - <Fragment> - {search} - {list} - </Fragment> - )} - </EuiSelectable> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer /> + roles && + users && + roles.length > 0 && + users.length > 0 && ( + <> + <EuiForm component="form"> + <EuiFlexGroup component="span"> + <EuiFlexItem component="span"> + <EuiFormRow label="Roles" fullWidth> + <EuiSelectable + searchable + singleSelection={true} + options={roles} + onChange={(newOptions) => setRoles(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem component="span"> + <EuiFormRow label="Users" fullWidth> + <EuiSelectable + searchable + options={users} + onChange={(newOptions) => setUsers(newOptions)} + > + {(list, search) => ( + <Fragment> + {search} + {list} + </Fragment> + )} + </EuiSelectable> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> - <EuiButton type="submit" onClick={onRoleAssignment} fill> - Save - </EuiButton> - </EuiForm> - </> + <EuiButton type="submit" onClick={onRoleAssignment} fill> + Save + </EuiButton> + </EuiForm> + </> + ) ); }; const assignedRoles = ({ @@ -132,7 +134,7 @@ const assignedRoles = ({ <EuiFormRow label="Select specific roles"> <EuiComboBox placeholder="Select a role" - singleSelection={{ asPlainText: true }} + singleSelection={true} options={roles} selectedOptions={selectedRole} onChange={(e) => { @@ -145,8 +147,6 @@ const assignedRoles = ({ items={assignedRoleToUsers} rowheader="" columns={assignedRolesColumns} - // rowProps={getRowProps} - // cellProps={getCellProps} /> </EuiFormRow> </EuiForm> @@ -155,56 +155,71 @@ const assignedRoles = ({ }; const Roles = () => { - const [globalState, globalActions] = store(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [roleNameValue, setRoleNameValue] = useState(); const [roleDescriptionValue, setRoleDescriptionValue] = useState(); const [roles, setRoles] = useState([]); const [users, setUsers] = useState([]); - const [open, setOpen] = useState(false); + const [alertMessage, setAlertMessage] = useState(''); const [severity, setSeverity] = useState('info'); + const [open, setOpen] = useState(false); + const [selectedRole, setSelectedRole] = useState(); const [assignedRoleToUsers, setAssignedRoleToUsers] = useState([]); const loads = useCallback(async () => { - const roles = await globalActions.user.findRole(); - const users = await globalActions.user.findUser(); - if (globalState.user.id) { - await globalActions.user.allocatedRoles(globalState.user.id); - } + const roles = await getRoles(); + const users = await getUsers(); if (roles && users) { const _roles = []; for (const role of roles) { - _roles.push({ value: role.id, label: role.name }); + _roles.push({ + value: role.id, + label: role.name, + description: role.description, + users: role.users, + }); } setRoles(_roles); const _users = []; for (const user of users) { - _users.push({ value: user.id, label: user.username }); + _users.push({ value: user.id, label: user.email, sub: user.kc_id }); } setUsers(_users); } - }, [globalActions.user, globalState.user.id]); + }, []); useEffect(() => { - // clean up controller - let isSubscribed = true; - if (isSubscribed) { - loads(); - } - // cancel subscription to useEffect - return () => (isSubscribed = false); + setSelectedRole([]); + setAssignedRoleToUsers([]); + loads(); + }, [loads, selectedTabNumber]); + + useEffect(() => { + loads(); }, [loads]); - const onSaveRole = async () => { - await globalActions.user.createRole(roleNameValue, roleDescriptionValue); - await globalActions.user.findRole(); + const onSaveRole = async (e) => { + e.preventDefault(); + const result = await createRole(roleNameValue, roleDescriptionValue); + if (result) { + setAlertMessage('Role has been created.'); + setSeverity('success'); + } else { + setAlertMessage('Role has not been created.'); + setSeverity('error'); + } + setOpen(true); + setRoleNameValue(''); + setRoleDescriptionValue(''); + await loads(); }; - const onRoleAssignment = async () => { + const onRoleAssignment = async (e) => { + e.preventDefault(); const checkedRoles = roles.filter((e) => { return e.checked === 'on'; }); @@ -215,37 +230,61 @@ const Roles = () => { for (const user of checkedUsers) { for (const role of checkedRoles) { - await globalActions.user.allocateRolesToUser(user.value, role.value); + await addUserToRole(user.sub, role.value); } } + await loads(); + setRoles(roles.map((role) => ({ ...role, checked: null }))); + setUsers(users.map((user) => ({ ...user, checked: null }))); + }; - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Role assignment was completed.'); + const removeUserFromRole = async (e) => { + const result = await removeUserFromRole(e.sub, e.roleId); + if (result) { + setAlertMessage('User has been removed from role.'); setSeverity('success'); + } else { + setAlertMessage('User has not been removed from role.'); + setSeverity('error'); } + setOpen(true); + onSelectedRole([]); + await loads(); }; const onSelectedRole = async (e) => { if (e.length > 0) { setSelectedRole(e); - const assignedUserByRoleItems = await globalActions.user.getAssignedUserByRole( - e[0].value - ); + const assignedUserByRoleItems = roles + .find((role) => { + return role.value === e[0].value; + }) + .users.map((user) => { + return { + email: user.user.email, + rolename: e[0].label, + roleId: e[0].value, + sub: user.user.kc_id, + }; + }); setAssignedRoleToUsers(assignedUserByRoleItems); } else { - setSelectedRole(e); + setSelectedRole([]); + setAssignedRoleToUsers([]); } }; const onDeleteRole = async (e) => { - await globalActions.user.deleteRole(e.id); - if (globalState.status === 'SUCCESS') { - setOpen(true); + const result = await deleteRole(e.value); + if (result) { setAlertMessage('Role has been deleted.'); setSeverity('success'); - await globalActions.user.findRole(); + } else { + setAlertMessage('Role has not been deleted.'); + setSeverity('error'); } + setOpen(true); + await loads(); }; const roleActions = [ @@ -258,17 +297,28 @@ const Roles = () => { onClick: onDeleteRole, }, ]; + + const assignedRoleActions = [ + { + name: 'Remove', + description: 'Remove this user from this role', + icon: 'trash', + type: 'icon', + color: 'danger', + onClick: removeUserFromRole, + }, + ]; + const roleColumns = [ - { field: 'name', name: 'Name' }, + { field: 'label', name: 'Name' }, { field: 'description', name: 'Description' }, { name: 'Actions', actions: roleActions }, ]; const assignedRolesColumns = [ - { field: 'username', name: 'Username' }, - { field: 'useremail', name: 'E-mail' }, + { field: 'email', name: 'E-mail' }, { field: 'rolename', name: 'Role' }, - { name: 'Actions', actions: roleActions }, + { name: 'Actions', actions: assignedRoleActions }, ]; const handleClose = (event, reason) => { @@ -290,7 +340,7 @@ const Roles = () => { setRoleNameValue, roleDescriptionValue, setRoleDescriptionValue, - globalState, + roles, onSaveRole, roleColumns, })} @@ -303,14 +353,7 @@ const Roles = () => { content: ( <> <br /> - {roleAssignment( - globalState, - roles, - setRoles, - users, - setUsers, - onRoleAssignment - )} + {roleAssignment(roles, setRoles, users, setUsers, onRoleAssignment)} </> ), }, diff --git a/src/pages/sources/Sources.js b/src/pages/sources/Sources.js index 9718765..11a8d3d 100644 --- a/src/pages/sources/Sources.js +++ b/src/pages/sources/Sources.js @@ -1,5 +1,4 @@ import React, { useState, useCallback, useEffect, memo, useRef } from 'react'; -import store from '../../store/index'; import { EuiForm, EuiPageContent, @@ -18,7 +17,6 @@ import { EuiFilePicker, EuiBasicTable, EuiHealth, - EuiProgress, EuiConfirmModal, EuiOverlayMask, EuiModal, @@ -31,11 +29,17 @@ import { } from '@elastic/eui'; import { ShowAlert } from '../../components/Common'; -import JsonView from '@in-sylva/json-view'; +import ReactJson from '@microlink/react-json-view'; +import { + getUser, + deleteSource, + createSource, + findUserBySub, + getSources, +} from '../../services/GatekeeperService'; const NewSourceForm = memo( ({ - globalState, nameValue, setNameValue, descriptionValue, @@ -55,14 +59,14 @@ const NewSourceForm = memo( <EuiFormRow label="Source or file name"> <EuiFieldText id="nameValue" - value={nameValue} + value={nameValue || ''} onChange={(e) => setNameValue(e.target.value)} /> </EuiFormRow> <EuiFormRow label="Source description"> <EuiFieldText id="descriptionValue" - value={descriptionValue} + value={descriptionValue || ''} onChange={(e) => setDescriptionValue(e.target.value)} /> </EuiFormRow> @@ -89,7 +93,7 @@ const NewSourceForm = memo( <EuiButton fill onClick={onSaveSource} - isLoading={globalState.isLoading} + disabled={!nameValue || !descriptionValue || !metaUrfms} > Save </EuiButton> @@ -99,7 +103,7 @@ const NewSourceForm = memo( <EuiFlexItem grow={3}> <> <br /> - <JsonView + <ReactJson name="Metadata records" collapsed={true} iconStyle={'triangle'} @@ -152,22 +156,6 @@ const SourcesForm = memo( } ); -const ShareSources = memo(() => { - return ( - <> - <br /> - </> - ); -}); - -const SharedSources = memo(() => { - return ( - <> - <br /> - </> - ); -}); - const renderFiles = (files) => { if (files.length > 0) { return ( @@ -185,9 +173,7 @@ const renderFiles = (files) => { }; const Sources = () => { const [metaUrfms, setMetaUrfms] = useState([]); - // const [metaUrfmsCol, setMetaUrfmsCol] = useState([]) const [selectedTabNumber, setSelectedTabNumber] = useState(0); - const [globalState, globalActions] = store(); const [files, setFiles] = useState([]); const [nameValue, setNameValue] = useState(); const [descriptionValue, setDescriptionValue] = useState(); @@ -246,19 +232,11 @@ const Sources = () => { pageSizeOptions: [3, 5, 8], }; - // const unsentSources = (typeof sources === ([] || null) ? [] : sources.filter(e => { return e.is_send === false })) - const selection = { selectable: (source) => !source.is_send, onSelectionChange: onSelectionChange, - // initialSelected: unsentSources, }; - /* - const onSelection = () => { - tableref.current.setSelection(typeof sources === ([] || null) ? [] : sources.filter(e => { return e.is_send === false })); - }; */ - const sorting = { sort: { field: sortField, @@ -280,14 +258,11 @@ const Sources = () => { const onDeleteSourceConfirm = async () => { closeModal(); - if (source.id && sessionStorage.getItem('userId')) { - await globalActions.source.deleteSource( - source.id, - sessionStorage.getItem('userId') - ); - setOpen(true); + if (source.id) { + const response = await deleteSource(source.id); setAlertMessage('Source is deleted successfully'); setSeverity('success'); + setOpen(true); await loadSources(); } else { @@ -301,51 +276,48 @@ const Sources = () => { const onMergeAndSendSourcesConfirmed = async () => { if (sourceName && sourceDescription && selectedSources) { - const kcId = sessionStorage.getItem('userId'); - const result = await globalActions.source.mergeAndSendSource( - kcId, - sourceName, - sourceDescription, - selectedSources - ); - if (result.status === 201) { - setOpen(true); - setAlertMessage('Sources are merged and send to elasticsearch successfully !'); - setSeverity('success'); - - await loadSources(); - } else { - setOpen(true); - setAlertMessage('While merging the sources, unexpected error has been occurred.'); - setSeverity('error'); - } - closeMergeSourceModal(); + //const kcId = sessionStorage.getItem('userId'); + // const result = await globalActions.source.mergeAndSendSource( + // kcId, + // sourceName, + // sourceDescription, + // selectedSources + // ); + // if (result.status === 201) { + // setOpen(true); + // setAlertMessage('Sources are merged and send to elasticsearch successfully !'); + // setSeverity('success'); + // await loadSources(); + // } else { + // setOpen(true); + // setAlertMessage('While merging the sources, unexpected error has been occurred.'); + // setSeverity('error'); + // } + // closeMergeSourceModal(); } }; - const updateSource = async (kc_id, source_id) => { - await globalActions.source.updateSource(kc_id, source_id); - }; + // const updateSource = async (kc_id, source_id) => { + // await globalActions.source.updateSource(kc_id, source_id); + // }; // import source document to elasticsearch const onSendSource = async (source) => { if (sessionStorage.getItem('userId') && source) { - const kc_id = sessionStorage.getItem('userId'); - const source_id = source.id; - - await updateSource(kc_id, source_id); - if (globalState.status === 'SUCCESS') { - setOpen(true); - setAlertMessage('Source imported to elastic stack successfully'); - setSeverity('success'); - - await loadSources(); - } else { - setOpen(true); - setAlertMessage( - 'While importation the sources to elastic stack, unexpected error has been occurred.' - ); - setSeverity('error'); - } + // const kc_id = sessionStorage.getItem('userId'); + // const source_id = source.id; + // const updatedSource = await updateSource(kc_id, source_id); + // if (updatedSource) { + // setOpen(true); + // setAlertMessage('Source imported to elastic stack successfully'); + // setSeverity('success'); + // await loadSources(); + // } else { + // setOpen(true); + // setAlertMessage( + // 'While importation the sources to elastic stack, unexpected error has been occurred.' + // ); + // setSeverity('error'); + // } } }; @@ -387,8 +359,13 @@ const Sources = () => { ]; const onFilePickerChange = async (files) => { - setFiles(files); - await handleSelectedFile(files); + if (files.length > 0) { + setFiles(files); + await handleSelectedFile(files); + } else { + setFiles([]); + setMetaUrfms([]); + } }; const handleSelectedFile = async (files) => { @@ -401,47 +378,24 @@ const Sources = () => { const handleData = (file) => { const { metadataRecords } = JSON.parse(file); - if (metadataRecords) { - const columns = []; - const rows = []; - - const prop = Object.keys(metadataRecords[0]); - - prop.forEach((item) => { - const column = { - name: item, - options: { - display: true, - }, - }; - columns.push(column); - }); - - metadataRecords.forEach((row) => { - rows.push(row); - }); - - if (columns && rows) { - // setMetaUrfmsCol(columns) - setMetaUrfms(rows); - } + setMetaUrfms(metadataRecords); } }; - const onSaveSource = async () => { + const onSaveSource = async (e) => { + e.preventDefault(); if (nameValue && descriptionValue && metaUrfms) { - await globalActions.source.createSource( - metaUrfms, - nameValue, - descriptionValue, - sessionStorage.getItem('userId') - ); + const result = await createSource(metaUrfms, nameValue, descriptionValue); - if (globalState.status === 'SUCCESS') { + if (result?.id) { setOpen(true); setAlertMessage('Source created.'); setSeverity('success'); + setNameValue(''); + setDescriptionValue(''); + setFiles([]); + setMetaUrfms([]); await loadSources(); } else { @@ -453,22 +407,30 @@ const Sources = () => { }; const loadSources = useCallback(async () => { - if (sessionStorage.getItem('userId')) { - const user = await globalActions.user.findOneUserWithGroupAndRole( - sessionStorage.getItem('userId') - ); - const role = user[0].roleid; - let result; - if (role === 1) { - result = await globalActions.source.allSources(); - } else { - result = await globalActions.source.sources(sessionStorage.getItem('userId')); - } - if (result) { - setSources(result); - } + const userInfo = getUser(); + const sub = userInfo.profile?.sub; + const user = await findUserBySub(sub); + const role = user.roles[0].role_id; + let result; + if (role === 1) { + result = await getSources(); + } else { + result = await getSources(); + } + if (result) { + result = result.map((source) => { + return { + id: source.id, + name: source.name, + description: source.description, + index_id: source.source_indices[0].index_id, + mng_id: source.source_indices[0].mng_id, + is_send: source.source_indices[0].is_send, + }; + }); + setSources(result); } - }, [globalActions.source, globalActions.user]); + }, [getSources, getUser, findUserBySub]); useEffect(() => { // clean up controller @@ -499,7 +461,6 @@ const Sources = () => { setNameValue={setNameValue} descriptionValue={descriptionValue} setDescriptionValue={setDescriptionValue} - globalState={globalState} onFilePickerChange={onFilePickerChange} files={files} renderFiles={renderFiles} @@ -527,24 +488,6 @@ const Sources = () => { </> ), }, - { - id: 'tab3', - name: 'Share sources', - content: ( - <> - <ShareSources /> - </> - ), - }, - { - id: 'tab4', - name: 'Shared sources', - content: ( - <> - <SharedSources /> - </> - ), - }, ]; const mergeModalForm = ( @@ -637,9 +580,6 @@ const Sources = () => { /> </EuiForm> </EuiForm> - {globalState.isLoading && ( - <EuiProgress postion="fixed" size="l" color="accent" /> - )} </EuiPageContentBody> </EuiPageContent> {ShowAlert(open, handleClose, alertMessage, severity)} diff --git a/src/pages/users/Users.js b/src/pages/users/Users.js index 2ccf6fd..4910000 100644 --- a/src/pages/users/Users.js +++ b/src/pages/users/Users.js @@ -1,5 +1,4 @@ -import React, { useState, useCallback, useEffect, memo, useRef } from 'react'; -import store from '../../store/index'; +import React, { useState, useEffect, useCallback, memo, useRef } from 'react'; import { ShowAlert } from '../../components/Common'; import { EuiForm, @@ -8,70 +7,10 @@ import { EuiPageContentHeaderSection, EuiTitle, EuiPageContentBody, - EuiTabbedContent, EuiFormRow, - EuiFieldText, - EuiFieldPassword, - EuiSelect, - EuiButton, EuiBasicTable, } from '@elastic/eui'; - -const NewUserForm = memo( - ({ - globalState, - usernameValue, - setUsername, - passwordValue, - setPasswordValue, - email, - setEmail, - roles, - selectedRole, - setSelectedRole, - onSaveUser, - }) => { - return ( - <> - <EuiForm component="form"> - <EuiFormRow label="Username"> - <EuiFieldText - value={usernameValue} - onChange={(e) => setUsername(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow label="Password"> - <EuiFieldPassword - fullWidth - placeholder="Password" - type={'dual'} - value={passwordValue} - onChange={(e) => setPasswordValue(e.target.value)} - aria-label="Use aria labels when no actual label is in use" - /> - </EuiFormRow> - <EuiFormRow label="e-mail"> - <EuiFieldText value={email} onChange={(e) => setEmail(e.target.value)} /> - </EuiFormRow> - <EuiFormRow label="Select specific roles"> - <EuiSelect - options={roles} - value={selectedRole} - onChange={(e) => setSelectedRole(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow label=""> - { - <EuiButton fill onClick={onSaveUser} isLoading={globalState.isLoading}> - Save - </EuiButton> - } - </EuiFormRow> - </EuiForm> - </> - ); - } -); +import { getUsers, deleteUser } from '../../services/GatekeeperService'; const UserList = memo( ({ users, userColumns, onTableChange, tableRef, pagination, sorting }) => { @@ -88,7 +27,6 @@ const UserList = memo( ref={tableRef} pagination={pagination} sorting={sorting} - // selection={selection} /> </EuiFormRow> </EuiForm> @@ -98,17 +36,10 @@ const UserList = memo( ); const Users = () => { - const [globalState, globalActions] = store(); - const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [open, setOpen] = useState(false); const [alertMessage, setAlertMessage] = useState(''); const [severity, setSeverity] = useState('info'); - const [dual] = useState(true); - const [passwordValue, setPasswordValue] = useState(''); - const [usernameValue, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const [roles, setRoles] = useState([{ value: 0, text: 'select a role' }]); - const [selectedRole, setSelectedRole] = useState(); + const [users, setUsers] = useState([]); const [sortDirection, setSortDirection] = useState('asc'); const [sortField, setSortField] = useState('username'); @@ -116,69 +47,46 @@ const Users = () => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(5); - const loadRoles = useCallback(async () => { - const _roles = []; - const roles = await globalActions.user.findRole(); - if (roles) { - for (const role of roles) { - _roles.push({ value: role.id, text: role.name }); - } - } - setRoles((prevRoles) => [...prevRoles, ..._roles]); - }, [globalActions.user]); - const loadUsers = useCallback(async () => { - const users = await globalActions.user.usersWithGroupAndRole(); - - if (users) { - const userList = []; - const userNameList = []; - users.forEach((user) => { - if (userNameList.includes(user.username)) { - userList[userNameList.indexOf(user.username)].groupname = - `${userList[userNameList.indexOf(user.username)].groupname}, ${user.groupname}`; - } else { - userNameList.push(user.username); - userList.push(user); - } + const response = await getUsers(); + if (response) { + const users = response.map((user) => { + return { + id: user.kc_id, + email: user.email, + groupname: user.groups + .map((element) => { + const { group } = element; + return group.name; + }) + .join(', '), + rolename: user.roles.map((element) => { + const { role } = element; + return role.name; + }), + }; }); - setUsers(userList); + setUsers(users); } - }, [globalActions.user]); + }, [getUsers]); useEffect(() => { - // clean up controller - let isSubscribed = true; - if (isSubscribed) { - loadRoles(); - loadUsers(); - } - // cancel subscription to useEffect - return () => (isSubscribed = false); - }, [loadRoles, loadUsers]); - - const onSaveUser = async () => { - if (usernameValue && passwordValue && email && selectedRole) { - await globalActions.user.createUser( - usernameValue, - email, - passwordValue, - selectedRole - ); - setOpen(true); - setAlertMessage('User has been created.'); - setSeverity('success'); - loadUsers(); - } - }; - - const onDeleteUser = async (user) => { - if (user) { - await globalActions.user.deleteUser(user.kc_id); - setOpen(true); - setAlertMessage('Users has been deleted.'); - setSeverity('success'); - await loadUsers(); + loadUsers(); + }, [loadUsers]); + + const onDeleteUser = async (id) => { + if (id) { + const response = await deleteUser(id); + if (!response) { + setAlertMessage('User has not been deleted.'); + setSeverity('error'); + setOpen(true); + } else { + setAlertMessage('User has been deleted.'); + setSeverity('success'); + setOpen(true); + await loadUsers(); + } } }; @@ -189,20 +97,12 @@ const Users = () => { icon: 'trash', type: 'icon', color: 'danger', - onClick: onDeleteUser, + onClick: async (user) => { + onDeleteUser(user.id); + }, }, ]; - const onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - - const { field: sortField, direction: sortDirection } = sort; - - setPageIndex(pageIndex); - setPageSize(pageSize); - setSortField(sortField); - setSortDirection(sortDirection); - }; const totalItemCount = typeof USERS === typeof undefined ? 0 : users.length; const pagination = { @@ -220,56 +120,22 @@ const Users = () => { }; const userColumns = [ - { field: 'username', name: 'Username' }, { field: 'email', name: 'Email' }, - { field: 'groupname', name: 'Group' }, - { field: 'rolename', name: 'Role' }, - { field: 'roledescription', name: 'Role description' }, + { field: 'groupname', name: 'Groups' }, + { field: 'rolename', name: 'Roles' }, { name: 'Actions', actions: userActions }, ]; - const tabContents = [ - { - id: 'tab1', - name: 'New user', - content: ( - <> - <br /> - <NewUserForm - globalState={globalState} - dual={dual} - usernameValue={usernameValue} - setUsername={setUsername} - passwordValue={passwordValue} - setPasswordValue={setPasswordValue} - email={email} - setEmail={setEmail} - onSaveUser={onSaveUser} - selectedRole={selectedRole} - setSelectedRole={setSelectedRole} - roles={roles} - /> - </> - ), - }, - { - id: 'tab2', - name: 'Users list', - content: ( - <> - <br /> - <UserList - users={users} - userColumns={userColumns} - onTableChange={onTableChange} - tableRef={tableRef} - pagination={pagination} - sorting={sorting} - /> - </> - ), - }, - ]; + const onTableChange = ({ page = {}, sort = {} }) => { + const { index: pageIndex, size: pageSize } = page; + + const { field: sortField, direction: sortDirection } = sort; + + setPageIndex(pageIndex); + setPageSize(pageSize); + setSortField(sortField); + setSortDirection(sortDirection); + }; const handleClose = (event, reason) => { if (reason === 'clickaway') { @@ -289,12 +155,13 @@ const Users = () => { </EuiPageContentHeader> <EuiPageContentBody> <EuiForm> - <EuiTabbedContent - tabs={tabContents} - selectedTab={tabContents[selectedTabNumber]} - onTabClick={(tab) => { - setSelectedTabNumber(tabContents.indexOf(tab)); - }} + <UserList + users={users} + userColumns={userColumns} + onTableChange={onTableChange} + tableRef={tableRef} + pagination={pagination} + sorting={sorting} /> </EuiForm> </EuiPageContentBody> diff --git a/src/services/GatekeeperService.js b/src/services/GatekeeperService.js new file mode 100644 index 0000000..3563f9b --- /dev/null +++ b/src/services/GatekeeperService.js @@ -0,0 +1,374 @@ +import { User, UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +export const userManager = new UserManager({ + authority: process.env.REACT_APP_KEYCLOAK_BASE_URL, + client_id: process.env.REACT_APP_KEYCLOAK_CLIENT_ID, + client_secret: process.env.REACT_APP_KEYCLOAK_CLIENT_SECRET, + redirect_uri: process.env.REACT_APP_BASE_URL, + userStore: new WebStorageStateStore({ store: window.localStorage }), +}); + +export function getUser() { + const oidcStorage = localStorage.getItem( + `oidc.user:${process.env.REACT_APP_KEYCLOAK_BASE_URL}:${process.env.REACT_APP_KEYCLOAK_CLIENT_ID}` + ); + if (!oidcStorage) { + return null; + } + + return User.fromStorageString(oidcStorage); +} + +const get = async (path, payload) => { + const user = getUser(); + const access_token = user?.access_token; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch(`${process.env.REACT_APP_GATEKEEPER_BASE_URL}${path}`, { + method: 'GET', + headers, + mode: 'cors', + body: JSON.stringify(payload), + }); + if (response.status === 401) { + try { + await userManager.signinSilent(); + return await this.get(path, payload); + } catch (error) { + const currentHash = window.location.hash; + await userManager.signoutRedirect({ + post_logout_redirect_uri: `${process.env.REACT_APP_BASE_URL}${currentHash}`, + }); + } + } + return await response.json(); +}; + +const post = async (path, payload) => { + const user = getUser(); + const access_token = user?.access_token; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch(`${process.env.REACT_APP_GATEKEEPER_BASE_URL}${path}`, { + method: 'POST', + headers, + body: JSON.stringify(payload), + mode: 'cors', + }); + if (response.status === 401) { + try { + await userManager.signinSilent(); + return await this.post(path, payload); + } catch (error) { + const currentHash = window.location.hash; + await userManager.signoutRedirect({ + post_logout_redirect_uri: `${process.env.REACT_APP_BASE_URL}${currentHash}`, + }); + } + } + return await response.json(); +}; + +const deleteRequest = async (path, payload) => { + const user = getUser(); + const access_token = user?.access_token; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch(`${process.env.REACT_APP_GATEKEEPER_BASE_URL}${path}`, { + method: 'DELETE', + headers, + body: JSON.stringify(payload), + mode: 'cors', + }); + if (response.status === 401) { + try { + await userManager.signinSilent(); + return await this.post(path, payload); + } catch (error) { + const currentHash = window.location.hash; + await userManager.signoutRedirect({ + post_logout_redirect_uri: `${process.env.REACT_APP_BASE_URL}${currentHash}`, + }); + } + } + if (response.status === 204) { + return { + success: true, + message: 'Request deleted successfully', + }; + } else { + return { + success: false, + message: 'Request not deleted', + }; + } +}; + +export const createUser = async (sub, email) => { + const path = `/users`; + return await post(path, { + kc_id: sub, + email, + }); +}; + +export const deleteUser = async (sub) => { + const path = `/users/${sub}`; + return await deleteRequest(path, {}); +}; + +export const getUsers = async () => { + const path = `/users`; + return await get(path); +}; + +export const findUserBySub = async (sub) => { + const path = `/users/${sub}`; + return await get(path); +}; + +export const getRoles = async () => { + const path = `/roles`; + return await get(path); +}; + +export const createRole = async (name, description) => { + const path = `/roles`; + return await post(path, { + name: name, + description: description, + }); +}; + +export const addUserToRole = async (sub, role_id) => { + const path = `/roles/${role_id}/users`; + return await post(path, { + kc_id: sub, + }); +}; + +export const removeUserFromRole = async (sub, role_id) => { + const path = `/roles/${role_id}/users/${sub}`; + return await deleteRequest(path, {}); +}; + +export const deleteRole = async (id) => { + const path = `/roles/${id}`; + return await deleteRequest(path, {}); +}; + +export const createUserRequest = async (message, sub) => { + const path = `/users/${sub}/requests`; + return await post(path, { + message: message, + }); +}; + +export const deleteUserRequest = async (id) => { + const path = `/user-requests/${id}`; + return await deleteRequest(path, {}); +}; + +export const getGroups = async () => { + const path = `/groups`; + return await get(path); +}; + +export const createGroup = async (name, description) => { + const path = `/groups`; + const user = getUser(); + const sub = user?.profile?.sub; + return await post(path, { + kc_id: sub, + name: name, + description: description, + }); +}; + +export const deleteGroup = async (id) => { + const path = `/groups/${id}`; + return await deleteRequest(path, {}); +}; + +export const addUserToGroup = async (sub, group_id) => { + const path = `/groups/${group_id}/users`; + return await post(path, { + kc_id: sub, + }); +}; + +export const removeUserFromGroup = async (sub, group_id) => { + const path = `/groups/${group_id}/users/${sub}`; + return await deleteRequest(path, {}); +}; + +export const getUserFieldsDisplaySettings = async (sub) => { + const path = `/users/${sub}/fields`; + return await get(path); +}; + +export const setUserFieldsDisplaySettings = async (sub, fields) => { + const path = `/users/${sub}/fields`; + return await post(path, { + fields_id: fields, + }); +}; + +export const createSource = async (metaUrfms, name, description) => { + const path = `/sources`; + const user = getUser(); + const sub = user.profile?.sub; + return await post(path, { + name: name, + description: description, + metaUrfms: metaUrfms, + kc_id: sub, + }); +}; + +export const getSources = async () => { + const path = `/sources`; + return await get(path); +}; + +export const deleteSource = async (source_id) => { + const path = `/sources/${source_id}`; + return await deleteRequest(path, {}); +}; + +export const getRequests = async () => { + const path = `/user-requests`; + return await get(path); +}; + +export const getPendingRequests = async () => { + const path = `/user-requests/pending`; + return await get(path); +}; + +export const deleteRequestById = async (id) => { + const path = `/user-requests/${id}`; + return await deleteRequest(path, {}); +}; + +export const updateUserRequest = async (id, is_processed) => { + const path = `/user-requests/${id}`; + return await post(path, { + is_processed: is_processed, + }); +}; + +export const getStdFields = async () => { + const path = `/std_fields`; + return await get(path); +}; + +export const createStdField = async ( + cardinality, + category, + definition_and_comment, + field_name, + field_type, + is_optional, + is_public, + obligation_or_condition, + values, + list_url, + default_display_fields +) => { + const path = `/std_fields`; + return await post(path, { + cardinality, + category, + definition_and_comment, + field_name, + field_type, + isoptional: is_optional, + ispublic: is_public, + obligation_or_condition, + values, + list_url, + default_display_fields + }); +}; + +export const addFieldToPolicy = async (field_id, policy_id) => { + const path = `/policies/${policy_id}/std_fields`; + return await post(path, { + field_id, + }); +}; + +export const removeFieldFromPolicy = async (field_id, policy_id) => { + const path = `/policies/${policy_id}/std_fields/${field_id}`; + return await deleteRequest(path, {}); +}; + +export const deleteAllStdFields = async () => { + const path = `/std_fields`; + return await deleteRequest(path, {}); +}; + +export const getPublicFields = async () => { + const path = `/public_std_fields`; + return await get(path); +}; + +export const createPolicy = async (name) => { + const path = `/policies`; + const kc_id = getUser().profile?.sub; + return await post(path, { + name, + kc_id, + }); +}; + +export const addSourceToPolicy = async (source_id, policy_id) => { + const path = `/policies/${policy_id}/sources`; + return await post(path, { + source_id, + }); +}; + +export const removeSourceFromPolicy = async (source_id, policy_id) => { + const path = `/policies/${policy_id}/sources/${source_id}`; + return await deleteRequest(path, {}); +}; + +export const addPolicyToGroup = async (policy_id, group_id) => { + const path = `/groups/${group_id}/policies`; + return await post(path, { + policy_id, + }); +}; + +export const removePolicyFromGroup = async (policy_id, group_id) => { + const path = `/groups/${group_id}/policies/${policy_id}`; + return await deleteRequest(path, {}); +}; + +export const deletePolicy = async (id) => { + const path = `/policies/${id}`; + return await deleteRequest(path, {}); +}; + +export const getPolicies = async () => { + const path = `/policies`; + return await get(path); +}; + +export const getUserPolicies = async (sub) => { + const path = `/users/${sub}/policies`; + return await get(path); +}; + +export const searchQuery = async (payload) => { + const path = `/search`; + return await post(path, payload); +}; diff --git a/src/store/CustomConnector.js b/src/store/CustomConnector.js deleted file mode 100644 index 28b24ca..0000000 --- a/src/store/CustomConnector.js +++ /dev/null @@ -1,53 +0,0 @@ -import React, { Component } from 'react'; - -export const CustomConnectorContext = React.createContext(); - -export class CustomConnectorProvider extends Component { - render() { - return ( - <CustomConnectorContext.Provider value={this.props.dataStore}> - {this.props.children} - </CustomConnectorContext.Provider> - ); - } -} - -export class CustomConnector extends React.Component { - static contextType = CustomConnectorContext; - - constructor(props, context) { - super(props, context); - this.state = this.selectData(); - this.functionProps = Object.entries(this.props.dispatchers) - .map(([k, v]) => [k, (...args) => this.context.dispatch(v(...args))]) - .reduce((result, [k, v]) => ({ ...result, [k]: v }), {}); - } - - render() { - return React.Children.map(this.props.children, (c) => - React.cloneElement(c, { ...this.state, ...this.functionProps }) - ); - } - - selectData() { - let storeState = this.context.getState(); - return Object.entries(this.props.selectors) - .map(([k, v]) => [k, v(storeState)]) - .reduce((result, [k, v]) => ({ ...result, [k]: v }), {}); - } - - handleDataStoreChange() { - let newData = this.selectData(); - Object.keys(this.props.selectors) - .filter((key) => this.state[key] !== newData[key]) - .forEach((key) => this.setState({ [key]: newData[key] })); - } - - componentDidMount() { - this.unsubscriber = this.context.subscribe(() => this.handleDataStoreChange()); - } - - componentWillUnmount() { - this.unsubscriber(); - } -} diff --git a/src/store/asyncEnhancer.js b/src/store/asyncEnhancer.js deleted file mode 100644 index 7f9e47e..0000000 --- a/src/store/asyncEnhancer.js +++ /dev/null @@ -1,16 +0,0 @@ -export const asyncEnhancer = - (delay) => - (createStoreFunction) => - (...args) => { - const store = createStoreFunction(...args); - return { - ...store, - dispatchAsync: (action) => - new Promise((resolve, reject) => { - setTimeout(() => { - store.dispatch(action); - resolve(); - }, delay); - }), - }; - }; diff --git a/src/store/index.js b/src/store/index.js deleted file mode 100644 index 905e71e..0000000 --- a/src/store/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import useStore from './useStore'; -import * as actions from '../actions'; - -const initialState = { - isLoading: false, - status: 'INITIAL', - users: [], - roles: [], - role: {}, - user: {}, - allocatedRoles: [], - error: false, - resources: [], -}; -const store = useStore(React, initialState, actions); -export default store; diff --git a/src/store/useStore.js b/src/store/useStore.js deleted file mode 100644 index bb16c7e..0000000 --- a/src/store/useStore.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useSessionStorage } from '@in-sylva/react-use-storage'; - -function setState(store, newState, afterUpdateCallback) { - store.state = { ...store.state, ...newState }; - store.listeners.forEach((listener) => { - listener.run(store.state); - }); - afterUpdateCallback && afterUpdateCallback(); -} - -function useCustom(store, React, mapState, mapActions) { - const [, originalHook] = useSessionStorage('portal', Object.create(null)); - const state = mapState ? mapState(store.state) : store.state; - const actions = React.useMemo( - () => (mapActions ? mapActions(store.actions) : store.actions), - [mapActions, store.actions] - ); - - React.useEffect(() => { - const newListener = { oldState: {} }; - newListener.run = mapState - ? (newState) => { - const mappedState = mapState(newState); - if (mappedState !== newListener.oldState) { - newListener.oldState = mappedState; - originalHook(mappedState); - } - } - : originalHook; - store.listeners.push(newListener); - newListener.run(store.state); - return () => { - store.listeners = store.listeners.filter((listener) => listener !== newListener); - }; - }, []); // eslint-disable-line - return [state, actions]; -} - -function associateActions(store, actions) { - const associatedActions = {}; - Object.keys(actions).forEach((key) => { - if (typeof actions[key] === 'function') { - associatedActions[key] = actions[key].bind(null, store); - } - if (typeof actions[key] === 'object') { - associatedActions[key] = associateActions(store, actions[key]); - } - }); - return associatedActions; -} - -const useStore = (React, initialState, actions, initializer) => { - const store = { state: initialState, listeners: [] }; - store.setState = setState.bind(null, store); - store.actions = associateActions(store, actions); - if (initializer) initializer(store); - return useCustom.bind(null, store, React); -}; - -export default useStore; diff --git a/src/utils.js b/src/utils.js index 9ad2ab2..6b39314 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,39 +1,5 @@ -import { EuiIcon } from '@elastic/eui'; import React from 'react'; - -export const getGatekeeperBaseUrl = () => { - return process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT - ? `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}:${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}`; -}; - -export const getLoginUrl = () => { - return process.env.REACT_APP_IN_SYLVA_LOGIN_PORT - ? `${process.env.REACT_APP_IN_SYLVA_LOGIN_HOST}:${process.env.REACT_APP_IN_SYLVA_LOGIN_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_LOGIN_HOST}`; -}; - -export const redirect = (url, condition = true) => { - if (condition) { - window.location.replace(url); - } -}; - -export const getUrlParam = (parameter, defaultValue) => { - let urlParameter = defaultValue; - if (window.location.href.indexOf(parameter) > -1) { - urlParameter = getUrlParams()[parameter]; - } - return urlParameter; -}; - -export const getUrlParams = () => { - let lets = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { - lets[key] = value; - }); - return lets; -}; +import { EuiIcon } from '@elastic/eui'; // Function used to mock statistics for dashboard page export const getRandomData = (arrayLength) => { @@ -44,11 +10,19 @@ export const getRandomData = (arrayLength) => { }); }; -export const getUserRoleId = () => { - if (sessionStorage.getItem('roleId').split('#')[0]) { - return parseInt(sessionStorage.getItem('roleId').split('#')[0]); - } else { - return parseInt(sessionStorage.getItem('roleId')); +export const safeJsonStringify = (str) => { + try { + return JSON.stringify(str); + } catch (e) { + return null; + } +}; + +export const safeJsonParse = (str) => { + try { + return JSON.parse(str); + } catch (e) { + return null; } }; @@ -56,70 +30,66 @@ export const getSideBarItems = () => { return [ { id: 0, + label: 'Home', + link: '/home', + roles: [1, 2, 3], + icon: <EuiIcon type="home" size="l" />, + }, + { + id: 1, label: 'Dashboard', - link: '/app/dashboard', + link: '/dashboard', roles: [1], icon: <EuiIcon type="dashboardApp" size="l" />, }, { - id: 1, + id: 2, label: 'Users', - link: '/app/users', + link: '/users', roles: [1], icon: <EuiIcon type="user" size="l" />, }, { - id: 2, + id: 3, label: 'Users roles', - link: '/app/roles', + link: '/roles', roles: [1], icon: <EuiIcon type="usersRolesApp" size="l" />, }, { - id: 3, + id: 4, label: 'Groups', - link: '/app/groups', + link: '/groups', roles: [1], icon: <EuiIcon type="users" size="l" />, }, { - id: 4, + id: 5, label: 'Requests', - link: '/app/requests', + link: '/requests', roles: [1], icon: <EuiIcon type="email" size="l" />, }, { - id: 5, + id: 6, label: 'Policies', - link: '/app/policies', - roles: [1, 2], + link: '/policies', + roles: [1], icon: <EuiIcon type="lockOpen" size="l" />, }, { - id: 6, + id: 7, label: 'Sources', - link: '/app/sources', + link: '/sources', roles: [1, 2], icon: <EuiIcon type="indexManagementApp" size="l" />, }, { - id: 7, + id: 8, label: 'Fields', - link: '/app/fields', - roles: [1], + link: '/fields', + roles: [1, 2], icon: <EuiIcon type="visTable" size="l" />, }, ]; }; - -export const changeNameToLabel = (object) => { - object.label = object.name; - delete object.name; - return object; -}; - -export const tokenTimedOut = (validityDurationInMin) => { - const timeSinceLastRefresh = Date.now() - sessionStorage.getItem('token_refresh_time'); - return timeSinceLastRefresh > validityDurationInMin * 60000; -}; -- GitLab