Feat/impersonate users (#2503)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
6197ae25c6
commit
1421b9c0af
13 changed files with 285 additions and 34 deletions
|
@ -1,49 +1,52 @@
|
||||||
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import Link, { LinkProps } from "next/link";
|
import Link, { LinkProps } from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ElementType, FC } from "react";
|
import React, { ElementType, FC, Fragment } from "react";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
interface Props {
|
export interface NavTabProps {
|
||||||
tabs: {
|
tabs: {
|
||||||
name: string;
|
name: string;
|
||||||
href: string;
|
href: string;
|
||||||
icon?: ElementType;
|
icon?: ElementType;
|
||||||
|
adminRequired?: boolean;
|
||||||
}[];
|
}[];
|
||||||
linkProps?: Omit<LinkProps, "href">;
|
linkProps?: Omit<LinkProps, "href">;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavTabs: FC<Props> = ({ tabs, linkProps }) => {
|
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav className="-mb-px flex space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse" aria-label="Tabs">
|
||||||
className="-mb-px flex space-x-2 space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse"
|
|
||||||
aria-label="Tabs">
|
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isCurrent = router.asPath === tab.href;
|
const isCurrent = router.asPath === tab.href;
|
||||||
|
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
||||||
return (
|
return (
|
||||||
<Link key={tab.name} href={tab.href} {...linkProps}>
|
<Component key={tab.name}>
|
||||||
<a
|
<Link href={tab.href} {...linkProps}>
|
||||||
className={classNames(
|
<a
|
||||||
isCurrent
|
className={classNames(
|
||||||
? "border-neutral-900 text-neutral-900"
|
isCurrent
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
? "border-neutral-900 text-neutral-900"
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||||
)}
|
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
|
||||||
aria-current={isCurrent ? "page" : undefined}>
|
)}
|
||||||
{tab.icon && (
|
aria-current={isCurrent ? "page" : undefined}>
|
||||||
<tab.icon
|
{tab.icon && (
|
||||||
className={classNames(
|
<tab.icon
|
||||||
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
className={classNames(
|
||||||
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
||||||
)}
|
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||||
aria-hidden="true"
|
)}
|
||||||
/>
|
aria-hidden="true"
|
||||||
)}
|
/>
|
||||||
<span>{tab.name}</span>
|
)}
|
||||||
</a>
|
<span>{tab.name}</span>
|
||||||
</Link>
|
</a>
|
||||||
|
</Link>
|
||||||
|
</Component>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
|
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs, { NavTabProps } from "./NavTabs";
|
||||||
|
|
||||||
export default function SettingsShell({ children }: { children: React.ReactNode }) {
|
export default function SettingsShell({ children }: { children: React.ReactNode }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -29,6 +29,12 @@ export default function SettingsShell({ children }: { children: React.ReactNode
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: CreditCardIcon,
|
icon: CreditCardIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t("admin"),
|
||||||
|
href: "/settings/admin",
|
||||||
|
icon: LockClosedIcon,
|
||||||
|
adminRequired: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, ReactNode, useEffect } from "react";
|
import React, { Fragment, ReactNode, useEffect } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core";
|
import { useIsEmbed } from "@calcom/embed-core";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -40,6 +40,7 @@ import { trpc } from "@lib/trpc";
|
||||||
import CustomBranding from "@components/CustomBranding";
|
import CustomBranding from "@components/CustomBranding";
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
|
import ImpersonatingBanner from "@components/ui/ImpersonatingBanner";
|
||||||
|
|
||||||
import pkg from "../package.json";
|
import pkg from "../package.json";
|
||||||
import { useViewerI18n } from "./I18nLanguageHandler";
|
import { useViewerI18n } from "./I18nLanguageHandler";
|
||||||
|
@ -128,6 +129,7 @@ const Layout = ({
|
||||||
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
|
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{
|
{
|
||||||
|
@ -311,6 +313,7 @@ const Layout = ({
|
||||||
props.flexChildrenContainer && "flex flex-1 flex-col",
|
props.flexChildrenContainer && "flex flex-1 flex-col",
|
||||||
!props.large && "py-8"
|
!props.large && "py-8"
|
||||||
)}>
|
)}>
|
||||||
|
<ImpersonatingBanner />
|
||||||
{!!props.backPath && (
|
{!!props.backPath && (
|
||||||
<div className="mx-3 mb-8 sm:mx-8">
|
<div className="mx-3 mb-8 sm:mx-8">
|
||||||
<Button
|
<Button
|
||||||
|
|
14
apps/web/components/ui/AdminRequired.tsx
Normal file
14
apps/web/components/ui/AdminRequired.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { FC, Fragment } from "react";
|
||||||
|
|
||||||
|
type AdminRequiredProps = {
|
||||||
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminRequired: FC<AdminRequiredProps> = ({ children, as, ...rest }) => {
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
if (session.data?.user.role !== "ADMIN") return null;
|
||||||
|
const Component = as ?? Fragment;
|
||||||
|
return <Component {...rest}>{children}</Component>;
|
||||||
|
};
|
34
apps/web/components/ui/ImpersonatingBanner.tsx
Normal file
34
apps/web/components/ui/ImpersonatingBanner.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
|
|
||||||
|
type Props = {};
|
||||||
|
|
||||||
|
function ImpersonatingBanner({}: Props) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { data } = useSession();
|
||||||
|
|
||||||
|
if (!data?.user.impersonatedByUID) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{t("impersonating_user_warning", { user: data.user.username })}{" "}
|
||||||
|
<Trans i18nKey="impersonating_stop_instructions">
|
||||||
|
<a href="/auth/logout" className="underline">
|
||||||
|
Click Here To stop
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className="mx-4 mb-2 sm:mx-6 md:mx-8"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImpersonatingBanner;
|
62
apps/web/ee/lib/impersonation/ImpersonationProvider.ts
Normal file
62
apps/web/ee/lib/impersonation/ImpersonationProvider.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
import { getSession } from "next-auth/react";
|
||||||
|
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
const ImpersonationProvider = CredentialsProvider({
|
||||||
|
id: "impersonation-auth",
|
||||||
|
name: "Impersonation",
|
||||||
|
type: "credentials",
|
||||||
|
credentials: {
|
||||||
|
username: { label: "Username", type: "text " },
|
||||||
|
},
|
||||||
|
async authorize(creds, req) {
|
||||||
|
// @ts-ignore need to figure out how to correctly type this
|
||||||
|
const session = await getSession({ req });
|
||||||
|
if (session?.user.role !== "ADMIN") {
|
||||||
|
throw new Error("You do not have permission to do this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session?.user.username === creds?.username) {
|
||||||
|
throw new Error("You cannot impersonate yourself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
username: creds?.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("This user does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log impersonations for audit purposes
|
||||||
|
await prisma.impersonations.create({
|
||||||
|
data: {
|
||||||
|
impersonatedBy: {
|
||||||
|
connect: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
impersonatedUser: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
impersonatedByUID: session?.user.id,
|
||||||
|
};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ImpersonationProvider;
|
|
@ -1,5 +1,5 @@
|
||||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider, UserPermissionRole } from "@prisma/client";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
import NextAuth, { Session } from "next-auth";
|
import NextAuth, { Session } from "next-auth";
|
||||||
|
@ -15,6 +15,7 @@ import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||||
|
import ImpersonationProvider from "@ee/lib/impersonation/ImpersonationProvider";
|
||||||
|
|
||||||
import { ErrorCode, verifyPassword } from "@lib/auth";
|
import { ErrorCode, verifyPassword } from "@lib/auth";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
@ -103,9 +104,11 @@ const providers: Provider[] = [
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
ImpersonationProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (IS_GOOGLE_LOGIN_ENABLED) {
|
if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||||
|
@ -213,6 +216,8 @@ export default NextAuth({
|
||||||
username: existingUser.username,
|
username: existingUser.username,
|
||||||
name: existingUser.name,
|
name: existingUser.name,
|
||||||
email: existingUser.email,
|
email: existingUser.email,
|
||||||
|
role: existingUser.role,
|
||||||
|
impersonatedByUID: token?.impersonatedByUID as number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +234,8 @@ export default NextAuth({
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
impersonatedByUID: user?.impersonatedByUID as number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +269,8 @@ export default NextAuth({
|
||||||
name: existingUser.name,
|
name: existingUser.name,
|
||||||
username: existingUser.username,
|
username: existingUser.username,
|
||||||
email: existingUser.email,
|
email: existingUser.email,
|
||||||
|
role: existingUser.role,
|
||||||
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +284,8 @@ export default NextAuth({
|
||||||
id: token.id as number,
|
id: token.id as number,
|
||||||
name: token.name,
|
name: token.name,
|
||||||
username: token.username as string,
|
username: token.username as string,
|
||||||
|
role: token.role as UserPermissionRole,
|
||||||
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return calendsoSession;
|
return calendsoSession;
|
||||||
|
|
73
apps/web/pages/settings/admin.tsx
Normal file
73
apps/web/pages/settings/admin.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import Button from "@calcom/ui/Button";
|
||||||
|
import { TextField } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
|
import SettingsShell from "@components/SettingsShell";
|
||||||
|
import Shell from "@components/Shell";
|
||||||
|
|
||||||
|
function AdminView() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const usernameRef = useRef<HTMLInputElement>(null!);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
<div className="py-6 lg:pb-8">
|
||||||
|
<form
|
||||||
|
className="mb-6 w-full sm:w-1/2"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const enteredUsername = usernameRef.current.value.toLowerCase();
|
||||||
|
signIn("impersonation-auth", { username: enteredUsername }).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
name="Impersonate User"
|
||||||
|
addOnLeading={
|
||||||
|
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
|
||||||
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
ref={usernameRef}
|
||||||
|
defaultValue={undefined}
|
||||||
|
/>
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id="email-description">
|
||||||
|
{t("impersonate_user_tip")}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end py-4">
|
||||||
|
<Button type="submit">{t("impersonate")}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<hr className="mt-8" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Shell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
|
||||||
|
<SettingsShell>
|
||||||
|
<AdminView />
|
||||||
|
</SettingsShell>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const session = await getSession(context);
|
||||||
|
|
||||||
|
if (!session?.user?.id || session.user.role !== "ADMIN") {
|
||||||
|
return { redirect: { permanent: false, destination: "/settings/profile" } };
|
||||||
|
}
|
||||||
|
return { props: {} };
|
||||||
|
};
|
|
@ -763,5 +763,9 @@
|
||||||
"send_reschedule_request": "Request reschedule ",
|
"send_reschedule_request": "Request reschedule ",
|
||||||
"edit_booking": "Edit booking",
|
"edit_booking": "Edit booking",
|
||||||
"reschedule_booking": "Reschedule booking",
|
"reschedule_booking": "Reschedule booking",
|
||||||
"former_time": "Former time"
|
"former_time": "Former time",
|
||||||
|
"impersonate":"Impersonate",
|
||||||
|
"impersonate_user_tip":"All uses of this feature is audited.",
|
||||||
|
"impersonating_user_warning":"Impersonating username \"{{user}}\".",
|
||||||
|
"impersonating_stop_instructions": "<0>Click Here to stop</0>."
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@lib/*": ["../../../apps/web/lib/*"]
|
"@lib/*": ["../../../apps/web/lib/*"]
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["dist", "build", "node_modules", "test-cal.tsx"]
|
"exclude": ["dist", "build", "node_modules", "test-cal.tsx"]
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "UserPermissionRole" AS ENUM ('USER', 'ADMIN');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "role" "UserPermissionRole" NOT NULL DEFAULT E'USER';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Impersonations" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"impersonatedUserId" INTEGER NOT NULL,
|
||||||
|
"impersonatedById" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Impersonations_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Impersonations" ADD CONSTRAINT "Impersonations_impersonatedUserId_fkey" FOREIGN KEY ("impersonatedUserId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Impersonations" ADD CONSTRAINT "Impersonations_impersonatedById_fkey" FOREIGN KEY ("impersonatedById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@ -107,6 +107,11 @@ model DestinationCalendar {
|
||||||
eventTypeId Int? @unique
|
eventTypeId Int? @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserPermissionRole {
|
||||||
|
USER
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String? @unique
|
username String? @unique
|
||||||
|
@ -155,6 +160,9 @@ model User {
|
||||||
allowDynamicBooking Boolean? @default(true)
|
allowDynamicBooking Boolean? @default(true)
|
||||||
metadata Json?
|
metadata Json?
|
||||||
verified Boolean? @default(false)
|
verified Boolean? @default(false)
|
||||||
|
role UserPermissionRole @default(USER)
|
||||||
|
impersonatedUsers Impersonations[] @relation("impersonated_user")
|
||||||
|
impersonatedBy Impersonations[] @relation("impersonated_by_user")
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
|
@ -376,6 +384,15 @@ model Webhook {
|
||||||
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
eventType EventType? @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Impersonations {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
impersonatedUser User @relation("impersonated_user", fields: [impersonatedUserId], references: [id])
|
||||||
|
impersonatedBy User @relation("impersonated_by_user", fields: [impersonatedById], references: [id])
|
||||||
|
impersonatedUserId Int
|
||||||
|
impersonatedById Int
|
||||||
|
}
|
||||||
|
|
||||||
model ApiKey {
|
model ApiKey {
|
||||||
id String @id @unique @default(cuid())
|
id String @id @unique @default(cuid())
|
||||||
userId Int
|
userId Int
|
||||||
|
|
3
packages/types/next-auth.d.ts
vendored
3
packages/types/next-auth.d.ts
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
import NextAuth, { DefaultSession } from "next-auth";
|
import NextAuth, { DefaultSession } from "next-auth";
|
||||||
|
|
||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
|
@ -5,6 +6,8 @@ declare module "next-auth" {
|
||||||
type CalendsoSessionUser = DefaultSessionUser & {
|
type CalendsoSessionUser = DefaultSessionUser & {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
impersonatedByUID?: number;
|
||||||
|
role: UserPermissionRole;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
|
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
|
||||||
|
|
Loading…
Reference in a new issue