first commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admin can check any api key
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if user can access the api key
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!apiKey) throw new HttpError({ statusCode: 404, message: "API key not found" });
|
||||
}
|
||||
15
calcom/apps/api/v1/pages/api/api-keys/[id]/_delete.ts
Normal file
15
calcom/apps/api/v1/pages/api/api-keys/[id]/_delete.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
await prisma.apiKey.delete({ where: { id } });
|
||||
return { message: `ApiKey with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
16
calcom/apps/api/v1/pages/api/api-keys/[id]/_get.ts
Normal file
16
calcom/apps/api/v1/pages/api/api-keys/[id]/_get.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const api_key = await prisma.apiKey.findUniqueOrThrow({ where: { id } });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
17
calcom/apps/api/v1/pages/api/api-keys/[id]/_patch.ts
Normal file
17
calcom/apps/api/v1/pages/api/api-keys/[id]/_patch.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyEditBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function patchHandler(req: NextApiRequest) {
|
||||
const { body } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
const data = apiKeyEditBodySchema.parse(body);
|
||||
const api_key = await prisma.apiKey.update({ where: { id }, data });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
18
calcom/apps/api/v1/pages/api/api-keys/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/api-keys/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import { authMiddleware } from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
41
calcom/apps/api/v1/pages/api/api-keys/_get.ts
Normal file
41
calcom/apps/api/v1/pages/api/api-keys/_get.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
|
||||
import { apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & {
|
||||
args?: Prisma.ApiKeyFindManyArgs;
|
||||
};
|
||||
|
||||
/** Admins can query other users' API keys */
|
||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||
// To match type safety with runtime
|
||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
req.args.where = { userId: { in: userIds } };
|
||||
if (Array.isArray(query.userId)) req.args.orderBy = { userId: "asc" };
|
||||
}
|
||||
}
|
||||
|
||||
function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiRequest, "args"> {
|
||||
return "args" in req;
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
return { api_keys: data.map((v) => apiKeyPublicSchema.parse(v)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
46
calcom/apps/api/v1/pages/api/api-keys/_post.ts
Normal file
46
calcom/apps/api/v1/pages/api/api-keys/_post.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
data: {
|
||||
id: v4(),
|
||||
userId,
|
||||
...input,
|
||||
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
||||
expiresAt: neverExpires ? null : input.expiresAt,
|
||||
hashedKey,
|
||||
},
|
||||
};
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.apiKey.create(args);
|
||||
return {
|
||||
api_key: {
|
||||
...apiKeyPublicSchema.parse(result),
|
||||
key: `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`,
|
||||
},
|
||||
message: "API key created successfully. Save the `key` value as it won't be displayed again.",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/api-keys/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/api-keys/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware("HTTP_GET_OR_POST")(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const query = schemaQueryIdParseInt.parse(req.query);
|
||||
// @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin.
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all user bookings, including attendees
|
||||
const attendee = await prisma.attendee.findFirst({
|
||||
where: { id: query.id, booking: { userId } },
|
||||
});
|
||||
// Flatten and merge all the attendees in one array
|
||||
if (!attendee) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
44
calcom/apps/api/v1/pages/api/attendees/[id]/_delete.ts
Normal file
44
calcom/apps/api/v1/pages/api/attendees/[id]/_delete.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* delete:
|
||||
* operationId: removeAttendeeById
|
||||
* summary: Remove an existing attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to delete
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee removed successfully
|
||||
* 400:
|
||||
* description: Bad request. Attendee id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.attendee.delete({ where: { id } });
|
||||
return { message: `Attendee with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
45
calcom/apps/api/v1/pages/api/attendees/[id]/_get.ts
Normal file
45
calcom/apps/api/v1/pages/api/attendees/[id]/_get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* get:
|
||||
* operationId: getAttendeeById
|
||||
* summary: Find an attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to get
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Attendee was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const attendee = await prisma.attendee.findUnique({ where: { id } });
|
||||
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
77
calcom/apps/api/v1/pages/api/attendees/[id]/_patch.ts
Normal file
77
calcom/apps/api/v1/pages/api/attendees/[id]/_patch.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* patch:
|
||||
* operationId: editAttendeeById
|
||||
* summary: Edit an existing attendee
|
||||
* requestBody:
|
||||
* description: Edit an existing attendee related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* name:
|
||||
* type: string
|
||||
* timeZone:
|
||||
* type: string
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to get
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee edited successfully
|
||||
* 400:
|
||||
* description: Bad request. Attendee body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaAttendeeEditBodyParams.parse(body);
|
||||
await checkPermissions(req, data);
|
||||
const attendee = await prisma.attendee.update({ where: { id }, data });
|
||||
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const { userId } = req;
|
||||
const { bookingId } = body;
|
||||
if (bookingId) {
|
||||
// Ensure that the booking the attendee is being added to belongs to the user
|
||||
const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId } });
|
||||
if (!booking) throw new HttpError({ statusCode: 403, message: "You don't have access to the booking" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
18
calcom/apps/api/v1/pages/api/attendees/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/attendees/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
42
calcom/apps/api/v1/pages/api/attendees/_get.ts
Normal file
42
calcom/apps/api/v1/pages/api/attendees/_get.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees:
|
||||
* get:
|
||||
* operationId: listAttendees
|
||||
* summary: Find all attendees
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No attendees were found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } };
|
||||
const data = await prisma.attendee.findMany(args);
|
||||
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
||||
if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" });
|
||||
return { attendees };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
82
calcom/apps/api/v1/pages/api/attendees/_post.ts
Normal file
82
calcom/apps/api/v1/pages/api/attendees/_post.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees:
|
||||
* post:
|
||||
* operationId: addAttendee
|
||||
* summary: Creates a new attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Create a new attendee related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - bookingId
|
||||
* - name
|
||||
* - email
|
||||
* - timeZone
|
||||
* properties:
|
||||
* bookingId:
|
||||
* type: number
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* name:
|
||||
* type: string
|
||||
* timeZone:
|
||||
* type: string
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee created
|
||||
* 400:
|
||||
* description: Bad request. Attendee body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
||||
|
||||
if (!isSystemWideAdmin) {
|
||||
const userBooking = await prisma.booking.findFirst({
|
||||
where: { userId, id: body.bookingId },
|
||||
select: { id: true },
|
||||
});
|
||||
// Here we make sure to only return attendee's of the user's own bookings.
|
||||
if (!userBooking) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
const data = await prisma.attendee.create({
|
||||
data: {
|
||||
email: body.email,
|
||||
name: body.name,
|
||||
timeZone: body.timeZone,
|
||||
booking: { connect: { id: body.bookingId } },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
attendee: schemaAttendeeReadPublic.parse(data),
|
||||
message: "Attendee created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/attendees/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/attendees/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
/** Admins can skip the ownership verification */
|
||||
if (isSystemWideAdmin) return;
|
||||
/**
|
||||
* There's a caveat here. If the availability exists but the user doesn't own it,
|
||||
* the user will see a 404 error which may or not be the desired behavior.
|
||||
*/
|
||||
await prisma.availability.findFirstOrThrow({
|
||||
where: { id, Schedule: { userId } },
|
||||
});
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
46
calcom/apps/api/v1/pages/api/availabilities/[id]/_delete.ts
Normal file
46
calcom/apps/api/v1/pages/api/availabilities/[id]/_delete.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* delete:
|
||||
* operationId: removeAvailabilityById
|
||||
* summary: Remove an existing availability
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the availability to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability removed successfully
|
||||
* 400:
|
||||
* description: Bad request. Availability id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.availability.delete({ where: { id } });
|
||||
return { message: `Availability with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
50
calcom/apps/api/v1/pages/api/availabilities/[id]/_get.ts
Normal file
50
calcom/apps/api/v1/pages/api/availabilities/[id]/_get.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAvailabilityReadPublic } from "~/lib/validations/availability";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* get:
|
||||
* operationId: getAvailabilityById
|
||||
* summary: Find an availability
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the availability to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid
|
||||
* 404:
|
||||
* description: Availability not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const availability = await prisma.availability.findUnique({
|
||||
where: { id },
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
return { availability: schemaAvailabilityReadPublic.parse(availability) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
87
calcom/apps/api/v1/pages/api/availabilities/[id]/_patch.ts
Normal file
87
calcom/apps/api/v1/pages/api/availabilities/[id]/_patch.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaAvailabilityEditBodyParams,
|
||||
schemaAvailabilityReadPublic,
|
||||
} from "~/lib/validations/availability";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* patch:
|
||||
* operationId: editAvailabilityById
|
||||
* summary: Edit an existing availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* description: Your API key
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: ID of the availability to edit
|
||||
* requestBody:
|
||||
* description: Edit an existing availability related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* days:
|
||||
* type: array
|
||||
* description: Array of integers depicting weekdays
|
||||
* items:
|
||||
* type: integer
|
||||
* enum: [0, 1, 2, 3, 4, 5]
|
||||
* scheduleId:
|
||||
* type: integer
|
||||
* description: ID of schedule this availability is associated with
|
||||
* startTime:
|
||||
* type: string
|
||||
* description: Start time of the availability
|
||||
* endTime:
|
||||
* type: string
|
||||
* description: End time of the availability
|
||||
* examples:
|
||||
* availability:
|
||||
* summary: An example of availability
|
||||
* value:
|
||||
* scheduleId: 123
|
||||
* days: [1,2,3,5]
|
||||
* startTime: 1970-01-01T17:00:00.000Z
|
||||
* endTime: 1970-01-01T17:00:00.000Z
|
||||
*
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability edited successfully
|
||||
* 400:
|
||||
* description: Bad request. Availability body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaAvailabilityEditBodyParams.parse(body);
|
||||
const availability = await prisma.availability.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
return { availability: schemaAvailabilityReadPublic.parse(availability) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
18
calcom/apps/api/v1/pages/api/availabilities/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/availabilities/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
99
calcom/apps/api/v1/pages/api/availabilities/_post.ts
Normal file
99
calcom/apps/api/v1/pages/api/availabilities/_post.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaAvailabilityCreateBodyParams,
|
||||
schemaAvailabilityReadPublic,
|
||||
} from "~/lib/validations/availability";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities:
|
||||
* post:
|
||||
* operationId: addAvailability
|
||||
* summary: Creates a new availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Edit an existing availability related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - scheduleId
|
||||
* - startTime
|
||||
* - endTime
|
||||
* properties:
|
||||
* days:
|
||||
* type: array
|
||||
* description: Array of integers depicting weekdays
|
||||
* items:
|
||||
* type: integer
|
||||
* enum: [0, 1, 2, 3, 4, 5]
|
||||
* scheduleId:
|
||||
* type: integer
|
||||
* description: ID of schedule this availability is associated with
|
||||
* startTime:
|
||||
* type: string
|
||||
* description: Start time of the availability
|
||||
* endTime:
|
||||
* type: string
|
||||
* description: End time of the availability
|
||||
* examples:
|
||||
* availability:
|
||||
* summary: An example of availability
|
||||
* value:
|
||||
* scheduleId: 123
|
||||
* days: [1,2,3,5]
|
||||
* startTime: 1970-01-01T17:00:00.000Z
|
||||
* endTime: 1970-01-01T17:00:00.000Z
|
||||
*
|
||||
*
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability created
|
||||
* 400:
|
||||
* description: Bad request. Availability body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
await checkPermissions(req);
|
||||
const availability = await prisma.availability.create({
|
||||
data,
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
req.statusCode = 201;
|
||||
return {
|
||||
availability: schemaAvailabilityReadPublic.parse(availability),
|
||||
message: "Availability created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { userId, id: data.scheduleId },
|
||||
});
|
||||
if (!schedule)
|
||||
throw new HttpError({ statusCode: 401, message: "You can't add availabilities to this schedule" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
9
calcom/apps/api/v1/pages/api/availabilities/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/availabilities/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
250
calcom/apps/api/v1/pages/api/availability/_get.ts
Normal file
250
calcom/apps/api/v1/pages/api/availability/_get.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /teams/{teamId}/availability:
|
||||
* get:
|
||||
* summary: Find team availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "1234abcd5678efgh"
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: teamId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: ID of the team to fetch the availability for
|
||||
* - in: query
|
||||
* name: dateFrom
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-14 00:00:00"
|
||||
* description: Start Date of the availability query
|
||||
* - in: query
|
||||
* name: dateTo
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-20 00:00:00"
|
||||
* description: End Date of the availability query
|
||||
* - in: query
|
||||
* name: eventTypeId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: Event Type ID of the event type to fetch the availability for
|
||||
* operationId: team-availability
|
||||
* tags:
|
||||
* - availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* example:
|
||||
* busy:
|
||||
* - start: "2023-05-14T10:00:00.000Z"
|
||||
* end: "2023-05-14T11:00:00.000Z"
|
||||
* title: "Team meeting between Alice and Bob"
|
||||
* - start: "2023-05-15T14:00:00.000Z"
|
||||
* end: "2023-05-15T15:00:00.000Z"
|
||||
* title: "Project review between Carol and Dave"
|
||||
* - start: "2023-05-16T09:00:00.000Z"
|
||||
* end: "2023-05-16T10:00:00.000Z"
|
||||
* - start: "2023-05-17T13:00:00.000Z"
|
||||
* end: "2023-05-17T14:00:00.000Z"
|
||||
* timeZone: "America/New_York"
|
||||
* workingHours:
|
||||
* - days: [1, 2, 3, 4, 5]
|
||||
* startTime: 540
|
||||
* endTime: 1020
|
||||
* userId: 101
|
||||
* dateOverrides:
|
||||
* - date: "2023-05-15"
|
||||
* startTime: 600
|
||||
* endTime: 960
|
||||
* userId: 101
|
||||
* currentSeats: 4
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Team not found | Team has no members
|
||||
*
|
||||
* /availability:
|
||||
* get:
|
||||
* summary: Find user availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "1234abcd5678efgh"
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 101
|
||||
* description: ID of the user to fetch the availability for
|
||||
* - in: query
|
||||
* name: username
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "alice"
|
||||
* description: username of the user to fetch the availability for
|
||||
* - in: query
|
||||
* name: dateFrom
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-14 00:00:00"
|
||||
* description: Start Date of the availability query
|
||||
* - in: query
|
||||
* name: dateTo
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-20 00:00:00"
|
||||
* description: End Date of the availability query
|
||||
* - in: query
|
||||
* name: eventTypeId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: Event Type ID of the event type to fetch the availability for
|
||||
* operationId: user-availability
|
||||
* tags:
|
||||
* - availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* example:
|
||||
* busy:
|
||||
* - start: "2023-05-14T10:00:00.000Z"
|
||||
* end: "2023-05-14T11:00:00.000Z"
|
||||
* title: "Team meeting between Alice and Bob"
|
||||
* - start: "2023-05-15T14:00:00.000Z"
|
||||
* end: "2023-05-15T15:00:00.000Z"
|
||||
* title: "Project review between Carol and Dave"
|
||||
* - start: "2023-05-16T09:00:00.000Z"
|
||||
* end: "2023-05-16T10:00:00.000Z"
|
||||
* - start: "2023-05-17T13:00:00.000Z"
|
||||
* end: "2023-05-17T14:00:00.000Z"
|
||||
* timeZone: "America/New_York"
|
||||
* workingHours:
|
||||
* - days: [1, 2, 3, 4, 5]
|
||||
* startTime: 540
|
||||
* endTime: 1020
|
||||
* userId: 101
|
||||
* dateOverrides:
|
||||
* - date: "2023-05-15"
|
||||
* startTime: 600
|
||||
* endTime: 960
|
||||
* userId: 101
|
||||
* currentSeats: 4
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: User not found
|
||||
*/
|
||||
interface MemberRoles {
|
||||
[userId: number | string]: MembershipRole;
|
||||
}
|
||||
|
||||
const availabilitySchema = z
|
||||
.object({
|
||||
userId: stringOrNumber.optional(),
|
||||
teamId: stringOrNumber.optional(),
|
||||
username: z.string().optional(),
|
||||
dateFrom: z.string(),
|
||||
dateTo: z.string(),
|
||||
eventTypeId: stringOrNumber.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => !!data.username || !!data.userId || !!data.teamId,
|
||||
"Either username or userId or teamId should be filled in."
|
||||
);
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
userId,
|
||||
returnDateOverrides: true,
|
||||
});
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: { members: true },
|
||||
});
|
||||
if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" });
|
||||
if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" });
|
||||
const allMemberIds = team.members.reduce((allMemberIds: number[], member) => {
|
||||
if (member.accepted) {
|
||||
allMemberIds.push(member.userId);
|
||||
}
|
||||
return allMemberIds;
|
||||
}, []);
|
||||
const members = await prisma.user.findMany({
|
||||
where: { id: { in: allMemberIds } },
|
||||
select: availabilityUserSelect,
|
||||
});
|
||||
const memberRoles: MemberRoles = team.members.reduce((acc: MemberRoles, membership) => {
|
||||
acc[membership.userId] = membership.role;
|
||||
return acc;
|
||||
}, {} as MemberRoles);
|
||||
// check if the user is a team Admin or Owner, if it is a team request, or a system Admin
|
||||
const isUserAdminOrOwner =
|
||||
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||
isSystemWideAdmin;
|
||||
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
userId: user.id,
|
||||
availability: await getUserAvailability({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
returnDateOverrides: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
const settled = await Promise.all(availabilities);
|
||||
if (!settled)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "We had an issue retrieving all your members availabilities",
|
||||
});
|
||||
return settled;
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
9
calcom/apps/api/v1/pages/api/availability/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/availability/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Here we make sure to only return references of the user's own bookings if the user is not an admin.
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
});
|
||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* delete:
|
||||
* operationId: removeBookingReferenceById
|
||||
* summary: Remove an existing booking reference
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, bookingReference removed successfully
|
||||
* 400:
|
||||
* description: Bad request. BookingReference id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.bookingReference.delete({ where: { id } });
|
||||
return { message: `BookingReference with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
45
calcom/apps/api/v1/pages/api/booking-references/[id]/_get.ts
Normal file
45
calcom/apps/api/v1/pages/api/booking-references/[id]/_get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* get:
|
||||
* operationId: getBookingReferenceById
|
||||
* summary: Find a booking reference
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: BookingReference was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaBookingEditBodyParams,
|
||||
schemaBookingReferenceReadPublic,
|
||||
} from "~/lib/validations/booking-reference";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* patch:
|
||||
* operationId: editBookingReferenceById
|
||||
* summary: Edit an existing booking reference
|
||||
* requestBody:
|
||||
* description: Edit an existing booking reference related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* meetingId:
|
||||
* type: string
|
||||
* meetingPassword:
|
||||
* type: string
|
||||
* externalCalendarId:
|
||||
* type: string
|
||||
* deleted:
|
||||
* type: boolean
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to edit
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, BookingReference edited successfully
|
||||
* 400:
|
||||
* description: Bad request. BookingReference body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body, isSystemWideAdmin, userId } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
/* If user tries to update bookingId, we run extra checks */
|
||||
if (data.bookingId) {
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: data.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
{ where: { id: data.bookingId, userId } };
|
||||
await prisma.booking.findFirstOrThrow(args);
|
||||
}
|
||||
const booking_reference = await prisma.bookingReference.update({ where: { id }, data });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
41
calcom/apps/api/v1/pages/api/booking-references/_get.ts
Normal file
41
calcom/apps/api/v1/pages/api/booking-references/_get.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references:
|
||||
* get:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* operationId: listBookingReferences
|
||||
* summary: Find all booking references
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
87
calcom/apps/api/v1/pages/api/booking-references/_post.ts
Normal file
87
calcom/apps/api/v1/pages/api/booking-references/_post.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaBookingCreateBodyParams,
|
||||
schemaBookingReferenceReadPublic,
|
||||
} from "~/lib/validations/booking-reference";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references:
|
||||
* post:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* operationId: addBookingReference
|
||||
* summary: Creates a new booking reference
|
||||
* requestBody:
|
||||
* description: Create a new booking reference related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - type
|
||||
* - uid
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* uid:
|
||||
* type: string
|
||||
* meetingId:
|
||||
* type: string
|
||||
* meetingPassword:
|
||||
* type: string
|
||||
* meetingUrl:
|
||||
* type: string
|
||||
* bookingId:
|
||||
* type: boolean
|
||||
* externalCalendarId:
|
||||
* type: string
|
||||
* deleted:
|
||||
* type: boolean
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, booking reference created
|
||||
* 400:
|
||||
* description: Bad request. BookingReference body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaBookingCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: body.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
{ where: { id: body.bookingId, userId } };
|
||||
await prisma.booking.findFirstOrThrow(args);
|
||||
|
||||
const data = await prisma.bookingReference.create({
|
||||
data: {
|
||||
...body,
|
||||
bookingId: body.bookingId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
booking_reference: schemaBookingReferenceReadPublic.parse(data),
|
||||
message: "Booking reference created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/booking-references/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/booking-references/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req;
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (booking) {
|
||||
const bookingUserId = booking.userId;
|
||||
if (bookingUserId) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [bookingUserId],
|
||||
});
|
||||
if (accessibleUsersIds.length > 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
bookings: true,
|
||||
teams: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||
|
||||
if (!userBookingIds.includes(id)) {
|
||||
const teamBookings = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
eventType: {
|
||||
team: {
|
||||
id: {
|
||||
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
78
calcom/apps/api/v1/pages/api/bookings/[id]/_delete.ts
Normal file
78
calcom/apps/api/v1/pages/api/bookings/[id]/_delete.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/cancel:
|
||||
* delete:
|
||||
* summary: Booking cancellation
|
||||
* operationId: cancelBookingById
|
||||
*
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to cancel
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: allRemainingBookings
|
||||
* required: false
|
||||
* schema:
|
||||
* type: boolean
|
||||
* description: Delete all remaining bookings
|
||||
* - in: query
|
||||
* name: cancellationReason
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The reason for cancellation of the booking
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK, booking cancelled successfully
|
||||
* 400:
|
||||
* description: |
|
||||
* Bad request
|
||||
* <table>
|
||||
* <tr>
|
||||
* <td>Message</td>
|
||||
* <td>Cause</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Booking not found</td>
|
||||
* <td>The provided id didn't correspond to any existing booking.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>User not found</td>
|
||||
* <td>The userId did not matched an existing user.</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
* 404:
|
||||
* description: User not found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { id, allRemainingBookings, cancellationReason } = schemaQueryIdParseInt
|
||||
.merge(schemaBookingCancelParams.pick({ allRemainingBookings: true, cancellationReason: true }))
|
||||
.parse({
|
||||
...req.query,
|
||||
allRemainingBookings: req.query.allRemainingBookings === "true",
|
||||
});
|
||||
// Normalizing for universal handler
|
||||
req.body = { id, allRemainingBookings, cancellationReason };
|
||||
return await handleCancelBooking(req);
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
99
calcom/apps/api/v1/pages/api/bookings/[id]/_get.ts
Normal file
99
calcom/apps/api/v1/pages/api/bookings/[id]/_get.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}:
|
||||
* get:
|
||||
* summary: Find a booking
|
||||
* operationId: getBookingById
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Booking"
|
||||
* examples:
|
||||
* booking:
|
||||
* value:
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 91,
|
||||
* "userId": 5,
|
||||
* "description": "",
|
||||
* "eventTypeId": 7,
|
||||
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||
* "title": "60min between Pro Example and John Doe",
|
||||
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||
* "attendees": [
|
||||
* {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* }
|
||||
* ],
|
||||
* "user": {
|
||||
* "email": "pro@example.com",
|
||||
* "name": "Pro Example",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* },
|
||||
* "payment": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "success": true,
|
||||
* "paymentOption": "ON_BOOKING"
|
||||
* }
|
||||
* ],
|
||||
* "metadata": {},
|
||||
* "status": "ACCEPTED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { attendees: true, user: true, payment: true },
|
||||
});
|
||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
136
calcom/apps/api/v1/pages/api/bookings/[id]/_patch.ts
Normal file
136
calcom/apps/api/v1/pages/api/bookings/[id]/_patch.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}:
|
||||
* patch:
|
||||
* summary: Edit an existing booking
|
||||
* operationId: editBookingById
|
||||
* requestBody:
|
||||
* description: Edit an existing booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* description: 'Booking event title'
|
||||
* start:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'Start time of the Event'
|
||||
* end:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'End time of the Event'
|
||||
* status:
|
||||
* type: string
|
||||
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
||||
* description:
|
||||
* type: string
|
||||
* description: 'Description of the meeting'
|
||||
* examples:
|
||||
* editBooking:
|
||||
* value:
|
||||
* {
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "start": "2023-05-24T13:00:00.000Z",
|
||||
* "end": "2023-05-24T13:30:00.000Z",
|
||||
* "status": "CANCELLED"
|
||||
* }
|
||||
*
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to edit
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK, booking edited successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* examples:
|
||||
* bookings:
|
||||
* value:
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 11223344,
|
||||
* "userId": 182,
|
||||
* "description": null,
|
||||
* "eventTypeId": 2323232,
|
||||
* "uid": "stoSJtnh83PEL4rZmqdHe2",
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
||||
* "metadata": {},
|
||||
* "status": "CANCELLED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 400:
|
||||
* description: Bad request. Booking body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
await checkPermissions(req, data);
|
||||
const booking = await prisma.booking.update({ where: { id }, data });
|
||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
|
||||
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
|
||||
if (body.userId && isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [body.userId],
|
||||
});
|
||||
if (accessibleUsersIds.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
14
calcom/apps/api/v1/pages/api/bookings/[id]/cancel.ts
Normal file
14
calcom/apps/api/v1/pages/api/bookings/[id]/cancel.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
});
|
||||
18
calcom/apps/api/v1/pages/api/bookings/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/bookings/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/recordings:
|
||||
* get:
|
||||
* summary: Find all Cal video recordings of that booking
|
||||
* operationId: getRecordingsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched. Recording download link is only valid for 12 hours and you would have to fetch the recordings again to get new download link
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/ArrayOfRecordings"
|
||||
* examples:
|
||||
* recordings:
|
||||
* value:
|
||||
* - id: "ad90a2e7-154f-49ff-a815-5da1db7bf899"
|
||||
* room_name: "0n22w24AQ5ZFOtEKX2gX"
|
||||
* start_ts: 1716215386
|
||||
* status: "finished"
|
||||
* max_participants: 1
|
||||
* duration: 11
|
||||
* share_token: "x94YK-69Gnh7"
|
||||
* download_link: "https://daily-meeting-recordings..."
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const recordings = await getRecordingsOfCalVideoByRoomName(roomName);
|
||||
|
||||
if (!recordings || !("data" in recordings)) return [];
|
||||
|
||||
const recordingWithDownloadLink = recordings.data.map((recording: RecordingItemSchema) => {
|
||||
return getDownloadLinkOfCalVideoByRecordingId(recording.id)
|
||||
.then((res) => ({
|
||||
...recording,
|
||||
download_link: res?.download_link,
|
||||
}))
|
||||
.catch((err) => ({ ...recording, download_link: null, error: err.message }));
|
||||
});
|
||||
const res = await Promise.all(recordingWithDownloadLink);
|
||||
return res;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import {
|
||||
getTranscriptsAccessLinkFromRecordingId,
|
||||
checkIfRoomNameMatchesInRecording,
|
||||
} from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { getTranscriptFromRecordingId } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/transcripts/{recordingId}:
|
||||
* get:
|
||||
* summary: Find all Cal video transcripts of that recording
|
||||
* operationId: getTranscriptsByRecordingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which transcripts need to be fetched.
|
||||
* - in: path
|
||||
* name: recordingId
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: ID of the recording(daily.co recording id) for which transcripts need to be fetched.
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id, recordingId } = getTranscriptFromRecordingId.parse(query);
|
||||
|
||||
await checkIfRecordingBelongsToBooking(id, recordingId);
|
||||
|
||||
const transcriptsAccessLinks = await getTranscriptsAccessLinkFromRecordingId(recordingId);
|
||||
|
||||
return transcriptsAccessLinks;
|
||||
}
|
||||
|
||||
const checkIfRecordingBelongsToBooking = async (bookingId: number, recordingId: string) => {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${bookingId}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking Reference with Daily Video found with booking id ${bookingId}`,
|
||||
});
|
||||
|
||||
const canUserAccessRecordingId = await checkIfRoomNameMatchesInRecording(roomName, recordingId);
|
||||
if (!canUserAccessRecordingId) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: `This Recording Id ${recordingId} does not belong to booking ${bookingId}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/transcripts:
|
||||
* get:
|
||||
* summary: Find all Cal video transcripts of that booking
|
||||
* operationId: getTranscriptsByBookingId
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking for which recordings need to be fetched.
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { references: true },
|
||||
});
|
||||
|
||||
if (!booking)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Booking found with booking id ${id}`,
|
||||
});
|
||||
|
||||
const roomName =
|
||||
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
|
||||
undefined;
|
||||
|
||||
if (!roomName)
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `No Cal Video reference found with booking id ${booking.id}`,
|
||||
});
|
||||
|
||||
const transcripts = await getAllTranscriptsAccessLinkFromRoomName(roomName);
|
||||
|
||||
return transcripts;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "../_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
331
calcom/apps/api/v1/pages/api/bookings/_get.ts
Normal file
331
calcom/apps/api/v1/pages/api/bookings/_get.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import {
|
||||
getAccessibleUsers,
|
||||
retrieveOrgScopedAccessibleUsers,
|
||||
} from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* get:
|
||||
* summary: Find all bookings
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* example: 123456789abcdefgh
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: false
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: integer
|
||||
* example: 1
|
||||
* - type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* example: [2, 3, 4]
|
||||
* - in: query
|
||||
* name: attendeeEmail
|
||||
* required: false
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: string
|
||||
* format: email
|
||||
* example: john.doe@example.com
|
||||
* - type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: email
|
||||
* example: [john.doe@example.com, jane.doe@example.com]
|
||||
* - in: query
|
||||
* name: order
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [asc, desc]
|
||||
* - in: query
|
||||
* name: sortBy
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [createdAt, updatedAt]
|
||||
* operationId: listBookings
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/ArrayOfBookings"
|
||||
* examples:
|
||||
* bookings:
|
||||
* value: [
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 91,
|
||||
* "userId": 5,
|
||||
* "description": "",
|
||||
* "eventTypeId": 7,
|
||||
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||
* "title": "60min between Pro Example and John Doe",
|
||||
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||
* "attendees": [
|
||||
* {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* }
|
||||
* ],
|
||||
* "user": {
|
||||
* "email": "pro@example.com",
|
||||
* "name": "Pro Example",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* },
|
||||
* "payment": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "success": true,
|
||||
* "paymentOption": "ON_BOOKING"
|
||||
* }
|
||||
* ],
|
||||
* "metadata": {},
|
||||
* "status": "ACCEPTED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ]
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No bookings were found
|
||||
*/
|
||||
type GetAdminArgsType = {
|
||||
adminDidQueryUserIds?: boolean;
|
||||
requestedUserIds: number[];
|
||||
userId: number;
|
||||
};
|
||||
/**
|
||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||
*
|
||||
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
||||
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
||||
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
||||
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
||||
*
|
||||
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
||||
*/
|
||||
function buildWhereClause(
|
||||
userId: number | null,
|
||||
attendeeEmails: string[],
|
||||
userIds: number[] = [],
|
||||
userEmails: string[] = []
|
||||
) {
|
||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
||||
let whereClause = {};
|
||||
if (filterByAttendeeEmails) {
|
||||
whereClause = {
|
||||
AND: [
|
||||
userFilter,
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: { in: attendeeEmails },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
whereClause = {
|
||||
OR: [
|
||||
userFilter,
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: { in: userEmails },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...whereClause,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handler(req: NextApiRequest) {
|
||||
const {
|
||||
userId,
|
||||
isSystemWideAdmin,
|
||||
isOrganizationOwnerOrAdmin,
|
||||
pagination: { take, skip },
|
||||
} = req;
|
||||
const { dateFrom, dateTo, order, sortBy } = schemaBookingGetParams.parse(req.query);
|
||||
|
||||
const args: Prisma.BookingFindManyArgs = {};
|
||||
if (req.query.take && req.query.page) {
|
||||
args.take = take;
|
||||
args.skip = skip;
|
||||
}
|
||||
args.include = {
|
||||
attendees: true,
|
||||
user: true,
|
||||
payment: true,
|
||||
};
|
||||
|
||||
const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
|
||||
const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail)
|
||||
? queryFilterForAttendeeEmails.attendeeEmail
|
||||
: typeof queryFilterForAttendeeEmails.attendeeEmail === "string"
|
||||
? [queryFilterForAttendeeEmails.attendeeEmail]
|
||||
: [];
|
||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (isSystemWideAdmin) {
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
|
||||
const systemWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
}
|
||||
} else if (isOrganizationOwnerOrAdmin) {
|
||||
let requestedUserIds = [userId];
|
||||
if (req.query.userId || filterByAttendeeEmails) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
requestedUserIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
}
|
||||
const orgWideAdminArgs = {
|
||||
adminDidQueryUserIds: !!req.query.userId,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
};
|
||||
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new HttpError({ message: "User not found", statusCode: 404 });
|
||||
}
|
||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
||||
}
|
||||
|
||||
if (dateFrom) {
|
||||
args.where = {
|
||||
...args.where,
|
||||
startTime: { gte: dateFrom },
|
||||
};
|
||||
}
|
||||
if (dateTo) {
|
||||
args.where = {
|
||||
...args.where,
|
||||
endTime: { lte: dateTo },
|
||||
};
|
||||
}
|
||||
|
||||
if (sortBy === "updatedAt") {
|
||||
args.orderBy = {
|
||||
updatedAt: order,
|
||||
};
|
||||
}
|
||||
|
||||
if (sortBy === "createdAt") {
|
||||
args.orderBy = {
|
||||
createdAt: order,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await prisma.booking.findMany(args);
|
||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||
}
|
||||
|
||||
const handleSystemWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: requestedUserIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
|
||||
return { userId, userIds: requestedUserIds, userEmails };
|
||||
}
|
||||
return { userId: null, userIds: [], userEmails: [] };
|
||||
};
|
||||
|
||||
const handleOrgWideAdminArgs = async ({
|
||||
adminDidQueryUserIds,
|
||||
requestedUserIds,
|
||||
userId,
|
||||
}: GetAdminArgsType) => {
|
||||
if (adminDidQueryUserIds) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: requestedUserIds,
|
||||
});
|
||||
|
||||
if (!accessibleUsersIds.length) throw new HttpError({ message: "No User found", statusCode: 404 });
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
} else {
|
||||
const accessibleUsersIds = await retrieveOrgScopedAccessibleUsers({
|
||||
adminId: userId,
|
||||
});
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: accessibleUsersIds } },
|
||||
select: { email: true },
|
||||
});
|
||||
const userEmails = users.map((u) => u.email);
|
||||
return { userId, userIds: accessibleUsersIds, userEmails };
|
||||
}
|
||||
};
|
||||
|
||||
export default withMiddleware("pagination")(defaultResponder(handler));
|
||||
235
calcom/apps/api/v1/pages/api/bookings/_post.ts
Normal file
235
calcom/apps/api/v1/pages/api/bookings/_post.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings:
|
||||
* post:
|
||||
* summary: Creates a new booking
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* operationId: addBooking
|
||||
* requestBody:
|
||||
* description: Create a new booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - eventTypeId
|
||||
* - start
|
||||
* - responses
|
||||
* - timeZone
|
||||
* - language
|
||||
* - metadata
|
||||
* properties:
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'ID of the event type to book'
|
||||
* start:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'Start time of the Event'
|
||||
* end:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'End time of the Event'
|
||||
* responses:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - email
|
||||
* - location
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: 'Attendee full name'
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 'Attendee email address'
|
||||
* location:
|
||||
* type: object
|
||||
* properties:
|
||||
* optionValue:
|
||||
* type: string
|
||||
* description: 'Option value for the location'
|
||||
* value:
|
||||
* type: string
|
||||
* description: 'The meeting URL, Phone number or Address'
|
||||
* description: 'Meeting location'
|
||||
* metadata:
|
||||
* type: object
|
||||
* properties: {}
|
||||
* description: 'Any metadata associated with the booking'
|
||||
* timeZone:
|
||||
* type: string
|
||||
* description: 'TimeZone of the Attendee'
|
||||
* language:
|
||||
* type: string
|
||||
* description: 'Language of the Attendee'
|
||||
* title:
|
||||
* type: string
|
||||
* description: 'Booking event title'
|
||||
* recurringEventId:
|
||||
* type: integer
|
||||
* description: 'Recurring event ID if the event is recurring'
|
||||
* description:
|
||||
* type: string
|
||||
* description: 'Event description'
|
||||
* status:
|
||||
* type: string
|
||||
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
||||
* seatsPerTimeSlot:
|
||||
* type: integer
|
||||
* description: 'The number of seats for each time slot'
|
||||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* smsReminderNumber:
|
||||
* type: number
|
||||
* description: 'SMS reminder number'
|
||||
* examples:
|
||||
* New Booking example:
|
||||
* value:
|
||||
* {
|
||||
* "eventTypeId": 2323232,
|
||||
* "start": "2023-05-24T13:00:00.000Z",
|
||||
* "end": "2023-05-24T13:30:00.000Z",
|
||||
* "responses":{
|
||||
* "name": "Hello Hello",
|
||||
* "email": "hello@gmail.com",
|
||||
* "metadata": {},
|
||||
* "location": "Calcom HQ",
|
||||
* },
|
||||
* "timeZone": "Europe/London",
|
||||
* "language": "en",
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "description": null,
|
||||
* "status": "PENDING",
|
||||
* "smsReminderNumber": null
|
||||
* }
|
||||
*
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Booking(s) created successfully.
|
||||
* content:
|
||||
* application/json:
|
||||
* examples:
|
||||
* booking created successfully example:
|
||||
* value:
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 91,
|
||||
* "userId": 5,
|
||||
* "description": "",
|
||||
* "eventTypeId": 7,
|
||||
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||
* "title": "60min between Pro Example and John Doe",
|
||||
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||
* "attendees": [
|
||||
* {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* }
|
||||
* ],
|
||||
* "user": {
|
||||
* "email": "pro@example.com",
|
||||
* "name": "Pro Example",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* },
|
||||
* "payment": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "success": true,
|
||||
* "paymentOption": "ON_BOOKING"
|
||||
* }
|
||||
* ],
|
||||
* "metadata": {},
|
||||
* "status": "ACCEPTED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 400:
|
||||
* description: |
|
||||
* Bad request
|
||||
* <table>
|
||||
* <tr>
|
||||
* <td>Message</td>
|
||||
* <td>Cause</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Booking body is invalid</td>
|
||||
* <td>Missing property on booking entity.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Invalid eventTypeId</td>
|
||||
* <td>The provided eventTypeId does not exist.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Missing recurringCount</td>
|
||||
* <td>The eventType is recurring, and no recurringCount was passed.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Invalid recurringCount</td>
|
||||
* <td>The provided recurringCount is greater than the eventType recurring config</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
|
||||
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [req.body.userId || userId],
|
||||
});
|
||||
const [requestedUserId] = accessibleUsersIds;
|
||||
req.userId = requestedUserId || userId;
|
||||
}
|
||||
|
||||
try {
|
||||
return await handleNewBooking(req, getBookingDataSchemaForApi);
|
||||
} catch (error: unknown) {
|
||||
const knownError = error as Error;
|
||||
if (knownError?.message === ErrorCode.NoAvailableUsersFound) {
|
||||
throw new HttpError({ statusCode: 400, message: knownError.message });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
10
calcom/apps/api/v1/pages/api/bookings/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/bookings/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
147
calcom/apps/api/v1/pages/api/connected-calendars/_get.ts
Normal file
147
calcom/apps/api/v1/pages/api/connected-calendars/_get.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||
import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-calendar";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /connected-calendars:
|
||||
* get:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: false
|
||||
* schema:
|
||||
* type: number
|
||||
* description: Admins can fetch connected calendars for other user e.g. &userId=1 or multiple users e.g. &userId=1&userId=2
|
||||
* summary: Fetch connected calendars
|
||||
* tags:
|
||||
* - connected-calendars
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* appId:
|
||||
* type: string
|
||||
* userId:
|
||||
* type: number
|
||||
* integration:
|
||||
* type: string
|
||||
* calendars:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* externalId:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* primary:
|
||||
* type: boolean
|
||||
* readOnly:
|
||||
* type: boolean
|
||||
* examples:
|
||||
* connectedCalendarExample:
|
||||
* value: [
|
||||
* {
|
||||
* "name": "Google Calendar",
|
||||
* "appId": "google-calendar",
|
||||
* "userId": 10,
|
||||
* "integration": "google_calendar",
|
||||
* "calendars": [
|
||||
* {
|
||||
* "externalId": "alice@gmail.com",
|
||||
* "name": "alice@gmail.com",
|
||||
* "primary": true,
|
||||
* "readOnly": false
|
||||
* },
|
||||
* {
|
||||
* "externalId": "addressbook#contacts@group.v.calendar.google.com",
|
||||
* "name": "birthdays",
|
||||
* "primary": false,
|
||||
* "readOnly": true
|
||||
* },
|
||||
* {
|
||||
* "externalId": "en.latvian#holiday@group.v.calendar.google.com",
|
||||
* "name": "Holidays in Narnia",
|
||||
* "primary": false,
|
||||
* "readOnly": true
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 403:
|
||||
* description: Non admin user trying to fetch other user's connected calendars.
|
||||
*/
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const usersWithCalendars = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
include: {
|
||||
selectedCalendars: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return await getConnectedCalendars(usersWithCalendars);
|
||||
}
|
||||
|
||||
async function getConnectedCalendars(users: UserWithCalendars[]) {
|
||||
const connectedDestinationCalendarsPromises = users.map((user) =>
|
||||
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
)
|
||||
);
|
||||
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
|
||||
|
||||
const flattenedCalendars = connectedDestinationCalendars.flat();
|
||||
|
||||
const mapped = flattenedCalendars.map((calendar) => ({
|
||||
name: calendar.integration.name,
|
||||
appId: calendar.integration.slug,
|
||||
userId: calendar.userId,
|
||||
integration: calendar.integration.type,
|
||||
calendars: (calendar.calendars ?? []).map((c) => ({
|
||||
externalId: c.externalId,
|
||||
name: c.name,
|
||||
primary: c.primary ?? false,
|
||||
readOnly: c.readOnly,
|
||||
})),
|
||||
}));
|
||||
|
||||
return schemaConnectedCalendarsReadPublic.parse(mapped);
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})
|
||||
);
|
||||
60
calcom/apps/api/v1/pages/api/credential-sync/_delete.ts
Normal file
60
calcom/apps/api/v1/pages/api/credential-sync/_delete.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaCredentialDeleteParams } from "~/lib/validations/credential-sync";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /credential-sync:
|
||||
* delete:
|
||||
* operationId: deleteUserAppCredential
|
||||
* summary: Delete a credential record for a user
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the user to fetch the credentials for
|
||||
* - in: query
|
||||
* name: credentialId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the credential to update
|
||||
* tags:
|
||||
* - credentials
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 505:
|
||||
* description: Credential syncing not enabled
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, credentialId } = schemaCredentialDeleteParams.parse(req.query);
|
||||
|
||||
const credential = await prisma.credential.delete({
|
||||
where: {
|
||||
id: credentialId,
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
appId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { credentialDeleted: credential };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
62
calcom/apps/api/v1/pages/api/credential-sync/_get.ts
Normal file
62
calcom/apps/api/v1/pages/api/credential-sync/_get.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaCredentialGetParams } from "~/lib/validations/credential-sync";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /credential-sync:
|
||||
* get:
|
||||
* operationId: getUserAppCredentials
|
||||
* summary: Get all app credentials for a user
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the user to fetch the credentials for
|
||||
* tags:
|
||||
* - credentials
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 505:
|
||||
* description: Credential syncing not enabled
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { appSlug, userId } = schemaCredentialGetParams.parse(req.query);
|
||||
|
||||
let credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId,
|
||||
...(appSlug && { appId: appSlug }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
appId: true,
|
||||
},
|
||||
});
|
||||
|
||||
// For apps we're transitioning to using the term slug to keep things consistent
|
||||
credentials = credentials.map((credential) => {
|
||||
return {
|
||||
...credential,
|
||||
appSlug: credential.appId,
|
||||
};
|
||||
});
|
||||
|
||||
return { credentials };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
85
calcom/apps/api/v1/pages/api/credential-sync/_patch.ts
Normal file
85
calcom/apps/api/v1/pages/api/credential-sync/_patch.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /credential-sync:
|
||||
* patch:
|
||||
* operationId: updateUserAppCredential
|
||||
* summary: Update a credential record for a user
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the user to fetch the credentials for
|
||||
* - in: query
|
||||
* name: credentialId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the credential to update
|
||||
* tags:
|
||||
* - credentials
|
||||
* requestBody:
|
||||
* description: Update a new credential
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - encryptedKey
|
||||
* properties:
|
||||
* encryptedKey:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 505:
|
||||
* description: Credential syncing not enabled
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, credentialId } = schemaCredentialPatchParams.parse(req.query);
|
||||
|
||||
const { encryptedKey } = schemaCredentialPatchBody.parse(req.body);
|
||||
|
||||
const decryptedKey = JSON.parse(
|
||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||
|
||||
const credential = await prisma.credential.update({
|
||||
where: {
|
||||
id: credentialId,
|
||||
userId,
|
||||
},
|
||||
data: {
|
||||
key,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
appId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { credential };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
145
calcom/apps/api/v1/pages/api/credential-sync/_post.ts
Normal file
145
calcom/apps/api/v1/pages/api/credential-sync/_post.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /credential-sync:
|
||||
* post:
|
||||
* operationId: createUserAppCredential
|
||||
* summary: Create a credential record for a user
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: ID of the user to fetch the credentials for
|
||||
* tags:
|
||||
* - credentials
|
||||
* requestBody:
|
||||
* description: Create a new credential
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - encryptedKey
|
||||
* - appSlug
|
||||
* properties:
|
||||
* encryptedKey:
|
||||
* type: string
|
||||
* appSlug:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 505:
|
||||
* description: Credential syncing not enabled
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
if (!req.body) {
|
||||
throw new HttpError({ message: "Request body is missing", statusCode: 400 });
|
||||
}
|
||||
|
||||
const { userId, createSelectedCalendar, createDestinationCalendar } = schemaCredentialPostParams.parse(
|
||||
req.query
|
||||
);
|
||||
|
||||
const { appSlug, encryptedKey } = schemaCredentialPostBody.parse(req.body);
|
||||
|
||||
const decryptedKey = JSON.parse(
|
||||
symmetricDecrypt(encryptedKey, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
|
||||
);
|
||||
|
||||
const key = OAuth2UniversalSchema.parse(decryptedKey);
|
||||
|
||||
// Need to get app type
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: appSlug },
|
||||
select: { dirName: true, categories: true },
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new HttpError({ message: "App not found", statusCode: 500 });
|
||||
}
|
||||
|
||||
const createCalendarResources =
|
||||
app.categories.some((category) => category === "calendar") &&
|
||||
(createSelectedCalendar || createDestinationCalendar);
|
||||
|
||||
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
|
||||
|
||||
const createdcredential = await prisma.credential.create({
|
||||
data: {
|
||||
userId,
|
||||
appId: appSlug,
|
||||
key,
|
||||
type: appMetadata.type,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
// createdcredential.user.email;
|
||||
// TODO: ^ Investigate why this select doesn't work.
|
||||
const credential = await prisma.credential.findUniqueOrThrow({
|
||||
where: {
|
||||
id: createdcredential.id,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
// ^ Workaround for the select in `create` not working
|
||||
|
||||
if (createCalendarResources) {
|
||||
const calendar = await getCalendar(credential);
|
||||
if (!calendar) throw new HttpError({ message: "Calendar missing for credential", statusCode: 500 });
|
||||
const calendars = await calendar.listCalendars();
|
||||
const calendarToCreate = calendars.find((calendar) => calendar.primary) || calendars[0];
|
||||
|
||||
if (createSelectedCalendar) {
|
||||
await prisma.selectedCalendar.createMany({
|
||||
data: [
|
||||
{
|
||||
userId,
|
||||
integration: appMetadata.type,
|
||||
externalId: calendarToCreate.externalId,
|
||||
credentialId: credential.id,
|
||||
},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
if (createDestinationCalendar) {
|
||||
await prisma.destinationCalendar.create({
|
||||
data: {
|
||||
integration: appMetadata.type,
|
||||
externalId: calendarToCreate.externalId,
|
||||
credential: { connect: { id: credential.id } },
|
||||
primaryEmail: calendarToCreate.email || credential.user?.email,
|
||||
user: { connect: { id: userId } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { credential: { id: credential.id, type: credential.type } };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
12
calcom/apps/api/v1/pages/api/credential-sync/index.ts
Normal file
12
calcom/apps/api/v1/pages/api/credential-sync/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware("verifyCredentialSyncEnabled")(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the event type of this input
|
||||
const eventTypeCustomInput = await prisma.eventTypeCustomInput.findFirst({
|
||||
where: { id, eventType: { userId } },
|
||||
});
|
||||
if (!eventTypeCustomInput) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
43
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_delete.ts
Normal file
43
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_delete.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /custom-inputs/{id}:
|
||||
* delete:
|
||||
* summary: Remove an existing eventTypeCustomInput
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventTypeCustomInput to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - custom-inputs
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventTypeCustomInput removed successfully
|
||||
* 400:
|
||||
* description: Bad request. EventType id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.eventTypeCustomInput.delete({ where: { id } });
|
||||
return { message: `CustomInputEventType with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
44
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_get.ts
Normal file
44
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_get.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-custom-input";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /custom-inputs/{id}:
|
||||
* get:
|
||||
* summary: Find a eventTypeCustomInput
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventTypeCustomInput to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - custom-inputs
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: EventType was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = await prisma.eventTypeCustomInput.findUniqueOrThrow({ where: { id } });
|
||||
return { event_type_custom_input: schemaEventTypeCustomInputPublic.parse(data) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
87
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_patch.ts
Normal file
87
calcom/apps/api/v1/pages/api/custom-inputs/[id]/_patch.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaEventTypeCustomInputEditBodyParams,
|
||||
schemaEventTypeCustomInputPublic,
|
||||
} from "~/lib/validations/event-type-custom-input";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /custom-inputs/{id}:
|
||||
* patch:
|
||||
* summary: Edit an existing eventTypeCustomInput
|
||||
* requestBody:
|
||||
* description: Edit an existing eventTypeCustomInput for an event type
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'ID of the event type to which the custom input is being added'
|
||||
* label:
|
||||
* type: string
|
||||
* description: 'Label of the custom input'
|
||||
* type:
|
||||
* type: string
|
||||
* description: 'Type of the custom input. The value is ENUM; one of [TEXT, TEXTLONG, NUMBER, BOOL, RADIO, PHONE]'
|
||||
* options:
|
||||
* type: object
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* description: 'Options for the custom input'
|
||||
* required:
|
||||
* type: boolean
|
||||
* description: 'If the custom input is required before booking'
|
||||
* placeholder:
|
||||
* type: string
|
||||
* description: 'Placeholder text for the custom input'
|
||||
*
|
||||
* examples:
|
||||
* custom-inputs:
|
||||
* summary: Example of patching an existing Custom Input
|
||||
* value:
|
||||
* required: true
|
||||
*
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventTypeCustomInput to edit
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
*
|
||||
* tags:
|
||||
* - custom-inputs
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventTypeCustomInput edited successfully
|
||||
* 400:
|
||||
* description: Bad request. EventType body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaEventTypeCustomInputEditBodyParams.parse(req.body);
|
||||
const result = await prisma.eventTypeCustomInput.update({ where: { id }, data });
|
||||
return { event_type_custom_input: schemaEventTypeCustomInputPublic.parse(result) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
18
calcom/apps/api/v1/pages/api/custom-inputs/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/custom-inputs/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
40
calcom/apps/api/v1/pages/api/custom-inputs/_get.ts
Normal file
40
calcom/apps/api/v1/pages/api/custom-inputs/_get.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeCustomInputPublic } from "~/lib/validations/event-type-custom-input";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /custom-inputs:
|
||||
* get:
|
||||
* summary: Find all eventTypeCustomInputs
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - custom-inputs
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No eventTypeCustomInputs were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.EventTypeCustomInputFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { eventType: { userId } } };
|
||||
const data = await prisma.eventTypeCustomInput.findMany(args);
|
||||
return { event_type_custom_inputs: data.map((v) => schemaEventTypeCustomInputPublic.parse(v)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
104
calcom/apps/api/v1/pages/api/custom-inputs/_post.ts
Normal file
104
calcom/apps/api/v1/pages/api/custom-inputs/_post.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaEventTypeCustomInputBodyParams,
|
||||
schemaEventTypeCustomInputPublic,
|
||||
} from "~/lib/validations/event-type-custom-input";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /custom-inputs:
|
||||
* post:
|
||||
* summary: Creates a new eventTypeCustomInput
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Create a new custom input for an event type
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - eventTypeId
|
||||
* - label
|
||||
* - type
|
||||
* - required
|
||||
* - placeholder
|
||||
* properties:
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'ID of the event type to which the custom input is being added'
|
||||
* label:
|
||||
* type: string
|
||||
* description: 'Label of the custom input'
|
||||
* type:
|
||||
* type: string
|
||||
* description: 'Type of the custom input. The value is ENUM; one of [TEXT, TEXTLONG, NUMBER, BOOL, RADIO, PHONE]'
|
||||
* options:
|
||||
* type: object
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* description: 'Options for the custom input'
|
||||
* required:
|
||||
* type: boolean
|
||||
* description: 'If the custom input is required before booking'
|
||||
* placeholder:
|
||||
* type: string
|
||||
* description: 'Placeholder text for the custom input'
|
||||
*
|
||||
* examples:
|
||||
* custom-inputs:
|
||||
* summary: An example of custom-inputs
|
||||
* value:
|
||||
* eventTypeID: 1
|
||||
* label: "Phone Number"
|
||||
* type: "PHONE"
|
||||
* required: true
|
||||
* placeholder: "100 101 1234"
|
||||
*
|
||||
* tags:
|
||||
* - custom-inputs
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventTypeCustomInput created
|
||||
* 400:
|
||||
* description: Bad request. EventTypeCustomInput body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body);
|
||||
|
||||
if (!isSystemWideAdmin) {
|
||||
/* We check that the user has access to the event type he's trying to add a custom input to. */
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: eventTypeId, userId },
|
||||
});
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
const data = await prisma.eventTypeCustomInput.create({
|
||||
data: { ...body, eventType: { connect: { id: eventTypeId } } },
|
||||
});
|
||||
|
||||
return {
|
||||
event_type_custom_input: schemaEventTypeCustomInputPublic.parse(data),
|
||||
message: "EventTypeCustomInput created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/custom-inputs/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/custom-inputs/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isSystemWideAdmin) return;
|
||||
const userEventTypes = await prisma.eventType.findMany({
|
||||
where: { userId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const userEventTypeIds = userEventTypes.map((eventType) => eventType.id);
|
||||
|
||||
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ id },
|
||||
{
|
||||
OR: [{ userId }, { eventTypeId: { in: userEventTypeIds } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!destinationCalendar)
|
||||
throw new HttpError({ statusCode: 404, message: "Destination calendar not found" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /destination-calendars/{id}:
|
||||
* delete:
|
||||
* summary: Remove an existing destination calendar
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the destination calendar to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - destination-calendars
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK, destinationCalendar removed successfully
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Destination calendar not found
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.destinationCalendar.delete({ where: { id } });
|
||||
return { message: `OK, Destination Calendar removed successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /destination-calendars/{id}:
|
||||
* get:
|
||||
* summary: Find a destination calendar
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the destination calendar to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - destination-calendars
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Destination calendar not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const destinationCalendar = await prisma.destinationCalendar.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse({ ...destinationCalendar }) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,313 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarEditBodyParams,
|
||||
schemaDestinationCalendarReadPublic,
|
||||
} from "~/lib/validations/destination-calendar";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /destination-calendars/{id}:
|
||||
* patch:
|
||||
* summary: Edit an existing destination calendar
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the destination calendar to edit
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Create a new booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* integration:
|
||||
* type: string
|
||||
* description: 'The integration'
|
||||
* externalId:
|
||||
* type: string
|
||||
* description: 'The external ID of the integration'
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'The ID of the eventType it is associated with'
|
||||
* bookingId:
|
||||
* type: integer
|
||||
* description: 'The booking ID it is associated with'
|
||||
* tags:
|
||||
* - destination-calendars
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Destination calendar not found
|
||||
*/
|
||||
type DestinationCalendarType = {
|
||||
userId?: number | null;
|
||||
eventTypeId?: number | null;
|
||||
credentialId: number | null;
|
||||
};
|
||||
|
||||
type UserCredentialType = {
|
||||
id: number;
|
||||
appId: string | null;
|
||||
type: string;
|
||||
userId: number | null;
|
||||
user: {
|
||||
email: string;
|
||||
} | null;
|
||||
teamId: number | null;
|
||||
key: Prisma.JsonValue;
|
||||
invalid: boolean | null;
|
||||
};
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||
const assignedUserId = isSystemWideAdmin ? parsedBody.userId || userId : userId;
|
||||
|
||||
validateIntegrationInput(parsedBody);
|
||||
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
|
||||
|
||||
const userCredentials = await getUserCredentials({
|
||||
credentialId: destinationCalendarObject.credentialId,
|
||||
userId: assignedUserId,
|
||||
prisma,
|
||||
});
|
||||
const credentialId = await verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId: destinationCalendarObject.credentialId,
|
||||
});
|
||||
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
|
||||
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
|
||||
const destinationCalendar = await prisma.destinationCalendar.update({
|
||||
where: { id },
|
||||
data: { ...parsedBody, credentialId },
|
||||
});
|
||||
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
|
||||
*
|
||||
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
|
||||
* @param userId - The user ID against which the credentials need to be verified.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An array containing the matching user credentials.
|
||||
*
|
||||
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
|
||||
*/
|
||||
async function getUserCredentials({
|
||||
credentialId,
|
||||
userId,
|
||||
prisma,
|
||||
}: {
|
||||
credentialId: number | null;
|
||||
userId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (!credentialId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar missing credential id`,
|
||||
});
|
||||
}
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: { id: credentialId, userId },
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (!userCredentials || userCredentials.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `Bad request, no associated credentials found`,
|
||||
});
|
||||
}
|
||||
return userCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided credentials and retrieves the associated credential ID.
|
||||
*
|
||||
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
|
||||
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
|
||||
* and checks for a matching external ID and integration from the list of connected calendars.
|
||||
*
|
||||
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
|
||||
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
|
||||
*
|
||||
* If the parsed body does not contain the necessary properties, the function
|
||||
* returns the `credentialId` from the destination calendar object.
|
||||
*
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* Checked if it contain properties like `integration` and `externalId`.
|
||||
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
|
||||
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
|
||||
* to fetch the default `credentialId`.
|
||||
*
|
||||
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
|
||||
* or the provided destination calendar object in other cases.
|
||||
*
|
||||
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
|
||||
*/
|
||||
async function verifyCredentialsAndGetId({
|
||||
parsedBody,
|
||||
userCredentials,
|
||||
currentCredentialId,
|
||||
}: {
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
userCredentials: UserCredentialType[];
|
||||
currentCredentialId: number | null;
|
||||
}) {
|
||||
if (parsedBody.integration && parsedBody.externalId) {
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
[],
|
||||
parsedBody.externalId
|
||||
);
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
return calendar?.credentialId;
|
||||
}
|
||||
return currentCredentialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the request for updating a destination calendar.
|
||||
*
|
||||
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
|
||||
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
|
||||
*
|
||||
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
|
||||
*
|
||||
* @param destinationCalendarObject - An object representing the destination calendar.
|
||||
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
|
||||
*/
|
||||
async function validateRequestAndOwnership({
|
||||
destinationCalendarObject,
|
||||
parsedBody,
|
||||
assignedUserId,
|
||||
prisma,
|
||||
}: {
|
||||
destinationCalendarObject: DestinationCalendarType;
|
||||
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||
assignedUserId: number;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
if (parsedBody.eventTypeId) {
|
||||
if (!destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can not be linked to an event type`,
|
||||
});
|
||||
}
|
||||
|
||||
const userEventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId },
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!userEventType || userEventType.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedBody.eventTypeId) {
|
||||
if (destinationCalendarObject.eventTypeId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `The provided destination calendar can only be linked to an event type`,
|
||||
});
|
||||
}
|
||||
if (destinationCalendarObject.userId !== assignedUserId) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: `Forbidden`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
|
||||
* indicating that the desired destination calendar was not found is thrown.
|
||||
*
|
||||
* @param id - The ID of the destination calendar to be retrieved.
|
||||
* @param prisma - An instance of PrismaClient for database operations.
|
||||
*
|
||||
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
|
||||
*
|
||||
* @throws HttpError - If no destination calendar matches the provided ID.
|
||||
*/
|
||||
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
|
||||
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
select: { userId: true, eventTypeId: true, credentialId: true },
|
||||
});
|
||||
|
||||
if (!destinationCalendarObject) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: `Destination calendar with ID ${id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return destinationCalendarObject;
|
||||
}
|
||||
|
||||
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
|
||||
if (parsedBody.integration && !parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
|
||||
}
|
||||
if (!parsedBody.integration && parsedBody.externalId) {
|
||||
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
59
calcom/apps/api/v1/pages/api/destination-calendars/_get.ts
Normal file
59
calcom/apps/api/v1/pages/api/destination-calendars/_get.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||
import { schemaDestinationCalendarReadPublic } from "~/lib/validations/destination-calendar";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /destination-calendars:
|
||||
* get:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* summary: Find all destination calendars
|
||||
* tags:
|
||||
* - destination-calendars
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No destination calendars were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const userEventTypes = await prisma.eventType.findMany({
|
||||
where: { userId: { in: userIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const userEventTypeIds = userEventTypes.map((eventType) => eventType.id);
|
||||
|
||||
const allDestinationCalendars = await prisma.destinationCalendar.findMany({
|
||||
where: {
|
||||
OR: [{ userId: { in: userIds } }, { eventTypeId: { in: userEventTypeIds } }],
|
||||
},
|
||||
});
|
||||
|
||||
if (allDestinationCalendars.length === 0)
|
||||
new HttpError({ statusCode: 404, message: "No destination calendars were found" });
|
||||
|
||||
return {
|
||||
destinationCalendars: allDestinationCalendars.map((destinationCalendar) =>
|
||||
schemaDestinationCalendarReadPublic.parse(destinationCalendar)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
143
calcom/apps/api/v1/pages/api/destination-calendars/_post.ts
Normal file
143
calcom/apps/api/v1/pages/api/destination-calendars/_post.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import {
|
||||
schemaDestinationCalendarReadPublic,
|
||||
schemaDestinationCalendarCreateBodyParams,
|
||||
} from "~/lib/validations/destination-calendar";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /destination-calendars:
|
||||
* post:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* summary: Creates a new destination calendar
|
||||
* requestBody:
|
||||
* description: Create a new destination calendar for your events
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - integration
|
||||
* - externalId
|
||||
* - credentialId
|
||||
* properties:
|
||||
* integration:
|
||||
* type: string
|
||||
* description: 'The integration'
|
||||
* externalId:
|
||||
* type: string
|
||||
* description: 'The external ID of the integration'
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
* description: 'The ID of the eventType it is associated with'
|
||||
* bookingId:
|
||||
* type: integer
|
||||
* description: 'The booking ID it is associated with'
|
||||
* userId:
|
||||
* type: integer
|
||||
* description: 'The user it is associated with'
|
||||
* tags:
|
||||
* - destination-calendars
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, destination calendar created
|
||||
* 400:
|
||||
* description: Bad request. DestinationCalendar body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||
await checkPermissions(req, userId);
|
||||
|
||||
const assignedUserId = isSystemWideAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||
|
||||
/* Check if credentialId data matches the ownership and integration passed in */
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
type: parsedBody.integration,
|
||||
userId: assignedUserId,
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
});
|
||||
|
||||
if (userCredentials.length === 0)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
|
||||
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||
|
||||
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||
const calendar = eligibleCalendars?.find(
|
||||
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||
);
|
||||
if (!calendar?.credentialId)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, credential id invalid",
|
||||
});
|
||||
const credentialId = calendar.credentialId;
|
||||
|
||||
if (parsedBody.eventTypeId) {
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
||||
});
|
||||
if (!eventType)
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Bad request, eventTypeId invalid",
|
||||
});
|
||||
parsedBody.userId = undefined;
|
||||
}
|
||||
|
||||
const destination_calendar = await prisma.destinationCalendar.create({
|
||||
data: { ...parsedBody, credentialId },
|
||||
});
|
||||
|
||||
return {
|
||||
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||
message: "Destination calendar created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, userId: number) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaDestinationCalendarCreateBodyParams.parse(req.body);
|
||||
|
||||
/* Non-admin users can only create destination calendars for themselves */
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId */
|
||||
if (isSystemWideAdmin && !body.userId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` required" });
|
||||
/* User should only be able to create for their own destination calendars*/
|
||||
if (!isSystemWideAdmin && body.eventTypeId) {
|
||||
const ownsEventType = await prisma.eventType.findFirst({ where: { id: body.eventTypeId, userId } });
|
||||
if (!ownsEventType) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
}
|
||||
// TODO:: Add support for team event types with validation
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/destination-calendars/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/destination-calendars/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
184
calcom/apps/api/v1/pages/api/docs.ts
Normal file
184
calcom/apps/api/v1/pages/api/docs.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { withSwagger } from "next-swagger-doc";
|
||||
|
||||
import pjson from "~/package.json";
|
||||
|
||||
const swaggerHandler = withSwagger({
|
||||
definition: {
|
||||
openapi: "3.0.3",
|
||||
servers: [
|
||||
{ url: "http://localhost:3002/v1" },
|
||||
{ url: "https://api.cal.dev/v1" },
|
||||
{ url: "https://api.cal.com/v1" },
|
||||
],
|
||||
externalDocs: {
|
||||
url: "https://docs.cal.com",
|
||||
description: "Find more info at our main docs: https://docs.cal.com/",
|
||||
},
|
||||
info: {
|
||||
title: `${pjson.name}: ${pjson.description}`,
|
||||
version: pjson.version,
|
||||
},
|
||||
components: {
|
||||
securitySchemes: { ApiKeyAuth: { type: "apiKey", in: "query", name: "apiKey" } },
|
||||
schemas: {
|
||||
ArrayOfBookings: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/components/schemas/Booking",
|
||||
},
|
||||
},
|
||||
ArrayOfRecordings: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/components/schemas/Recording",
|
||||
},
|
||||
},
|
||||
Recording: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
room_name: {
|
||||
type: "string",
|
||||
},
|
||||
start_ts: {
|
||||
type: "number",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
},
|
||||
max_participants: {
|
||||
type: "number",
|
||||
},
|
||||
duration: {
|
||||
type: "number",
|
||||
},
|
||||
download_link: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
Booking: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "number",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
eventTypeId: {
|
||||
type: "number",
|
||||
},
|
||||
uid: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
},
|
||||
startTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
},
|
||||
timeZone: {
|
||||
type: "string",
|
||||
example: "Europe/London",
|
||||
},
|
||||
fromReschedule: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
format: "uuid",
|
||||
},
|
||||
attendees: {
|
||||
type: "array",
|
||||
items: {
|
||||
properties: {
|
||||
email: {
|
||||
type: "string",
|
||||
example: "example@cal.com",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
timeZone: {
|
||||
type: "string",
|
||||
example: "Europe/London",
|
||||
},
|
||||
locale: {
|
||||
type: "string",
|
||||
example: "en",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
properties: {
|
||||
email: {
|
||||
type: "string",
|
||||
example: "example@cal.com",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
timeZone: {
|
||||
type: "string",
|
||||
example: "Europe/London",
|
||||
},
|
||||
locale: {
|
||||
type: "string",
|
||||
example: "en",
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
type: Array,
|
||||
items: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "number",
|
||||
example: 1,
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
example: true,
|
||||
},
|
||||
paymentOption: {
|
||||
type: "string",
|
||||
example: "ON_BOOKING",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
tags: [
|
||||
{ name: "users" },
|
||||
{ name: "event-types" },
|
||||
{ name: "bookings" },
|
||||
{ name: "attendees" },
|
||||
{ name: "payments" },
|
||||
{ name: "schedules" },
|
||||
{ name: "teams" },
|
||||
{ name: "memberships" },
|
||||
{
|
||||
name: "availabilities",
|
||||
description: "Allows modifying unique availabilities tied to a schedule.",
|
||||
},
|
||||
{ name: "custom-inputs" },
|
||||
{ name: "event-references" },
|
||||
{ name: "booking-references" },
|
||||
{ name: "destination-calendars" },
|
||||
{ name: "selected-calendars" },
|
||||
],
|
||||
},
|
||||
apiFolder: "pages/api",
|
||||
});
|
||||
|
||||
export default swaggerHandler();
|
||||
57
calcom/apps/api/v1/pages/api/event-types/[id]/_delete.ts
Normal file
57
calcom/apps/api/v1/pages/api/event-types/[id]/_delete.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /event-types/{id}:
|
||||
* delete:
|
||||
* operationId: removeEventTypeById
|
||||
* summary: Remove an existing eventType
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventType to delete
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventType removed successfully
|
||||
* 400:
|
||||
* description: Bad request. EventType id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await checkPermissions(req);
|
||||
await prisma.eventType.delete({ where: { id } });
|
||||
return { message: `Event Type with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can delete it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
102
calcom/apps/api/v1/pages/api/event-types/[id]/_get.ts
Normal file
102
calcom/apps/api/v1/pages/api/event-types/[id]/_get.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import getCalLink from "../_utils/getCalLink";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /event-types/{id}:
|
||||
* get:
|
||||
* operationId: getEventTypeById
|
||||
* summary: Find a eventType
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* - in: path
|
||||
* name: id
|
||||
* example: 4
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventType to get
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: EventType was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
});
|
||||
await checkPermissions(req, eventType);
|
||||
|
||||
const link = eventType ? getCalLink(eventType) : null;
|
||||
// user.defaultScheduleId doesn't work the same for team events.
|
||||
if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
eventType.scheduleId = user.defaultScheduleId;
|
||||
}
|
||||
|
||||
// TODO: eventType when not found should be a 404
|
||||
// but API consumers may depend on the {} behaviour.
|
||||
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
|
||||
}
|
||||
|
||||
type BaseEventTypeCheckPermissions = {
|
||||
userId: number | null;
|
||||
teamId: number | null;
|
||||
};
|
||||
|
||||
async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
req: NextApiRequest,
|
||||
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||
) {
|
||||
if (req.isSystemWideAdmin) return true;
|
||||
if (eventType?.teamId) {
|
||||
req.query.teamId = String(eventType.teamId);
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
249
calcom/apps/api/v1/pages/api/event-types/[id]/_patch.ts
Normal file
249
calcom/apps/api/v1/pages/api/event-types/[id]/_patch.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import type { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type";
|
||||
import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
import ensureOnlyMembersAsHosts from "~/pages/api/event-types/_utils/ensureOnlyMembersAsHosts";
|
||||
|
||||
import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /event-types/{id}:
|
||||
* patch:
|
||||
* operationId: editEventTypeById
|
||||
* summary: Edit an existing eventType
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the eventType to edit
|
||||
* requestBody:
|
||||
* description: Create a new event-type related to your user or team
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* length:
|
||||
* type: integer
|
||||
* description: Duration of the event type in minutes
|
||||
* metadata:
|
||||
* type: object
|
||||
* description: Metadata relating to event type. Pass {} if empty
|
||||
* title:
|
||||
* type: string
|
||||
* description: Title of the event type
|
||||
* slug:
|
||||
* type: string
|
||||
* description: Unique slug for the event type
|
||||
* scheduleId:
|
||||
* type: number
|
||||
* description: The ID of the schedule for this event type
|
||||
* hosts:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* userId:
|
||||
* type: number
|
||||
* isFixed:
|
||||
* type: boolean
|
||||
* description: Host MUST be available for any slot to be bookable.
|
||||
* hidden:
|
||||
* type: boolean
|
||||
* description: If the event type should be hidden from your public booking page
|
||||
* position:
|
||||
* type: integer
|
||||
* description: The position of the event type on the public booking page
|
||||
* teamId:
|
||||
* type: integer
|
||||
* description: Team ID if the event type should belong to a team
|
||||
* periodType:
|
||||
* type: string
|
||||
* enum: [UNLIMITED, ROLLING, RANGE]
|
||||
* description: To decide how far into the future an invitee can book an event with you
|
||||
* periodStartDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: Start date of bookable period (Required if periodType is 'range')
|
||||
* periodEndDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: End date of bookable period (Required if periodType is 'range')
|
||||
* periodDays:
|
||||
* type: integer
|
||||
* description: Number of bookable days (Required if periodType is rolling)
|
||||
* periodCountCalendarDays:
|
||||
* type: boolean
|
||||
* description: If calendar days should be counted for period days
|
||||
* requiresConfirmation:
|
||||
* type: boolean
|
||||
* description: If the event type should require your confirmation before completing the booking
|
||||
* recurringEvent:
|
||||
* type: object
|
||||
* description: If the event should recur every week/month/year with the selected frequency
|
||||
* properties:
|
||||
* interval:
|
||||
* type: integer
|
||||
* count:
|
||||
* type: integer
|
||||
* freq:
|
||||
* type: integer
|
||||
* disableGuests:
|
||||
* type: boolean
|
||||
* description: If the event type should disable adding guests to the booking
|
||||
* hideCalendarNotes:
|
||||
* type: boolean
|
||||
* description: If the calendar notes should be hidden from the booking
|
||||
* minimumBookingNotice:
|
||||
* type: integer
|
||||
* description: Minimum time in minutes before the event is bookable
|
||||
* beforeEventBuffer:
|
||||
* type: integer
|
||||
* description: Number of minutes of buffer time before a Cal Event
|
||||
* afterEventBuffer:
|
||||
* type: integer
|
||||
* description: Number of minutes of buffer time after a Cal Event
|
||||
* schedulingType:
|
||||
* type: string
|
||||
* description: The type of scheduling if a Team event. Required for team events only
|
||||
* enum: [ROUND_ROBIN, COLLECTIVE]
|
||||
* price:
|
||||
* type: integer
|
||||
* description: Price of the event type booking
|
||||
* currency:
|
||||
* type: string
|
||||
* description: Currency acronym. Eg- usd, eur, gbp, etc.
|
||||
* slotInterval:
|
||||
* type: integer
|
||||
* description: The intervals of available bookable slots in minutes
|
||||
* successRedirectUrl:
|
||||
* type: string
|
||||
* format: url
|
||||
* description: A valid URL where the booker will redirect to, once the booking is completed successfully
|
||||
* description:
|
||||
* type: string
|
||||
* description: Description of the event type
|
||||
* seatsPerTimeSlot:
|
||||
* type: integer
|
||||
* description: 'The number of seats for each time slot'
|
||||
* seatsShowAttendees:
|
||||
* type: boolean
|
||||
* description: 'Share Attendee information in seats'
|
||||
* seatsShowAvailabilityCount:
|
||||
* type: boolean
|
||||
* description: 'Show the number of available seats'
|
||||
* locations:
|
||||
* type: array
|
||||
* description: A list of all available locations for the event type
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* oneOf:
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['integrations:daily']
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['attendeeInPerson']
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['inPerson']
|
||||
* address:
|
||||
* type: string
|
||||
* displayLocationPublicly:
|
||||
* type: boolean
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['link']
|
||||
* link:
|
||||
* type: string
|
||||
* displayLocationPublicly:
|
||||
* type: boolean
|
||||
* example:
|
||||
* event-type:
|
||||
* summary: An example of event type PATCH request
|
||||
* value:
|
||||
* length: 60
|
||||
* requiresConfirmation: true
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, eventType edited successfully
|
||||
* 400:
|
||||
* description: Bad request. EventType body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
/** FIXME: Updating event-type children from API not supported for now */
|
||||
children: _,
|
||||
...parsedBody
|
||||
} = schemaEventTypeEditBodyParams.parse(body);
|
||||
|
||||
const data: Prisma.EventTypeUpdateArgs["data"] = {
|
||||
...parsedBody,
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
if (hosts) {
|
||||
await ensureOnlyMembersAsHosts(req, parsedBody);
|
||||
data.hosts = {
|
||||
deleteMany: {},
|
||||
create: hosts.map((host) => ({
|
||||
...host,
|
||||
isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed,
|
||||
})),
|
||||
};
|
||||
}
|
||||
await checkPermissions(req, parsedBody);
|
||||
const eventType = await prisma.eventType.update({ where: { id }, data });
|
||||
return { event_type: schemaEventTypeReadPublic.parse(eventType) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaEventTypeBaseBodyParams>) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
if (isSystemWideAdmin) return;
|
||||
/** Only event type owners can modify it */
|
||||
const eventType = await prisma.eventType.findFirst({ where: { id, userId } });
|
||||
if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
await checkTeamEventEditPermission(req, body);
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
15
calcom/apps/api/v1/pages/api/event-types/[id]/index.ts
Normal file
15
calcom/apps/api/v1/pages/api/event-types/[id]/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
135
calcom/apps/api/v1/pages/api/event-types/_get.ts
Normal file
135
calcom/apps/api/v1/pages/api/event-types/_get.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { schemaQuerySlug } from "~/lib/validations/shared/querySlug";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
import getCalLink from "./_utils/getCalLink";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /event-types:
|
||||
* get:
|
||||
* summary: Find all event types
|
||||
* operationId: listEventTypes
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* - in: query
|
||||
* name: slug
|
||||
* schema:
|
||||
* type: string
|
||||
* required: false
|
||||
* description: Slug to filter event types by
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No event types were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
const { slug } = schemaQuerySlug.parse(req.query);
|
||||
const shouldUseUserId = !isSystemWideAdmin || !slug || !!req.query.userId;
|
||||
// When user is admin and no query params are provided we should return all event types.
|
||||
// But currently we return only the event types of the user. Not changing this for backwards compatibility.
|
||||
const data = await prisma.eventType.findMany({
|
||||
where: {
|
||||
userId: shouldUseUserId ? { in: userIds } : undefined,
|
||||
slug: slug, // slug will be undefined if not provided in query
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
team: { select: { slug: true } },
|
||||
hosts: { select: { userId: true, isFixed: true } },
|
||||
owner: { select: { username: true, id: true } },
|
||||
children: { select: { id: true, userId: true } },
|
||||
},
|
||||
});
|
||||
// this really should return [], but backwards compatibility..
|
||||
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
||||
return {
|
||||
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
|
||||
(eventType) => {
|
||||
const link = getCalLink(eventType);
|
||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
// TODO: Extract & reuse.
|
||||
function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||
}
|
||||
|
||||
type DefaultScheduleIdEventTypeBase = {
|
||||
scheduleId: number | null;
|
||||
userId: number | null;
|
||||
};
|
||||
// If an eventType is given w/o a scheduleId
|
||||
// Then we associate the default user schedule id to the eventType
|
||||
async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
|
||||
prisma,
|
||||
eventTypes,
|
||||
userIds,
|
||||
}: {
|
||||
prisma: PrismaClient;
|
||||
eventTypes: (T & Partial<Omit<T, keyof DefaultScheduleIdEventTypeBase>>)[];
|
||||
userIds: number[];
|
||||
}) {
|
||||
// there is no event types without a scheduleId, skip the user query
|
||||
if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes;
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
defaultScheduleId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!users.length) {
|
||||
return eventTypes;
|
||||
}
|
||||
|
||||
const defaultScheduleIds = users.reduce((result, user) => {
|
||||
result[user.id] = user.defaultScheduleId;
|
||||
return result;
|
||||
}, {} as { [x: number]: number | null });
|
||||
|
||||
return eventTypes.map((eventType) => {
|
||||
// realistically never happens, userId should't be null on personal event types.
|
||||
if (!eventType.userId) return eventType;
|
||||
return {
|
||||
...eventType,
|
||||
scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
338
calcom/apps/api/v1/pages/api/event-types/_post.ts
Normal file
338
calcom/apps/api/v1/pages/api/event-types/_post.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||
import { canUserAccessTeamWithRole } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||
|
||||
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
|
||||
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
|
||||
import checkUserMembership from "./_utils/checkUserMembership";
|
||||
import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /event-types:
|
||||
* post:
|
||||
* summary: Creates a new event type
|
||||
* operationId: addEventType
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* schema:
|
||||
* type: string
|
||||
* required: true
|
||||
* description: Your API Key
|
||||
* requestBody:
|
||||
* description: Create a new event-type related to your user or team
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - title
|
||||
* - slug
|
||||
* - length
|
||||
* - metadata
|
||||
* properties:
|
||||
* length:
|
||||
* type: integer
|
||||
* description: Duration of the event type in minutes
|
||||
* metadata:
|
||||
* type: object
|
||||
* description: Metadata relating to event type. Pass {} if empty
|
||||
* title:
|
||||
* type: string
|
||||
* description: Title of the event type
|
||||
* slug:
|
||||
* type: string
|
||||
* description: Unique slug for the event type
|
||||
* hosts:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* userId:
|
||||
* type: number
|
||||
* isFixed:
|
||||
* type: boolean
|
||||
* description: Host MUST be available for any slot to be bookable.
|
||||
* hidden:
|
||||
* type: boolean
|
||||
* description: If the event type should be hidden from your public booking page
|
||||
* scheduleId:
|
||||
* type: number
|
||||
* description: The ID of the schedule for this event type
|
||||
* position:
|
||||
* type: integer
|
||||
* description: The position of the event type on the public booking page
|
||||
* teamId:
|
||||
* type: integer
|
||||
* description: Team ID if the event type should belong to a team
|
||||
* periodType:
|
||||
* type: string
|
||||
* enum: [UNLIMITED, ROLLING, RANGE]
|
||||
* description: To decide how far into the future an invitee can book an event with you
|
||||
* periodStartDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: Start date of bookable period (Required if periodType is 'range')
|
||||
* periodEndDate:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: End date of bookable period (Required if periodType is 'range')
|
||||
* periodDays:
|
||||
* type: integer
|
||||
* description: Number of bookable days (Required if periodType is rolling)
|
||||
* periodCountCalendarDays:
|
||||
* type: boolean
|
||||
* description: If calendar days should be counted for period days
|
||||
* requiresConfirmation:
|
||||
* type: boolean
|
||||
* description: If the event type should require your confirmation before completing the booking
|
||||
* recurringEvent:
|
||||
* type: object
|
||||
* description: If the event should recur every week/month/year with the selected frequency
|
||||
* properties:
|
||||
* interval:
|
||||
* type: integer
|
||||
* count:
|
||||
* type: integer
|
||||
* freq:
|
||||
* type: integer
|
||||
* disableGuests:
|
||||
* type: boolean
|
||||
* description: If the event type should disable adding guests to the booking
|
||||
* hideCalendarNotes:
|
||||
* type: boolean
|
||||
* description: If the calendar notes should be hidden from the booking
|
||||
* minimumBookingNotice:
|
||||
* type: integer
|
||||
* description: Minimum time in minutes before the event is bookable
|
||||
* beforeEventBuffer:
|
||||
* type: integer
|
||||
* description: Number of minutes of buffer time before a Cal Event
|
||||
* afterEventBuffer:
|
||||
* type: integer
|
||||
* description: Number of minutes of buffer time after a Cal Event
|
||||
* schedulingType:
|
||||
* type: string
|
||||
* description: The type of scheduling if a Team event. Required for team events only
|
||||
* enum: [ROUND_ROBIN, COLLECTIVE, MANAGED]
|
||||
* price:
|
||||
* type: integer
|
||||
* description: Price of the event type booking
|
||||
* parentId:
|
||||
* type: integer
|
||||
* description: EventTypeId of the parent managed event
|
||||
* currency:
|
||||
* type: string
|
||||
* description: Currency acronym. Eg- usd, eur, gbp, etc.
|
||||
* slotInterval:
|
||||
* type: integer
|
||||
* description: The intervals of available bookable slots in minutes
|
||||
* successRedirectUrl:
|
||||
* type: string
|
||||
* format: url
|
||||
* description: A valid URL where the booker will redirect to, once the booking is completed successfully
|
||||
* description:
|
||||
* type: string
|
||||
* description: Description of the event type
|
||||
* locations:
|
||||
* type: array
|
||||
* description: A list of all available locations for the event type
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* oneOf:
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['integrations:daily']
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['attendeeInPerson']
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['inPerson']
|
||||
* address:
|
||||
* type: string
|
||||
* displayLocationPublicly:
|
||||
* type: boolean
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* enum: ['link']
|
||||
* link:
|
||||
* type: string
|
||||
* displayLocationPublicly:
|
||||
* type: boolean
|
||||
* examples:
|
||||
* event-type:
|
||||
* summary: An example of an individual event type POST request
|
||||
* value:
|
||||
* title: Hello World
|
||||
* slug: hello-world
|
||||
* length: 30
|
||||
* hidden: false
|
||||
* position: 0
|
||||
* eventName: null
|
||||
* timeZone: null
|
||||
* scheduleId: 5
|
||||
* periodType: UNLIMITED
|
||||
* periodStartDate: 2023-02-15T08:46:16.000Z
|
||||
* periodEndDate: 2023-0-15T08:46:16.000Z
|
||||
* periodDays: null
|
||||
* periodCountCalendarDays: false
|
||||
* requiresConfirmation: false
|
||||
* recurringEvent: null
|
||||
* disableGuests: false
|
||||
* hideCalendarNotes: false
|
||||
* minimumBookingNotice: 120
|
||||
* beforeEventBuffer: 0
|
||||
* afterEventBuffer: 0
|
||||
* price: 0
|
||||
* currency: usd
|
||||
* slotInterval: null
|
||||
* successRedirectUrl: null
|
||||
* description: A test event type
|
||||
* metadata: {
|
||||
* apps: {
|
||||
* stripe: {
|
||||
* price: 0,
|
||||
* enabled: false,
|
||||
* currency: usd
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* team-event-type:
|
||||
* summary: An example of a team event type POST request
|
||||
* value:
|
||||
* title: "Tennis class"
|
||||
* slug: "tennis-class-{{$guid}}"
|
||||
* length: 60
|
||||
* hidden: false
|
||||
* position: 0
|
||||
* teamId: 3
|
||||
* eventName: null
|
||||
* timeZone: null
|
||||
* periodType: "UNLIMITED"
|
||||
* periodStartDate: null
|
||||
* periodEndDate: null
|
||||
* periodDays: null
|
||||
* periodCountCalendarDays: null
|
||||
* requiresConfirmation: true
|
||||
* recurringEvent:
|
||||
* interval: 2
|
||||
* count: 10
|
||||
* freq: 2
|
||||
* disableGuests: false
|
||||
* hideCalendarNotes: false
|
||||
* minimumBookingNotice: 120
|
||||
* beforeEventBuffer: 0
|
||||
* afterEventBuffer: 0
|
||||
* schedulingType: "COLLECTIVE"
|
||||
* price: 0
|
||||
* currency: "usd"
|
||||
* slotInterval: null
|
||||
* successRedirectUrl: null
|
||||
* description: null
|
||||
* locations:
|
||||
* - address: "London"
|
||||
* type: "inPerson"
|
||||
* metadata: {}
|
||||
* tags:
|
||||
* - event-types
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/core-features/event-types
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, event type created
|
||||
* 400:
|
||||
* description: Bad request. EventType body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, body } = req;
|
||||
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
/** FIXME: Adding event-type children from API not supported for now */
|
||||
children: _,
|
||||
...parsedBody
|
||||
} = schemaEventTypeCreateBodyParams.parse(body || {});
|
||||
|
||||
let data: Prisma.EventTypeCreateArgs["data"] = {
|
||||
...parsedBody,
|
||||
userId,
|
||||
users: { connect: { id: userId } },
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
if (parsedBody.parentId) {
|
||||
await checkParentEventOwnership(req);
|
||||
await checkUserMembership(req);
|
||||
}
|
||||
|
||||
if (isSystemWideAdmin && parsedBody.userId) {
|
||||
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
|
||||
}
|
||||
|
||||
await checkTeamEventEditPermission(req, parsedBody);
|
||||
await ensureOnlyMembersAsHosts(req, parsedBody);
|
||||
|
||||
if (hosts) {
|
||||
data.hosts = { createMany: { data: hosts } };
|
||||
}
|
||||
|
||||
const eventType = await prisma.eventType.create({ data, include: { hosts: true } });
|
||||
|
||||
return {
|
||||
event_type: schemaEventTypeReadPublic.parse(eventType),
|
||||
message: "Event type created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
const body = schemaEventTypeCreateBodyParams.parse(req.body);
|
||||
/* Non-admin users can only create event types for themselves */
|
||||
if (!isSystemWideAdmin && body.userId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (
|
||||
body.teamId &&
|
||||
!isSystemWideAdmin &&
|
||||
!(await canUserAccessTeamWithRole(req.userId, isSystemWideAdmin, body.teamId, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
}))
|
||||
)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `teamId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isSystemWideAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* Checks if a user, identified by the provided userId, has ownership (or admin rights) over
|
||||
* the team associated with the event type identified by the parentId.
|
||||
*
|
||||
* @param req - The current request
|
||||
*
|
||||
* @throws {HttpError} If the parent event type is not found,
|
||||
* if the parent event type doesn't belong to any team,
|
||||
* or if the user doesn't have ownership or admin rights to the associated team.
|
||||
*/
|
||||
export default async function checkParentEventOwnership(req: NextApiRequest) {
|
||||
const { userId, body } = req;
|
||||
/** These are already parsed upstream, we can assume they're good here. */
|
||||
const parentId = Number(body.parentId);
|
||||
const parentEventType = await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: parentId,
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!parentEventType) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: "Parent event type not found.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!parentEventType.teamId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "This event type is not capable of having children",
|
||||
});
|
||||
}
|
||||
|
||||
const teamMember = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId: parentEventType.teamId,
|
||||
userId: userId,
|
||||
OR: [{ role: "OWNER" }, { role: "ADMIN" }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMember) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "User is not authorized to access the team to which the parent event type belongs.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type";
|
||||
|
||||
export default async function checkTeamEventEditPermission(
|
||||
req: NextApiRequest,
|
||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "teamId" | "userId">
|
||||
) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
let userId = req.userId;
|
||||
if (isSystemWideAdmin && body.userId) {
|
||||
userId = body.userId;
|
||||
}
|
||||
if (body.teamId) {
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId: body.teamId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "No permission to operate on event-type for this team",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* Checks if a user, identified by the provided userId, is a member of the team associated
|
||||
* with the event type identified by the parentId.
|
||||
*
|
||||
* @param req - The current request
|
||||
*
|
||||
* @throws {HttpError} If the event type is not found,
|
||||
* if the event type doesn't belong to any team,
|
||||
* or if the user isn't a member of the associated team.
|
||||
*/
|
||||
export default async function checkUserMembership(req: NextApiRequest) {
|
||||
const { body } = req;
|
||||
/** These are already parsed upstream, we can assume they're good here. */
|
||||
const parentId = Number(body.parentId);
|
||||
const userId = Number(body.userId);
|
||||
const parentEventType = await prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: parentId,
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!parentEventType) {
|
||||
throw new HttpError({
|
||||
statusCode: 404,
|
||||
message: "Event type not found.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!parentEventType.teamId) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "This event type is not capable of having children.",
|
||||
});
|
||||
}
|
||||
|
||||
const teamMember = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId: parentEventType.teamId,
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamMember) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "User is not a team member.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { schemaEventTypeCreateBodyParams } from "~/lib/validations/event-type";
|
||||
|
||||
export default async function ensureOnlyMembersAsHosts(
|
||||
req: NextApiRequest,
|
||||
body: Pick<z.infer<typeof schemaEventTypeCreateBodyParams>, "hosts" | "teamId">
|
||||
) {
|
||||
if (body.teamId && body.hosts && body.hosts.length > 0) {
|
||||
const teamMemberCount = await prisma.membership.count({
|
||||
where: {
|
||||
teamId: body.teamId,
|
||||
userId: { in: body.hosts.map((host) => host.userId) },
|
||||
},
|
||||
});
|
||||
if (teamMemberCount !== body.hosts.length) {
|
||||
throw new Error("You can only add members of the team to a team event type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
export default function getCalLink(eventType: {
|
||||
team?: { slug: string | null } | null;
|
||||
owner?: { username: string | null } | null;
|
||||
users?: { username: string | null }[];
|
||||
slug: string;
|
||||
}) {
|
||||
return `${WEBSITE_URL}/${
|
||||
eventType?.team
|
||||
? `team/${eventType?.team?.slug}`
|
||||
: eventType?.owner
|
||||
? eventType.owner.username
|
||||
: eventType?.users?.[0]?.username
|
||||
}/${eventType?.slug}`;
|
||||
}
|
||||
10
calcom/apps/api/v1/pages/api/event-types/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/event-types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
5
calcom/apps/api/v1/pages/api/index.ts
Normal file
5
calcom/apps/api/v1/pages/api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||
}
|
||||
72
calcom/apps/api/v1/pages/api/invites/_post.ts
Normal file
72
calcom/apps/api/v1/pages/api/invites/_post.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
|
||||
import { ZInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const data = ZInviteMemberInputSchema.parse(req.body);
|
||||
await checkPermissions(req, data);
|
||||
|
||||
async function sessionGetter() {
|
||||
return {
|
||||
user: {
|
||||
id: req.userId,
|
||||
username: "",
|
||||
profile: {
|
||||
id: null,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
username: "",
|
||||
upId: "",
|
||||
} satisfies UserProfile,
|
||||
},
|
||||
hasValidLicense: true,
|
||||
expires: "",
|
||||
upId: "",
|
||||
};
|
||||
}
|
||||
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
try {
|
||||
const caller = viewerTeamsRouter.createCaller(ctx);
|
||||
await caller.inviteMember({
|
||||
role: data.role,
|
||||
language: data.language,
|
||||
teamId: data.teamId,
|
||||
usernameOrEmail: data.usernameOrEmail,
|
||||
});
|
||||
|
||||
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
throw new HttpError({ statusCode, message: cause.message });
|
||||
}
|
||||
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: TInviteMemberInputSchema) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } },
|
||||
});
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
9
calcom/apps/api/v1/pages/api/invites/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/invites/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
18
calcom/apps/api/v1/pages/api/me/_get.ts
Normal file
18
calcom/apps/api/v1/pages/api/me/_get.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaUserReadPublic } from "~/lib/validations/user";
|
||||
|
||||
async function handler({ userId }: NextApiRequest) {
|
||||
const data = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
||||
return {
|
||||
user: schemaUserReadPublic.parse({
|
||||
...data,
|
||||
avatar: data.avatarUrl,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
9
calcom/apps/api/v1/pages/api/me/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/me/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { membershipIdSchema } from "~/lib/validations/membership";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { teamId } = membershipIdSchema.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only team members can modify a membership
|
||||
const membership = await prisma.membership.findFirst({ where: { userId, teamId } });
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
103
calcom/apps/api/v1/pages/api/memberships/[id]/_delete.ts
Normal file
103
calcom/apps/api/v1/pages/api/memberships/[id]/_delete.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { membershipIdSchema } from "~/lib/validations/membership";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /memberships/{userId}_{teamId}:
|
||||
* delete:
|
||||
* summary: Remove an existing membership
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric userId of the membership to get
|
||||
* - in: path
|
||||
* name: teamId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric teamId of the membership to get
|
||||
* tags:
|
||||
* - memberships
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, membership removed successfuly
|
||||
* 400:
|
||||
* description: Bad request. Membership id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
await checkPermissions(req);
|
||||
await prisma.membership.delete({ where: { userId_teamId } });
|
||||
return { message: `Membership with id: ${query.id} deleted successfully` };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin, userId, query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
// Admin User can do anything including deletion of Admin Team Member in any team
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Owner can delete Admin and Member
|
||||
// Admin Team Member can delete Member
|
||||
// Member can't delete anyone
|
||||
const PRIVILEGE_ORDER = ["OWNER", "ADMIN", "MEMBER"];
|
||||
|
||||
const memberShipToBeDeleted = await prisma.membership.findUnique({
|
||||
where: { userId_teamId },
|
||||
});
|
||||
|
||||
if (!memberShipToBeDeleted) {
|
||||
throw new HttpError({ statusCode: 404, message: "Membership not found" });
|
||||
}
|
||||
|
||||
// If a user is deleting their own membership, then they can do it
|
||||
if (userId === memberShipToBeDeleted.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUserMembership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId,
|
||||
teamId: memberShipToBeDeleted.teamId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentUserMembership) {
|
||||
// Current User isn't a member of the team
|
||||
throw new HttpError({ statusCode: 403, message: "You are not a member of the team" });
|
||||
}
|
||||
|
||||
if (
|
||||
PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) === -1 ||
|
||||
PRIVILEGE_ORDER.indexOf(currentUserMembership.role) === -1
|
||||
) {
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid role" });
|
||||
}
|
||||
|
||||
// If Role that is being deleted comes before the current User's Role, or it's the same ROLE, throw error
|
||||
if (
|
||||
PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) <= PRIVILEGE_ORDER.indexOf(currentUserMembership.role)
|
||||
) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "You don't have the appropriate role to delete this membership",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
||||
47
calcom/apps/api/v1/pages/api/memberships/[id]/_get.ts
Normal file
47
calcom/apps/api/v1/pages/api/memberships/[id]/_get.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { membershipIdSchema, schemaMembershipPublic } from "~/lib/validations/membership";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /memberships/{userId}_{teamId}:
|
||||
* get:
|
||||
* summary: Find a membership by userID and teamID
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric userId of the membership to get
|
||||
* - in: path
|
||||
* name: teamId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric teamId of the membership to get
|
||||
* tags:
|
||||
* - memberships
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Membership was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
const args: Prisma.MembershipFindUniqueOrThrowArgs = { where: { userId_teamId } };
|
||||
// Just in case the user want to get more info about the team itself
|
||||
if (req.query.include === "team") args.include = { team: true };
|
||||
const data = await prisma.membership.findUniqueOrThrow(args);
|
||||
return { membership: schemaMembershipPublic.parse(data) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
76
calcom/apps/api/v1/pages/api/memberships/[id]/_patch.ts
Normal file
76
calcom/apps/api/v1/pages/api/memberships/[id]/_patch.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
membershipEditBodySchema,
|
||||
membershipIdSchema,
|
||||
schemaMembershipPublic,
|
||||
} from "~/lib/validations/membership";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /memberships/{userId}_{teamId}:
|
||||
* patch:
|
||||
* summary: Edit an existing membership
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric userId of the membership to get
|
||||
* - in: path
|
||||
* name: teamId
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: Numeric teamId of the membership to get
|
||||
* tags:
|
||||
* - memberships
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, membership edited successfully
|
||||
* 400:
|
||||
* description: Bad request. Membership body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_teamId = membershipIdSchema.parse(query);
|
||||
const data = membershipEditBodySchema.parse(req.body);
|
||||
const args: Prisma.MembershipUpdateArgs = { where: { userId_teamId }, data };
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
const result = await prisma.membership.update(args);
|
||||
return { membership: schemaMembershipPublic.parse(result) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: queryUserId, teamId } = membershipIdSchema.parse(req.query);
|
||||
const data = membershipEditBodySchema.parse(req.body);
|
||||
// Admins can just skip this check
|
||||
if (isSystemWideAdmin) return;
|
||||
// Only the invited user can accept the invite
|
||||
if ("accepted" in data && queryUserId !== userId)
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only the invited user can accept the invite",
|
||||
});
|
||||
// Only team OWNERS and ADMINS can modify `role`
|
||||
if ("role" in data) {
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: { userId, teamId, role: { in: ["ADMIN", "OWNER"] } },
|
||||
});
|
||||
if (!membership || (membership.role !== "OWNER" && req.body.role === "OWNER"))
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
||||
18
calcom/apps/api/v1/pages/api/memberships/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/memberships/[id]/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
||||
78
calcom/apps/api/v1/pages/api/memberships/_get.ts
Normal file
78
calcom/apps/api/v1/pages/api/memberships/_get.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaMembershipPublic } from "~/lib/validations/membership";
|
||||
import {
|
||||
schemaQuerySingleOrMultipleTeamIds,
|
||||
schemaQuerySingleOrMultipleUserIds,
|
||||
} from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /memberships:
|
||||
* get:
|
||||
* summary: Find all memberships
|
||||
* tags:
|
||||
* - memberships
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No memberships were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const args: Prisma.MembershipFindManyArgs = {
|
||||
where: {
|
||||
/** Admins can query multiple users */
|
||||
userId: { in: getUserIds(req) },
|
||||
/** Admins can query multiple teams as well */
|
||||
teamId: { in: getTeamIds(req) },
|
||||
},
|
||||
};
|
||||
// Just in case the user want to get more info about the team itself
|
||||
if (req.query.include === "team") args.include = { team: true };
|
||||
|
||||
const data = await prisma.membership.findMany(args);
|
||||
return { memberships: data.map((v) => schemaMembershipPublic.parse(v)) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requested users IDs only if admin, otherwise return only current user ID
|
||||
*/
|
||||
function getUserIds(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/** Only admins can query other users */
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
return userIds;
|
||||
}
|
||||
// Return all memberships for ADMIN, limit to current user to non-admins
|
||||
return isSystemWideAdmin ? undefined : [userId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns requested teams IDs only if admin
|
||||
*/
|
||||
function getTeamIds(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
/** Only admins can query other teams */
|
||||
if (!isSystemWideAdmin && req.query.teamId)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN required" });
|
||||
if (isSystemWideAdmin && req.query.teamId) {
|
||||
const query = schemaQuerySingleOrMultipleTeamIds.parse(req.query);
|
||||
const teamIds = Array.isArray(query.teamId) ? query.teamId : [query.teamId];
|
||||
return teamIds;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
53
calcom/apps/api/v1/pages/api/memberships/_post.ts
Normal file
53
calcom/apps/api/v1/pages/api/memberships/_post.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/validations/membership";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /memberships:
|
||||
* post:
|
||||
* summary: Creates a new membership
|
||||
* tags:
|
||||
* - memberships
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, membership created
|
||||
* 400:
|
||||
* description: Bad request. Membership body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const data = membershipCreateBodySchema.parse(req.body);
|
||||
const args: Prisma.MembershipCreateArgs = { data };
|
||||
|
||||
await checkPermissions(req);
|
||||
|
||||
const result = await prisma.membership.create(args);
|
||||
|
||||
return {
|
||||
membership: schemaMembershipPublic.parse(result),
|
||||
message: "Membership created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const body = membershipCreateBodySchema.parse(req.body);
|
||||
// To prevent auto-accepted invites, limit it to ADMIN users
|
||||
if (!isSystemWideAdmin && "accepted" in body)
|
||||
throw new HttpError({ statusCode: 403, message: "ADMIN needed for `accepted`" });
|
||||
// Only team OWNERS and ADMINS can add other members
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: { userId, teamId: body.teamId, role: { in: ["ADMIN", "OWNER"] } },
|
||||
});
|
||||
if (!membership) throw new HttpError({ statusCode: 403, message: "You can't add members to this team" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
||||
10
calcom/apps/api/v1/pages/api/memberships/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/memberships/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
||||
69
calcom/apps/api/v1/pages/api/payments/[id].ts
Normal file
69
calcom/apps/api/v1/pages/api/payments/[id].ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import type { PaymentResponse } from "~/lib/types";
|
||||
import { schemaPaymentPublic } from "~/lib/validations/payment";
|
||||
import {
|
||||
schemaQueryIdParseInt,
|
||||
withValidQueryIdTransformParseInt,
|
||||
} from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /payments/{id}:
|
||||
* get:
|
||||
* summary: Find a payment
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the payment to get
|
||||
* tags:
|
||||
* - payments
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Payment was not found
|
||||
*/
|
||||
export async function paymentById(
|
||||
{ method, query, userId }: NextApiRequest,
|
||||
res: NextApiResponse<PaymentResponse>
|
||||
) {
|
||||
const safeQuery = schemaQueryIdParseInt.safeParse(query);
|
||||
if (safeQuery.success && method === "GET") {
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { bookings: true },
|
||||
});
|
||||
await prisma.payment
|
||||
.findUnique({ where: { id: safeQuery.data.id } })
|
||||
.then((data) => schemaPaymentPublic.parse(data))
|
||||
.then((payment) => {
|
||||
if (!userWithBookings?.bookings.map((b) => b.id).includes(payment.bookingId)) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
} else {
|
||||
res.status(200).json({ payment });
|
||||
}
|
||||
})
|
||||
.catch((error: Error) =>
|
||||
res.status(404).json({
|
||||
message: `Payment with id: ${safeQuery.data.id} not found`,
|
||||
error,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
export default withMiddleware("HTTP_GET")(withValidQueryIdTransformParseInt(paymentById));
|
||||
44
calcom/apps/api/v1/pages/api/payments/index.ts
Normal file
44
calcom/apps/api/v1/pages/api/payments/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import type { PaymentsResponse } from "~/lib/types";
|
||||
import { schemaPaymentPublic } from "~/lib/validations/payment";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /payments:
|
||||
* get:
|
||||
* summary: Find all payments
|
||||
* tags:
|
||||
* - payments
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No payments were found
|
||||
*/
|
||||
async function allPayments({ userId }: NextApiRequest, res: NextApiResponse<PaymentsResponse>) {
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { bookings: true },
|
||||
});
|
||||
if (!userWithBookings) throw new Error("No user found");
|
||||
const bookings = userWithBookings.bookings;
|
||||
const bookingIds = bookings.map((booking) => booking.id);
|
||||
const data = await prisma.payment.findMany({ where: { bookingId: { in: bookingIds } } });
|
||||
const payments = data.map((payment) => schemaPaymentPublic.parse(payment));
|
||||
|
||||
if (payments) res.status(200).json({ payments });
|
||||
else
|
||||
(error: Error) =>
|
||||
res.status(404).json({
|
||||
message: "No Payments were found",
|
||||
error,
|
||||
});
|
||||
}
|
||||
// NO POST FOR PAYMENTS FOR NOW
|
||||
export default withMiddleware("HTTP_GET")(allPayments);
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Admins can just skip this check
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if the current user can access the schedule
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!schedule) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user