feat: Zapier For Teams (#9851)
Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
dcd7e80e6a
commit
e98bebb9b2
|
@ -56,6 +56,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const handlerMap = (await import("@calcom/app-store/apps.server.generated")).apiHandlers;
|
||||
const handlerKey = deriveAppDictKeyFromType(appName, handlerMap);
|
||||
const handlers = await handlerMap[handlerKey as keyof typeof handlerMap];
|
||||
if (!handlers) throw new HttpError({ statusCode: 404, message: `No handlers found for ${handlerKey}` });
|
||||
const handler = handlers[apiEndpoint as keyof typeof handlers] as AppHandler;
|
||||
let redirectUrl = "/apps/installed";
|
||||
if (typeof handler === "undefined")
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"verify_email_banner_body": "Verify your email address to guarantee the best email and calendar deliverability",
|
||||
"verify_email_email_header": "Verify your email address",
|
||||
"verify_email_email_button": "Verify email",
|
||||
"copy_somewhere_safe": "Save this API key somewhere safe. You will not be able to view it again.",
|
||||
"verify_email_email_body": "Please verify your email address by clicking the button below.",
|
||||
"verify_email_email_link_text": "Here's the link in case you don't like clicking buttons:",
|
||||
"email_sent": "Email sent successfully",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
@ -27,6 +28,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
data: {
|
||||
id: v4(),
|
||||
userId: validKey.userId,
|
||||
teamId: validKey.teamId,
|
||||
eventTriggers: [triggerEvent],
|
||||
subscriberUrl,
|
||||
active: true,
|
||||
|
@ -36,9 +38,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
|
||||
//schedule job for already existing bookings
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
|
||||
else where.userId = validKey.userId;
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
...where,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
|
@ -23,17 +24,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
if (!validKey) {
|
||||
return res.status(401).json({ message: "API key not valid" });
|
||||
}
|
||||
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: {
|
||||
id,
|
||||
userId: validKey.userId,
|
||||
teamId: validKey.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!webhook) {
|
||||
return res.status(401).json({ message: "Not authorized to delete this webhook" });
|
||||
}
|
||||
if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
|
||||
else where.userId = validKey.userId;
|
||||
const bookingsWithScheduledJobs = await prisma.booking.findMany({
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
...where,
|
||||
scheduledJobs: {
|
||||
isEmpty: false,
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getHumanReadableLocationValue } from "@calcom/core/location";
|
||||
|
@ -20,11 +21,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
}
|
||||
|
||||
try {
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
|
||||
else where.userId = validKey.userId;
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where: {
|
||||
userId: validKey.userId,
|
||||
},
|
||||
where,
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
|
@ -56,6 +58,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
currency: true,
|
||||
length: true,
|
||||
bookingFields: true,
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
|
@ -68,6 +71,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
});
|
||||
|
||||
if (bookings.length === 0) {
|
||||
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
|
||||
return res.status(404).json({
|
||||
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
|
||||
});
|
||||
}
|
||||
|
||||
const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
|
||||
|
||||
const updatedBookings = bookings.map((booking) => {
|
||||
|
|
|
@ -18,6 +18,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
if (req.method === "GET") {
|
||||
try {
|
||||
if (validKey.teamId) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: validKey.teamId,
|
||||
},
|
||||
});
|
||||
return res.status(201).json({ username: team?.name });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: validKey.userId,
|
||||
|
@ -26,7 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
username: true,
|
||||
},
|
||||
});
|
||||
res.status(201).json(user);
|
||||
return res.status(201).json(user);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Unable to get User." });
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Toaster } from "react-hot-toast";
|
|||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast, Tooltip } from "@calcom/ui";
|
||||
import { Button, Tooltip, showToast } from "@calcom/ui";
|
||||
import { Clipboard } from "@calcom/ui/components/icon";
|
||||
|
||||
export interface IZapierSetupProps {
|
||||
|
@ -15,13 +15,20 @@ export interface IZapierSetupProps {
|
|||
const ZAPIER = "zapier";
|
||||
|
||||
export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
const [newApiKey, setNewApiKey] = useState("");
|
||||
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const integrations = trpc.viewer.integrations.useQuery({ variant: "automation" });
|
||||
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: ZAPIER });
|
||||
|
||||
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation();
|
||||
const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name }));
|
||||
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.apiKeys.findKeyOfType.invalidate();
|
||||
},
|
||||
});
|
||||
const zapierCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
|
||||
(item: { type: string }) => item.type === "zapier_automation"
|
||||
);
|
||||
|
@ -29,15 +36,28 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
const showContent = integrations.data && integrations.isSuccess && credentialId;
|
||||
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
|
||||
|
||||
async function createApiKey() {
|
||||
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER };
|
||||
async function createApiKey(teamId?: number) {
|
||||
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER, teamId };
|
||||
const apiKey = await utils.client.viewer.apiKeys.create.mutate(event);
|
||||
|
||||
if (oldApiKey.data) {
|
||||
deleteApiKey.mutate({
|
||||
id: oldApiKey.data.id,
|
||||
});
|
||||
const oldKey = teamId
|
||||
? oldApiKey.data.find((key) => key.teamId === teamId)
|
||||
: oldApiKey.data.find((key) => !key.teamId);
|
||||
|
||||
if (oldKey) {
|
||||
deleteApiKey.mutate({
|
||||
id: oldKey.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
setNewApiKey(apiKey);
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
async function generateApiKey(teamId?: number) {
|
||||
const apiKey = await createApiKey(teamId);
|
||||
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
|
||||
}
|
||||
|
||||
if (integrations.isLoading) {
|
||||
|
@ -54,36 +74,43 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
</div>
|
||||
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
|
||||
<div className="text-default">{t("setting_up_zapier")}</div>
|
||||
{!newApiKey ? (
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
|
||||
<Button color="primary" onClick={() => createApiKey()} className="mb-4 mt-4">
|
||||
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
|
||||
{!teams ? (
|
||||
<Button color="secondary" onClick={() => createApiKey()} className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("your_unique_api_key")}</div>
|
||||
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
|
||||
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
|
||||
{newApiKey}
|
||||
</code>
|
||||
<Tooltip side="top" content={t("copy_to_clipboard")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newApiKey);
|
||||
showToast(t("api_key_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
|
||||
<Clipboard className="h-5 w-5 text-gray-100 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy")}
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-8 text-sm font-semibold">Your event types:</div>
|
||||
{!newApiKeys[""] ? (
|
||||
<Button color="secondary" onClick={() => generateApiKey()} className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-default mb-5 mt-2 text-sm font-semibold">{t("copy_safe_api_key")}</div>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<CopyApiKey apiKey={newApiKeys[""]} />
|
||||
)}
|
||||
{teams.map((team) => {
|
||||
return (
|
||||
<div key={team.name}>
|
||||
<div className="mt-2 text-sm font-semibold">{team.name}:</div>
|
||||
{!newApiKeys[team.id] ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => generateApiKey(team.id)}
|
||||
className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
) : (
|
||||
<CopyApiKey apiKey={newApiKeys[team.id]} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
|
||||
{isCalDev && (
|
||||
|
@ -122,3 +149,29 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
|
||||
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
|
||||
{apiKey}
|
||||
</code>
|
||||
<Tooltip side="top" content={t("copy_to_clipboard")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
showToast(t("api_key_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
|
||||
<Clipboard className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-subtle mb-5 mt-2 text-sm">{t("copy_somewhere_safe")}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,14 +6,8 @@ const findValidApiKey = async (apiKey: string, appId?: string) => {
|
|||
|
||||
const validKey = await prisma.apiKey.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
hashedKey,
|
||||
},
|
||||
{
|
||||
appId,
|
||||
},
|
||||
],
|
||||
hashedKey,
|
||||
appId,
|
||||
OR: [
|
||||
{
|
||||
expiresAt: {
|
||||
|
|
|
@ -20,6 +20,7 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
|
|||
OR: [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
eventTypeId,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "ApiKey" ADD COLUMN "teamId" INTEGER;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -276,6 +276,7 @@ model Team {
|
|||
timeZone String @default("Europe/London")
|
||||
weekStart String @default("Sunday")
|
||||
routingForms App_RoutingForms_Form[]
|
||||
apiKeys ApiKey[]
|
||||
credentials Credential[]
|
||||
|
||||
@@unique([slug, parentId])
|
||||
|
@ -564,12 +565,14 @@ model Impersonations {
|
|||
model ApiKey {
|
||||
id String @id @unique @default(cuid())
|
||||
userId Int
|
||||
teamId Int?
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
lastUsedAt DateTime?
|
||||
hashedKey String @unique()
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
appId String?
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export async function checkPermissions(args: {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
role: Prisma.MembershipWhereInput["role"];
|
||||
}) {
|
||||
const { teamId, userId, role } = args;
|
||||
if (!teamId) return;
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
role,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
|
@ -2,8 +2,10 @@ import { v4 } from "uuid";
|
|||
|
||||
import { generateUniqueAPIKey } from "@calcom/ee/api-keys/lib/apiKeys";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import { checkPermissions } from "./_auth-middleware";
|
||||
import type { TCreateInputSchema } from "./create.schema";
|
||||
|
||||
type CreateHandlerOptions = {
|
||||
|
@ -17,12 +19,17 @@ export const createHandler = async ({ ctx, input }: CreateHandlerOptions) => {
|
|||
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
|
||||
|
||||
// Here we snap never expires before deleting it so it's not passed to prisma create call.
|
||||
const { neverExpires, ...rest } = input;
|
||||
const { neverExpires, teamId, ...rest } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
/** Only admin or owner can create apiKeys of team (if teamId is passed) */
|
||||
await checkPermissions({ userId, teamId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||
|
||||
await prisma.apiKey.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
...rest,
|
||||
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
||||
expiresAt: neverExpires ? null : rest.expiresAt,
|
||||
|
|
|
@ -5,6 +5,7 @@ export const ZCreateInputSchema = z.object({
|
|||
expiresAt: z.date().optional().nullable(),
|
||||
neverExpires: z.boolean().optional(),
|
||||
appId: z.string().optional().nullable(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TCreateInputSchema = z.infer<typeof ZCreateInputSchema>;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import { checkPermissions } from "./_auth-middleware";
|
||||
import type { TFindKeyOfTypeInputSchema } from "./findKeyOfType.schema";
|
||||
|
||||
type FindKeyOfTypeOptions = {
|
||||
|
@ -11,16 +13,16 @@ type FindKeyOfTypeOptions = {
|
|||
};
|
||||
|
||||
export const findKeyOfTypeHandler = async ({ ctx, input }: FindKeyOfTypeOptions) => {
|
||||
return await prisma.apiKey.findFirst({
|
||||
const { teamId, appId } = input;
|
||||
const userId = ctx.user.id;
|
||||
/** Only admin or owner can create apiKeys of team (if teamId is passed) */
|
||||
await checkPermissions({ userId, teamId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||
|
||||
return await prisma.apiKey.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
{
|
||||
appId: input.appId,
|
||||
},
|
||||
],
|
||||
teamId,
|
||||
userId,
|
||||
appId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||
|
||||
export const ZFindKeyOfTypeInputSchema = z.object({
|
||||
appId: z.string().optional(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TFindKeyOfTypeInputSchema = z.infer<typeof ZFindKeyOfTypeInputSchema>;
|
||||
|
|
|
@ -22,6 +22,7 @@ import { ZUpdateMembershipInputSchema } from "./updateMembership.schema";
|
|||
type TeamsRouterHandlerCache = {
|
||||
get?: typeof import("./get.handler").getHandler;
|
||||
list?: typeof import("./list.handler").listHandler;
|
||||
listOwnedTeams?: typeof import("./listOwnedTeams.handler").listOwnedTeamsHandler;
|
||||
create?: typeof import("./create.handler").createHandler;
|
||||
update?: typeof import("./update.handler").updateHandler;
|
||||
delete?: typeof import("./delete.handler").deleteHandler;
|
||||
|
@ -79,6 +80,23 @@ export const viewerTeamsRouter = router({
|
|||
ctx,
|
||||
});
|
||||
}),
|
||||
// Returns Teams I am a owner/admin of
|
||||
listOwnedTeams: authedProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOwnedTeams) {
|
||||
UNSTABLE_HANDLER_CACHE.listOwnedTeams = await import("./listOwnedTeams.handler").then(
|
||||
(mod) => mod.listOwnedTeamsHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOwnedTeams) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.listOwnedTeams({
|
||||
ctx,
|
||||
});
|
||||
}),
|
||||
|
||||
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.create) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
|
||||
type ListOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
};
|
||||
|
||||
export const listOwnedTeamsHandler = async ({ ctx }: ListOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
teams: {
|
||||
where: {
|
||||
accepted: true,
|
||||
role: {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return user?.teams
|
||||
?.filter((m) => {
|
||||
const metadata = teamMetadataSchema.parse(m.team.metadata);
|
||||
return !metadata?.isOrganization;
|
||||
})
|
||||
?.map(({ team }) => team);
|
||||
};
|
Loading…
Reference in New Issue
Block a user