Team Impersonation (#3450)
* Team Impersonation * Refactor - Disable Imeprsonate button * Change copy * Add .env toggle * Fix eslint * Update role selection Co-authored-by: Omar López <zomars@me.com> * New 'admin' seed user, improve flow ImpersonationProvider * Fix impersonation string * Adds fullstop Co-authored-by: Sean Brydon <seanbrydon.me@gmail.ocm> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>pull/3452/head^2
parent
471420c1d4
commit
d3fcb8bf7d
|
@ -127,3 +127,6 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
|
||||||
## @see https://support.google.com/accounts/answer/185833
|
## @see https://support.google.com/accounts/answer/185833
|
||||||
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
|
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
|
||||||
# **********************************************************************************************************
|
# **********************************************************************************************************
|
||||||
|
|
||||||
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
|
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
|
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
const query = trpc.useQuery(["viewer.teams.getMembershipbyUser", { teamId, memberId }]);
|
||||||
|
|
||||||
|
const mutation = trpc.useMutation("viewer.teams.updateMembership", {
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||||
|
await utils.invalidateQueries(["viewer.teams.getMembershipbyUser"]);
|
||||||
|
},
|
||||||
|
async onSettled() {
|
||||||
|
await utils.invalidateQueries(["viewer.public.i18n"]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (query.isLoading) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="font-cal mt-7 pb-4 text-xl leading-6 text-gray-900">{t("settings")}</h3>
|
||||||
|
<div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 pb-4 sm:px-6">
|
||||||
|
<div className="flex flex-col justify-between pt-4 sm:flex-row">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<h2 className="font-cal font-bold leading-6 text-gray-900">
|
||||||
|
{t("user_impersonation_heading")}
|
||||||
|
</h2>
|
||||||
|
<Badge
|
||||||
|
className="ml-2 text-xs"
|
||||||
|
variant={!query.data?.disableImpersonation ? "success" : "gray"}>
|
||||||
|
{!query.data?.disableImpersonation ? t("enabled") : t("disabled")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-700">{t("team_impersonation_description")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
!query.data?.disableImpersonation
|
||||||
|
? mutation.mutate({ teamId, memberId, disableImpersonation: true })
|
||||||
|
: mutation.mutate({ teamId, memberId, disableImpersonation: false })
|
||||||
|
}>
|
||||||
|
{!query.data?.disableImpersonation ? t("disable") : t("enable")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisableTeamImpersonation;
|
|
@ -1,6 +1,7 @@
|
||||||
import { PencilIcon, UserRemoveIcon } from "@heroicons/react/outline";
|
import { LockClosedIcon, PencilIcon, UserRemoveIcon } from "@heroicons/react/outline";
|
||||||
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon } from "@heroicons/react/solid";
|
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon } from "@heroicons/react/solid";
|
||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@prisma/client";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ export default function MemberListItem(props: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
|
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
|
||||||
const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false);
|
const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false);
|
||||||
|
const [showImpersonateModal, setShowImpersonateModal] = useState(false);
|
||||||
|
|
||||||
const removeMemberMutation = trpc.useMutation("viewer.teams.removeMember", {
|
const removeMemberMutation = trpc.useMutation("viewer.teams.removeMember", {
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
|
@ -147,6 +149,24 @@ export default function MemberListItem(props: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
|
{/* Only show impersonate box if - The user has impersonation enabled,
|
||||||
|
They have accepted the team invite, and it is enabled for this instance */}
|
||||||
|
{!props.member.disableImpersonation &&
|
||||||
|
props.member.accepted &&
|
||||||
|
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true" && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowImpersonateModal(true)}
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={LockClosedIcon}
|
||||||
|
className="w-full flex-shrink-0 font-normal">
|
||||||
|
{t("impersonate")}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
@ -185,6 +205,39 @@ export default function MemberListItem(props: Props) {
|
||||||
onExit={() => setShowChangeMemberRoleModal(false)}
|
onExit={() => setShowChangeMemberRoleModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showImpersonateModal && props.member.username && (
|
||||||
|
<ModalContainer isOpen={showImpersonateModal} onExit={() => setShowImpersonateModal(false)}>
|
||||||
|
<>
|
||||||
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
{t("impersonate")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await signIn("impersonation-auth", {
|
||||||
|
username: props.member.username,
|
||||||
|
teamId: props.team.id,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
{t("impersonate_user_tip")}
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
|
<Button type="submit" color="primary" className="ltr:ml-2 rtl:mr-2">
|
||||||
|
{t("impersonate")}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" color="secondary" onClick={() => setShowImpersonateModal(false)}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
</ModalContainer>
|
||||||
|
)}
|
||||||
{showTeamAvailabilityModal && (
|
{showTeamAvailabilityModal && (
|
||||||
<ModalContainer
|
<ModalContainer
|
||||||
wide
|
wide
|
||||||
|
|
|
@ -1,66 +1,136 @@
|
||||||
|
import { User } from "@prisma/client";
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
|
|
||||||
|
import { asNumberOrThrow } from "@lib/asStringOrNull";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
const auditAndReturnNextUser = async (
|
||||||
|
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role">,
|
||||||
|
impersonatedByUID: number
|
||||||
|
) => {
|
||||||
|
// Log impersonations for audit purposes
|
||||||
|
await prisma.impersonations.create({
|
||||||
|
data: {
|
||||||
|
impersonatedBy: {
|
||||||
|
connect: {
|
||||||
|
id: impersonatedByUID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
impersonatedUser: {
|
||||||
|
connect: {
|
||||||
|
id: impersonatedUser.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: impersonatedUser.id,
|
||||||
|
username: impersonatedUser.username,
|
||||||
|
email: impersonatedUser.email,
|
||||||
|
name: impersonatedUser.name,
|
||||||
|
role: impersonatedUser.role,
|
||||||
|
impersonatedByUID,
|
||||||
|
};
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
const ImpersonationProvider = CredentialsProvider({
|
const ImpersonationProvider = CredentialsProvider({
|
||||||
id: "impersonation-auth",
|
id: "impersonation-auth",
|
||||||
name: "Impersonation",
|
name: "Impersonation",
|
||||||
type: "credentials",
|
type: "credentials",
|
||||||
credentials: {
|
credentials: {
|
||||||
username: { label: "Username", type: "text " },
|
username: { type: "text" },
|
||||||
|
teamId: { type: "text" },
|
||||||
},
|
},
|
||||||
async authorize(creds, req) {
|
async authorize(creds, req) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore need to figure out how to correctly type this
|
// @ts-ignore need to figure out how to correctly type this
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
if (session?.user.role !== "ADMIN") {
|
const teamId = creds?.teamId ? asNumberOrThrow(creds.teamId) : undefined;
|
||||||
throw new Error("You do not have permission to do this.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session?.user.username === creds?.username) {
|
if (session?.user.username === creds?.username) {
|
||||||
throw new Error("You cannot impersonate yourself.");
|
throw new Error("You cannot impersonate yourself.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
if (!creds?.username) throw new Error("Username must be present");
|
||||||
|
// If you are an ADMIN we return way before team impersonation logic is executed, so NEXT_PUBLIC_TEAM_IMPERSONATION certainly true
|
||||||
|
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
|
||||||
|
throw new Error("You do not have permission to do this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user who is being impersonated
|
||||||
|
const impersonatedUser = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username: creds?.username,
|
username: creds?.username,
|
||||||
},
|
},
|
||||||
});
|
select: {
|
||||||
|
id: true,
|
||||||
if (!user) {
|
username: true,
|
||||||
throw new Error("This user does not exist");
|
role: true,
|
||||||
}
|
name: true,
|
||||||
|
email: true,
|
||||||
if (user.disableImpersonation) {
|
disableImpersonation: true,
|
||||||
throw new Error("This user has disabled Impersonation.");
|
teams: {
|
||||||
}
|
where: {
|
||||||
|
disableImpersonation: false, // Ensure they have impersonation enabled
|
||||||
// Log impersonations for audit purposes
|
accepted: true, // Ensure they are apart of the team and not just invited.
|
||||||
await prisma.impersonations.create({
|
team: {
|
||||||
data: {
|
id: teamId, // Bring back only the right team
|
||||||
impersonatedBy: {
|
},
|
||||||
connect: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
},
|
||||||
},
|
select: {
|
||||||
impersonatedUser: {
|
teamId: true,
|
||||||
connect: {
|
disableImpersonation: true,
|
||||||
id: user.id,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj = {
|
// Check if impersonating is allowed for this user
|
||||||
id: user.id,
|
if (!impersonatedUser) {
|
||||||
username: user.username,
|
throw new Error("This user does not exist");
|
||||||
email: user.email,
|
}
|
||||||
name: user.name,
|
|
||||||
role: user.role,
|
if (session?.user.role === "ADMIN") {
|
||||||
impersonatedByUID: session?.user.id,
|
if (impersonatedUser.disableImpersonation) {
|
||||||
};
|
throw new Error("This user has disabled Impersonation.");
|
||||||
return obj;
|
}
|
||||||
|
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session
|
||||||
|
const sessionUserFromDb = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session?.user.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
teams: {
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
role: {
|
||||||
|
in: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionUserFromDb?.teams.length === 0 || impersonatedUser.teams.length === 0) {
|
||||||
|
throw new Error("You do not have permission to do this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ export async function getTeamWithMembers(id?: number, slug?: string) {
|
||||||
isMissingSeat: obj.user.plan === UserPlan.FREE,
|
isMissingSeat: obj.user.plan === UserPlan.FREE,
|
||||||
role: membership?.role,
|
role: membership?.role,
|
||||||
accepted: membership?.accepted,
|
accepted: membership?.accepted,
|
||||||
|
disableImpersonation: membership?.disableImpersonation,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,12 @@ import SAMLConfiguration from "@ee/components/saml/Configuration";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||||
|
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
|
import DisableTeamImpersonation from "@components/team/DisableTeamImpersonation";
|
||||||
import MemberInvitationModal from "@components/team/MemberInvitationModal";
|
import MemberInvitationModal from "@components/team/MemberInvitationModal";
|
||||||
import MemberList from "@components/team/MemberList";
|
import MemberList from "@components/team/MemberList";
|
||||||
import TeamSettings from "@components/team/TeamSettings";
|
import TeamSettings from "@components/team/TeamSettings";
|
||||||
|
@ -25,6 +27,7 @@ import Avatar from "@components/ui/Avatar";
|
||||||
export function TeamSettingsPage() {
|
export function TeamSettingsPage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const userId = useCurrentUserId();
|
||||||
|
|
||||||
const upgraded = router.query.upgraded as string;
|
const upgraded = router.query.upgraded as string;
|
||||||
|
|
||||||
|
@ -165,6 +168,7 @@ export function TeamSettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
<MemberList team={team} members={team.members || []} />
|
<MemberList team={team} members={team.members || []} />
|
||||||
{isAdmin && <SAMLConfiguration teamsView={true} teamId={team.id} />}
|
{isAdmin && <SAMLConfiguration teamsView={true} teamId={team.id} />}
|
||||||
|
{userId && <DisableTeamImpersonation teamId={team.id} memberId={userId} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-32 mt-8 w-full px-2 ltr:ml-2 rtl:mr-2 sm:mt-0 md:w-3/12">
|
<div className="min-w-32 mt-8 w-full px-2 ltr:ml-2 rtl:mr-2 sm:mt-0 md:w-3/12">
|
||||||
<TeamSettingsRightSidebar role={team.membership.role} team={team} />
|
<TeamSettingsRightSidebar role={team.membership.role} team={team} />
|
||||||
|
|
|
@ -838,6 +838,7 @@
|
||||||
"impersonate": "Impersonate",
|
"impersonate": "Impersonate",
|
||||||
"user_impersonation_heading": "User Impersonation",
|
"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.",
|
"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 admins to temporarily sign in as you.",
|
||||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||||
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
|
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
|
import { resolve } from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||||
|
@ -460,4 +461,54 @@ export const viewerTeamsRouter = createProtectedRouter()
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
return await ensureSubscriptionQuantityCorrectness(ctx.user.id, input.teamId);
|
return await ensureSubscriptionQuantityCorrectness(ctx.user.id, input.teamId);
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.query("getMembershipbyUser", {
|
||||||
|
input: z.object({
|
||||||
|
teamId: z.number(),
|
||||||
|
memberId: z.number(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
if (ctx.user.id !== input.memberId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You cannot view memberships that are not your own.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ctx.prisma.membership.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_teamId: {
|
||||||
|
userId: input.memberId,
|
||||||
|
teamId: input.teamId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("updateMembership", {
|
||||||
|
input: z.object({
|
||||||
|
teamId: z.number(),
|
||||||
|
memberId: z.number(),
|
||||||
|
disableImpersonation: z.boolean(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
if (ctx.user.id !== input.memberId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You cannot edit memberships that are not your own.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ctx.prisma.membership.update({
|
||||||
|
where: {
|
||||||
|
userId_teamId: {
|
||||||
|
userId: input.memberId,
|
||||||
|
teamId: input.teamId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
disableImpersonation: input.disableImpersonation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Membership" ADD COLUMN "disableImpersonation" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -206,12 +206,13 @@ enum MembershipRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Membership {
|
model Membership {
|
||||||
teamId Int
|
teamId Int
|
||||||
userId Int
|
userId Int
|
||||||
accepted Boolean @default(false)
|
accepted Boolean @default(false)
|
||||||
role MembershipRole
|
role MembershipRole
|
||||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamId], references: [id])
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
disableImpersonation Boolean @default(false)
|
||||||
|
|
||||||
@@id([userId, teamId])
|
@@id([userId, teamId])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BookingStatus, MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
import { BookingStatus, MembershipRole, Prisma, UserPermissionRole, UserPlan } from "@prisma/client";
|
||||||
import { uuid } from "short-uuid";
|
import { uuid } from "short-uuid";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
@ -18,6 +18,7 @@ async function createUserAndEventType(opts: {
|
||||||
name: string;
|
name: string;
|
||||||
completedOnboarding?: boolean;
|
completedOnboarding?: boolean;
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
|
role?: UserPermissionRole;
|
||||||
};
|
};
|
||||||
eventTypes: Array<
|
eventTypes: Array<
|
||||||
Prisma.EventTypeCreateInput & {
|
Prisma.EventTypeCreateInput & {
|
||||||
|
@ -468,6 +469,18 @@ async function main() {
|
||||||
eventTypes: [],
|
eventTypes: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await createUserAndEventType({
|
||||||
|
user: {
|
||||||
|
email: "admin@example.com",
|
||||||
|
password: "admin",
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin Example",
|
||||||
|
plan: "PRO",
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
eventTypes: [],
|
||||||
|
});
|
||||||
|
|
||||||
const pro2UserTeam = await createUserAndEventType({
|
const pro2UserTeam = await createUserAndEventType({
|
||||||
user: {
|
user: {
|
||||||
email: "teampro2@example.com",
|
email: "teampro2@example.com",
|
||||||
|
@ -536,6 +549,7 @@ async function main() {
|
||||||
{
|
{
|
||||||
id: pro2UserTeam.id,
|
id: pro2UserTeam.id,
|
||||||
username: pro2UserTeam.name || "Unknown",
|
username: pro2UserTeam.name || "Unknown",
|
||||||
|
role: "MEMBER",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pro3UserTeam.id,
|
id: pro3UserTeam.id,
|
||||||
|
|
Loading…
Reference in New Issue