import React, { useState, useEffect, useContext, memo } from "react"; import { toast } from 'react-toastify'; import { FormControl, InputLabel, OutlinedInput, Checkbox, Tooltip, Typography, Select, MenuItem, Divider, TextField, Button, List, ListItem, ListItemText, IconButton, Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress, Skeleton, Switch, Box, } from "@mui/material"; import { Cached as CachedIcon, Edit as EditIcon, Style, } from "@mui/icons-material"; import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; import theme from "../theme.jsx"; const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; const MenuProps = { PaperProps: { style: { maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, width: 500, }, }, getContentAnchorEl: () => null, }; const logsViewModal = false; const userdata = ""; const UserManagmentTab = memo((props) => { const { userdata, isCloud, globalUrl, selectedOrganization, handleEditOrg} = props; const [modalOpen, setModalOpen] = React.useState(false); const [loginInfo, setLoginInfo] = React.useState(""); const [modalUser, setModalUser] = React.useState({}); const [selectedUser, setSelectedUser] = React.useState({}); const [matchingOrganizations, setMatchingOrganizations] = React.useState([]); const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); const [image2FA, setImage2FA] = React.useState(""); const [secret2FA, setSecret2FA] = React.useState(""); const [value2FA, setValue2FA] = React.useState(""); const [newUsername, setNewUsername] = React.useState(""); const [newPassword, setNewPassword] = React.useState(""); const [show2faSetup, setShow2faSetup] = useState(false); const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] = React.useState(false); const [MFARequired, setMFARequired] = React.useState(selectedOrganization.mfa_required === undefined ? false : selectedOrganization.mfa_required); const [deleteAccountText, setDeleteAccountText] = React.useState(""); const [users, setUsers] = React.useState([]); const [showLoader, setShowLoader] = useState(true); const [logsLoading, setLogsLoading] = React.useState(true); const [logs, setLogs] = React.useState([]); const [logsViewModal, setLogsViewModal] = React.useState(false); const [ipSelected, setIpSelected] = React.useState(""); const [userLogViewing, setUserLogViewing] = React.useState({}); useEffect(() => { if (selectedOrganization?.mfa_required !== MFARequired) { setMFARequired(selectedOrganization?.mfa_required); } }, [selectedOrganization]); useEffect(() => { if(users?.length === 0){ getUsers(); } }, []); const changeModalData = (field, value) => { modalUser[field] = value; }; const submitUser = (data) => { console.log("INPUT: ", data); setLoginInfo(""); // Just use this one? var data = { username: data.Username, password: data.Password }; var baseurl = globalUrl; const url = baseurl + "/api/v1/users/register"; fetch(url, { method: "POST", credentials: "include", body: JSON.stringify(data), headers: { "Content-Type": "application/json", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { setLoginInfo("Error: " + responseJson.reason); } else { setLoginInfo(""); toast.success("User added successfully. They will show up in the list when they have accepted the invite."); setModalOpen(false); setTimeout(() => { getUsers(); }, 1000); } }) ) .catch((error) => { console.log("Error in userdata: ", error); }); }; const setUser = (userId, field, value) => { const data = { user_id: userId }; data[field] = value; fetch(globalUrl + "/api/v1/users/updateuser", { method: "PUT", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(data), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } else { getUsers(); } return response.json(); }) .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed setting user: " + responseJson.reason); } else if (responseJson.success === false) { toast("Failed to update user"); } else { //toast("Set the user field " + field + " to " + value); toast("Successfully updated user field " + field); if (field !== "suborgs") { setSelectedUserModalOpen(false); } } }) .catch((error) => { console.log(error); }); }; const inviteUser = (data) => { //console.log("INPUT: ", data); setLoginInfo(""); // Just use this one? var data = { username: data.Username, type: "invite", org_id: selectedOrganization.id, }; var baseurl = globalUrl; const url = baseurl + "/api/v1/users/register_org"; fetch(url, { method: "POST", credentials: "include", body: JSON.stringify(data), headers: { "Content-Type": "application/json", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { setLoginInfo("Error: " + responseJson.reason); toast("Failed to send email (2). Please try again and contact support if this persists.") } else { setLoginInfo(""); setModalOpen(false); setTimeout(() => { getUsers(); }, 1000); toast("Invite sent! They will show up in the list when they have accepted the invite.") } }) ) .catch((error) => { console.log("Error in userdata: ", error); toast("Failed to send email. Please try again and contact support if this persists.") }); }; const onPasswordChange = () => { const data = { username: selectedUser.username, newpassword: newPassword }; const url = globalUrl + "/api/v1/users/passwordchange"; fetch(url, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { if (responseJson.reason !== undefined) { toast(responseJson.reason); } else { toast("Failed setting new password"); } } else { toast("Successfully updated password!"); setSelectedUserModalOpen(false); } }), ) .catch((error) => { toast("Err: " + error.toString()); }); }; const handleOrgEditChange = (event) => { if (userdata.id === selectedUser.id) { toast("Can't remove orgs from yourself"); return; } console.log("event: ", event.target.value); setMatchingOrganizations(event.target.value); // Workaround for empty orgs if (event.target.value.length === 0) { event.target.value.push("REMOVE"); } setUser(selectedUser.id, "suborgs", event.target.value); //setUser(selectedUser.id, "suborgs", matchingOrganizations) }; const userOrgEdit = selectedUser.id !== undefined && selectedUser?.orgs !== undefined && selectedUser?.orgs !== null && selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== null && selectedOrganization?.child_orgs?.length > 0 ? ( Accessible Sub-Organizations ( {selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0}) ) : null; const getUsers = () => { fetch(globalUrl + "/api/v1/getusers", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { // Ahh, this happens because they're not admin // window.location.pathname = "/workflows" return; } return response.json(); }) .then((responseJson) => { setUsers(responseJson); setShowLoader(false) }) .catch((error) => { toast(error.toString()); }); }; const deleteUser = (data) => { // Just use this one? const userId = data.id; const url = globalUrl + "/api/v1/users/" + userId; fetch(url, { method: "DELETE", credentials: "include", headers: { "Content-Type": "application/json", }, }) .then((response) => { if (response.status === 200) { getUsers(); } return response.json(); }) .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed to deactivate user: " + responseJson.reason); } else if (responseJson.success === false) { toast( "Failed to deactivate user. Please contact support@shuffler.io if this persists.", ); } else { toast("Changed activation for user " + data.id); } }) .catch((error) => { console.log("Error in userdata: ", error); }); }; const handleDeleteAccount = (userID) => { if (userID === undefined || userID === null || userID === "") { return; } const url = `${globalUrl}/api/v1/users/${userID}/remove`; fetch(url, { mode: "cors", method: "DELETE", credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json", }, }) .then((response) => response.json()) .then((data) => { if (data.success) { toast.success( "Deleted their account. Would reload users in a few seconds.", ); setTimeout(() => { getUsers(); }); } else { toast.error(`${data.reason}`); } }) .catch((error) => { console.error( "There was a problem with deleting the account. Please try again:", error, ); toast.error( "There was a problem with the delete request. Please try again", ); }); }; const handleVerify2FA = (userId, code) => { const data = { code: code, user_id: userId, }; fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => { if (response.status === 200) { } else { //toast("Wrong code sent.") //toast("Wrong code sent. Please try again.") } return response.json(); }) .then((responseJson) => { if (responseJson.success === true) { toast("Successfully enabled 2fa"); setTimeout(() => { getUsers(); setImage2FA(""); setValue2FA(""); setSecret2FA(""); setShow2faSetup(false); setSelectedUserModalOpen(false); }, 1000); } else { toast("Wrong code sent. Please try again."); //toast("Failed setting 2fa: ", responseJson.reason) } }) .catch((error) => { toast("Wrong code sent. Please try again."); //toast("Err: " + error.toString()) }); }; const get2faCode = (userId) => { fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); } return response.json(); }) .then((responseJson) => { //console.log("RESPONSE: ", responseJson) if (responseJson.success === true) { //toast(responseJson.reason) setImage2FA(responseJson.reason); setSecret2FA(responseJson.extra); } }) .catch((error) => { toast(error.toString()); }); }; const generateApikey = (user) => { const userId = user.id; const data = { user_id: userId }; toast("Generating new API key"); var fetchdata = { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }; if (userId === userdata.id) { fetchdata.method = "GET"; } else { fetchdata.body = JSON.stringify(data); } fetch(globalUrl + "/api/v1/generateapikey", fetchdata) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!"); } else { getUsers(); } return response.json(); }) .then((responseJson) => { console.log("RESP: ", responseJson); if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed getting new: " + responseJson.reason); } else { toast("Got new API key"); } }) .catch((error) => { console.log(error); }); }; const UpdateMFAInUserOrg = (org_id) => { handleEditOrg( selectedOrganization?.name, selectedOrganization?.description, selectedOrganization?.id, selectedOrganization?.image, { app_download_repo: selectedOrganization?.defaults?.app_download_repo, app_download_branch: selectedOrganization?.defaults?.app_download_branch, workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, notification_workflow: selectedOrganization?.defaults?.notification_workflow, documentation_reference: selectedOrganization?.defaults?.documentation_reference, workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, newsletter: selectedOrganization?.defaults?.newsletter, weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, }, { sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, sso_certificate: selectedOrganization?.sso_config?.sso_certificate, client_id: selectedOrganization?.sso_config?.client_id, client_secret: selectedOrganization?.sso_config?.client_secret, openid_authorization: selectedOrganization?.sso_config?.openid_authorization, openid_token: selectedOrganization?.sso_config?.openid_token, SSORequired: selectedOrganization?.sso_config?.SSORequired, auto_provision: selectedOrganization?.sso_config?.auto_provision, }, [], { mfa_required: !MFARequired } ); setMFARequired((prev)=> !prev) } const modalView = ( { setModalOpen(false); }} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, minWidth: '440px', fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, } }} > Add user We will send an email to invite them to your organization.
Username { if(e.key === "Enter"){ if (isCloud) { inviteUser(modalUser); } else { submitUser(modalUser); } } }} onChange={(event) => changeModalData("Username", event.target.value) } /> {isCloud ? null : ( Password { if(e.key === "enter"){ if (isCloud) { inviteUser(modalUser); } else { submitUser(modalUser); } } }} onChange={(event) => changeModalData("Password", event.target.value) } /> )}
{loginInfo}
); const run2FASetup = (data) => { if (!show2faSetup) { get2faCode(data.id); } else { // Should remove? setImage2FA(""); setSecret2FA(""); } setShow2faSetup(!show2faSetup); //setShow2faSetup(true); }; const editUserModal = ( { setSelectedUserModalOpen(false); setImage2FA(""); setValue2FA(""); setSecret2FA(""); setShow2faSetup(false); }} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, minWidth: "800px", minHeight: "320px", overflow: "hidden", '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, } }} > Editing {selectedUser.username} {isCloud ? null : (
{ setNewUsername(e.target.value); }} />
)} {isCloud ? null : (
setNewPassword(e.target.value)} />
)} {userOrgEdit}
{isCloud && userdata.support && selectedUser.id !== userdata.id ? ( ) : null} {showDeleteAccountTextbox ? ( { setDeleteAccountText(e.target.value); }} /> ) : null}
{show2faSetup ? (
{/**/} {secret2FA !== undefined && secret2FA !== null && secret2FA?.length > 0 ? ( Scan the image below with the two-factor authentication app on your phone. If you can’t use a QR code, use the code{" "} {secret2FA} instead. ) : null} {image2FA !== undefined && image2FA !== null && image2FA?.length > 0 ? ( {"2 ) : ( )} After scanning the QR code image, the app will display a code that you can enter below.
{ if (event.target.value.length > 6) { return; } setValue2FA(event.target.value); }} />
) : null}
); const getLogs = (ip, userId) => { setLogsLoading(true); console.log("logs loading: ", logsLoading); fetch(`${globalUrl}/api/v1/users/${userId}/audit?user_ip=${ip}`, { mode: "cors", method: "GET", credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => { return response.json(); }) .then((responseJson) => { console.log("ResponseJSON: ", responseJson); if (responseJson.success === true) { setLogs(responseJson.logs); } else { if ( responseJson.success === false || responseJson.reason !== undefined ) { console.log("Reason given: ", responseJson.reason); toast("Failed getting logs: " + responseJson.reason); setLogs([]); } else { toast("Failed getting logs"); } } console.log("logs loading now: ", logsLoading); setLogsLoading(false); }) .catch((error) => { console.log("Error: ", error); toast("Failed getting logs. Please contact: ", error); console.log("logs loading now: ", logsLoading); setLogsLoading(false); }); }; const logview = logsViewModal ? ( { setLogsViewModal(false); }} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, minWidth: "1200px", minHeight: "320px", fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} > User Logs {/* ask user for which IP they want to see logs for by iterating of user.login_info */} User IP {logsLoading && ipSelected.length !== 0 ? (
Loading logs
) : null} {logs.map((data, index) => { //console.log("LOG: ", data) return ( // redirect user to logs // using request id or trace id )})}
) : null return (
{modalView} {editUserModal} {logview}

User Management

Add, edit, distribute or remove users from your organization.{" "} Configure SSO   or   learn more about users
MFA Required { UpdateMFAInUserOrg(selectedOrganization.id); }} />
{["Username", "API Key", "Role", "Active", "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => ( ))} {showLoader ? ( [...Array(6)].map((_, rowIndex) => ( {Array(9) .fill() .map((_, colIndex) => ( ))} )) ): users === 0 ? null : users?.map((data, index) => { var bgColor = "#212121"; if (index % 2 === 0) { bgColor = "#1A1A1A"; } const timeNow = new Date().getTime(); // Get the highest timestamp in data.login_info var lastLogin = "N/A"; if (data.login_info !== undefined && data.login_info !== null) { var loginInfo = 0; for (var i = 0; i < data?.login_info?.length; i++) { if (data.login_info[i].timestamp > loginInfo) { loginInfo = data.login_info[i].timestamp; } } if (loginInfo > 0) { lastLogin = new Date(loginInfo * 1000).toISOString().slice(0, 10) + " (" + data?.login_info?.length + ")"; } } var userData = data.username; if (userdata.support === true) { userData = ( { setLogsViewModal(true); setUserLogViewing(data); if (userLogViewing.login_info !== undefined && userLogViewing.login_info !== null && userLogViewing.login_info.length > 0) { getLogs(userLogViewing.login_info[0].ip, userLogViewing.id) setIpSelected(userLogViewing.login_info[0].ip); } }} > {data.username} ); } return ( {userData || 'No username'} )} primaryTypographyProps={{ style: { maxWidth: 150, minWidth: 100, width: 'auto', color: "#FF8444", textOverflow: "ellipsis", whiteSpace: "nowrap", overflow: "hidden", padding: "8px 8px 8px 15px", }, }} style={{display:'table-cell', verticalAlign: 'middle' }} /> { navigator.clipboard.writeText(data.apikey); toast.success("Apikey copied to clipboard"); }} > ) } /> { console.log("VALUE: ", e.target.value); setUser(data.id, "role", e.target.value); }} sx={{ backgroundColor: "#1A1A1A", color: "white", height: "50px", borderRadius: "4px", marginTop: "8px", marginBottom: "8px", padding: "8px", }} MenuProps={{ PaperProps: { sx: { "& .MuiList-root": { padding: 0, }, }, }, }} > Org Admin Org User Org Reader } style={{ display:'table-cell', verticalAlign: 'middle' }} /> {selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== null && selectedOrganization?.child_orgs?.length > 0 ? ( ) : null} { setSelectedUserModalOpen(true); setSelectedUser(data); // Find matching orgs between current org and current user's access to those orgs if ( userdata?.orgs !== undefined && userdata?.orgs !== null && userdata?.orgs?.length > 0 && selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== null && selectedOrganization?.child_orgs?.length > 0 ) { var active = []; for (var key in userdata.orgs) { const found = selectedOrganization.child_orgs.find( (item) => item.id === userdata.orgs[key].id ); if (found !== null && found !== undefined) { if ( data.orgs === undefined || data.orgs === null ) { continue; } const subfound = data.orgs.find( (item) => item === found.id ); if ( subfound !== null && subfound !== undefined ) { active.push(subfound); } } } setMatchingOrganizations(active); } }} > edit icon {/* */} ); })}
); }) export default UserManagmentTab;