From 33524ce3cc710fd72dde779da26cdbf41e01ddbf Mon Sep 17 00:00:00 2001 From: Raul Kele Date: Sun, 18 Feb 2024 15:53:30 +0200 Subject: [PATCH] feat: Implement api key management (#403) Signed-off-by: Raul-Cristian Kele --- package-lock.json | 210 +++++++++++++++--- package.json | 3 +- src/App.js | 6 +- src/api.js | 6 +- src/components/Header/UserAccountMenu.jsx | 10 +- src/components/Repo/RepoDetails.jsx | 16 +- src/components/Shared/LayerCard.jsx | 10 +- src/components/Tag/TagDetails.jsx | 21 +- src/components/Tag/TagDetailsMetadata.jsx | 8 +- src/components/User/ApiKeys/ApiKeyCard.jsx | 163 ++++++++++++++ .../User/ApiKeys/ApiKeyConfirmDialog.jsx | 57 +++++ src/components/User/ApiKeys/ApiKeyDialog.jsx | 172 ++++++++++++++ .../User/ApiKeys/ApiKeyRevokeDialog.jsx | 71 ++++++ src/components/User/ApiKeys/ApiKeys.jsx | 146 ++++++++++++ src/index.js | 6 +- src/pages/HomePage.jsx | 1 - src/pages/UserManagementPage.jsx | 57 +++++ src/utilities/authUtilities.js | 7 +- 18 files changed, 903 insertions(+), 67 deletions(-) create mode 100644 src/components/User/ApiKeys/ApiKeyCard.jsx create mode 100644 src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx create mode 100644 src/components/User/ApiKeys/ApiKeyDialog.jsx create mode 100644 src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx create mode 100644 src/components/User/ApiKeys/ApiKeys.jsx create mode 100644 src/pages/UserManagementPage.jsx diff --git a/package-lock.json b/package-lock.json index 226762f0..b4e872b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@mui/lab": "^5.0.0-alpha.89", "@mui/material": "^5.8.6", "@mui/styles": "^5.8.6", + "@mui/x-date-pickers": "^6.18.4", "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", @@ -21,7 +22,7 @@ "downshift": "^6.1.12", "export-from-json": "^1.7.3", "lodash": "^4.17.21", - "luxon": "^2.5.2", + "luxon": "^3.4.4", "markdown-to-jsx": "^7.1.7", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -2130,16 +2131,21 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/@babel/template": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", @@ -2678,6 +2684,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -3935,11 +3975,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.11", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", + "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3948,25 +3988,134 @@ } }, "node_modules/@mui/utils": { - "version": "5.13.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.6.tgz", - "integrity": "sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz", + "integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==", "dependencies": { - "@babel/runtime": "^7.22.5", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.0", + "@babel/runtime": "^7.23.5", + "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, "engines": { "node": ">=12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "6.18.4", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.4.tgz", + "integrity": "sha512-YqJ6lxZHBIt344B3bvRAVbdYSQz4dcmJQXGcfvJTn26VdKjpgzjAqwhlbQhbAt55audJOWzGB99ImuQuljDROA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0" + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/base": { + "version": "5.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.27.tgz", + "integrity": "sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==", + "dependencies": { + "@babel/runtime": "^7.23.5", + "@floating-ui/react-dom": "^2.0.4", + "@mui/types": "^7.2.11", + "@mui/utils": "^5.15.0", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" } }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { @@ -4844,9 +4993,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -4884,18 +5033,10 @@ "@types/react": "^17" } }, - "node_modules/@types/react-is": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz", - "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "dependencies": { "@types/react": "*" } @@ -13250,9 +13391,9 @@ } }, "node_modules/luxon": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", - "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -16202,7 +16343,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.1", diff --git a/package.json b/package.json index aa2a0ddd..e11b02c7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@mui/lab": "^5.0.0-alpha.89", "@mui/material": "^5.8.6", "@mui/styles": "^5.8.6", + "@mui/x-date-pickers": "^6.18.4", "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", @@ -16,7 +17,7 @@ "downshift": "^6.1.12", "export-from-json": "^1.7.3", "lodash": "^4.17.21", - "luxon": "^2.5.2", + "luxon": "^3.4.4", "markdown-to-jsx": "^7.1.7", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/App.js b/src/App.js index 7e03a54f..6fdd96cd 100644 --- a/src/App.js +++ b/src/App.js @@ -1,14 +1,15 @@ import React, { useState } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { isAuthenticated } from 'utilities/authUtilities'; +import { isAuthenticated, isApiKeyEnabled } from 'utilities/authUtilities'; +import { AuthWrapper } from 'utilities/AuthWrapper'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; -import { AuthWrapper } from 'utilities/AuthWrapper'; import RepoPage from 'pages/RepoPage'; import TagPage from 'pages/TagPage'; import ExplorePage from 'pages/ExplorePage'; +import UserManagementPage from 'pages/UserManagementPage'; import './App.css'; @@ -25,6 +26,7 @@ function App() { } /> } /> } /> + {isApiKeyEnabled() && } />} } /> }> diff --git a/src/api.js b/src/api.js index acab45f4..6bcb0de1 100644 --- a/src/api.js +++ b/src/api.js @@ -67,11 +67,14 @@ const api = { return axios.put(urli, payload, config); }, - delete(urli, abortSignal, cfg) { + delete(urli, params, abortSignal, cfg) { let config = isEmpty(cfg) ? this.getRequestCfg() : cfg; if (!isEmpty(abortSignal) && isEmpty(config.signal)) { config = { ...config, signal: abortSignal }; } + if (!isEmpty(params)) { + config = { ...config, params }; + } return axios.delete(urli, config); } }; @@ -81,6 +84,7 @@ const endpoints = { authConfig: `/v2/_zot/ext/mgmt`, openidAuth: `/zot/auth/login`, logout: `/zot/auth/logout`, + apiKeys: '/zot/auth/apikey', deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`, repoList: ({ pageNumber = 1, pageSize = 15 } = {}) => `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${ diff --git a/src/components/Header/UserAccountMenu.jsx b/src/components/Header/UserAccountMenu.jsx index a27fb947..d22fe1a6 100644 --- a/src/components/Header/UserAccountMenu.jsx +++ b/src/components/Header/UserAccountMenu.jsx @@ -2,11 +2,17 @@ import React, { useState } from 'react'; import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material'; -import { getLoggedInUser, logoutUser } from '../../utilities/authUtilities'; +import { getLoggedInUser, logoutUser, isApiKeyEnabled } from '../../utilities/authUtilities'; +import { useNavigate } from 'react-router-dom'; function UserAccountMenu() { const [anchorEl, setAnchorEl] = useState(null); const openMenu = Boolean(anchorEl); + const navigate = useNavigate(); + + const apiKeyManagement = () => { + navigate('/user/apikey'); + }; const handleUserClick = (event) => { setAnchorEl(event.currentTarget); @@ -37,6 +43,8 @@ function UserAccountMenu() { > {getLoggedInUser()} + {isApiKeyEnabled() && API Keys} + Log out diff --git a/src/components/Repo/RepoDetails.jsx b/src/components/Repo/RepoDetails.jsx index 5ee2b158..f31d2dc2 100644 --- a/src/components/Repo/RepoDetails.jsx +++ b/src/components/Repo/RepoDetails.jsx @@ -9,6 +9,8 @@ import { isEmpty, uniq } from 'lodash'; import { api, endpoints } from '../../api'; import { host } from '../../host'; import { useParams, useNavigate, createSearchParams } from 'react-router-dom'; +import { mapToRepoFromRepoInfo } from 'utilities/objectModels'; +import { isAuthenticated } from 'utilities/authUtilities'; // components import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material'; @@ -16,7 +18,11 @@ import BookmarkIcon from '@mui/icons-material/Bookmark'; import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder'; import StarIcon from '@mui/icons-material/Star'; import StarBorderIcon from '@mui/icons-material/StarBorder'; -import makeStyles from '@mui/styles/makeStyles'; +import Tags from './Tabs/Tags.jsx'; +import RepoDetailsMetadata from './RepoDetailsMetadata'; +import Loading from '../Shared/Loading'; +import { Markdown } from 'utilities/MarkdowntojsxWrapper'; +import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; // placeholder images import repocube1 from '../../assets/repocube-1.png'; @@ -24,13 +30,7 @@ import repocube2 from '../../assets/repocube-2.png'; import repocube3 from '../../assets/repocube-3.png'; import repocube4 from '../../assets/repocube-4.png'; -import Tags from './Tabs/Tags.jsx'; -import RepoDetailsMetadata from './RepoDetailsMetadata'; -import Loading from '../Shared/Loading'; -import { Markdown } from 'utilities/MarkdowntojsxWrapper'; -import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; -import { mapToRepoFromRepoInfo } from 'utilities/objectModels'; -import { isAuthenticated } from 'utilities/authUtilities'; +import makeStyles from '@mui/styles/makeStyles'; const useStyles = makeStyles((theme) => ({ pageWrapper: { diff --git a/src/components/Shared/LayerCard.jsx b/src/components/Shared/LayerCard.jsx index f88a12db..133cadc8 100644 --- a/src/components/Shared/LayerCard.jsx +++ b/src/components/Shared/LayerCard.jsx @@ -1,9 +1,11 @@ -import React from 'react'; -import makeStyles from '@mui/styles/makeStyles'; +import React, { useState } from 'react'; + +import transform from 'utilities/transform'; + import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse } from '@mui/material'; import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; -import transform from 'utilities/transform'; -import { useState } from 'react'; + +import makeStyles from '@mui/styles/makeStyles'; const useStyles = makeStyles(() => ({ card: { diff --git a/src/components/Tag/TagDetails.jsx b/src/components/Tag/TagDetails.jsx index a9579bd7..8242edc5 100644 --- a/src/components/Tag/TagDetails.jsx +++ b/src/components/Tag/TagDetails.jsx @@ -3,7 +3,10 @@ import React, { useEffect, useMemo, useState, useRef } from 'react'; // utility import { api, endpoints } from '../../api'; +import { host } from '../../host'; import { mapToImage } from '../../utilities/objectModels'; +import { isEmpty, head } from 'lodash'; + // components import { Card, @@ -19,23 +22,21 @@ import { Typography, InputLabel } from '@mui/material'; +import TagDetailsMetadata from './TagDetailsMetadata'; +import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails'; +import HistoryLayers from './Tabs/HistoryLayers'; +import DependsOn from './Tabs/DependsOn'; +import IsDependentOn from './Tabs/IsDependentOn'; +import Loading from '../Shared/Loading'; +import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; +import ReferredBy from './Tabs/ReferredBy'; import makeStyles from '@mui/styles/makeStyles'; -import { host } from '../../host'; // placeholder images import repocube1 from '../../assets/repocube-1.png'; import repocube2 from '../../assets/repocube-2.png'; import repocube3 from '../../assets/repocube-3.png'; import repocube4 from '../../assets/repocube-4.png'; -import TagDetailsMetadata from './TagDetailsMetadata'; -import VulnerabilitiesDetails from './Tabs/VulnerabilitiesDetails'; -import HistoryLayers from './Tabs/HistoryLayers'; -import DependsOn from './Tabs/DependsOn'; -import IsDependentOn from './Tabs/IsDependentOn'; -import { isEmpty, head } from 'lodash'; -import Loading from '../Shared/Loading'; -import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; -import ReferredBy from './Tabs/ReferredBy'; const useStyles = makeStyles((theme) => ({ pageWrapper: { diff --git a/src/components/Tag/TagDetailsMetadata.jsx b/src/components/Tag/TagDetailsMetadata.jsx index 3a33d77c..3da99cd3 100644 --- a/src/components/Tag/TagDetailsMetadata.jsx +++ b/src/components/Tag/TagDetailsMetadata.jsx @@ -1,12 +1,14 @@ import React from 'react'; -import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; +import transform from '../../utilities/transform'; import { DateTime } from 'luxon'; import { Markdown } from 'utilities/MarkdowntojsxWrapper'; -import transform from '../../utilities/transform'; + +import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material'; import PullCommandButton from 'components/Shared/PullCommandButton'; +import makeStyles from '@mui/styles/makeStyles'; + const useStyles = makeStyles((theme) => ({ card: { display: 'flex', diff --git a/src/components/User/ApiKeys/ApiKeyCard.jsx b/src/components/User/ApiKeys/ApiKeyCard.jsx new file mode 100644 index 00000000..02b663bf --- /dev/null +++ b/src/components/User/ApiKeys/ApiKeyCard.jsx @@ -0,0 +1,163 @@ +import React, { useState } from 'react'; + +import { DateTime } from 'luxon'; +import { isNil } from 'lodash'; + +import { Card, CardContent, Typography, Grid, Divider, Stack, Collapse, Button } from '@mui/material'; +import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; +import ApiKeyRevokeDialog from './ApiKeyRevokeDialog'; + +import makeStyles from '@mui/styles/makeStyles'; + +const useStyles = makeStyles(() => ({ + card: { + marginBottom: 2, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#FFFFFF', + border: '1px solid #E0E5EB', + borderRadius: '0.75rem', + alignSelf: 'stretch', + flexGrow: 0, + order: 0, + width: '100%' + }, + content: { + textAlign: 'left', + color: '#52637A', + width: '100%', + boxSizing: 'border-box', + padding: '1rem', + backgroundColor: '#FFFFFF', + '&:hover': { + backgroundColor: '#FFFFFF' + }, + '&:last-child': { + paddingBottom: '1rem' + } + }, + label: { + fontSize: '1rem', + fontWeight: '400', + paddingRight: '0.5rem', + paddingBottom: '0.5rem', + paddingTop: '0.5rem', + textAlign: 'left', + width: '100%', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + cursor: 'pointer' + }, + expirationDate: { + fontSize: '1rem', + fontWeight: '400', + paddingBottom: '0.5rem', + paddingTop: '0.5rem', + textAlign: 'right' + }, + revokeButton: { + display: 'flex', + alignItems: 'center', + justifyContent: 'right' + }, + dropdownText: { + color: '#1479FF', + fontSize: '1rem', + fontWeight: '600', + cursor: 'pointer', + textAlign: 'center' + }, + dropdownButton: { + color: '#1479FF', + fontSize: '0.8125rem', + fontWeight: '600', + cursor: 'pointer' + }, + dropdownContentBox: { + boxSizing: 'border-box', + color: '#52637A', + fontSize: '1rem', + fontWeight: '400', + padding: '0.75rem', + backgroundColor: '#F7F7F7', + borderRadius: '0.9rem', + overflowWrap: 'break-word' + }, + keyCardDivider: { + margin: '1rem 0' + } +})); + +function ApiKeyCard(props) { + const classes = useStyles(); + const { apiKey, onRevoke } = props; + const [openDropdown, setOpenDropdown] = useState(false); + const [apiKeyRevokeOpen, setApiKeyRevokeOpen] = useState(false); + + const getExpirationDisplay = () => { + const expDateTime = DateTime.fromISO(apiKey.expirationDate); + return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`; + }; + + const handleApiKeyRevokeDialogOpen = () => { + setApiKeyRevokeOpen(true); + }; + + return ( + + + + + + {apiKey.label} + + + + + {getExpirationDisplay()} + + + + + + {!isNil(apiKey.apiKey) && ( + <> + + + + + setOpenDropdown((prevOpenState) => !prevOpenState)}> + {!openDropdown ? ( + + ) : ( + + )} + KEY + + + + + {apiKey.apiKey} + + + + + + )} + + + + + ); +} + +export default ApiKeyCard; diff --git a/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx new file mode 100644 index 00000000..ec51930e --- /dev/null +++ b/src/components/User/ApiKeys/ApiKeyConfirmDialog.jsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material'; + +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles(() => ({ + gridWrapper: { + paddingTop: '2rem', + paddingBottom: '2rem' + }, + apiKeyDisplay: { + boxSizing: 'border-box', + color: '#52637A', + fontSize: '1rem', + fontWeight: '400', + padding: '0.75rem', + backgroundColor: '#F7F7F7', + borderRadius: '0.9rem', + overflowWrap: 'break-word' + } +})); + +function ApiKeyConfirmDialog(props) { + const { open, setOpen, apiKey } = props; + + const classes = useStyles(); + + const handleClose = () => { + setOpen(false); + }; + + return ( + + Api Key "{apiKey?.label}" Created + + + + Please copy the api key, you will not be able to see it once the page is refreshed + + + + {apiKey?.apiKey} + + + + + + + + + ); +} + +export default ApiKeyConfirmDialog; diff --git a/src/components/User/ApiKeys/ApiKeyDialog.jsx b/src/components/User/ApiKeys/ApiKeyDialog.jsx new file mode 100644 index 00000000..f6fc1a06 --- /dev/null +++ b/src/components/User/ApiKeys/ApiKeyDialog.jsx @@ -0,0 +1,172 @@ +import React, { useState } from 'react'; + +import { isNil, isNumber } from 'lodash'; +import { DateTime } from 'luxon'; +import { api, endpoints } from 'api'; +import { host } from 'host'; + +import { + Dialog, + DialogContent, + TextField, + DialogTitle, + DialogActions, + Button, + FormControl, + InputLabel, + Select, + MenuItem, + Typography, + Grid +} from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers'; + +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles(() => ({ + gridWrapper: { + paddingTop: '2rem', + paddingBottom: '2rem' + }, + apiKeyLabel: { + paddingBottom: '1rem' + }, + expirationDateContainer: { + width: '100%' + }, + expirationDateInput: { + width: '100%' + }, + expirationDateDisplay: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } +})); + +function ApiKeyDialog(props) { + const { open, setOpen, onConfirm } = props; + + const [apiKeyLabel, setApiKeyLabel] = useState(); + const [expirationDateOffset, setExpirationDateOffset] = useState(30); + const [selectedExpirationDate, setSelectedExpirationDate] = useState(); + + const classes = useStyles(); + + const handleClose = () => { + setOpen(false); + }; + + const handleSubmit = () => { + api + .post(`${host()}${endpoints.apiKeys}`, { + label: apiKeyLabel, + expirationDate: getExpirationDatetime().toISO() + }) + .then((response) => { + if (response.data) { + onConfirm(response.data); + setOpen(false); + } + }) + .catch((error) => { + console.error(error); + }); + }; + + const handleLabelChange = (e) => { + const { value } = e.target; + setApiKeyLabel(value); + }; + + const handleExpirationDateChange = (e) => { + const { value } = e.target; + setExpirationDateOffset(value); + }; + + const handleDatePickerChange = (newValue) => { + setSelectedExpirationDate(newValue); + }; + + const getExpirationDatetime = () => { + if (isNumber(expirationDateOffset)) { + return DateTime.now().plus({ days: expirationDateOffset }).endOf('day'); + } else if (expirationDateOffset === 'custom') { + return DateTime.fromISO(selectedExpirationDate); + } + return null; + }; + + const getExpirationDisplay = () => { + const expDateTime = getExpirationDatetime(); + return `Expires on ${expDateTime.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)}`; + }; + + return ( + + Create Api Key + + + + + + + + + Expiration date + + + + + {expirationDateOffset === 'custom' ? ( + + ) : ( + {getExpirationDisplay()} + )} + + + + + + + + + + ); +} + +export default ApiKeyDialog; diff --git a/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx new file mode 100644 index 00000000..d5dcb5d7 --- /dev/null +++ b/src/components/User/ApiKeys/ApiKeyRevokeDialog.jsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import { api, endpoints } from 'api'; +import { host } from 'host'; + +import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography, Grid } from '@mui/material'; + +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles(() => ({ + gridWrapper: { + paddingTop: '2rem', + paddingBottom: '2rem' + }, + apiKeyDisplay: { + boxSizing: 'border-box', + color: '#52637A', + fontSize: '1rem', + fontWeight: '400', + padding: '0.75rem', + backgroundColor: '#F7F7F7', + borderRadius: '0.9rem', + overflowWrap: 'break-word' + } +})); + +function ApiKeyRevokeDialog(props) { + const { open, setOpen, apiKey, onConfirm } = props; + + const classes = useStyles(); + + const handleClose = () => { + setOpen(false); + }; + + const handleSubmit = () => { + api + .delete(`${host()}${endpoints.apiKeys}`, { id: apiKey.uuid }) + .then((response) => { + onConfirm(response?.status, apiKey); + setOpen(false); + }) + .catch((error) => { + console.error(error); + }); + }; + + return ( + + Revoke "{apiKey?.label}" key + + + + Are you sure you want to revoke this api key? + + + + + + + + + + ); +} + +export default ApiKeyRevokeDialog; diff --git a/src/components/User/ApiKeys/ApiKeys.jsx b/src/components/User/ApiKeys/ApiKeys.jsx new file mode 100644 index 00000000..9117111b --- /dev/null +++ b/src/components/User/ApiKeys/ApiKeys.jsx @@ -0,0 +1,146 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import { isEmpty, isNil } from 'lodash'; +import { api, endpoints } from 'api'; +import { host } from '../../../host'; + +import { Grid, Stack, Card, CardContent, Typography, Button } from '@mui/material'; +import Loading from '../../Shared/Loading'; +import ApiKeyDialog from './ApiKeyDialog'; +import ApiKeyConfirmDialog from './ApiKeyConfirmDialog'; +import ApiKeyCard from './ApiKeyCard'; + +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles((theme) => ({ + pageWrapper: { + backgroundColor: 'transparent', + height: '100%' + }, + header: { + [theme.breakpoints.down('md')]: { + padding: '0' + } + }, + cardRoot: { + boxShadow: 'none!important' + }, + pageTitle: { + fontWeight: '600', + fontSize: '1.5rem', + color: theme.palette.secondary.main, + textAlign: 'left' + }, + apikeysContainer: { + marginTop: '1.5rem', + height: '100%', + [theme.breakpoints.down('md')]: { + padding: '0' + } + }, + apikeysContent: { + padding: '1.5rem' + } +})); + +function ApiKeys() { + const abortController = useMemo(() => new AbortController(), []); + const [isLoading, setIsLoading] = useState(true); + const [apiKeys, setApiKeys] = useState([]); + const [newApiKey, setNewApiKey] = useState(); + const classes = useStyles(); + + // ApiKey dialog props + const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false); + const [apiKeyConfirmationOpen, setApiKeyConfirmationOpen] = useState(false); + + useEffect(() => { + setIsLoading(true); + api + .get(`${host()}${endpoints.apiKeys}`) + .then((response) => { + if (response.data && response.data.apiKeys) { + setApiKeys(response.data.apiKeys); + } + setIsLoading(false); + }) + .catch((e) => { + console.error(e); + setIsLoading(false); + }); + return () => { + abortController.abort(); + }; + }, []); + + useEffect(() => { + if (!isNil(newApiKey) && !apiKeyConfirmationOpen) { + setApiKeyConfirmationOpen(true); + } + }, [newApiKey]); + + const handleApiKeyDialogOpen = () => { + setApiKeyDialogOpen(true); + }; + + const handleApiKeyCreateConfirm = (apiKey) => { + setNewApiKey(apiKey); + setApiKeys((prevState) => [...prevState, apiKey]); + }; + + const handleApiKeyRevokeConfirm = (status, apiKey) => { + if (status === 200) setApiKeys((prevState) => prevState.filter((ak) => ak.uuid != apiKey.uuid)); + }; + + const renderApiKeys = () => { + return apiKeys.map((apiKey) => ( + + )); + }; + + return ( + <> + {isLoading ? ( + + ) : ( + + + + + + + + + Manage your API Keys + + + + + + + + + {!isLoading && !isEmpty(apiKeys) && ( + + + + + {renderApiKeys()} + + + + + )} + + {!isNil(newApiKey) && ( + + )} + + )} + + ); +} + +export default ApiKeys; diff --git a/src/index.js b/src/index.js index dff19ee6..c21d1b97 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,8 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; import { createTheme, ThemeProvider, StyledEngineProvider, adaptV4Theme } from '@mui/material/styles'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; const theme = createTheme( adaptV4Theme({ @@ -36,7 +38,9 @@ ReactDOM.render( - + + + , diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index c09daaa6..29ed5f1e 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -14,7 +14,6 @@ const useStyles = makeStyles(() => ({ minWidth: '60%' }, gridWrapper: { - // backgroundColor: "#fff", border: '0.0625em #f2f2f2 dashed' }, pageWrapper: { diff --git a/src/pages/UserManagementPage.jsx b/src/pages/UserManagementPage.jsx new file mode 100644 index 00000000..85119e0a --- /dev/null +++ b/src/pages/UserManagementPage.jsx @@ -0,0 +1,57 @@ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { isEmpty } from 'lodash'; + +import { getLoggedInUser } from 'utilities/authUtilities.js'; + +import { Container, Grid, Stack } from '@mui/material'; + +import Header from '../components/Header/Header.jsx'; +import ApiKeys from '../components/User/ApiKeys/ApiKeys.jsx'; + +import makeStyles from '@mui/styles/makeStyles'; + +const useStyles = makeStyles(() => ({ + container: { + paddingTop: 30, + paddingBottom: 5, + height: '100%', + minWidth: '60%' + }, + gridWrapper: { + border: '0.0625rem #f2f2f2 dashed' + }, + pageWrapper: { + height: '100%' + }, + tile: { + width: '100%', + padding: 5 + } +})); + +function UserManagementPage() { + const classes = useStyles(); + const navigate = useNavigate(); + + useEffect(() => { + if (isEmpty(getLoggedInUser())) { + navigate('/home'); + } + }, []); + + return ( + +
+ + + + + + + + + ); +} + +export default UserManagementPage; diff --git a/src/utilities/authUtilities.js b/src/utilities/authUtilities.js index edd4eaa0..fd37ec65 100644 --- a/src/utilities/authUtilities.js +++ b/src/utilities/authUtilities.js @@ -41,10 +41,15 @@ const isAuthenticationEnabled = () => { return Object.keys(authMethods).length > 0; }; +const isApiKeyEnabled = () => { + const authConfig = JSON.parse(localStorage.getItem('authConfig')) || {}; + return authConfig?.apikey; +}; + const getLoggedInUser = () => { const userCookie = getCookie('user'); if (!userCookie) return null; return userCookie; }; -export { isAuthenticated, isAuthenticationEnabled, getLoggedInUser, logoutUser }; +export { isAuthenticated, isAuthenticationEnabled, isApiKeyEnabled, getLoggedInUser, logoutUser };