Team impersonation (#5863)

* Re Implement frontend logic to impersonate team members

* Refactor dialog + correct disable toggle.

* fix translation

* Update packages/features/ee/impersonation/lib/ImpersonationProvider.ts
This commit is contained in:
sean-brydon
2022-12-05 12:03:36 +00:00
committed by GitHub
parent 1a26a254ab
commit 58270fa653
4 changed files with 77 additions and 49 deletions
@@ -909,7 +909,7 @@
"impersonate": "Impersonate",
"user_impersonation_heading": "User Impersonation",
"user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.",
"team_impersonation_description": "Allows your team members to temporarily sign in as you.",
"team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.",
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
@@ -6,7 +6,7 @@ import { z } from "zod";
import prisma from "@calcom/prisma";
const teamIdschema = z.object({
teamId: z.number(),
teamId: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number().positive()),
});
const auditAndReturnNextUser = async (
@@ -54,7 +54,7 @@ const ImpersonationProvider = CredentialsProvider({
// @ts-ignore need to figure out how to correctly type this
const session = await getSession({ req });
// If teamId is present -> parse the teamId and throw error itn ot number. If not present teamId is set to undefined
const teamId = creds?.teamId ? teamIdschema.parse(creds).teamId : undefined;
const teamId = creds?.teamId ? teamIdschema.parse({ teamId: creds.teamId }).teamId : undefined;
if (session?.user.username === creds?.username) {
throw new Error("You cannot impersonate yourself.");
@@ -49,9 +49,9 @@ const DisableTeamImpersonation = ({
<div className="mt-5 sm:mt-0 sm:self-center">
<Switch
disabled={disabled}
defaultChecked={query.data?.disableImpersonation}
defaultChecked={!query.data?.disableImpersonation}
onCheckedChange={(isChecked) => {
mutation.mutate({ teamId, memberId, disableImpersonation: isChecked });
mutation.mutate({ teamId, memberId, disableImpersonation: !isChecked });
}}
/>
</div>
@@ -1,5 +1,6 @@
import { MembershipRole } from "@prisma/client";
import classNames from "classnames";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -12,9 +13,9 @@ import {
ButtonGroup,
ConfirmationDialogContent,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
Dropdown,
DropdownItem,
DropdownMenuContent,
@@ -48,6 +49,8 @@ export default function MemberListItem(props: Props) {
const utils = trpc.useContext();
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false);
const [showImpersonateModal, setShowImpersonateModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const removeMemberMutation = trpc.viewer.teams.removeMember.useMutation({
async onSuccess() {
@@ -83,6 +86,11 @@ export default function MemberListItem(props: Props) {
ownersInTeam() > 1 ||
props.member.id !== currentUserId)) ||
(props.team.membership.role === MembershipRole.ADMIN && props.member.role !== MembershipRole.OWNER);
const impersonationMode =
editMode &&
!props.member.disableImpersonation &&
props.member.accepted &&
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
return (
<li className="divide-y px-5">
@@ -161,28 +169,28 @@ export default function MemberListItem(props: Props) {
{t("edit") as string}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild className="p-0">
{impersonationMode && (
<>
<DropdownMenuItem>
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="destructive"
StartIcon={Icon.FiTrash}
className="px-3 py-2 font-normal">
{t("delete")}
onClick={() => setShowImpersonateModal(true)}
color="minimal"
StartIcon={Icon.FiLock}
className="w-full flex-shrink-0 font-normal">
{t("impersonate")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("remove_member")}
confirmBtnText={t("confirm_remove_member")}
onConfirm={removeMember}>
{t("remove_member_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
</>
)}
<DropdownMenuItem>
<DropdownItem
type="button"
onClick={() => setShowDeleteModal(true)}
color="destructive"
StartIcon={Icon.FiTrash}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
@@ -216,30 +224,14 @@ export default function MemberListItem(props: Props) {
{t("edit") as string}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild className="p-0">
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="destructive"
StartIcon={Icon.FiTrash}
className="px-3 py-2 font-normal">
{t("delete")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("remove_member")}
confirmBtnText={t("confirm_remove_member")}
onConfirm={removeMember}>
{t("remove_member_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
<DropdownItem
type="button"
color="destructive"
onClick={() => setShowDeleteModal(true)}
StartIcon={Icon.FiTrash}>
{t("edit") as string}
</DropdownItem>
</DropdownMenuItem>
</>
)}
@@ -249,6 +241,42 @@ export default function MemberListItem(props: Props) {
</div>
)}
</div>
{editMode && (
<Dialog open={showDeleteModal} onOpenChange={() => setShowDeleteModal(false)}>
<ConfirmationDialogContent
variety="danger"
title={t("remove_member")}
confirmBtnText={t("confirm_remove_member")}
onConfirm={removeMember}>
{t("remove_member_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
)}
{showImpersonateModal && props.member.username && (
<Dialog open={showImpersonateModal} onOpenChange={() => setShowImpersonateModal(false)}>
<DialogContent type="creation" title={t("impersonate")} description={t("impersonation_user_tip")}>
<form
onSubmit={async (e) => {
e.preventDefault();
await signIn("impersonation-auth", {
username: props.member.username,
teamId: props.team.id,
});
setShowImpersonateModal(false);
}}>
<DialogFooter>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
<Button color="primary" type="submit">
{t("impersonate")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)}
{showChangeMemberRoleModal && (
<MemberChangeRoleModal
isOpen={showChangeMemberRoleModal}