diff --git a/package-lock.json b/package-lock.json index 596cd2714..fcf94db23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "axios": "^1.11.0", "bcryptjs": "^3.0.2", "bit-mask": "^1.0.2", - "classnames": "2.5.1", + "clsx": "^2.1.1", "concurrently": "^9.2.0", "connect-mongo": "^5.1.0", "cors": "^2.8.5", @@ -1883,6 +1883,15 @@ } } }, + "node_modules/@material-ui/core/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/icons": { "version": "4.11.3", "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", @@ -1946,6 +1955,15 @@ } } }, + "node_modules/@material-ui/styles/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/system": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", @@ -3984,11 +4002,6 @@ "node": ">=8" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/clean-git-ref": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", @@ -4137,9 +4150,10 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -7753,7 +7767,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -11131,15 +11144,23 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shebang-command": { @@ -11896,6 +11917,20 @@ "node": ">=14.14" } }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12506,7 +12541,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", diff --git a/package.json b/package.json index ddaeb190c..4ec5d39d3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "axios": "^1.11.0", "bcryptjs": "^3.0.2", "bit-mask": "^1.0.2", - "classnames": "2.5.1", + "clsx": "^2.1.1", "concurrently": "^9.2.0", "connect-mongo": "^5.1.0", "cors": "^2.8.5", diff --git a/src/constants/languageColors.ts b/src/constants/languageColors.ts index bdbbead73..f2038c89f 100644 --- a/src/constants/languageColors.ts +++ b/src/constants/languageColors.ts @@ -1,5 +1,3 @@ -// src/constants/languageColors.ts - export const languageColors: Record = { '1C Enterprise': '#814CCC', '2-Dimensional Array': '#38761D', diff --git a/src/routes.tsx b/src/routes.tsx index 41a9b7123..51ae327a6 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -25,9 +25,11 @@ import User from './ui/views/User/User'; import UserList from './ui/views/UserList/UserList'; import RepoDetails from './ui/views/RepoDetails/RepoDetails'; import RepoList from './ui/views/RepoList/RepoList'; +import SettingsView from './ui/views/Settings/Settings'; import { RepoIcon } from '@primer/octicons-react'; -import { Group, AccountCircle, Dashboard } from '@material-ui/icons'; +import { Group, AccountCircle, Dashboard, Settings } from '@material-ui/icons'; + import { Route } from './types/models'; const dashboardRoutes: Route[] = [ @@ -97,6 +99,18 @@ const dashboardRoutes: Route[] = [ layout: '/dashboard', visible: false, }, + { + path: '/admin/settings', + name: 'Settings', + icon: Settings, + component: (props) => + , + layout: '/dashboard', + visible: true, + }, ]; export default dashboardRoutes; diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 32c81304d..9ebfa2bcb 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -17,10 +17,6 @@ const jwtAuthHandler = (overrideConfig = null) => { return next(); } - if (req.isAuthenticated()) { - return next(); - } - const token = req.header("Authorization"); if (!token) { return res.status(401).send("No token provided\n"); @@ -30,11 +26,15 @@ const jwtAuthHandler = (overrideConfig = null) => { const audience = expectedAudience || clientID; if (!authorityURL) { - return res.status(500).send("OIDC authority URL is not configured\n"); + return res.status(500).send({ + message: "JWT handler: authority URL is not configured\n" + }); } if (!clientID) { - return res.status(500).send("OIDC client ID is not configured\n"); + return res.status(500).send({ + message: "JWT handler: client ID is not configured\n" + }); } const tokenParts = token.split(" "); diff --git a/src/ui/components/Card/Card.tsx b/src/ui/components/Card/Card.tsx index edbf2ee2c..0ae6704db 100644 --- a/src/ui/components/Card/Card.tsx +++ b/src/ui/components/Card/Card.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardStyle'; @@ -23,7 +23,7 @@ const Card: React.FC = ({ }) => { const classes = useStyles(); - const cardClasses = classNames({ + const cardClasses = clsx({ [classes.card]: true, [classes.cardPlain]: plain, [classes.cardProfile]: profile, diff --git a/src/ui/components/Card/CardAvatar.tsx b/src/ui/components/Card/CardAvatar.tsx index 2bbfe9db4..c23554d3d 100644 --- a/src/ui/components/Card/CardAvatar.tsx +++ b/src/ui/components/Card/CardAvatar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardAvatarStyle'; @@ -21,7 +21,7 @@ const CardAvatar: React.FC = ({ }) => { const classes = useStyles(); - const cardAvatarClasses = classNames({ + const cardAvatarClasses = clsx({ [classes.cardAvatar]: true, [classes.cardAvatarProfile]: profile, [classes.cardAvatarPlain]: plain, diff --git a/src/ui/components/Card/CardBody.tsx b/src/ui/components/Card/CardBody.tsx index 1a32e2354..dd4de1171 100644 --- a/src/ui/components/Card/CardBody.tsx +++ b/src/ui/components/Card/CardBody.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardBodyStyle'; @@ -21,7 +21,7 @@ const CardBody: React.FC = ({ }) => { const classes = useStyles(); - const cardBodyClasses = classNames({ + const cardBodyClasses = clsx({ [classes.cardBody]: true, [classes.cardBodyPlain]: plain, [classes.cardBodyProfile]: profile, diff --git a/src/ui/components/Card/CardFooter.tsx b/src/ui/components/Card/CardFooter.tsx index 57f9f36a5..635beabfc 100644 --- a/src/ui/components/Card/CardFooter.tsx +++ b/src/ui/components/Card/CardFooter.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardFooterStyle'; @@ -25,7 +25,7 @@ const CardFooter: React.FC = ({ }) => { const classes = useStyles(); - const cardFooterClasses = classNames({ + const cardFooterClasses = clsx({ [classes.cardFooter]: true, [classes.cardFooterPlain]: plain, [classes.cardFooterProfile]: profile, diff --git a/src/ui/components/Card/CardHeader.tsx b/src/ui/components/Card/CardHeader.tsx index c893715cc..a808a262f 100644 --- a/src/ui/components/Card/CardHeader.tsx +++ b/src/ui/components/Card/CardHeader.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardHeaderStyle'; @@ -20,7 +20,7 @@ const CardHeader: React.FC = (props) => { const classes = useStyles(); const { className, children, color, plain, stats, icon, ...rest } = props; - const cardHeaderClasses = classNames({ + const cardHeaderClasses = clsx({ [classes.cardHeader]: true, [color ? classes[`${color}CardHeader`] : '']: color, [classes.cardHeaderPlain]: plain, diff --git a/src/ui/components/Card/CardIcon.tsx b/src/ui/components/Card/CardIcon.tsx index 353f07c86..c9a6544de 100644 --- a/src/ui/components/Card/CardIcon.tsx +++ b/src/ui/components/Card/CardIcon.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/cardIconStyle'; @@ -18,7 +18,7 @@ const CardIcon: React.FC = (props) => { const classes = useStyles(); const { className, children, color, ...rest } = props; - const cardIconClasses = classNames({ + const cardIconClasses = clsx({ [classes.cardIcon]: true, [color ? classes[`${color}CardHeader`] : '']: color, [className || '']: className !== undefined, diff --git a/src/ui/components/CustomButtons/Button.tsx b/src/ui/components/CustomButtons/Button.tsx index 01e8e0eb1..e6eea281a 100644 --- a/src/ui/components/CustomButtons/Button.tsx +++ b/src/ui/components/CustomButtons/Button.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import Button, { ButtonProps } from '@material-ui/core/Button'; import styles from '../../assets/jss/material-dashboard-react/components/buttonStyle'; @@ -48,7 +48,7 @@ export default function RegularButton(props: RegularButtonProps) { ...rest } = props; - const btnClasses = classNames({ + const btnClasses = clsx({ [classes.button]: true, [classes[size as Size]]: size, [classes[color as Color]]: color, diff --git a/src/ui/components/CustomButtons/CodeActionButton.tsx b/src/ui/components/CustomButtons/CodeActionButton.tsx index 47b3e3e20..7962fdb48 100644 --- a/src/ui/components/CustomButtons/CodeActionButton.tsx +++ b/src/ui/components/CustomButtons/CodeActionButton.tsx @@ -1,5 +1,6 @@ import Popper from '@material-ui/core/Popper'; import Paper from '@material-ui/core/Paper'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; import { CheckIcon, ChevronDownIcon, @@ -9,6 +10,7 @@ import { } from '@primer/octicons-react'; import React, { useState } from 'react'; import { PopperPlacementType } from '@material-ui/core/Popper'; +import Button from './Button'; interface CodeActionButtonProps { cloneURL: string; @@ -28,26 +30,27 @@ const CodeActionButton: React.FC = ({ cloneURL }) => { setPlacement(newPlacement); }; + const handleClickAway = () => { + setOpen(false); + }; + return ( <> -
{' '} - Code + Code -
+ = ({ cloneURL }) => { zIndex: 99, }} > - -
- {' '} - Clone -
-
- + +
+ {' '} + Clone +
+
- {cloneURL} - - - {!isCopied && ( - { - navigator.clipboard.writeText(`git clone ${cloneURL}`); - setIsCopied(true); - }} - > - - - )} - {isCopied && ( - - - - )} + + {cloneURL} + + + {!isCopied && ( + { + navigator.clipboard.writeText(`git clone ${cloneURL}`); + setIsCopied(true); + }} + > + + + )} + {isCopied && ( + + + + )} + +
+
+
+ + Use Git and run this command in your IDE or Terminal 👍
-
- - Use Git and run this command in your IDE or Terminal 👍 - -
-
- + + ); diff --git a/src/ui/components/CustomInput/CustomInput.jsx b/src/ui/components/CustomInput/CustomInput.jsx index b5eea6834..831f8c804 100644 --- a/src/ui/components/CustomInput/CustomInput.jsx +++ b/src/ui/components/CustomInput/CustomInput.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import FormControl from '@material-ui/core/FormControl'; @@ -15,25 +15,25 @@ export default function CustomInput(props) { const classes = useStyles(); const { formControlProps, labelText, id, labelProps, inputProps, error, success } = props; - const labelClasses = classNames({ - [' ' + classes.labelRootError]: error, - [' ' + classes.labelRootSuccess]: success && !error, + const labelClasses = clsx({ + [classes.labelRootError]: error, + [classes.labelRootSuccess]: success && !error, }); - const underlineClasses = classNames({ + const underlineClasses = clsx({ [classes.underlineError]: error, [classes.underlineSuccess]: success && !error, [classes.underline]: true, }); - const marginTop = classNames({ + const marginTop = clsx({ [classes.marginTop]: labelText === undefined, }); const generateIcon = () => { if (error) { - return ; + return ; } if (success) { - return ; + return ; } return null; }; @@ -41,7 +41,7 @@ export default function CustomInput(props) { return ( {labelText !== undefined ? ( diff --git a/src/ui/components/CustomTabs/CustomTabs.tsx b/src/ui/components/CustomTabs/CustomTabs.tsx index 8f9daf3a3..8cd0c2d81 100644 --- a/src/ui/components/CustomTabs/CustomTabs.tsx +++ b/src/ui/components/CustomTabs/CustomTabs.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; @@ -40,7 +40,7 @@ const CustomTabs: React.FC = ({ setValue(newValue); }; - const cardTitle = classNames({ + const cardTitle = clsx({ [classes.cardTitle]: true, [classes.cardTitleRTL]: rtlActive, }); diff --git a/src/ui/components/Navbars/DashboardNavbarLinks.tsx b/src/ui/components/Navbars/DashboardNavbarLinks.tsx index d08245147..e13c927d2 100644 --- a/src/ui/components/Navbars/DashboardNavbarLinks.tsx +++ b/src/ui/components/Navbars/DashboardNavbarLinks.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import MenuItem from '@material-ui/core/MenuItem'; import MenuList from '@material-ui/core/MenuList'; @@ -15,7 +15,7 @@ import { useNavigate } from 'react-router-dom'; import { AccountCircle } from '@material-ui/icons'; import { getUser } from '../../services/user'; import axios from 'axios'; -import { getCookie } from '../../utils'; +import { getAxiosConfig } from '../../services/auth'; import { UserData } from '../../../types/models'; const useStyles = makeStyles(styles); @@ -51,18 +51,13 @@ const DashboardNavbarLinks: React.FC = () => { const logout = async () => { try { - const response = await axios.post( + const { data } = await axios.post( `${process.env.VITE_API_URI || 'http://localhost:3000'}/api/auth/logout`, {}, - { - withCredentials: true, - headers: { - 'X-CSRF-TOKEN': getCookie('csrf'), - }, - }, + getAxiosConfig() ); - if (!response.data.isAuth && !response.data.user) { + if (!data.isAuth && !data.user) { setAuth(false); navigate(0); } @@ -93,7 +88,7 @@ const DashboardNavbarLinks: React.FC = () => { anchorEl={openProfile} transition disablePortal - className={classNames({ [classes.popperClose]: !openProfile }) + ' ' + classes.popperNav} + className={clsx(classes.popperNav, { [classes.popperClose]: !openProfile })} > {({ TransitionProps, placement }) => ( = (props) => { }; const { color = 'primary' } = props; - const appBarClasses = classNames({ + const appBarClasses = clsx({ [` ${classes[color]}`]: color, }); return ( - +
{/* Here we create navbar brand, based on route name */} diff --git a/src/ui/components/Sidebar/Sidebar.tsx b/src/ui/components/Sidebar/Sidebar.tsx index 5d6b27c40..a2f745948 100644 --- a/src/ui/components/Sidebar/Sidebar.tsx +++ b/src/ui/components/Sidebar/Sidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { NavLink } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; import Drawer from '@material-ui/core/Drawer'; @@ -40,10 +40,10 @@ const Sidebar: React.FC = ({ {routes.map((prop, key) => { const activePro = ' '; - const listItemClasses = classNames({ + const listItemClasses = clsx({ [` ${classes[color]}`]: activeRoute(prop.layout + prop.path), }); - const whiteFontClasses = classNames({ + const whiteFontClasses = clsx({ [` ${classes.whiteFont}`]: activeRoute(prop.layout + prop.path), }); @@ -61,7 +61,7 @@ const Sidebar: React.FC = ({ {typeof prop.icon === 'string' ? ( @@ -69,14 +69,14 @@ const Sidebar: React.FC = ({ ) : ( )} = ({ anchor={rtlActive ? 'left' : 'right'} open={open} classes={{ - paper: classNames(classes.drawerPaper, { + paper: clsx(classes.drawerPaper, { [classes.drawerPaperRTL]: rtlActive, }), }} @@ -131,7 +131,7 @@ const Sidebar: React.FC = ({ variant='permanent' open classes={{ - paper: classNames(classes.drawerPaper, { + paper: clsx(classes.drawerPaper, { [classes.drawerPaperRTL]: rtlActive, }), }} diff --git a/src/ui/components/Snackbar/Snackbar.tsx b/src/ui/components/Snackbar/Snackbar.tsx index 13dc6679d..df3134a64 100644 --- a/src/ui/components/Snackbar/Snackbar.tsx +++ b/src/ui/components/Snackbar/Snackbar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import Snack from '@material-ui/core/Snackbar'; import IconButton from '@material-ui/core/IconButton'; @@ -27,7 +27,7 @@ const Snackbar: React.FC = (props) => { const { message, color = 'info', close, icon: Icon, place = 'tr', open, rtlActive } = props; let action: React.ReactNode[] = []; - const messageClasses = classNames({ + const messageClasses = clsx({ [classes.iconMessage]: Icon !== undefined, }); @@ -70,9 +70,9 @@ const Snackbar: React.FC = (props) => { action={action} ContentProps={{ classes: { - root: `${classes.root} ${classes[color]}`, + root: clsx(classes.root, classes[color]), message: classes.message, - action: classNames({ [classes.actionRTL]: rtlActive }), + action: clsx({ [classes.actionRTL]: rtlActive }), }, }} /> diff --git a/src/ui/components/Snackbar/SnackbarContent.tsx b/src/ui/components/Snackbar/SnackbarContent.tsx index 76e3c12b8..30f4a4e18 100644 --- a/src/ui/components/Snackbar/SnackbarContent.tsx +++ b/src/ui/components/Snackbar/SnackbarContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import MuiSnackbarContent from '@material-ui/core/SnackbarContent'; import IconButton from '@material-ui/core/IconButton'; @@ -23,7 +23,7 @@ const SnackbarContent: React.FC = (props) => { const { message, color = 'info', close, icon: Icon, rtlActive } = props; let action: React.ReactNode[] = []; - const messageClasses = classNames({ + const messageClasses = clsx({ [classes.iconMessage]: Icon !== undefined, }); @@ -44,9 +44,9 @@ const SnackbarContent: React.FC = (props) => {
} classes={{ - root: `${classes.root} ${classes[color]}`, + root: clsx(classes.root, classes[color]), message: classes.message, - action: classNames({ [classes.actionRTL]: rtlActive }), + action: clsx({ [classes.actionRTL]: rtlActive }), }} action={action} /> diff --git a/src/ui/components/Table/Table.jsx b/src/ui/components/Table/Table.jsx index e754a090d..c2cebfecf 100644 --- a/src/ui/components/Table/Table.jsx +++ b/src/ui/components/Table/Table.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; @@ -18,11 +19,11 @@ export default function CustomTable(props) {
{tableHead !== undefined ? ( - + {tableHead.map((prop, key) => { return ( - + {prop} ); @@ -33,10 +34,10 @@ export default function CustomTable(props) { {tableData.map((prop, key) => { return ( - + {prop.map((p, k) => { return ( - + {p} ); diff --git a/src/ui/components/Typography/Danger.jsx b/src/ui/components/Typography/Danger.jsx index aee271323..ee6e94b59 100644 --- a/src/ui/components/Typography/Danger.jsx +++ b/src/ui/components/Typography/Danger.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; @@ -8,7 +9,7 @@ const useStyles = makeStyles(styles); export default function Danger(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Danger.propTypes = { diff --git a/src/ui/components/Typography/Info.jsx b/src/ui/components/Typography/Info.jsx index f8aeebac1..17c3a9ddc 100644 --- a/src/ui/components/Typography/Info.jsx +++ b/src/ui/components/Typography/Info.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; import { makeStyles } from '@material-ui/core/styles'; import styles from '../../assets/jss/material-dashboard-react/components/typographyStyle'; @@ -8,7 +9,7 @@ const useStyles = makeStyles(styles); export default function Info(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Info.propTypes = { diff --git a/src/ui/components/Typography/Muted.jsx b/src/ui/components/Typography/Muted.jsx index 686547970..9b625c5f2 100644 --- a/src/ui/components/Typography/Muted.jsx +++ b/src/ui/components/Typography/Muted.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; // @material-ui/core components import { makeStyles } from '@material-ui/core/styles'; // core components @@ -10,7 +11,7 @@ const useStyles = makeStyles(styles); export default function Muted(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Muted.propTypes = { diff --git a/src/ui/components/Typography/Primary.jsx b/src/ui/components/Typography/Primary.jsx index 38f3d81ed..b58206c4f 100644 --- a/src/ui/components/Typography/Primary.jsx +++ b/src/ui/components/Typography/Primary.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; // @material-ui/core components import { makeStyles } from '@material-ui/core/styles'; // core components @@ -10,7 +11,7 @@ const useStyles = makeStyles(styles); export default function Primary(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Primary.propTypes = { diff --git a/src/ui/components/Typography/Quote.jsx b/src/ui/components/Typography/Quote.jsx index 5ade54e41..3dedbc7bb 100644 --- a/src/ui/components/Typography/Quote.jsx +++ b/src/ui/components/Typography/Quote.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; // @material-ui/core components import { makeStyles } from '@material-ui/core/styles'; // core components @@ -11,7 +12,7 @@ export default function Quote(props) { const classes = useStyles(); const { text, author } = props; return ( -
+

{text}

{author}
diff --git a/src/ui/components/Typography/Success.jsx b/src/ui/components/Typography/Success.jsx index 56951ce6b..a40affc47 100644 --- a/src/ui/components/Typography/Success.jsx +++ b/src/ui/components/Typography/Success.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; // @material-ui/core components import { makeStyles } from '@material-ui/core/styles'; // core components @@ -10,7 +11,7 @@ const useStyles = makeStyles(styles); export default function Success(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Success.propTypes = { diff --git a/src/ui/components/Typography/Warning.jsx b/src/ui/components/Typography/Warning.jsx index b8349411d..70db1ea6d 100644 --- a/src/ui/components/Typography/Warning.jsx +++ b/src/ui/components/Typography/Warning.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; // @material-ui/core components import { makeStyles } from '@material-ui/core/styles'; // core components @@ -10,7 +11,7 @@ const useStyles = makeStyles(styles); export default function Warning(props) { const classes = useStyles(); const { children } = props; - return
{children}
; + return
{children}
; } Warning.propTypes = { diff --git a/src/ui/services/auth.js b/src/ui/services/auth.js index e1155e9f5..ae022bfc8 100644 --- a/src/ui/services/auth.js +++ b/src/ui/services/auth.js @@ -1,3 +1,5 @@ +import { getCookie } from '../utils'; + const baseUrl = import.meta.env.VITE_API_URI ? `${import.meta.env.VITE_API_URI}` : `${location.origin}`; @@ -20,3 +22,33 @@ export const getUserInfo = async () => { return null; } }; + +/** + * Gets the Axios config for the UI + * @return {Object} The Axios config + */ +export const getAxiosConfig = () => { + const jwtToken = localStorage.getItem('ui_jwt_token'); + return { + withCredentials: true, + headers: { + 'X-CSRF-TOKEN': getCookie('csrf'), + Authorization: jwtToken ? `Bearer ${jwtToken}` : undefined, + }, + }; +}; + +/** + * Processes authentication errors and returns a user-friendly error message + * @param {Object} error - The error object + * @return {string} The error message + */ +export const processAuthError = (error) => { + let errorMessage = `Failed to authorize user: ${error.response.data.trim()}. `; + if (!localStorage.getItem('ui_jwt_token')) { + errorMessage += 'Set your JWT token in the settings page or disable JWT auth in your app configuration.' + } else { + errorMessage += 'Check your JWT token or disable JWT auth in your app configuration.' + } + return errorMessage; +}; diff --git a/src/ui/services/git-push.js b/src/ui/services/git-push.js index 483275a37..23ac9f3a5 100644 --- a/src/ui/services/git-push.js +++ b/src/ui/services/git-push.js @@ -1,29 +1,26 @@ import axios from 'axios'; -import { getCookie } from '../utils.tsx'; +import { getAxiosConfig, processAuthError } from './auth.js'; const baseUrl = import.meta.env.VITE_API_URI ? `${import.meta.env.VITE_API_URI}/api/v1` : `${location.origin}/api/v1`; -const config = { - withCredentials: true, -}; + const getPush = async (id, setIsLoading, setData, setAuth, setIsError) => { + const url = `${baseUrl}/push/${id}`; + setIsLoading(true); -const getPush = async (id, setIsLoading, setData, setAuth, setIsError) => { - const url = `${baseUrl}/push/${id}`; - await axios(url, config) - .then((response) => { + try { + const response = await axios(url, getAxiosConfig()); const data = response.data; data.diff = data.steps.find((x) => x.stepName === 'diff'); setData(data); - setIsLoading(false); - }) - .catch((error) => { - if (error.response && error.response.status === 401) setAuth(false); + } catch (error) { + if (error.response?.status === 401) setAuth(false); else setIsError(true); + } finally { setIsLoading(false); - }); -}; + } + }; const getPushes = async ( setIsLoading, @@ -36,28 +33,29 @@ const getPushes = async ( canceled: false, authorised: false, rejected: false, - }, + } ) => { const url = new URL(`${baseUrl}/push`); url.search = new URLSearchParams(query); setIsLoading(true); - await axios(url.toString(), { withCredentials: true }) - .then((response) => { - const data = response.data; - setData(data); - }) - .catch((error) => { - setIsError(true); - if (error.response && error.response.status === 401) { - setAuth(false); - setErrorMessage('Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.'); - } else { - setErrorMessage(`Error fetching pushes: ${error.response.data.message}`); - } - }).finally(() => { - setIsLoading(false); - }); + + try { + const response = await axios(url.toString(), getAxiosConfig()); + setData(response.data); + } catch (error) { + setIsError(true); + + if (error.response?.status === 401) { + setAuth(false); + setErrorMessage(processAuthError(error)); + } else { + const message = error.response?.data?.message || error.message; + setErrorMessage(`Error fetching pushes: ${message}`); + } + } finally { + setIsLoading(false); + } }; const authorisePush = async (id, setMessage, setUserAllowedToApprove, attestation) => { @@ -72,7 +70,7 @@ const authorisePush = async (id, setMessage, setUserAllowedToApprove, attestatio attestation, }, }, - { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }, + getAxiosConfig(), ) .catch((error) => { if (error.response && error.response.status === 401) { @@ -89,7 +87,7 @@ const rejectPush = async (id, setMessage, setUserAllowedToReject) => { let errorMsg = ''; let isUserAllowedToReject = true; await axios - .post(url, {}, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .post(url, {}, getAxiosConfig()) .catch((error) => { if (error.response && error.response.status === 401) { errorMsg = 'You are not authorised to reject...'; @@ -103,7 +101,7 @@ const rejectPush = async (id, setMessage, setUserAllowedToReject) => { const cancelPush = async (id, setAuth, setIsError) => { const url = `${baseUrl}/push/${id}/cancel`; await axios - .post(url, {}, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .post(url, {}, getAxiosConfig()) .catch((error) => { if (error.response && error.response.status === 401) { setAuth(false); diff --git a/src/ui/services/repo.js b/src/ui/services/repo.js index 7f787660f..cdbfe3c84 100644 --- a/src/ui/services/repo.js +++ b/src/ui/services/repo.js @@ -1,18 +1,14 @@ import axios from 'axios'; -import { getCookie } from '../utils.tsx'; +import { getAxiosConfig, processAuthError } from './auth.js'; const baseUrl = import.meta.env.VITE_API_URI ? `${import.meta.env.VITE_API_URI}/api/v1` : `${location.origin}/api/v1`; -const config = { - withCredentials: true, -}; - const canAddUser = (repoId, user, action) => { const url = new URL(`${baseUrl}/repo/${repoId}`); return axios - .get(url.toString(), config) + .get(url.toString(), getAxiosConfig()) .then((response) => { const data = response.data; if (action === 'authorise') { @@ -44,7 +40,7 @@ const getRepos = async ( const url = new URL(`${baseUrl}/repo`); url.search = new URLSearchParams(query); setIsLoading(true); - await axios(url.toString(), config) + await axios(url.toString(), getAxiosConfig()) .then((response) => { const data = response.data; setData(data); @@ -53,11 +49,9 @@ const getRepos = async ( setIsError(true); if (error.response && error.response.status === 401) { setAuth(false); - setErrorMessage( - 'Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.', - ); + setErrorMessage(processAuthError(error)); } else { - setErrorMessage(`Error fetching repositories: ${error.response.data.message}`); + setErrorMessage(`Error fetching repos: ${error.response.data.message}`); } }) .finally(() => { @@ -68,7 +62,7 @@ const getRepos = async ( const getRepo = async (setIsLoading, setData, setAuth, setIsError, id) => { const url = new URL(`${baseUrl}/repo/${id}`); setIsLoading(true); - await axios(url.toString(), config) + await axios(url.toString(), getAxiosConfig()) .then((response) => { const data = response.data; setData(data); @@ -87,8 +81,9 @@ const getRepo = async (setIsLoading, setData, setAuth, setIsError, id) => { const addRepo = async (onClose, setError, data) => { const url = new URL(`${baseUrl}/repo`); + return axios - .post(url, data, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .post(url, data, getAxiosConfig()) .then((response) => { onClose(); return response.data; @@ -105,7 +100,7 @@ const addUser = async (repoId, user, action) => { const url = new URL(`${baseUrl}/repo/${repoId}/user/${action}`); const data = { username: user }; await axios - .patch(url, data, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .patch(url, data, getAxiosConfig()) .catch((error) => { console.log(error.response.data.message); throw error; @@ -120,7 +115,7 @@ const deleteUser = async (user, repoId, action) => { const url = new URL(`${baseUrl}/repo/${repoId}/user/${action}/${user}`); await axios - .delete(url, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .delete(url, getAxiosConfig()) .catch((error) => { console.log(error.response.data.message); throw error; @@ -131,7 +126,7 @@ const deleteRepo = async (repoId) => { const url = new URL(`${baseUrl}/repo/${repoId}/delete`); await axios - .delete(url, { withCredentials: true, headers: { 'X-CSRF-TOKEN': getCookie('csrf') } }) + .delete(url, getAxiosConfig()) .catch((error) => { console.log(error.response.data.message); throw error; diff --git a/src/ui/services/user.ts b/src/ui/services/user.ts index f7be1ea87..18b2d36cf 100644 --- a/src/ui/services/user.ts +++ b/src/ui/services/user.ts @@ -1,13 +1,10 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; -import { getCookie } from '../utils'; +import { getAxiosConfig, processAuthError } from './auth'; import { UserData } from '../../types/models'; type SetStateCallback = (value: T | ((prevValue: T) => T)) => void; const baseUrl = process.env.VITE_API_URI || location.origin; -const config = { - withCredentials: true, -}; const getUser = async ( setIsLoading?: SetStateCallback, @@ -23,7 +20,7 @@ const getUser = async ( console.log(url); try { - const response: AxiosResponse = await axios(url, config); + const response: AxiosResponse = await axios(url, getAxiosConfig()); const data = response.data; setData?.(data); @@ -43,7 +40,6 @@ const getUsers = async ( setIsLoading: SetStateCallback, setData: SetStateCallback, setAuth: SetStateCallback, - setIsError: SetStateCallback, setErrorMessage: SetStateCallback, query: Record = {}, ): Promise => { @@ -53,17 +49,13 @@ const getUsers = async ( setIsLoading(true); try { - const response: AxiosResponse = await axios(url.toString(), { - withCredentials: true, - }); + const response: AxiosResponse = await axios(url.toString(), getAxiosConfig()); setData(response.data); } catch (error) { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { setAuth(false); - setErrorMessage( - 'Failed to authorize user. If JWT auth is enabled, please check your configuration or disable it.', - ); + setErrorMessage(processAuthError(error)); } else { const msg = (error.response?.data as any)?.message ?? error.message; setErrorMessage(`Error fetching users: ${msg}`); @@ -81,10 +73,7 @@ const updateUser = async (data: UserData): Promise => { const url = new URL(`${baseUrl}/api/auth/gitAccount`); try { - await axios.post(url.toString(), data, { - withCredentials: true, - headers: { 'X-CSRF-TOKEN': getCookie('csrf') }, - }); + await axios.post(url.toString(), data, getAxiosConfig()); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -103,9 +92,7 @@ const getUserLoggedIn = async ( const url = new URL(`${baseUrl}/api/auth/me`); try { - const response: AxiosResponse = await axios(url.toString(), { - withCredentials: true, - }); + const response: AxiosResponse = await axios(url.toString(), getAxiosConfig()); const data = response.data; setIsLoading(false); setIsAdmin(data.admin || false); diff --git a/src/ui/views/OpenPushRequests/OpenPushRequests.tsx b/src/ui/views/OpenPushRequests/OpenPushRequests.tsx index bd332237e..256453a96 100644 --- a/src/ui/views/OpenPushRequests/OpenPushRequests.tsx +++ b/src/ui/views/OpenPushRequests/OpenPushRequests.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import GridItem from '../../components/Grid/GridItem'; import GridContainer from '../../components/Grid/GridContainer'; import PushesTable from './components/PushesTable'; import CustomTabs from '../../components/CustomTabs/CustomTabs'; +import Danger from '../../components/Typography/Danger'; import { Visibility, CheckCircle, Cancel, Block } from '@material-ui/icons'; import { SvgIconProps } from '@material-ui/core'; @@ -13,6 +14,12 @@ interface TabConfig { } const Dashboard: React.FC = () => { + const [errorMessage, setErrorMessage] = useState(null); + + const handlePushTableError = (errorMessage: string) => { + setErrorMessage(errorMessage); + } + const tabs: TabConfig[] = [ { tabName: 'Pending', @@ -29,30 +36,47 @@ const Dashboard: React.FC = () => { { tabName: 'Approved', tabIcon: CheckCircle, - tabContent: , + tabContent: , }, { tabName: 'Canceled', tabIcon: Cancel, - tabContent: , + tabContent: ( + + ), }, { tabName: 'Rejected', tabIcon: Block, - tabContent: , + tabContent: ( + + ), }, ]; return (
- - - - - + {errorMessage && {errorMessage}} + {!errorMessage && ( + + + + + + )}
); }; diff --git a/src/ui/views/OpenPushRequests/components/PushesTable.tsx b/src/ui/views/OpenPushRequests/components/PushesTable.tsx index 4df866d2d..dee131002 100644 --- a/src/ui/views/OpenPushRequests/components/PushesTable.tsx +++ b/src/ui/views/OpenPushRequests/components/PushesTable.tsx @@ -30,8 +30,7 @@ const PushesTable: React.FC = (props) => { const [data, setData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); - const [errorMessage, setErrorMessage] = useState(''); + const [, setIsError] = useState(false); const navigate = useNavigate(); const [, setAuth] = useState(true); const [currentPage, setCurrentPage] = useState(1); @@ -47,7 +46,7 @@ const PushesTable: React.FC = (props) => { authorised: props.authorised ?? false, rejected: props.rejected ?? false, }; - getPushes(setIsLoading, setData, setAuth, setIsError, setErrorMessage, query); + getPushes(setIsLoading, setData, setAuth, setIsError, props.handleError, query); }, [props]); useEffect(() => { @@ -79,7 +78,6 @@ const PushesTable: React.FC = (props) => { const currentItems = filteredData.slice(indexOfFirstItem, indexOfLastItem); if (isLoading) return
Loading...
; - if (isError) return
{errorMessage}
; return (
diff --git a/src/ui/views/RepoDetails/Components/AddUser.tsx b/src/ui/views/RepoDetails/Components/AddUser.tsx index 4ed9df42c..549b3fb44 100644 --- a/src/ui/views/RepoDetails/Components/AddUser.tsx +++ b/src/ui/views/RepoDetails/Components/AddUser.tsx @@ -17,6 +17,7 @@ import { addUser } from '../../../services/repo'; import { getUsers } from '../../../services/user'; import { PersonAdd } from '@material-ui/icons'; import { UserData } from '../../../../types/models'; +import Danger from '../../../components/Typography/Danger'; interface AddUserDialogProps { repoId: string; @@ -37,7 +38,7 @@ const AddUserDialog: React.FC = ({ const [data, setData] = useState([]); const [, setAuth] = useState(true); const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); const [error, setError] = useState(''); const [tip, setTip] = useState(false); @@ -76,10 +77,10 @@ const AddUserDialog: React.FC = ({ }; useEffect(() => { - getUsers(setIsLoading, setData, setAuth, setIsError, setError, {}); + getUsers(setIsLoading, setData, setAuth, setErrorMessage, {}); }, []); - if (isError) return
Something went wrong ...
; + if (errorMessage) return {errorMessage}; return ( <> diff --git a/src/ui/views/RepoList/Components/Repositories.tsx b/src/ui/views/RepoList/Components/Repositories.tsx index 5c5d75ca3..36e1f181a 100644 --- a/src/ui/views/RepoList/Components/Repositories.tsx +++ b/src/ui/views/RepoList/Components/Repositories.tsx @@ -14,6 +14,7 @@ import { UserContext } from '../../../../context'; import Search from '../../../components/Search/Search'; import Pagination from '../../../components/Pagination/Pagination'; import Filtering, { FilterOption, SortOrder } from '../../../components/Filtering/Filtering'; +import Danger from '../../../components/Typography/Danger'; import { RepositoriesProps } from '../repositories.types'; interface GridContainerLayoutProps { @@ -127,7 +128,7 @@ export default function Repositories(props: RepositoriesProps): React.ReactEleme const paginatedData = filteredData.slice(startIdx, startIdx + itemsPerPage); if (isLoading) return
Loading...
; - if (isError) return
{errorMessage}
; + if (isError) return {errorMessage}; const addrepoButton = user.admin ? ( diff --git a/src/ui/views/Settings/Settings.jsx b/src/ui/views/Settings/Settings.jsx new file mode 100644 index 000000000..7649981cb --- /dev/null +++ b/src/ui/views/Settings/Settings.jsx @@ -0,0 +1,119 @@ +import React, { useState, useEffect } from 'react'; +import { + TextField, + IconButton, + InputAdornment, + FormLabel, + Snackbar, + Typography, +} from '@material-ui/core'; +import { Visibility, VisibilityOff, Save, Clear } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/core/styles'; + +import GridContainer from '../../components/Grid/GridContainer'; +import GridItem from '../../components/Grid/GridItem'; +import Card from '../../components/Card/Card'; +import CardBody from '../../components/Card/CardBody'; +import Button from '../../components/CustomButtons/Button'; + +const useStyles = makeStyles((theme) => ({ + root: { + '& .MuiTextField-root': { + margin: theme.spacing(1), + width: '100%', + }, + }, + buttonRow: { + display: 'flex', + justifyContent: 'flex-end', + marginTop: theme.spacing(2), + gap: theme.spacing(1), + }, +})); + +export default function SettingsView() { + const classes = useStyles(); + + const [jwtToken, setJwtToken] = useState(''); + const [showToken, setShowToken] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + const [snackbarOpen, setSnackbarOpen] = useState(false); + + useEffect(() => { + const savedToken = localStorage.getItem('ui_jwt_token'); + if (savedToken) setJwtToken(savedToken); + }, []); + + const handleSave = () => { + localStorage.setItem('ui_jwt_token', jwtToken); + setSnackbarMessage('JWT token saved'); + setSnackbarOpen(true); + }; + + const handleClear = () => { + setJwtToken(''); + localStorage.removeItem('ui_jwt_token'); + setSnackbarMessage('JWT token cleared'); + setSnackbarOpen(true); + }; + + const toggleShowToken = () => { + setShowToken(!showToken); + }; + + return ( +
+ + + + + {/* Title */} + JWT Token for UI Authentication + + Authenticates UI requests to the server when "apiAuthentication" is enabled in the config. + + setJwtToken(e.target.value)} + InputProps={{ + endAdornment: ( + + + {showToken ? : } + + + ), + style: { + marginTop: '10px', + marginLeft: '-8px', + marginRight: '8px', + }, + }} + /> +
+ + +
+
+
+
+
+ setSnackbarOpen(false)} + message={snackbarMessage} + /> + + ); +} diff --git a/src/ui/views/User/User.tsx b/src/ui/views/User/User.tsx index b5b51e9b4..517e09b4e 100644 --- a/src/ui/views/User/User.tsx +++ b/src/ui/views/User/User.tsx @@ -71,6 +71,7 @@ export default function UserProfile(): React.ReactElement { if (isLoading) return
Loading...
; if (isError) return
Something went wrong ...
; + if (!auth && window.location.pathname === '/dashboard/profile') { return ; } diff --git a/src/ui/views/UserList/Components/UserList.tsx b/src/ui/views/UserList/Components/UserList.tsx index b343efcd0..c38ed026d 100644 --- a/src/ui/views/UserList/Components/UserList.tsx +++ b/src/ui/views/UserList/Components/UserList.tsx @@ -16,6 +16,7 @@ import { getUsers } from '../../../services/user'; import Pagination from '../../../components/Pagination/Pagination'; import { CloseRounded, Check, KeyboardArrowRight } from '@material-ui/icons'; import Search from '../../../components/Search/Search'; +import Danger from '../../../components/Typography/Danger'; import { UserData } from '../../../../types/models'; interface UserListProps { @@ -29,7 +30,6 @@ const UserList: React.FC = (props) => { const [data, setData] = useState([]); const [, setAuth] = useState(true); const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); @@ -46,11 +46,11 @@ const UserList: React.FC = (props) => { if (!k) continue; query[k] = props[k]; } - getUsers(setIsLoading, setData, setAuth, setIsError, setErrorMessage, query); + getUsers(setIsLoading, setData, setAuth, setErrorMessage, query); }, [props]); if (isLoading) return
Loading...
; - if (isError) return
{errorMessage}
; + if (errorMessage) return {errorMessage}; const filteredUsers = data.filter( (user) => diff --git a/test/testJwtAuthHandler.test.js b/test/testJwtAuthHandler.test.js index af2bb1bb2..536d10d05 100644 --- a/test/testJwtAuthHandler.test.js +++ b/test/testJwtAuthHandler.test.js @@ -167,7 +167,7 @@ describe('jwtAuthHandler', () => { await jwtAuthHandler(jwtConfig)(req, res, next); expect(res.status.calledWith(500)).to.be.true; - expect(res.send.calledWith('OIDC authority URL is not configured\n')).to.be.true; + expect(res.send.calledWith({ message: 'JWT handler: authority URL is not configured\n' })).to.be.true; }); it('should return 500 if clientID not configured', async () => { @@ -178,7 +178,7 @@ describe('jwtAuthHandler', () => { await jwtAuthHandler(jwtConfig)(req, res, next); expect(res.status.calledWith(500)).to.be.true; - expect(res.send.calledWith('OIDC client ID is not configured\n')).to.be.true; + expect(res.send.calledWith({ message: 'JWT handler: client ID is not configured\n' })).to.be.true; }); it('should return 401 if JWT validation fails', async () => {