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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user