blob: 64c57cb3edf4ba3ef4117c824d43e28733f334f4 [file] [log] [blame]
/*
* Copyright (C) 2020-2024 by Savoir-faire Linux
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { FC, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import axios from "axios";
import i18next from "i18next";
import { makeStyles } from "@mui/styles";
import {
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Grid,
Chip,
Theme,
} from "@mui/material";
import auth from "auth";
import configApiCall from "api";
import {
api_path_get_admin_user,
api_path_get_auth_user,
api_path_get_user_profile,
api_path_delete_admin_user_revoke,
api_path_get_group,
api_path_get_admin_user_groups,
api_path_delete_group_member,
} from "globalUrls";
import GridContainer from "components/Grid/GridContainer";
import Button from "components/CustomButtons/Button";
import Card from "components/Card/Card";
import CardAvatar from "components/Card/CardAvatar";
import CardBody from "components/Card/CardBody";
import CardFooter from "components/Card/CardFooter";
import PasswordDialog from "components/PasswordDialog/PasswordDialog";
import noProfilePicture from "assets/img/faces/no-profile-picture.png";
import dashboardStyle from "assets/jss/material-dashboard-react/views/dashboardStyle";
import { hexToRgb, blackColor } from "assets/jss/material-dashboard-react";
import UserProfileFieldsList from "./UserProfileFieldsList";
import AdminAddUserToGroup from "./AdminAddUserToGroup";
import {
DeleteOutlineOutlined,
EditOutlined,
VpnKeyOutlined,
} from "@mui/icons-material";
const styles = (theme: 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: "'Ubuntu'",
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",
},
profileFooter: {
[theme.breakpoints.down("lg")]: {
display: "flex",
flexDirection: "column",
},
},
footerActionButtons: {
display: "flex",
flexDirection: "row",
alignItems: "end",
"& button": {
marginRight: "1rem",
},
[theme.breakpoints.down("lg")]: {
flexDirection: "column",
alignItems: "stretch",
"& button": {
marginRight: "0",
width: "100%",
},
},
},
footerActionButtonsRight: {
display: "flex",
justifyContent: "flex-end",
alignItems: "end",
[theme.breakpoints.down("lg")]: {
flexDirection: "column",
justifyContent: "center",
alignItems: "stretch",
"& button": {
marginRight: "0",
width: "100%",
},
},
},
userProfileHeader: {
display: "flex",
justifyContent: "space-between",
[theme.breakpoints.down("lg")]: {
display: "flex",
flexDirection: "column",
textAlign: "center",
},
},
groups: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
"& > *": {
margin: theme.spacing(0.5),
},
},
cardAvatarMobile: {
[theme.breakpoints.down("lg")]: {
textAlign: "center",
},
},
loading: {
width: "100%",
},
});
const useStyles = makeStyles(styles as any);
interface DisplayUserProfileProps {
setDisplayUser: (displayUser: boolean) => void;
username: string;
}
export interface UserProfile {
username: string;
firstName: string;
lastName: string;
email: string;
profilePicture: string;
organization: string;
phoneNumber: string;
phoneNumberExtension: string;
faxNumber: string;
mobileNumber: string;
id: string;
}
export interface GroupMembership {
groupId: string;
name: string;
}
export interface Group {
id: string;
name: string;
blueprint: string;
}
interface UserGroupMapping {
groupId: string;
username: string;
}
const DisplayUserProfile: FC<DisplayUserProfileProps> = ({
setDisplayUser,
username,
}) => {
const classes = useStyles();
const history = useHistory();
const [user, setUser] = useState<UserProfile>({
username: "",
firstName: "",
lastName: "",
email: "",
profilePicture: "",
organization: "",
phoneNumber: "",
phoneNumberExtension: "",
faxNumber: "",
mobileNumber: "",
id: "",
});
const [groupMemberships, setGroupMemberships] = useState<GroupMembership[]>(
[]
);
const [revoked, setRevoked] = useState(false);
const [open, setOpen] = useState(false);
const [revokedUser, setRevokedUser] = useState("");
const [changePasswordOpen, setChangePasswordOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const removeUserFromGroup = (group: GroupMembership) => {
axios(
configApiCall(
api_path_delete_group_member + group.groupId,
"DELETE",
{ username },
null
)
)
.then(() => {
const newGroupMemberships = groupMemberships;
newGroupMemberships.splice(newGroupMemberships.indexOf(group), 1);
setGroupMemberships(newGroupMemberships);
})
.catch((error) => {
console.log(error);
});
};
useEffect(() => {
setLoading(true);
}, [history, username]);
const getAdminUserGroups = () => {
// TODO do this in a single sql query on the server, with a JOIN
axios(
configApiCall(
api_path_get_admin_user_groups + username,
"GET",
null,
null
)
).then((userGroups) => {
const userGroupsData: UserGroupMapping[] = userGroups.data;
const promises = userGroupsData.map((group) =>
axios(
configApiCall(api_path_get_group + group.groupId, "GET", null, null)
).then((groupInfo) => {
const g: GroupMembership = {
groupId: group.groupId,
name: groupInfo.data.name,
};
return g;
})
);
Promise.all(promises).then((groupMemberships) => {
setGroupMemberships(groupMemberships);
});
});
};
useEffect(() => {
auth.checkDirectoryType(() => {
const requestConfig = auth.hasAdminScope()
? configApiCall(api_path_get_admin_user, "GET", { username }, null)
: configApiCall(
api_path_get_auth_user + "?username=" + username,
"GET",
null,
null
);
axios(requestConfig)
.then((response) => {
const result = response.data;
setRevoked(result.revoked);
axios(
configApiCall(
api_path_get_user_profile + username,
"GET",
null,
null
)
).then((response) => {
setUser(response.data);
getAdminUserGroups();
setLoading(false);
});
})
.catch((error) => {
if (error.response && error.response.status === 401) {
auth.authenticated = false;
history.push("/signin");
} else {
console.error("Error getting user: " + error);
}
});
});
}, [history, username]);
const getUserStatus = () => {
const label = revoked
? (i18next.t("revoked", "Revoked") as string)
: (i18next.t("active", "Active") as string);
return (
<Chip
style={{ flex: 1 }}
label={label}
variant="outlined"
clickable={false}
disabled={revoked}
/>
);
};
const revokeUser = () => {
axios(
configApiCall(
api_path_delete_admin_user_revoke,
"DELETE",
{ username: revokedUser },
null
)
)
.then(() => {
setRevoked(true);
})
.catch((error) => {
console.log(
"Error revoking user: " + revokedUser + " with error: " + error
);
});
setOpen(false);
};
const handleClickOpen = (username: string) => {
setRevokedUser(username);
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const canEdit = () => {
if (!auth.isLocalDirectory()) {
return false;
}
if (!auth.hasAdminScope() && auth.getUsername() !== user.username) {
return false;
}
return true;
};
return (
<div>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{i18next.t("revoke_user_account", "Revoke user account") as string}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{
i18next.t(
"are_you_sure_want_revoke",
"Are you sure you want to revoke"
) as string
}{" "}
<strong>{revokedUser}</strong> ?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
{i18next.t("cancel", "Cancel") as string}
</Button>
<Button onClick={revokeUser} color="info" autoFocus>
{i18next.t("revoke", "Revoke") as string}
</Button>
</DialogActions>
</Dialog>
<PasswordDialog
username={username}
open={changePasswordOpen}
onClose={() => setChangePasswordOpen(false)}
/>
{!loading && (
<GridContainer>
<Grid item xs={12} sm={12} md={6}>
<Card profile>
<CardBody profile>
<div className={classes.root}>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} md={6}>
<CardAvatar
displayProfile
className={classes.cardAvatarMobile}
>
<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}>
<div className={classes.userProfileHeader}>
<div>
<h3 className={classes.cardTitle}>{user.username}</h3>
{getUserStatus()}
</div>
</div>
</Grid>
<Grid item xs={12}>
<UserProfileFieldsList user={user} />
</Grid>
</Grid>
</div>
</CardBody>
<CardFooter className={classes.profileFooter}>
<Grid container className={classes.footerActionButtons}>
<Grid item>
{canEdit() && (
<Button
color="info"
onClick={() => setDisplayUser(false)}
>
<EditOutlined />{" "}
{i18next.t("edit_profile", "Edit profile") as string}
</Button>
)}
</Grid>
<Grid item>
{auth.isLocalDirectory() && auth.hasAdminScope() && (
<Button
color="info"
onClick={() => {
setChangePasswordOpen(true);
}}
>
<VpnKeyOutlined />{" "}
{
i18next.t(
"change_password",
"Change password"
) as string
}
</Button>
)}
</Grid>
</Grid>
<Grid container className={classes.footerActionButtonsRight}>
<Grid item>
{auth.hasAdminScope() && revoked === false && (
<Button
color="info"
onClick={() => handleClickOpen(user.username)}
>
<DeleteOutlineOutlined fontSize="small" />{" "}
{i18next.t("revoke_user", "Revoke user") as string}
</Button>
)}
</Grid>
</Grid>
</CardFooter>
</Card>
</Grid>
<AdminAddUserToGroup
username={username}
openDrawer={openDrawer}
setOpenDrawer={setOpenDrawer}
classes={classes}
groupMemberships={groupMemberships}
setGroupMemberships={setGroupMemberships}
removeUserFromGroup={removeUserFromGroup}
/>
</GridContainer>
)}
</div>
);
};
export default DisplayUserProfile;