diff --git a/jams-react-client/package-lock.json b/jams-react-client/package-lock.json index 5700cf13ec525363addc56dfcb9485a9a2410aca..9baf29269b219a4c7c638b3635a45da646137121 100644 --- a/jams-react-client/package-lock.json +++ b/jams-react-client/package-lock.json @@ -4142,6 +4142,14 @@ "is-plain-object": "^2.0.1" } }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", @@ -4954,12 +4962,19 @@ } }, "dom-helpers": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", - "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", "requires": { "@babel/runtime": "^7.8.7", - "csstype": "^2.6.7" + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", + "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==" + } } }, "dom-serializer": { @@ -6740,6 +6755,11 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "generate-password": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.5.1.tgz", + "integrity": "sha512-XdsyfiF4mKoOEuzA44w9jSNav50zOurdWOV3V8DbA7SJIxR3Xm9ob14HKYTnMQOPX3ylqiJMnQF0wEa8gXZIMw==" + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -12568,6 +12588,15 @@ "prop-types": "^15.5.8" } }, + "react-copy-to-clipboard": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", + "integrity": "sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==", + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -15172,6 +15201,11 @@ "through2": "^2.0.3" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", diff --git a/jams-react-client/package.json b/jams-react-client/package.json index 21a6de972311ff8d5e0874f01ba0ced44f1ac4b6..2bac3e70f75e26781d3a0cf6b2183368071daf57 100644 --- a/jams-react-client/package.json +++ b/jams-react-client/package.json @@ -13,6 +13,7 @@ "classnames": "2.2.6", "formik": "^2.1.5", "fuctbase64": "^1.4.0", + "generate-password": "^1.5.1", "history": "4.10.1", "image-to-base64": "^2.1.1", "lodash": "^4.17.19", @@ -21,8 +22,9 @@ "package.json": "^2.0.1", "perfect-scrollbar": "1.5.0", "prop-types": "15.7.2", - "react": "16.13.1", + "react": "^16.13.1", "react-chartist": "0.14.3", + "react-copy-to-clipboard": "^5.0.2", "react-dom": "16.13.1", "react-easy-crop": "^3.1.1", "react-google-maps": "9.4.5", @@ -30,6 +32,7 @@ "react-router-dom": "5.2.0", "react-scripts": "^3.4.3", "react-swipeable-views": "0.13.9", + "react-transition-group": "^4.4.1", "yup": "^0.29.3", "yup-phone": "^1.2.3" }, diff --git a/jams-react-client/src/components/PasswordDialog/PasswordDialog.js b/jams-react-client/src/components/PasswordDialog/PasswordDialog.js new file mode 100644 index 0000000000000000000000000000000000000000..741ac49286470c5717c6913a630b14f151e10d11 --- /dev/null +++ b/jams-react-client/src/components/PasswordDialog/PasswordDialog.js @@ -0,0 +1,278 @@ +import React, { useEffect } from "react"; +// @material-ui/core components +import { makeStyles } from "@material-ui/core/styles"; +// core components + +import Button from "components/CustomButtons/Button.js"; + +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; + +import Grid from "@material-ui/core/Grid"; + +import InputAdornment from "@material-ui/core/InputAdornment"; + +import { Formik } from "formik"; +import FormikField from "components/FormikField/FormikField"; +import * as Yup from "yup"; + +import axios from "axios"; +import configApiCall from "api.js"; +import { api_path_put_update_user } from "globalUrls"; + +import VpnKeyIcon from "@material-ui/icons/VpnKey"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; +import VisibilityIcon from "@material-ui/icons/Visibility"; +import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; +import IconButton from "@material-ui/core/IconButton"; + +import { CopyToClipboard } from "react-copy-to-clipboard"; + +let generator = require("generate-password"); + +const styles = (theme) => ({ + root: { + flexGrow: 1, + }, + button: { + margin: theme.spacing(1), + }, + actionButtons: { + display: "flex", + justifyContent: "space-between", + }, +}); + +const useStyles = makeStyles(styles); + +export default function PasswordDialog(props) { + const classes = useStyles(); + + const [copied, setCopied] = React.useState(false); + const [generated, setGenerated] = React.useState(false); + const [passwordVisible, setPasswordVisible] = React.useState(false); + + const passwordGenerator = () => { + return generator.generate({ + length: 10, + uppercase: false, + numbers: true, + symbols: true, + }); + }; + + const changePassword = (values) => { + const data = { + username: props.username, + password: values.password, + }; + + axios(configApiCall(api_path_put_update_user, "PUT", data, null)) + .then(() => { + props.setChangePasswordOpen(false); + }) + .catch((error) => { + console.log( + "Updating user " + + props.username + + " password failed with error: " + + error + ); + }); + props.setChangePasswordOpen(false); + }; + + const passwordSchema = Yup.object().shape({ + password: Yup.string().required("Password is required"), + passwordConfirmation: Yup.string().oneOf( + [Yup.ref("password"), null], + "Passwords must match" + ), + }); + + const handleMouseDownPassword = () => { + setPasswordVisible(true); + }; + const handleMouseUpPassword = () => { + setPasswordVisible(false); + }; + + const getStatus = () => { + if (copied) + return ( + <span + style={{ + fontSize: "12px", + marginRight: "10px", + marginTop: "14px", + color: "rgba(0, 0, 0, 0.54)", + }} + > + Copied + </span> + ); + else if (generated) { + return ( + <span + style={{ + fontSize: "12px", + marginRight: "10px", + marginTop: "14px", + color: "rgba(0, 0, 0, 0.54)", + }} + > + Generated + </span> + ); + } else return ""; + }; + + React.useEffect(() => { + setCopied(false); + setGenerated(false); + }, []); + + return ( + <div> + <Dialog + open={props.changePasswordOpen} + onClose={props.handleClosechangePassword} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <Formik + initialValues={{ + password: "", + passwordConfirmation: "", + }} + validationSchema={passwordSchema} + onSubmit={changePassword} + > + {({ isValid, dirty, handleSubmit, values, setFieldValue }) => ( + <form onSubmit={handleSubmit}> + <DialogContent> + <DialogContentText id="change-password-description"> + Change <strong>{props.username}'s</strong> password{" "} + </DialogContentText> + <FormikField + name="password" + label="Password" + placeholder="Password" + type={passwordVisible ? "text" : "password"} + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <div style={{ display: "flex" }}> + {getStatus()} + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + </div> + } + /> + <FormikField + name="passwordConfirmation" + label="Confirm password" + placeholder="Confirm password" + type={passwordVisible ? "text" : "password"} + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + } + /> + </DialogContent> + <DialogActions> + <Grid> + <Grid item> + <Button + variant="contained" + color="primary" + size="large" + className={classes.button} + startIcon={<RefreshIcon />} + onClick={() => { + const newGeneratedPassword = passwordGenerator(); + setFieldValue("password", newGeneratedPassword, false); + setFieldValue( + "passwordConfirmation", + newGeneratedPassword, + false + ); + setCopied(false); + setGenerated(true); + }} + > + {"Generate"} + </Button> + <CopyToClipboard + text={values.password} + onCopy={() => { + setCopied(true); + setGenerated(false); + }} + > + <Button + variant="contained" + color="primary" + size="large" + className={classes.button} + startIcon={<FileCopyIcon />} + > + Copy to clipboard + </Button> + </CopyToClipboard> + </Grid> + <Grid item className={classes.actionButtons}> + <Button + onClick={props.handleClosechangePassword} + color="primary" + > + Cancel + </Button> + <Button + type="submit" + disable={!isValid && !dirty} + color="info" + autoFocus + > + Update password + </Button> + </Grid> + </Grid> + </DialogActions> + </form> + )} + </Formik> + </Dialog> + </div> + ); +} diff --git a/jams-react-client/src/layouts/Admin.js b/jams-react-client/src/layouts/Admin.js index 04b177d0ec9b62ed14aab9a812d1a830da071307..4bfb56dcafe98fa0c9b1bd3ddf9f0df690c904cb 100644 --- a/jams-react-client/src/layouts/Admin.js +++ b/jams-react-client/src/layouts/Admin.js @@ -9,7 +9,7 @@ import { makeStyles } from "@material-ui/core/styles"; import Navbar from "components/Navbars/Navbar.js"; import Footer from "components/Footer/Footer.js"; import Sidebar from "components/Sidebar/Sidebar.js"; -import Snackbar from '@material-ui/core/Snackbar'; +import Snackbar from "@material-ui/core/Snackbar"; import routes from "routes.js"; @@ -18,24 +18,27 @@ import styles from "assets/jss/material-dashboard-react/layouts/adminStyle.js"; import bgImage from "assets/img/sidebar-2.jpg"; import logo from "assets/img/logo-jams.svg"; -import auth from 'auth' +import auth from "auth"; import configApiCall from "api.js"; -import { api_path_get_start_update, api_path_get_subscription_status } from "globalUrls"; +import { + api_path_get_start_update, + api_path_get_subscription_status, +} from "globalUrls"; import axios from "axios"; import ReactDOM from "react-dom"; -import {ConfiguredRoute} from "../configured.route"; +import { ConfiguredRoute } from "../configured.route"; import SignIn from "./SignIn"; -import {ProtectedRoute} from "../protected.route"; +import { ProtectedRoute } from "../protected.route"; import Dialog from "@material-ui/core/Dialog/Dialog"; import DialogTitle from "@material-ui/core/DialogTitle/DialogTitle"; -import {Field, Formik} from "formik"; +import { Field, Formik } from "formik"; import DialogContent from "@material-ui/core/DialogContent/DialogContent"; import TextField from "@material-ui/core/TextField/TextField"; import DialogActions from "@material-ui/core/DialogActions/DialogActions"; import DialogContentText from "@material-ui/core/DialogContentText/DialogContentText"; -import Button from '@material-ui/core/Button'; +import Button from "@material-ui/core/Button"; let ps; @@ -58,7 +61,6 @@ const switchRoutes = ( <Redirect from="/admin/g" to="/admin/groups" /> <Redirect from="/admin/b" to="/admin/blueprints" /> <Redirect from="/admin/s" to="/admin/settings" /> - </Switch> ); @@ -75,23 +77,23 @@ export default function Admin({ ...rest }) { const [fixedClasses, setFixedClasses] = React.useState("dropdown"); const [mobileOpen, setMobileOpen] = React.useState(false); const [open, setOpen] = React.useState(false); - const [message, setMessage] = React.useState(false) + const [message, setMessage] = React.useState(false); const [signedUp, setSignedUp] = React.useState(false); const [signedIn, setSignedIn] = React.useState(false); const [token, setToken] = React.useState(false); - const [severity, setSeverity] = React.useState("success") - const [openUpdate, setOpenUpdate] = React.useState(false) - const [dialogMessage, setDialogMessage] = React.useState("") - const [messageYes, setMessageYes] = React.useState("") - const [messageNo, setMessageNo] = React.useState("") + const [severity, setSeverity] = React.useState("success"); + const [openUpdate, setOpenUpdate] = React.useState(false); + const [dialogMessage, setDialogMessage] = React.useState(""); + const [messageYes, setMessageYes] = React.useState(""); + const [messageNo, setMessageNo] = React.useState(""); const [updating, setUpdating] = React.useState(false); const [query, setQuery] = React.useState(false); const [snackbarMessage, setSnackbarMessage] = React.useState(""); - const handleImageClick = image => { + const handleImageClick = (image) => { setImage(image); }; - const handleColorClick = color => { + const handleColorClick = (color) => { setColor(color); }; const handleFixedClick = () => { @@ -116,32 +118,35 @@ export default function Admin({ ...rest }) { const handleQuery = () => { setQuery(true); if (auth.isActivated()) { - setDialogMessage("A new version of JAMS is available. Would you like to update now?") - setMessageYes("Update Now") - setMessageNo("Update Later") + setDialogMessage( + "A new version of JAMS is available. Would you like to update now?" + ); + setMessageYes("Update Now"); + setMessageNo("Update Later"); } else { - setDialogMessage("You are currently running the community version of JAMS. Would you like to purchase a JAMS subscription?") - setMessageYes("Yes, go to Jami Store") - setMessageNo("No thanks") + setDialogMessage( + "You are currently running the community version of JAMS. Would you like to purchase a JAMS subscription?" + ); + setMessageYes("Yes, go to Jami Store"); + setMessageNo("No thanks"); } - } + }; // initialize and destroy the PerfectScrollbar plugin React.useEffect(() => { - auth.checkForUpdates(() => { - auth.getUpdates(() => { - if(auth.isUpdateAvailable()) { - setOpen(true) - setMessage("An update is available for JAMS.") - } - }); + auth.getUpdates(() => { + if (auth.isUpdateAvailable()) { + setOpen(true); + setMessage("An update is available for JAMS."); + } + }); }); if (navigator.platform.indexOf("Win") > -1) { ps = new PerfectScrollbar(mainPanel.current, { suppressScrollX: true, - suppressScrollY: false + suppressScrollY: false, }); document.body.style.overflow = "hidden"; } @@ -155,46 +160,51 @@ export default function Admin({ ...rest }) { }; }, [openUpdate, mainPanel]); - - const handleCancel = () => { setOpenUpdate(false); - } + }; const handleUpdate = () => { - setQuery(false) - if (auth.isActivated()) { - setSnackbarMessage("Updating JAMS, shutting down shortly...") - axios(configApiCall(api_path_get_start_update, 'POST', null, null)).then(() => { - handleCancel() - setUpdating(true) - }).catch((error) => { - setSnackbarMessage("Error while attempting to update JAMS: " + error) - }) - } - } + setQuery(false); + if (auth.isActivated()) { + setSnackbarMessage("Updating JAMS, shutting down shortly..."); + axios(configApiCall(api_path_get_start_update, "POST", null, null)) + .then(() => { + handleCancel(); + setUpdating(true); + }) + .catch((error) => { + setSnackbarMessage("Error while attempting to update JAMS: " + error); + }); + } + }; return ( <div className={classes.wrapper}> - <Dialog open={openUpdate} onClose={handleCancel} aria-labelledby="form-dialog-title"> + <Dialog + open={openUpdate} + onClose={handleCancel} + aria-labelledby="form-dialog-title" + > <DialogTitle id="form-revoke-dialog-title">Jams Update</DialogTitle> <DialogContent> <DialogContentText id="alert-dialog-description"> - {dialogMessage} + {dialogMessage} </DialogContentText> </DialogContent> <DialogActions> - <Button onClick={e => { - if (auth.isActivated()) - handleUpdate() - else - window.location.href ='https://staging.jami.biz/' - }} color="primary"> + <Button + onClick={(e) => { + if (auth.isActivated()) handleUpdate(); + else window.location.href = "https://staging.jami.biz/"; + }} + color="primary" + > {messageYes} </Button> <Button onClick={handleCancel} color="primary"> {messageNo} - </Button> + </Button> </DialogActions> </Dialog> <Sidebar @@ -224,8 +234,8 @@ export default function Admin({ ...rest }) { <div className={classes.container}>{switchRoutes}</div> </div> ) : ( - <div className={classes.map}>{switchRoutes}</div> - )} + <div className={classes.map}>{switchRoutes}</div> + )} {getRoute() ? <Footer /> : null} {/* <FixedPlugin handleImageClick={handleImageClick} @@ -238,5 +248,4 @@ export default function Admin({ ...rest }) { </div> </div> ); - } diff --git a/jams-react-client/src/views/Settings/UpdatePassword.js b/jams-react-client/src/views/Settings/UpdatePassword.js index b950b7d7029d41a79116ec5b6692ea5c35c2e371..52de3b057fd055a6a9641dd7bf2b132560768f6d 100644 --- a/jams-react-client/src/views/Settings/UpdatePassword.js +++ b/jams-react-client/src/views/Settings/UpdatePassword.js @@ -1,129 +1,251 @@ -import React from 'react'; +import React from "react"; import { useHistory } from "react-router-dom"; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import Button from '@material-ui/core/Button'; -import TextField from '@material-ui/core/TextField'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { Formik } from "formik"; +import FormikField from "components/FormikField/FormikField"; +import * as Yup from "yup"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import Grid from "@material-ui/core/Grid"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; -import axios from 'axios'; -import configApiCall from '../../api' -import { api_path_put_update_user } from '../../globalUrls' +import RefreshIcon from "@material-ui/icons/Refresh"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; +import VisibilityIcon from "@material-ui/icons/Visibility"; +import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; +import IconButton from "@material-ui/core/IconButton"; +import VpnKeyIcon from "@material-ui/icons/VpnKey"; +import InputAdornment from "@material-ui/core/InputAdornment"; -import auth from '../../auth' +import axios from "axios"; +import configApiCall from "../../api"; +import { api_path_put_update_user } from "../../globalUrls"; + +import { CopyToClipboard } from "react-copy-to-clipboard"; + +let generator = require("generate-password"); const useStyles = makeStyles((theme) => ({ - paper: { - marginTop: theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - form: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(1), - }, - submit: { - margin: theme.spacing(3, 0, 2), - }, + paper: { + marginTop: theme.spacing(8), + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: "100%", // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, + button: { + margin: theme.spacing(1), + }, })); export default function UpdatePassword(props) { - const classes = useStyles(); - const history = useHistory(); - - const changePassword = (values) => { - axios(configApiCall(api_path_put_update_user+'?username='+props.username+'&password='+values.password, 'PUT', null, null)).then(()=>{ - props.setSeverity("success"); - props.setErrorMessage("Admin password updated successfully!"); - props.setError(true); - }).catch((error)=> { - props.setSeverity("error"); - props.setErrorMessage("Updating user " + props.username + ' password failed with error: ' + error); - props.setError(true); - }); - } - - /** - * Formik Validation Fields - */ - const formik = useFormik({ - initialValues: { - password: '', - confirmPassword: '', - - }, - validationSchema: Yup.object({ - password: Yup.string() - .max(50, 'Must be 50 characters or less') - .required('Password is required'), - confirmPassword: Yup.string() - .oneOf([Yup.ref('password'), null], 'Passwords must match') - .max(50, 'Must be 50 characters or less') - .required('Password confirmation is required'), - }), - - onSubmit: values => { - changePassword(values); - }, + const classes = useStyles(); + const history = useHistory(); + + const [copied, setCopied] = React.useState(false); + const [generated, setGenerated] = React.useState(false); + const [passwordVisible, setPasswordVisible] = React.useState(false); + + const changePassword = (values) => { + const data = { + username: props.username, + password: values.password, + }; + + axios(configApiCall(api_path_put_update_user, "PUT", data, null)) + .then(() => { + props.setSeverity("success"); + props.setErrorMessage("Admin password updated successfully!"); + props.setError(true); + }) + .catch((error) => { + props.setSeverity("error"); + props.setErrorMessage( + "Updating user " + + props.username + + " password failed with error: " + + error + ); + props.setError(true); + }); + }; + + const passwordGenerator = () => { + return generator.generate({ + length: 10, + uppercase: false, + numbers: true, + symbols: true, }); + }; - return ( - <form className={classes.form} onSubmit={formik.handleSubmit}> - <Typography variant="h5" gutterBottom color="primary">Enter the following information below to change your admin password.</Typography> - <Grid container spacing={3}> - <Grid item xs={6}> - <TextField - variant="outlined" - margin="normal" - required - fullWidth - name="password" - label="Password" - type="password" - id="password" - autoComplete="new-password" - {...formik.getFieldProps('password')} - /> - {formik.touched.password && formik.errors.password ? (<span>{formik.errors.password}</span>) : null} - - </Grid> - - <Grid item xs={6}> - - <TextField - variant="outlined" - margin="normal" - required - fullWidth - name="confirmPassword" - label="Confirm password" - type="password" - id="confirmPassword" - autoComplete="new-password" - {...formik.getFieldProps('confirmPassword')} - /> - {formik.touched.confirmPassword && formik.errors.confirmPassword ? (<span>{formik.errors.confirmPassword}</span>) : null} - - </Grid> + /** + * Formik Validation Fields + */ + + const passwordSchema = Yup.object().shape({ + password: Yup.string().required("Password is required"), + passwordConfirmation: Yup.string().oneOf( + [Yup.ref("password"), null], + "Passwords must match" + ), + }); + + const handleMouseDownPassword = () => { + setPasswordVisible(true); + }; + const handleMouseUpPassword = () => { + setPasswordVisible(false); + }; + + return ( + <Formik + initialValues={{ + password: "", + confirmPassword: "", + }} + validationSchema={passwordSchema} + onSubmit={changePassword} + > + {({ + isValid, + dirty, + handleSubmit, + values, + setFieldValue, + touched, + errors, + }) => ( + <form onSubmit={handleSubmit} className={classes.form}> + <Typography variant="h5" gutterBottom color="primary"> + Enter the following information below to change your admin password. + </Typography> + <Grid container spacing={3}> + <Grid item xs={6}> + <FormikField + name="password" + label="Password" + placeholder="Password" + type={passwordVisible ? "text" : "password"} + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + } + /> + {touched.password && errors.password ? ( + <span>{errors.password}</span> + ) : null} </Grid> - <Button - type="submit" - fullWidth + <Grid item xs={6}> + <FormikField + name="confirmPassword" + label="Confirm password" + placeholder="Confirm password" + type={passwordVisible ? "text" : "password"} + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + } + /> + {touched.confirmPassword && errors.confirmPassword ? ( + <span>{errors.confirmPassword}</span> + ) : null} + </Grid> + + <Grid item align="left" xs={12} sm={12} md={6}> + <Button variant="contained" color="primary" - className={classes.submit} - > - Change Admin Password - </Button> + size="large" + className={classes.button} + startIcon={<RefreshIcon />} + onClick={() => { + const newGeneratedPassword = passwordGenerator(); + setFieldValue("password", newGeneratedPassword, false); + setFieldValue("confirmPassword", newGeneratedPassword, false); + setCopied(false); + setGenerated(true); + }} + > + {"Generate"} + </Button> + <CopyToClipboard + text={values.password} + onCopy={() => { + setCopied(true); + setGenerated(false); + }} + > + <Button + variant="contained" + color="primary" + size="large" + className={classes.button} + startIcon={<FileCopyIcon />} + > + Copy to clipboard + </Button> + </CopyToClipboard> + {copied ? ( + <span style={{ marginLeft: "10px" }}>Copied</span> + ) : null} + {generated ? ( + <span style={{ marginLeft: "10px" }}>Generated</span> + ) : null} + </Grid> + </Grid> + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + disable={!isValid && !dirty} + className={classes.submit} + > + Change Admin Password + </Button> </form> - ); -} \ No newline at end of file + )} + </Formik> + ); +} diff --git a/jams-react-client/src/views/UserProfile/DisplayUserProfile.js b/jams-react-client/src/views/UserProfile/DisplayUserProfile.js index 5569fd3c68665e0498f7498f92b893e52e6852db..c918bdf6f7d98567e4d75c7243e31c16f89fd9f2 100644 --- a/jams-react-client/src/views/UserProfile/DisplayUserProfile.js +++ b/jams-react-client/src/views/UserProfile/DisplayUserProfile.js @@ -1,4 +1,4 @@ -import React, {useEffect} from "react"; +import React, { useEffect } from "react"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; // core components @@ -11,41 +11,41 @@ import CardBody from "components/Card/CardBody.js"; import CardFooter from "components/Card/CardFooter.js"; import noProfilePicture from "assets/img/faces/no-profile-picture.png"; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import EditIcon from '@material-ui/icons/Edit'; -import DeleteIcon from '@material-ui/icons/Delete'; - -import Grid from '@material-ui/core/Grid'; -import BusinessCenterOutlinedIcon from '@material-ui/icons/BusinessCenterOutlined'; -import AlternateEmailOutlinedIcon from '@material-ui/icons/AlternateEmailOutlined'; -import PhoneInTalkOutlinedIcon from '@material-ui/icons/PhoneInTalkOutlined'; -import SmartphoneOutlinedIcon from '@material-ui/icons/SmartphoneOutlined'; -import LocalPrintshopOutlinedIcon from '@material-ui/icons/LocalPrintshopOutlined'; -import PhoneForwardedOutlinedIcon from '@material-ui/icons/PhoneForwardedOutlined'; -import PersonOutlinedIcon from '@material-ui/icons/PersonOutlined'; -import PermIdentityOutlinedIcon from '@material-ui/icons/PermIdentityOutlined'; - -import Avatar from '@material-ui/core/Avatar'; -import Chip from '@material-ui/core/Chip'; -import DoneIcon from '@material-ui/icons/Done'; -import CancelIcon from '@material-ui/icons/Cancel'; - -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemText from '@material-ui/core/ListItemText'; - -import auth from "auth.js" +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import EditIcon from "@material-ui/icons/Edit"; +import DeleteIcon from "@material-ui/icons/Delete"; + +import Grid from "@material-ui/core/Grid"; +import BusinessCenterOutlinedIcon from "@material-ui/icons/BusinessCenterOutlined"; +import AlternateEmailOutlinedIcon from "@material-ui/icons/AlternateEmailOutlined"; +import PhoneInTalkOutlinedIcon from "@material-ui/icons/PhoneInTalkOutlined"; +import SmartphoneOutlinedIcon from "@material-ui/icons/SmartphoneOutlined"; +import LocalPrintshopOutlinedIcon from "@material-ui/icons/LocalPrintshopOutlined"; +import PhoneForwardedOutlinedIcon from "@material-ui/icons/PhoneForwardedOutlined"; +import PersonOutlinedIcon from "@material-ui/icons/PersonOutlined"; +import PermIdentityOutlinedIcon from "@material-ui/icons/PermIdentityOutlined"; + +import Avatar from "@material-ui/core/Avatar"; +import Chip from "@material-ui/core/Chip"; +import DoneIcon from "@material-ui/icons/Done"; + +import List from "@material-ui/core/List"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemAvatar from "@material-ui/core/ListItemAvatar"; +import ListItemText from "@material-ui/core/ListItemText"; + +import auth from "auth.js"; import configApiCall from "api.js"; -import { api_path_get_admin_user, - api_path_get_auth_user, - api_path_get_user_directory_search, - api_path_delete_admin_user_revoke, - api_path_put_update_user} from "globalUrls"; +import { + api_path_get_admin_user, + api_path_get_auth_user, + api_path_get_user_directory_search, + api_path_delete_admin_user_revoke, +} from "globalUrls"; import dashboardStyle from "assets/jss/material-dashboard-react/views/dashboardStyle.js"; @@ -53,445 +53,425 @@ import { hexToRgb, blackColor } from "assets/jss/material-dashboard-react.js"; import axios from "axios"; import PersonIcon from "../../../node_modules/@material-ui/icons/Person"; -import Input from '@material-ui/core/Input'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import Visibility from '@material-ui/icons/Visibility'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; - -import VpnKeyIcon from '@material-ui/icons/VpnKey'; -import IconButton from '@material-ui/core/IconButton'; - -import {Formik} from 'formik'; -import FormikField from 'components/FormikField/FormikField'; -import * as Yup from 'yup'; -import CardAvatar from "components/Card/CardAvatar"; - -const styles = (theme)=> ( { - ...dashboardStyle, - root: { - flexGrow: 1 - }, - cardCategoryWhite: { - color: "rgba(255,255,255,.62)", - margin: "0", - fontSize: "14px", - marginTop: "0", - marginBottom: "0" - }, - cardTitleWhite: { - color: "#FFFFFF", - marginTop: "0px", - minHeight: "auto", - fontWeight: "300", - fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", - marginBottom: "3px", - textDecoration: "none" - }, - input: { - display: 'none', - }, - profileAsBackground: { - backgroundSize: "100% 100%", - width: "80px", - height: "80px", - }, - centerIconMiddle: { - position:"relative", - top: "20px", - left: "15px" - }, - editProfilePicture: { - borderRadius: "50%", - width: "200px", - height: "200px", - boxShadow: - "0 6px 8px -12px rgba(" + - hexToRgb(blackColor) + - ", 0.56), 0 4px 25px 0px rgba(" + - hexToRgb(blackColor) + - ", 0.12), 0 8px 10px -5px rgba(" + - hexToRgb(blackColor) + - ", 0.2)", - }, - dialogPaper: { - minHeight: '60vh', - maxHeight: '60vh', - minWidth: '80vh', - maxWidth: '80vh' - } - - // controls: { - // padding: 16, - // display: 'flex', - // flexDirection: 'column', - // alignItems: 'stretch', - // [theme.breakpoints.up('sm')]: { - // flexDirection: 'row', - // alignItems: 'center', - // },passwordSchematainer: { - // display: 'flex', - // flex: '1', - // alignItems: 'center', - // }, - // sliderLabel: { - // [theme.breakpoints.down('xs')]: { - // minWidth: 65, - // }, - // }, - // slider: { - // padding: '22px 0px', - // marginLeft: 16, - // [theme.breakpoints.up('sm')]: { - // flexDirection: 'row', - // alignItems: 'center', - // margin: '0 16px', - // }, - // }, +import VpnKeyIcon from "@material-ui/icons/VpnKey"; + +import PasswordDialog from "components/PasswordDialog/PasswordDialog"; + +const styles = (theme) => ({ + ...dashboardStyle, + root: { + flexGrow: 1, + }, + cardCategoryWhite: { + color: "rgba(255,255,255,.62)", + margin: "0", + fontSize: "14px", + marginTop: "0", + marginBottom: "0", + }, + cardTitleWhite: { + color: "#FFFFFF", + marginTop: "0px", + minHeight: "auto", + fontWeight: "300", + fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", + marginBottom: "3px", + textDecoration: "none", + }, + input: { + display: "none", + }, + profileAsBackground: { + backgroundSize: "100% 100%", + width: "80px", + height: "80px", + }, + centerIconMiddle: { + position: "relative", + top: "20px", + left: "15px", + }, + editProfilePicture: { + borderRadius: "50%", + width: "200px", + height: "200px", + boxShadow: + "0 6px 8px -12px rgba(" + + hexToRgb(blackColor) + + ", 0.56), 0 4px 25px 0px rgba(" + + hexToRgb(blackColor) + + ", 0.12), 0 8px 10px -5px rgba(" + + hexToRgb(blackColor) + + ", 0.2)", + }, + dialogPaper: { + minHeight: "60vh", + maxHeight: "60vh", + minWidth: "80vh", + maxWidth: "80vh", + }, + + // controls: { + // padding: 16, + // display: 'flex', + // flexDirection: 'column', + // alignItems: 'stretch', + // [theme.breakpoints.up('sm')]: { + // flexDirection: 'row', + // alignItems: 'center', + // },passwordSchematainer: { + // display: 'flex', + // flex: '1', + // alignItems: 'center', + // }, + // sliderLabel: { + // [theme.breakpoints.down('xs')]: { + // minWidth: 65, + // }, + // }, + // slider: { + // padding: '22px 0px', + // marginLeft: 16, + // [theme.breakpoints.up('sm')]: { + // flexDirection: 'row', + // alignItems: 'center', + // margin: '0 16px', + // }, + // }, }); const useStyles = makeStyles(styles); export default function DisplayUserProfile(props) { - const classes = useStyles(); - - const [users, setUsers] = React.useState([]) - const [userStatus, setUserStatus] = React.useState(false) - const [open, setOpen] = React.useState(false); - const [revokedUser, setRevokedUser] = React.useState(""); - const [changePasswordOpen, setChangePasswordOpen] = React.useState(false) - const [currentPassword, setCurrentPassword] = React.useState(""); - const [visibilityValues, setVisibilityValues] = React.useState({ - showPassword: false - }); - - const searchData = { - "queryString":props.username - }; - - const userData = { - "username":props.username - }; - - useEffect(() => { - auth.checkDirectoryType(() => { - if(auth.hasAdminScope()){ - axios(configApiCall(api_path_get_admin_user, 'GET', userData, null)).then((response)=>{ - const result = JSON.parse(response.data.replace(/\s+/g, ' ').trim()); - setUserStatus(result.revoked) - setCurrentPassword(result.password) - } - ).catch((error) =>{ - console.log(error); - }); - } - else{ - axios(configApiCall(api_path_get_auth_user, 'GET', userData, null)).then((response)=>{ - const result = JSON.parse(response.data.replace(/\s+/g, ' ').trim()); - setUserStatus(result.revoked) - } - ).catch((error) =>{ - console.log(error); - }); - } - }) - axios(configApiCall(api_path_get_user_directory_search, 'GET', searchData, null)).then((response)=>{ - setUsers(response.data) - } - ).catch((error) =>{ + const classes = useStyles(); + + const [users, setUsers] = React.useState([]); + const [userStatus, setUserStatus] = React.useState(false); + const [open, setOpen] = React.useState(false); + const [revokedUser, setRevokedUser] = React.useState(""); + const [changePasswordOpen, setChangePasswordOpen] = React.useState(false); + + const searchData = { + queryString: props.username, + }; + + const userData = { + username: props.username, + }; + + useEffect(() => { + auth.checkDirectoryType(() => { + if (auth.hasAdminScope()) { + axios(configApiCall(api_path_get_admin_user, "GET", userData, null)) + .then((response) => { + const result = JSON.parse( + response.data.replace(/\s+/g, " ").trim() + ); + setUserStatus(result.revoked); + }) + .catch((error) => { console.log(error); - }); - }, []); - - const getUserStatus = (user) => { - if(userStatus === false) { - return <Chip style={{ flex: 1 }} label="Active" variant="outlined" clickable={false} /> - }else{ - return <Chip style={{ flex: 1 }} label="Revoked" variant="outlined" clickable={false} disabled/> - } - } - - function revokeUser(){ - - const data = { - 'username': revokedUser - } - - axios(configApiCall(api_path_delete_admin_user_revoke, 'DELETE', data, null)).then(()=>{ - setUserStatus(true) - }).catch((error)=> { - console.log("Error revoking user: " + revokedUser + ' with error: ' + error); - }); - setOpen(false); - } - - const handleClickOpen = (username) => { - setRevokedUser(username) - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - const handleClosechangePassword = () => { - setChangePasswordOpen(false); - } - - const changePassword = (values) => { - axios(configApiCall(api_path_put_update_user+'?username='+props.username+'&password='+values.password, 'PUT', null, null)).then(()=>{ - setChangePasswordOpen(false); - setCurrentPassword(values.password); - }).catch((error)=> { - console.log("Updating user " + props.username + ' password failed with error: ' + error); - }); - setChangePasswordOpen(false); - } - - const passwordSchema = Yup.object().shape({ - password: Yup.string().required('Password is required'), - passwordConfirmation: Yup.string() - .oneOf([Yup.ref('password'), null], 'Passwords must match') + }); + } else { + axios(configApiCall(api_path_get_auth_user, "GET", userData, null)) + .then((response) => { + const result = JSON.parse( + response.data.replace(/\s+/g, " ").trim() + ); + setUserStatus(result.revoked); + }) + .catch((error) => { + console.log(error); + }); + } }); + axios( + configApiCall(api_path_get_user_directory_search, "GET", searchData, null) + ) + .then((response) => { + setUsers(response.data); + }) + .catch((error) => { + console.log(error); + }); + }, []); + + const getUserStatus = (user) => { + if (userStatus === false) { + return ( + <Chip + label="Active" + avatar={ + <Avatar + alt={user.username} + src={ + user.profilePicture + ? "data:image/png;base64, " + user.profilePicture + : noProfilePicture + } + /> + } + color="primary" + deleteIcon={<DoneIcon />} + /> + ); + } else { + return ( + <Chip + label="Revoked" + avatar={ + <Avatar + alt={user.username} + src={ + user.profilePicture + ? "data:image/png;base64, " + user.profilePicture + : noProfilePicture + } + /> + } + deleteIcon={<DoneIcon />} + /> + ); + } + }; - const handleClickShowPassword = () => { - setVisibilityValues({ ...visibilityValues, showPassword: !visibilityValues.showPassword }); - }; - - const handleMouseDownPassword = (event) => { - event.preventDefault(); + function revokeUser() { + const data = { + username: revokedUser, }; - return ( - <div> - <Dialog - open={open} - onClose={handleClose} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - <DialogTitle id="alert-dialog-title">{"Revoke user account"}</DialogTitle> - <DialogContent> - <DialogContentText id="alert-dialog-description"> - Are you sure you want to revoke <strong>{revokedUser}</strong> ? - </DialogContentText> - </DialogContent> - <DialogActions> - <Button onClick={handleClose} color="primary"> - Cancel - </Button> - <Button onClick={revokeUser} color="info" autoFocus> - Revoke - </Button> - </DialogActions> - </Dialog> - <Dialog - open={changePasswordOpen} - onClose={handleClosechangePassword} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - <DialogTitle id="change-password-dialog">{"Change password"}</DialogTitle> - <Formik initialValues={{ - password: "", - passwordConfirmation: "" - }} - validationSchema={passwordSchema} - onSubmit={changePassword} - > - {({ isValid, dirty, handleSubmit}) => ( - <form onSubmit={handleSubmit}> - <DialogContent> - <DialogContentText id="change-password-description"> - Password change for <strong>{props.username}</strong> - </DialogContentText> - <FormikField - name="password" - label="Password" - placeholder="Password" - type="password" - startAdornment={ - <InputAdornment position="start"> - <VpnKeyIcon /> - </InputAdornment> - } - /> - <FormikField - name="passwordConfirmation" - label="Confirm password" - placeholder="Confirm password" - type="password" - startAdornment={ - <InputAdornment position="start"> - <VpnKeyIcon /> - </InputAdornment> - } - /> - </DialogContent> - <DialogActions> - <Button onClick={handleClosechangePassword} color="primary"> - Cancel - </Button> - <Button type="submit" disable={!isValid && !dirty} color="info" autoFocus> - Update password - </Button> - </DialogActions> - </form> - )} - </Formik> - </Dialog> - {users.map(user => - <GridContainer> - <Grid item xs={12} sm={12} md={8}> - <Card profile> - <CardBody profile> - <div className={classes.root}> - <Grid container spacing={2}> - <Grid item xs={12} sm={12} md={6}> - <CardAvatar displayProfile> - <img src={user.profilePicture ? ('data:image/png;base64, ' + user.profilePicture) : noProfilePicture} className={classes.editProfilePicture} alt="..." /> - </CardAvatar> - </Grid> - <Grid item xs={12} sm={12} md={6}> - <h3 className={classes.cardTitle} >{user.username ? user.username : 'no username'}</h3> - {getUserStatus(user)} - </Grid> - <Grid item xs={12} sm={12} md={6}> - <List dense={false}> - <ListItem> - <ListItemAvatar> - <Avatar> - <PersonIcon /> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.firstName ? user.firstName : 'no firstname'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <PhoneInTalkOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.phoneNumber ? user.phoneNumber : 'no phone number'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <AlternateEmailOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.email ? user.email : 'no email'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <BusinessCenterOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.organization ? user.organization : 'no organization'} - /> - </ListItem> - </List> - </Grid> - - <Grid item xs={12} sm={12} md={6}> - <List dense={false}> - <ListItem> - <ListItemAvatar> - <Avatar> - <PersonOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.lastName ? user.lastName : 'no lastname'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <PhoneForwardedOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.phoneNumberExtension ? user.phoneNumberExtension : 'no Extension'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <SmartphoneOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.mobileNumber ? user.mobileNumber : 'no mobile number'} - /> - </ListItem> - <ListItem> - <ListItemAvatar> - <Avatar> - <LocalPrintshopOutlinedIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - primary={user.faxNumber ? user.faxNumber : 'no fax number'} - /> - </ListItem> - {auth.isLocalDirectory() && auth.hasAdminScope() && <ListItem> - <ListItemAvatar> - <Avatar> - <VpnKeyIcon/> - </Avatar> - </ListItemAvatar> - <ListItemText - children={ - <Input - disabled - id="current-password" - type={visibilityValues.showPassword ? 'text' : 'password'} - value={currentPassword} - endAdornment={ - <InputAdornment position="end"> - <IconButton - aria-label="toggle password visibility" - onClick={handleClickShowPassword} - onMouseDown={handleMouseDownPassword} - > - {visibilityValues.showPassword ? <Visibility /> : <VisibilityOff />} - </IconButton> - </InputAdornment> - } - /> - } - /> - </ListItem>} - </List> - </Grid> - </Grid> - </div> - </CardBody> - <CardFooter> - {auth.isLocalDirectory() && <Button color="info" onClick={() => props.setDisplayUser(false)}> - <EditIcon /> Edit Profile - </Button>} - {auth.isLocalDirectory() && auth.hasAdminScope() && <Button color="info" onClick={() => {setChangePasswordOpen(true);}}> - <VpnKeyIcon /> Change password - </Button>} - {auth.hasAdminScope() && (userStatus === "Active" || userStatus === '') && - <Button color="info" onClick={() => handleClickOpen(user.username)} > - <DeleteIcon fontSize="small"/> Revoke user - </Button>} - </CardFooter> - </Card> - </Grid> - - </GridContainer> - )} - </div> - ); -} \ No newline at end of file + axios( + configApiCall(api_path_delete_admin_user_revoke, "DELETE", data, null) + ) + .then(() => { + setUserStatus(true); + }) + .catch((error) => { + console.log( + "Error revoking user: " + revokedUser + " with error: " + error + ); + }); + setOpen(false); + } + + const handleClickOpen = (username) => { + setRevokedUser(username); + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleClosechangePassword = () => { + setChangePasswordOpen(false); + }; + + return ( + <div> + <Dialog + open={open} + onClose={handleClose} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + <DialogTitle id="alert-dialog-title"> + {"Revoke user account"} + </DialogTitle> + <DialogContent> + <DialogContentText id="alert-dialog-description"> + Are you sure you want to revoke <strong>{revokedUser}</strong> ? + </DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={handleClose} color="primary"> + Cancel + </Button> + <Button onClick={revokeUser} color="info" autoFocus> + Revoke + </Button> + </DialogActions> + </Dialog> + <PasswordDialog + username={props.username} + changePasswordOpen={changePasswordOpen} + setChangePasswordOpen={setChangePasswordOpen} + handleClosechangePassword={handleClosechangePassword} + /> + {users.map((user) => ( + <GridContainer> + <Grid item xs={12} sm={12} md={8}> + <Card profile> + <CardHeader color="info" stats icon> + <CardIcon color="info"> + <PermIdentityOutlinedIcon /> + </CardIcon> + <p className={classes.cardCategory}>{"Profile information"}</p> + <h3 className={classes.cardTitle}>{user.username}</h3> + {getUserStatus(user)} + </CardHeader> + <CardBody profile> + <div className={classes.root}> + <Grid container spacing={2}> + <Grid item align="center" xs={12} sm={12} md={12}> + <img + src={ + user.profilePicture + ? "data:image/png;base64, " + user.profilePicture + : noProfilePicture + } + className={classes.editProfilePicture} + alt="..." + /> + <h4>{user.username ? user.username : "no username"}</h4> + </Grid> + <Grid item xs={12} sm={12} md={6}> + <List dense={false}> + <ListItem> + <ListItemAvatar> + <Avatar> + <PersonIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.firstName ? user.firstName : "no firstname" + } + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <PersonOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.lastName ? user.lastName : "no lastname" + } + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <AlternateEmailOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={user.email ? user.email : "no email"} + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <BusinessCenterOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.organization + ? user.organization + : "no organization" + } + /> + </ListItem> + </List> + </Grid> + + <Grid item xs={12} sm={12} md={6}> + <List dense={false}> + <ListItem> + <ListItemAvatar> + <Avatar> + <PhoneInTalkOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.phoneNumber + ? user.phoneNumber + : "no phone number" + } + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <PhoneForwardedOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.phoneNumberExtension + ? user.phoneNumberExtension + : "no Extension" + } + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <SmartphoneOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.mobileNumber + ? user.mobileNumber + : "no mobile number" + } + /> + </ListItem> + <ListItem> + <ListItemAvatar> + <Avatar> + <LocalPrintshopOutlinedIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText + primary={ + user.faxNumber ? user.faxNumber : "no fax number" + } + /> + </ListItem> + </List> + </Grid> + </Grid> + </div> + </CardBody> + <CardFooter> + {auth.isLocalDirectory() && ( + <Button + color="info" + onClick={() => props.setDisplayUser(false)} + > + <EditIcon /> Edit Profile + </Button> + )} + {auth.isLocalDirectory() && auth.hasAdminScope() && ( + <Button + color="info" + onClick={() => { + setChangePasswordOpen(true); + }} + > + <VpnKeyIcon /> Change password + </Button> + )} + {auth.hasAdminScope() && + (userStatus === "Active" || userStatus === "") && ( + <Button + color="info" + onClick={() => handleClickOpen(user.username)} + > + <DeleteIcon fontSize="small" /> Revoke user + </Button> + )} + </CardFooter> + </Card> + </Grid> + </GridContainer> + ))} + </div> + ); +} diff --git a/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js b/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js index b21a929999da806c1fa1e5c484e18adcca870066..e14a4103a261af8d10dbe83964cbcdd6e843f68d 100644 --- a/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js +++ b/jams-react-client/src/views/UserProfile/EditCreateUserProfile.js @@ -1,12 +1,12 @@ import React from "react"; -import { useHistory } from 'react-router-dom'; +import { useHistory } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; import InputLabel from "@material-ui/core/InputLabel"; // core components -import Grid from '@material-ui/core/Grid'; +import Grid from "@material-ui/core/Grid"; import GridItem from "components/Grid/GridItem.js"; import GridContainer from "components/Grid/GridContainer.js"; import Button from "components/CustomButtons/Button.js"; @@ -16,527 +16,741 @@ import CardHeader from "components/Card/CardHeader.js"; import CardIcon from "components/Card/CardIcon.js"; import CardBody from "components/Card/CardBody.js"; import CardFooter from "components/Card/CardFooter.js"; -import FormControl from '@material-ui/core/FormControl'; -import Input from '@material-ui/core/Input'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import Slider from '@material-ui/core/Slider'; +import FormControl from "@material-ui/core/FormControl"; +import Input from "@material-ui/core/Input"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import Slider from "@material-ui/core/Slider"; import Typography from "@material-ui/core/Typography"; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import DialogTitle from '@material-ui/core/DialogTitle'; -import Cropper from 'react-easy-crop' -import getCroppedImg from './cropImage' +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Cropper from "react-easy-crop"; +import getCroppedImg from "./cropImage"; import { hexToRgb, blackColor } from "assets/jss/material-dashboard-react.js"; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; -import BusinessCenterOutlinedIcon from '@material-ui/icons/BusinessCenterOutlined'; -import AlternateEmailOutlinedIcon from '@material-ui/icons/AlternateEmailOutlined'; -import PhoneInTalkOutlinedIcon from '@material-ui/icons/PhoneInTalkOutlined'; -import PhoneForwardedOutlinedIcon from '@material-ui/icons/PhoneForwardedOutlined'; -import SmartphoneOutlinedIcon from '@material-ui/icons/SmartphoneOutlined'; -import LocalPrintshopOutlinedIcon from '@material-ui/icons/LocalPrintshopOutlined'; -import PersonIcon from '@material-ui/icons/Person'; -import PersonOutlinedIcon from '@material-ui/icons/PersonOutlined'; - -import IconButton from '@material-ui/core/IconButton'; -import PhotoCamera from '@material-ui/icons/PhotoCamera'; -import EditIcon from '@material-ui/icons/Edit'; -import PersonAddOutlinedIcon from '@material-ui/icons/PersonAddOutlined'; +import AccountCircleIcon from "@material-ui/icons/AccountCircle"; +import BusinessCenterOutlinedIcon from "@material-ui/icons/BusinessCenterOutlined"; +import AlternateEmailOutlinedIcon from "@material-ui/icons/AlternateEmailOutlined"; +import PhoneInTalkOutlinedIcon from "@material-ui/icons/PhoneInTalkOutlined"; +import PhoneForwardedOutlinedIcon from "@material-ui/icons/PhoneForwardedOutlined"; +import SmartphoneOutlinedIcon from "@material-ui/icons/SmartphoneOutlined"; +import LocalPrintshopOutlinedIcon from "@material-ui/icons/LocalPrintshopOutlined"; +import PersonIcon from "@material-ui/icons/Person"; +import PersonOutlinedIcon from "@material-ui/icons/PersonOutlined"; +import VpnKeyIcon from "@material-ui/icons/VpnKey"; +import RefreshIcon from "@material-ui/icons/Refresh"; +import VisibilityIcon from "@material-ui/icons/Visibility"; +import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; + +import IconButton from "@material-ui/core/IconButton"; +import PhotoCamera from "@material-ui/icons/PhotoCamera"; +import FileCopyIcon from "@material-ui/icons/FileCopy"; + +import { CopyToClipboard } from "react-copy-to-clipboard"; import noProfilePicture from "assets/img/faces/no-profile-picture.png"; -import axios from "axios" -import configApiCall from "../../api" +import axios from "axios"; +import configApiCall from "../../api"; import { - api_path_put_update_user_profile, - api_path_post_create_user, - api_path_post_create_user_profile, - api_path_get_user_directory_search, - api_path_get_exists_user -} from "../../globalUrls" + api_path_put_update_user_profile, + api_path_post_create_user, + api_path_post_create_user_profile, + api_path_get_user_directory_search, + api_path_get_exists_user, +} from "../../globalUrls"; import dashboardStyle from "assets/jss/material-dashboard-react/views/dashboardStyle.js"; -import FormikField from 'components/FormikField/FormikField'; -import {Formik} from "formik"; -import * as Yup from 'yup'; - -const fileUpload = require('fuctbase64'); - - - -const styles = (theme)=> ( { - ...dashboardStyle, - root: { - flexGrow: 1 - }, - cardCategoryWhite: { - color: "rgba(255,255,255,.62)", - margin: "0", - fontSize: "14px", - marginTop: "0", - marginBottom: "0" - }, - cardTitleWhite: { - color: "#FFFFFF", - marginTop: "0px", - minHeight: "auto", - fontWeight: "300", - fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", - marginBottom: "3px", - textDecoration: "none" - }, - input: { - display: 'none', - }, - profileAsBackground: { - backgroundSize: "100% 100%", - width: "80px", - height: "80px", - }, - centerIconMiddle: { - position:"relative", - top: "20px", - left: "15px" - }, - editProfilePicture: { - borderRadius: "50%", - width: "200px", - height: "200px", - - boxShadow: - "0 6px 8px -12px rgba(" + - hexToRgb(blackColor) + - ", 0.56), 0 4px 25px 0px rgba(" + - hexToRgb(blackColor) + - ", 0.12), 0 8px 10px -5px rgba(" + - hexToRgb(blackColor) + - ", 0.2)", - }, - dialogPaper: { - minHeight: '60vh', - maxHeight: '60vh', - minWidth: '80vh', - maxWidth: '80vh' - }, - alignRight: { - float: 'right' - } +import FormikField from "components/FormikField/FormikField"; +import { Formik } from "formik"; +import * as Yup from "yup"; + +let generator = require("generate-password"); + +const fileUpload = require("fuctbase64"); + +const styles = (theme) => ({ + ...dashboardStyle, + root: { + flexGrow: 1, + }, + cardCategoryWhite: { + color: "rgba(255,255,255,.62)", + margin: "0", + fontSize: "14px", + marginTop: "0", + marginBottom: "0", + }, + cardTitleWhite: { + color: "#FFFFFF", + marginTop: "0px", + minHeight: "auto", + fontWeight: "300", + fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", + marginBottom: "3px", + textDecoration: "none", + }, + input: { + display: "none", + }, + profileAsBackground: { + backgroundSize: "100% 100%", + width: "80px", + height: "80px", + }, + centerIconMiddle: { + position: "relative", + top: "20px", + left: "15px", + }, + editProfilePicture: { + borderRadius: "50%", + width: "200px", + height: "200px", + + boxShadow: + "0 6px 8px -12px rgba(" + + hexToRgb(blackColor) + + ", 0.56), 0 4px 25px 0px rgba(" + + hexToRgb(blackColor) + + ", 0.12), 0 8px 10px -5px rgba(" + + hexToRgb(blackColor) + + ", 0.2)", + }, + dialogPaper: { + minHeight: "60vh", + maxHeight: "60vh", + minWidth: "80vh", + maxWidth: "80vh", + }, + alignRight: { + float: "right", + }, + button: { + margin: theme.spacing(1), + }, }); const useStyles = makeStyles(styles); export default function EditCreateUserProfile(props) { - const classes = useStyles(); - const history = useHistory(); - const [createUser, setCreateUser] = React.useState(props.createUser); - const [userExists, setUserExists] = React.useState(false) - - const [firstName, setFirstName] = React.useState("") - const [lastName, setLastName] = React.useState("") - const [userName, setUserName] = React.useState("") - const [email, setEmail] = React.useState("") - const [profilePicture, setProfilePicture] = React.useState("") - const [profilePicturePreview, setProfilePicturePreview] = React.useState(noProfilePicture) - const [originalUploadedImage, setOriginalUploadedImage] = React.useState("") - const [organization, setOrganization] = React.useState("") - const [faxNumber, setFaxNumber] = React.useState("") - const [phoneNumber, setPhoneNumber] = React.useState("") - const [extension, setExtension] = React.useState("") - const [mobileNumber, setMobileNumber] = React.useState("") - const [openPassword, setOpenPassword] = React.useState(false) - const [temporaryPassword, setTemporaryPassword] = React.useState("") - - const [open, setOpen] = React.useState(false); - const [crop, setCrop] = React.useState({ x: 0, y: 0 }) - const [zoom, setZoom] = React.useState(1) - const [aspect, setAspect] = React.useState(1/1) - const [rotation, setRotation] = React.useState(0) - - const initialValues = { - username: "", - firstName: "", - lastName: "", - email: "", - profilePicture: "", - organization: "", - faxNumber: "", - phoneNumber: "", - phoneNumberExtension: "", - mobileNumber: "", - jamiId: "" - } - - React.useEffect(()=>{ - if(!createUser){ - setUserName(props.username) - axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString": props.username}, null)).then((response) => { - response.data.map((user) => { - initialValues.username = user.username; - initialValues.firstName = user.firstName; - initialValues.lastName = user.lastName; - initialValues.email = user.email; - initialValues.profilePicture = user.profilePicture; - setProfilePicture(user.profilePicture) - setProfilePicturePreview('data:image/png;base64, ' + user.profilePicture) - initialValues.organization = user.organization; - initialValues.faxNumber = user.faxNumber; - initialValues.phoneNumber = user.phoneNumber; - initialValues.phoneNumberExtension = user.phoneNumberExtension; - initialValues.mobileNumber = user.mobileNumber; - }) - }).catch((error) => { - console.log("Error fetching user : " + props.username + " " + error) - }) - } - }, []) - - const handleProfilePictureChange = (event) => { - setOriginalUploadedImage(URL.createObjectURL(event.target.files[0])) - fileUpload(event).then((data) => { - setProfilePicture(data.base64) - setProfilePicturePreview('data:image/png;base64, ' + data.base64) - setOpen(true); - }).catch((error) => { - console.log(error) - }) - } - - const handleCancel = () => { - history.push('/users'); - } - - const handleCancelUpdate = () => { - props.setDisplayUser(true) - } - - const handleUserProfileCreation = (data) => { - axios(configApiCall(api_path_post_create_user_profile, 'POST', data, null)).then(() => { - setOpenPassword(true) - }).catch((error) => { - console.log("Error creating user profile: " + error) - }) - } - - const handleCreateUser = (data) => { - axios(configApiCall(api_path_post_create_user + "?username="+ data.username, 'POST', null, null)).then((response) => { - setTemporaryPassword(response.data["password"]) - handleUserProfileCreation(data) - }).catch((error) => { - console.log("Failed to create new user. This is either because the username is already in use" + - " on the public nameserver, or another unknown error has occurred. " + - "Please choose another one."); - }) - } - - const handleUserExists = (input) => { - const data = { - "username": input - } - axios(configApiCall(api_path_get_exists_user, 'GET', data, null)).then((response) =>{ - if(response.status === 200){ - setUserExists(false) - } - }).catch((error) => { - console.log("Error checking for existing users: " + error) - }) - } - - const handleUserUpdate = () => { - props.setDisplayUser(true) - } - - const handleUpdateUser = (data) => { - axios(configApiCall(api_path_put_update_user_profile, 'PUT', data, null)).then((response) => { - handleUserUpdate(); - }).catch((error) => { - console.log("Error updating user: " + error) - }) - } - - const onCropChange = (crop) => { - setCrop(crop) - } - - const onCropComplete = (croppedArea, croppedAreaPixels) => { - getCroppedImg( - originalUploadedImage, - croppedAreaPixels, - rotation - ).then((value) => { - setProfilePicturePreview(value) + const classes = useStyles(); + const history = useHistory(); + const [createUser, setCreateUser] = React.useState(props.createUser); + const [copied, setCopied] = React.useState(false); + const [generated, setGenerated] = React.useState(false); + const [userExists, setUserExists] = React.useState(false); + + const [userName, setUserName] = React.useState(""); + + const [profilePicture, setProfilePicture] = React.useState(""); + const [profilePicturePreview, setProfilePicturePreview] = React.useState( + noProfilePicture + ); + const [originalUploadedImage, setOriginalUploadedImage] = React.useState(""); + + const [open, setOpen] = React.useState(false); + const [crop, setCrop] = React.useState({ x: 0, y: 0 }); + const [zoom, setZoom] = React.useState(1); + const [aspect, setAspect] = React.useState(1); + const [rotation, setRotation] = React.useState(0); + const [passwordVisible, setPasswordVisible] = React.useState(false); + + const initialValues = { + username: "", + password: "", + confirmPassword: "", + firstName: "", + lastName: "", + email: "", + profilePicture: "", + organization: "", + faxNumber: "", + phoneNumber: "", + phoneNumberExtension: "", + mobileNumber: "", + jamiId: "", + }; + + const passwordGenerator = () => { + return generator.generate({ + length: 10, + uppercase: false, + numbers: true, + symbols: true, + }); + }; + + React.useEffect(() => { + if (!createUser) { + setUserName(props.username); + axios( + configApiCall( + api_path_get_user_directory_search, + "GET", + { queryString: props.username }, + null + ) + ) + .then((response) => { + response.data.map((user) => { + initialValues.username = user.username; + initialValues.firstName = user.firstName; + initialValues.lastName = user.lastName; + initialValues.email = user.email; + initialValues.profilePicture = user.profilePicture; + setProfilePicture(user.profilePicture); + setProfilePicturePreview( + "data:image/png;base64, " + user.profilePicture + ); + initialValues.organization = user.organization; + initialValues.faxNumber = user.faxNumber; + initialValues.phoneNumber = user.phoneNumber; + initialValues.phoneNumberExtension = user.phoneNumberExtension; + initialValues.mobileNumber = user.mobileNumber; + }); }) + .catch((error) => { + console.log("Error fetching user : " + props.username + " " + error); + }); } + }, []); + + const handleProfilePictureChange = (event) => { + setOriginalUploadedImage(URL.createObjectURL(event.target.files[0])); + fileUpload(event) + .then((data) => { + setProfilePicture(data.base64); + setProfilePicturePreview("data:image/png;base64, " + data.base64); + setOpen(true); + }) + .catch((error) => { + console.log(error); + }); + }; + + const handleCancelUpdate = () => { + props.setDisplayUser(true); + }; + + const handleUserProfileCreation = (data) => { + axios(configApiCall(api_path_post_create_user_profile, "POST", data, null)) + .then(() => { + history.push("/"); + }) + .catch((error) => { + console.log("Error creating user profile: " + error); + }); + }; + + const handleCreateUser = (data) => { + const body = { + username: data.username, + password: data.password, + }; - const onZoomChange = (zoom) => { - setZoom(zoom) - } + axios(configApiCall(api_path_post_create_user, "POST", body, null)) + .then((response) => { + handleUserProfileCreation(data); + }) + .catch((error) => { + console.log( + "Failed to create new user. This is either because the username is already in use" + + " on the public nameserver, or another unknown error has occurred. " + + "Please choose another one." + ); + }); + }; + + const handleUserUpdate = () => { + props.setDisplayUser(true); + }; + + const handleUpdateUser = (data) => { + axios(configApiCall(api_path_put_update_user_profile, "PUT", data, null)) + .then((response) => { + handleUserUpdate(); + }) + .catch((error) => { + console.log("Error updating user: " + error); + }); + }; + + const onCropChange = (crop) => { + setCrop(crop); + }; + + const onCropComplete = (croppedArea, croppedAreaPixels) => { + getCroppedImg(originalUploadedImage, croppedAreaPixels, rotation).then( + (value) => { + setProfilePicturePreview(value); + } + ); + }; - const handleClose = () => { - setProfilePicturePreview(originalUploadedImage) - setOpen(false); - }; + const onZoomChange = (zoom) => { + setZoom(zoom); + }; - const cropProfilePicture = () => { - setProfilePicture(profilePicturePreview.replace("data:image/jpeg;base64,", "")) - setOpen(false) - } + const handleClose = () => { + setProfilePicturePreview(originalUploadedImage); + setOpen(false); + }; - const handleClosePassword = () => { - setOpenPassword(false); - history.push('/users'); + const cropProfilePicture = () => { + setProfilePicture( + profilePicturePreview.replace("data:image/jpeg;base64,", "") + ); + setOpen(false); + }; + + const handleFormikSubmit = (values) => { + values.profilePicture = profilePicture; + if (createUser) { + handleCreateUser(values); + } else { + handleUpdateUser(values); } - - const handleFormikSubmit = (values) => { - values.profilePicture=profilePicture; - if(createUser){ - handleCreateUser(values); - }else{ - handleUpdateUser(values); + }; + + const handleMouseDownPassword = () => { + setPasswordVisible(true); + }; + const handleMouseUpPassword = () => { + setPasswordVisible(false); + }; + + const EditCreateSchema = Yup.object().shape({ + username: Yup.string() + .min(2, "Username is too short!") + .required("Required!") + .test("checkDuplUsername", "Username exists!", function (value) { + if (createUser) { + return new Promise((resolve, reject) => { + const data = { + username: value.toLowerCase(), + }; + axios(configApiCall(api_path_get_exists_user, "GET", data, null)) + .then((response) => { + if (response.status === 200) { + resolve(false); + } + resolve(true); + }) + .catch((error) => { + console.log("Error checking for existing users: " + error); + resolve(true); + }); + }); + } else { + return true; } - } - - const EditCreateSchema = Yup.object().shape({ - username: Yup.string().min(2, 'Username is too short!').required('Required!').test('checkDuplUsername', 'Username exists!', function (value) { - if(createUser){ - return new Promise((resolve, reject) => { - const data = { - "username": value.toLowerCase() - } - axios(configApiCall(api_path_get_exists_user, 'GET', data, null)).then((response) =>{ - if(response.status === 200){ - resolve(false) - } - resolve(true) - }).catch((error) => { - console.log("Error checking for existing users: " + error) - resolve(true) - }) - }); - }else{ - return true; - } - }).matches(/^[a-zA-Z0-9]*$/, 'Only alphanumeric characters!'), - firstName: Yup.string().min(2, 'First name is too short!'), - lastName: Yup.string().min(2, 'Last Name is too short!'), - email: Yup.string().email('Invalid email!'), - profilePicture: Yup.string(), - organization: Yup.string().min(2, 'Organization name is too short!'), - faxNumber: Yup.string().matches(/^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, 'Fax not valid!'), - phoneNumber: Yup.string().matches(/^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, 'Phone not valid!'), - phoneNumberExtension: Yup.number().positive().integer(), - mobileNumber: Yup.string().matches(/^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, 'Mobile not valid!'), - }) - - return( - <div> - <Dialog - open={openPassword} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - <DialogTitle id="alert-dialog-title">{"Temporary password"}</DialogTitle> - <DialogContent> - <DialogContentText id="alert-dialog-description"> - New user successfully created. Here is the one time password: <strong>{temporaryPassword}</strong> - </DialogContentText> - </DialogContent> - <DialogActions> - <Button onClick={handleClosePassword} color="primary"> - Ok - </Button> - </DialogActions> - </Dialog> - <Dialog - open={open} - onClose={handleClose} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - classes={{ paper: classes.dialogPaper }} - > - <DialogTitle id="alert-dialog-title">{"Crop image"}</DialogTitle> - <DialogContent> - <Cropper - image={originalUploadedImage} - crop={crop} - zoom={zoom} - aspect={aspect} - onCropChange={onCropChange} - onCropComplete={onCropComplete} - onZoomChange={onZoomChange} - /> - </DialogContent> - <DialogActions> - <div className={classes.controls}> - <div className={classes.sliderContainer}> - <Typography - variant="overline" - classes={{ root: classes.sliderLabel }} + }) + .matches(/^[a-zA-Z0-9]*$/, "Only alphanumeric characters!"), + password: createUser + ? Yup.string().required("Password is required!") + : null, + confirmPassword: createUser + ? Yup.string().oneOf([Yup.ref("password"), null], "Passwords must match") + : null, + firstName: Yup.string().min(2, "First name is too short!"), + lastName: Yup.string().min(2, "Last Name is too short!"), + email: Yup.string().email("Invalid email!"), + profilePicture: Yup.string(), + organization: Yup.string().min(2, "Organization name is too short!"), + faxNumber: Yup.string().matches( + /^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, + "Fax not valid!" + ), + phoneNumber: Yup.string().matches( + /^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, + "Phone not valid!" + ), + phoneNumberExtension: Yup.number().positive().integer(), + mobileNumber: Yup.string().matches( + /^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/, + "Mobile not valid!" + ), + }); + + return ( + <div> + <Dialog + open={open} + onClose={handleClose} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + classes={{ paper: classes.dialogPaper }} + > + <DialogTitle id="alert-dialog-title">{"Crop image"}</DialogTitle> + <DialogContent> + <Cropper + image={originalUploadedImage} + crop={crop} + zoom={zoom} + aspect={aspect} + onCropChange={onCropChange} + onCropComplete={onCropComplete} + onZoomChange={onZoomChange} + /> + </DialogContent> + <DialogActions> + <div className={classes.controls}> + <div className={classes.sliderContainer}> + <Typography + variant="overline" + classes={{ root: classes.sliderLabel }} + > + Zoom + </Typography> + <Slider + value={zoom} + min={1} + max={3} + step={0.1} + aria-labelledby="Zoom" + classes={{ container: classes.slider }} + onChange={(e, zoom) => setZoom(zoom)} + /> + </div> + <div className={classes.sliderContainer}> + <Typography + variant="overline" + classes={{ root: classes.sliderLabel }} + > + Rotation + </Typography> + <Slider + value={rotation} + min={0} + max={360} + step={1} + aria-labelledby="Rotation" + classes={{ container: classes.slider }} + onChange={(e, rotation) => setRotation(rotation)} + /> + </div> + </div> + <Button onClick={handleClose} color="info"> + Cancel + </Button> + <Button onClick={cropProfilePicture} color="success" autoFocus> + Crop + </Button> + </DialogActions> + </Dialog> + <GridContainer> + <GridItem xs={12} sm={12} md={8}> + <Formik + initialValues={initialValues} + onSubmit={handleFormikSubmit} + validationSchema={EditCreateSchema} + > + {({ isValid, dirty, handleSubmit, setFieldValue, values }) => ( + <form onSubmit={handleSubmit}> + <Card profile> + <CardHeader color="info" stats icon> + <p className={classes.cardCategory}> + {createUser ? "Create new profile" : "Edit profile"} + </p> + {createUser ? ( + "" + ) : ( + <h3 className={classes.cardTitle}>{userName}</h3> + )} + </CardHeader> + <CardBody profile> + <CardAvatar editProfile> + <img + src={profilePicturePreview} + alt="..." + className={classes.editProfilePicture} + /> + </CardAvatar> + <div className={classes.root}> + <Grid container spacing={5}> + <Grid item xs={12} sm={12} md={12}> + <input + accept="image/*" + className={classes.input} + id="icon-button-file" + type="file" + onChange={handleProfilePictureChange} + /> + <label htmlFor="icon-button-file"> + <IconButton + color="info" + aria-label="upload picture" + component="span" > - Zoom - </Typography> - <Slider - value={zoom} - min={1} - max={3} - step={0.1} - aria-labelledby="Zoom" - classes={{ container: classes.slider }} - onChange={(e, zoom) => setZoom(zoom)} + <PhotoCamera /> + </IconButton>{" "} + Change profile image + </label> + </Grid> + {createUser && ( + <Grid item align="center" xs={12} sm={12} md={12}> + <FormikField + name="username" + label="Username" + placeholder="Username" + startAdornment={ + <InputAdornment position="start"> + <AccountCircleIcon /> + </InputAdornment> + } + required /> - </div> - <div className={classes.sliderContainer}> - <Typography - variant="overline" - classes={{ root: classes.sliderLabel }} - > - Rotation - </Typography> - <Slider - value={rotation} - min={0} - max={360} - step={1} - aria-labelledby="Rotation" - classes={{ container: classes.slider }} - onChange={(e, rotation) => setRotation(rotation)} + </Grid> + )} + {createUser && ( + <Grid item xs={12} sm={12} md={6}> + <FormikField + type={passwordVisible ? "text" : "password"} + name="password" + label="Password" + placeholder="Password" + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + } + required /> - </div> + </Grid> + )} + {createUser && ( + <Grid item xs={12} sm={12} md={6}> + <FormikField + type={passwordVisible ? "text" : "password"} + name="confirmPassword" + label="Confirm Password" + placeholder="Confirm Password" + startAdornment={ + <InputAdornment position="start"> + <VpnKeyIcon /> + </InputAdornment> + } + endAdornment={ + <IconButton + aria-label="toggle password visibility" + onMouseDown={handleMouseDownPassword} + onMouseUp={handleMouseUpPassword} + > + {passwordVisible ? ( + <VisibilityIcon /> + ) : ( + <VisibilityOffIcon /> + )} + </IconButton> + } + required + /> + </Grid> + )} + {createUser && ( + <Grid item align="left" xs={12} sm={12} md={6}></Grid> + )} + {createUser && ( + <Grid item align="left" xs={12} sm={12} md={6}> + <Button + variant="contained" + color="primary" + size="large" + className={classes.button} + startIcon={<RefreshIcon />} + onClick={() => { + const newGeneratedPassword = passwordGenerator(); + setFieldValue( + "password", + newGeneratedPassword, + false + ); + setFieldValue( + "confirmPassword", + newGeneratedPassword, + false + ); + setCopied(false); + setGenerated(true); + }} + > + {"Generate"} + </Button> + <CopyToClipboard + text={values.password} + onCopy={() => { + setCopied(true); + setGenerated(false); + }} + > + <Button + variant="contained" + color="primary" + size="large" + className={classes.button} + startIcon={<FileCopyIcon />} + > + Copy to clipboard + </Button> + </CopyToClipboard> + {copied ? ( + <span style={{ marginLeft: "10px" }}>Copied</span> + ) : null} + {generated ? ( + <span style={{ marginLeft: "10px" }}> + Generated + </span> + ) : null} + </Grid> + )} + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="firstName" + label="First name" + placeholder="First name" + startAdornment={ + <InputAdornment position="start"> + <PersonIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="lastName" + label="Last name" + placeholder="Last name" + startAdornment={ + <InputAdornment position="start"> + <PersonOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="email" + label="Email" + placeholder="Email" + startAdornment={ + <InputAdornment position="start"> + <AlternateEmailOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="organization" + label="Organization" + placeholder="Organization" + startAdornment={ + <InputAdornment position="start"> + <BusinessCenterOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="faxNumber" + label="Fax number" + placeholder="Fax number" + startAdornment={ + <InputAdornment position="start"> + <LocalPrintshopOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="phoneNumber" + label="Phone number" + placeholder="Phone number" + startAdornment={ + <InputAdornment position="start"> + <LocalPrintshopOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="phoneNumberExtension" + label="Extension" + placeholder="Extension" + startAdornment={ + <InputAdornment position="start"> + <PhoneForwardedOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + <Grid item align="center" xs={12} sm={12} md={6}> + <FormikField + name="mobileNumber" + label="Mobile" + placeholder="Mobile" + startAdornment={ + <InputAdornment position="start"> + <SmartphoneOutlinedIcon /> + </InputAdornment> + } + /> + </Grid> + </Grid> </div> - <Button onClick={handleClose} color="info"> + </CardBody> + <CardFooter className={classes.alignRight}> + {!createUser && ( + <Button color="info" onClick={handleCancelUpdate}> Cancel - </Button> - <Button onClick={cropProfilePicture} color="success" autoFocus> - Crop - </Button> - </DialogActions> - </Dialog> - <GridContainer> - <GridItem xs={12} sm={12} md={8}> - <Formik initialValues={initialValues} - onSubmit={handleFormikSubmit} - validationSchema={EditCreateSchema} - > - {({ isValid, dirty, handleSubmit}) => ( - <form onSubmit={handleSubmit}> - <Card profile> - <CardHeader color="info" stats icon> - <p className={classes.cardCategory}>{createUser ? "Create user" : "Edit user"}</p> - {createUser ? "" : <h3 className={classes.cardTitle}>{userName}</h3>} - </CardHeader> - <CardBody profile> - <CardAvatar editProfile> - <img src={profilePicturePreview} alt="..." className={classes.editProfilePicture}/> - </CardAvatar> - <div className={classes.root}> - - <Grid container spacing={5}> - <Grid item xs={12} sm={12} md={12}> - <input accept="image/*" className={classes.input} id="icon-button-file" type="file" onChange={handleProfilePictureChange}/> - <label htmlFor="icon-button-file" > - <IconButton color="info" aria-label="upload picture" component="span" > - <PhotoCamera /> - </IconButton> Change profile image - </label> - </Grid> - <Grid item xs={12} sm={12} md={12}> - <Grid container spacing={5} > - {createUser && - <Grid item align="center" xs={12} sm={12} md={12}> - <FormikField name="username" label="Username" placeholder="Username" - startAdornment={ - <InputAdornment position="start"> - <AccountCircleIcon /> - </InputAdornment> - } - required/> - </Grid> - } - </Grid> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="firstName" label="First name" placeholder="First name" - startAdornment={ - <InputAdornment position="start"> - <PersonIcon /> - </InputAdornment> - } - /> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="lastName" label="Last name" placeholder="Last name" - startAdornment={ - <InputAdornment position="start"> - <PersonOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="email" label="Email" placeholder="Email" - startAdornment={ - <InputAdornment position="start"> - <AlternateEmailOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="organization" label="Organization" placeholder="Organization" - startAdornment={ - <InputAdornment position="start"> - <BusinessCenterOutlinedIcon /> - </InputAdornment> - } - /> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="faxNumber" label="Fax number" placeholder="Fax number" - startAdornment={ - <InputAdornment position="start"> - <LocalPrintshopOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="phoneNumber" label="Phone number" placeholder="Phone number" - startAdornment={ - <InputAdornment position="start"> - <LocalPrintshopOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="phoneNumberExtension" label="Extension" placeholder="Extension" - startAdornment={ - <InputAdornment position="start"> - <PhoneForwardedOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - <Grid item align="center" xs={12} sm={12} md={6}> - <FormikField name="mobileNumber" label="Mobile" placeholder="Mobile" - startAdornment={ - <InputAdornment position="start"> - <SmartphoneOutlinedIcon /> - </InputAdornment> - }/> - </Grid> - </Grid> - </div> - </CardBody> - <CardFooter className={classes.alignRight}> - {!createUser && <Button color="info" onClick={handleCancelUpdate}>Cancel</Button>} - {createUser ? <Button type="submit" disabled={!isValid || !dirty} color="info">Create user</Button> : <Button type="submit" disabled={!isValid || !dirty} color="info">Save user</Button>} - </CardFooter> - </Card> - </form> - )} - </Formik> - </GridItem> - </GridContainer> - </div> - ); -} \ No newline at end of file + </Button> + )} + {createUser ? ( + <Button + type="submit" + disabled={!isValid || !dirty} + color="info" + > + Create Profile + </Button> + ) : ( + <Button + type="submit" + disabled={!isValid || !dirty} + color="info" + > + Save Profile + </Button> + )} + </CardFooter> + </Card> + </form> + )} + </Formik> + </GridItem> + </GridContainer> + </div> + ); +} diff --git a/jams-react-client/src/views/UserProfile/UserProfile.js b/jams-react-client/src/views/UserProfile/UserProfile.js index 8d78dc2961741f6715346233b746e8fb7aadc04e..3ae9d6f36acb1f64038e1eb2229b51ac61c3bb0a 100755 --- a/jams-react-client/src/views/UserProfile/UserProfile.js +++ b/jams-react-client/src/views/UserProfile/UserProfile.js @@ -4,23 +4,20 @@ import { makeStyles } from "@material-ui/core/styles"; // core components import Devices from "components/Devices/Devices.js"; -import PropTypes from 'prop-types'; -import AppBar from '@material-ui/core/AppBar'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; -import Typography from '@material-ui/core/Typography'; -import Box from '@material-ui/core/Box'; +import PropTypes from "prop-types"; +import AppBar from "@material-ui/core/AppBar"; +import Tabs from "@material-ui/core/Tabs"; +import Tab from "@material-ui/core/Tab"; +import Typography from "@material-ui/core/Typography"; +import Box from "@material-ui/core/Box"; -import EditCreateUserProfile from "./EditCreateUserProfile" +import EditCreateUserProfile from "./EditCreateUserProfile"; -import Contacts from "views/Contacts/Contacts" +import Contacts from "views/Contacts/Contacts"; -import { - infoColor -} from "assets/jss/material-dashboard-react.js"; +import { infoColor } from "assets/jss/material-dashboard-react.js"; import DisplayUserProfile from "./DisplayUserProfile"; - function TabPanel(props) { const { children, value, index, ...other } = props; @@ -50,7 +47,7 @@ TabPanel.propTypes = { function a11yProps(index) { return { id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, + "aria-controls": `simple-tabpanel-${index}`, }; } @@ -60,7 +57,7 @@ const styles = { margin: "0", fontSize: "14px", marginTop: "0", - marginBottom: "0" + marginBottom: "0", }, cardTitleWhite: { color: "#FFFFFF", @@ -69,8 +66,8 @@ const styles = { fontWeight: "300", fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", marginBottom: "3px", - textDecoration: "none" - } + textDecoration: "none", + }, }; const useStyles = makeStyles(styles); @@ -88,19 +85,32 @@ export default function UserProfile(props) { return ( <div> <AppBar position="static" style={{ background: infoColor[0] }}> - <Tabs value={value} onChange={handleChange} aria-label="simple tabs example"> + <Tabs + value={value} + onChange={handleChange} + aria-label="simple tabs example" + > <Tab label="Profile" {...a11yProps(0)} /> <Tab label="Devices" {...a11yProps(1)} /> <Tab label="Contacts" {...a11yProps(2)} /> </Tabs> </AppBar> <TabPanel value={value} index={0}> - { - displayUser ? <DisplayUserProfile username={username} setDisplayUser={setDisplayUser}/>: <EditCreateUserProfile username={username} createUser={false} setDisplayUser={setDisplayUser}/> - } + {displayUser ? ( + <DisplayUserProfile + username={username} + setDisplayUser={setDisplayUser} + /> + ) : ( + <EditCreateUserProfile + username={username} + createUser={false} + setDisplayUser={setDisplayUser} + /> + )} </TabPanel> <TabPanel value={value} index={1}> - <Devices username={props.username}/> + <Devices username={props.username} /> </TabPanel> <TabPanel value={value} index={2}> <Contacts username={props.username} /> diff --git a/jams-react-client/src/views/Users/Users.js b/jams-react-client/src/views/Users/Users.js index 42920a3a209106aab2788de2fb14883fb0544ad9..c0e54c2ede034bd17984836dbf853d5a7a6b3286 100644 --- a/jams-react-client/src/views/Users/Users.js +++ b/jams-react-client/src/views/Users/Users.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; -import { useHistory } from 'react-router-dom'; +import { useHistory } from "react-router-dom"; import { Switch, Route, Redirect } from "react-router-dom"; // @material-ui/core components import { makeStyles } from "@material-ui/core/styles"; @@ -14,205 +14,267 @@ import CardHeader from "components/Card/CardHeader.js"; import CardAvatar from "components/Card/CardAvatar.js"; import CardBody from "components/Card/CardBody.js"; import CardFooter from "components/Card/CardFooter.js"; -import UserProfile from "views/UserProfile/UserProfile.js" -import Divider from '@material-ui/core/Divider'; +import UserProfile from "views/UserProfile/UserProfile.js"; +import Divider from "@material-ui/core/Divider"; -import PersonIcon from '@material-ui/icons/Person'; -import PermIdentityIcon from '@material-ui/icons/PermIdentity'; -import PhoneOutlinedIcon from '@material-ui/icons/PhoneOutlined'; -import BusinessOutlinedIcon from '@material-ui/icons/BusinessOutlined'; +import PersonIcon from "@material-ui/icons/Person"; +import PermIdentityIcon from "@material-ui/icons/PermIdentity"; +import PhoneOutlinedIcon from "@material-ui/icons/PhoneOutlined"; +import BusinessOutlinedIcon from "@material-ui/icons/BusinessOutlined"; import Search from "@material-ui/icons/Search"; -import MailOutlineIcon from '@material-ui/icons/MailOutline'; +import MailOutlineIcon from "@material-ui/icons/MailOutline"; import axios from "axios"; import configApiCall from "api.js"; -import auth from 'auth.js' +import auth from "auth.js"; import { api_path_get_user_directory_search } from "globalUrls"; -import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; -import KeyboardReturnIcon from '@material-ui/icons/KeyboardReturn'; +import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; +import KeyboardReturnIcon from "@material-ui/icons/KeyboardReturn"; import jami from "assets/img/faces/jami.png"; import noProfilePicture from "assets/img/faces/no-profile-picture.png"; import EditCreateUserProfile from "views/UserProfile/EditCreateUserProfile"; -import LinearProgress from '@material-ui/core/LinearProgress'; +import LinearProgress from "@material-ui/core/LinearProgress"; import headerLinksStyle from "assets/jss/material-dashboard-react/components/headerLinksStyle.js"; import { debounce } from "lodash"; const styles = { - ...headerLinksStyle, - cardCategoryWhite: { - color: "rgba(255,255,255,.62)", - margin: "0", - fontSize: "14px", - marginTop: "0", - marginBottom: "0" - }, - cardTitleWhite: { - color: "#FFFFFF", - marginTop: "0px", - minHeight: "auto", - fontWeight: "300", - fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", - marginBottom: "3px", - textDecoration: "none" - }, - deleteIcon : { - float: "right" - }, - search : { - width: "90%" - }, - loading: { - width: '100%', - } + ...headerLinksStyle, + cardCategoryWhite: { + color: "rgba(255,255,255,.62)", + margin: "0", + fontSize: "14px", + marginTop: "0", + marginBottom: "0", + }, + cardTitleWhite: { + color: "#FFFFFF", + marginTop: "0px", + minHeight: "auto", + fontWeight: "300", + fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif", + marginBottom: "3px", + textDecoration: "none", + }, + deleteIcon: { + float: "right", + }, + search: { + width: "90%", + }, + loading: { + width: "100%", + }, }; const useStyles = makeStyles(styles); -export default function Users() { - const classes = useStyles(); - const history = useHistory(); - const [users, setUsers] = React.useState([]) - const [selectedUsername, setSelectedUsername] = React.useState("") - const [createUser, setCreateUser] = React.useState(false) - const [loading, setLoading] = React.useState(false) - const [progress, setProgress] = React.useState(0); - - useEffect(() => { - setLoading(true) - const timer = setInterval(() => { - setProgress((oldProgress) => { - if (oldProgress === 100) { - return 0; - } - const diff = Math.random() * 10; - return Math.min(oldProgress + diff, 100); - }); - }, 500); - axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString":"*"}, null)).then((response)=>{ - setUsers(response.data) - setLoading(false) - }).catch((error) =>{ - console.log(error); - if(error.response.status == 401){ - auth.authenticated = false - history.push('/') - } - }); - return () => { - clearInterval(timer); - }; - }, []); - - const [selectedProfile, setSelectedProfile] = useState(false); - - const redirectToUserProfile = (e, username) => { - e.preventDefault() - setSelectedProfile(true); - setSelectedUsername(username) - } - - const searchUsers = (value) => { - axios(configApiCall(api_path_get_user_directory_search, 'GET', {"queryString": value ? value : "*"}, null)).then((response)=>{ - setUsers(response.data) - }).catch((error) =>{ - console.log(error); - setUsers([]) - if(error.response.status == 401){ - auth.authenticated = false - history.push('/') - } - }); - - } - - const initSearchUsers = useCallback(debounce((searchValue) => searchUsers(searchValue), 500), []) - - const handleSearchUsers = (e) => { - const searchValue = e.target.value; - initSearchUsers(searchValue) - } - - if (!auth.hasAdminScope()) { - return ( - <div> - <UserProfile username={auth.getUsername()}/> - </div> - ) - } else if(selectedProfile && auth.hasAdminScope()){ - return ( - <div> - <UserProfile username={selectedUsername}/> - </div> - ) - } else if (createUser) { - return( - <EditCreateUserProfile createUser={createUser} setCreateUser={setCreateUser}/> - ) - } - else { - return ( - <div> - <GridContainer > - <GridItem xs={12} sm={12} md={12}> - {auth.isLocalDirectory() && - <Button variant="contained" color="primary" href="#contained-buttons" onClick={e => setCreateUser(true)}> - <AddCircleOutlineIcon /> Create user - </Button> - } - <div className={classes.searchWrapper}> - <CustomInput - formControlProps={{ - className: classes.margin + " " + classes.search - }} - inputProps={{ - placeholder: "Search users using (username, name, phone, email, ...)", - inputProps: { - "aria-label": "Search users" - }, - onKeyUp: handleSearchUsers, - }} - /> - <Search /> - <div className={classes.loading}> - {loading && <LinearProgress variant="determinate" value={progress} />} - </div> - </div> - </GridItem> - { - users.sort(function(a, b){ - if(a.username < b.username) { return -1; } - if(a.username > b.username) { return 1; } - return 0; - }).map(user => - <GridItem xs={12} sm={12} md={2} key={user.username} wrap="nowrap"> - - <Card profile> - <CardBody profile> - <a href="#" onClick={(e) => redirectToUserProfile(e, user.username)}> - <CardAvatar profile> - <img src={user.profilePicture ? ('data:image/png;base64, ' + user.profilePicture) : noProfilePicture} alt="..." /> - </CardAvatar> - - <h4 className={classes.cardTitle}>{user.firstName} {user.lastName}</h4> - <ul> - <li><img src={jami} width="20" alt="Jami" style={{marginRight: "10px"}} /> {user.username}</li> - <li><BusinessOutlinedIcon fontSize='small' style={{marginRight: "10px"}} /> {user.organization ? user.organization : 'No organization'}</li> - </ul> - </a> - </CardBody> - </Card> - - </GridItem>) - } - </GridContainer> - </div> - ); - } +export default function Users(props) { + const classes = useStyles(); + const history = useHistory(); + const [users, setUsers] = React.useState([]); + const [selectedUsername, setSelectedUsername] = React.useState(""); + const [createUser, setCreateUser] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [progress, setProgress] = React.useState(0); + + useEffect(() => { + setLoading(true); + const timer = setInterval(() => { + setProgress((oldProgress) => { + if (oldProgress === 100) { + return 0; + } + const diff = Math.random() * 10; + return Math.min(oldProgress + diff, 100); + }); + }, 500); + axios( + configApiCall( + api_path_get_user_directory_search, + "GET", + { queryString: "*" }, + null + ) + ) + .then((response) => { + setUsers(response.data); + setLoading(false); + }) + .catch((error) => { + console.log(error); + if (error.response.status == 401) { + auth.authenticated = false; + history.push("/"); + } + }); + return () => { + clearInterval(timer); + }; + }, []); + + const [selectedProfile, setSelectedProfile] = useState(false); + + const redirectToUserProfile = (e, username) => { + e.preventDefault(); + setSelectedProfile(true); + setSelectedUsername(username); + }; + const searchUsers = (value) => { + axios( + configApiCall( + api_path_get_user_directory_search, + "GET", + { queryString: value ? value : "*" }, + null + ) + ) + .then((response) => { + setUsers(response.data); + }) + .catch((error) => { + console.log(error); + setUsers([]); + if (error.response.status == 401) { + auth.authenticated = false; + history.push("/"); + } + }); + }; + + const initSearchUsers = useCallback( + debounce((searchValue) => searchUsers(searchValue), 500), + [] + ); + + const handleSearchUsers = (e) => { + const searchValue = e.target.value; + initSearchUsers(searchValue); + }; + + if (!auth.hasAdminScope()) { + return ( + <div> + <UserProfile username={auth.getUsername()} /> + </div> + ); + } else if (selectedProfile && auth.hasAdminScope()) { + return ( + <div> + <UserProfile username={selectedUsername} /> + </div> + ); + } else if (createUser) { + return ( + <EditCreateUserProfile + createUser={createUser} + setCreateUser={setCreateUser} + setError={props.setError} + setErrorMessage={props.setErrorMessage} + setSeverity={props.setSeverity} + /> + ); + } else { + return ( + <div> + <GridContainer> + <GridItem xs={12} sm={12} md={12}> + {auth.isLocalDirectory() && ( + <Button + variant="contained" + color="primary" + href="#contained-buttons" + onClick={(e) => setCreateUser(true)} + > + <AddCircleOutlineIcon /> Create user + </Button> + )} + <div className={classes.searchWrapper}> + <CustomInput + formControlProps={{ + className: classes.margin + " " + classes.search, + }} + inputProps={{ + placeholder: + "Search users using (username, name, phone, email, ...)", + inputProps: { + "aria-label": "Search users", + }, + onKeyUp: handleSearchUsers, + }} + /> + <Search /> + <div className={classes.loading}> + {loading && ( + <LinearProgress variant="determinate" value={progress} /> + )} + </div> + </div> + </GridItem> + {users + .sort(function (a, b) { + if (a.username < b.username) { + return -1; + } + if (a.username > b.username) { + return 1; + } + return 0; + }) + .map((user) => ( + <GridItem xs={12} sm={12} md={2} key={user.username}> + <Card profile> + <CardBody profile> + <a + href="#" + onClick={(e) => redirectToUserProfile(e, user.username)} + > + <CardAvatar profile> + <img + src={ + user.profilePicture + ? "data:image/png;base64, " + user.profilePicture + : noProfilePicture + } + alt="..." + /> + </CardAvatar> + <h4 className={classes.cardTitle}> + {user.firstName} {user.lastName} + </h4> + <ul> + <li> + <img + src={jami} + width="20" + alt="Jami" + style={{ marginRight: "10px" }} + />{" "} + {user.username} + </li> + <li> + <BusinessOutlinedIcon + fontSize="small" + style={{ marginRight: "10px" }} + />{" "} + {user.organization + ? user.organization + : "No organization"} + </li> + </ul> + </a> + </CardBody> + </Card> + </GridItem> + ))} + </GridContainer> + </div> + ); + } }