Merge branch 'feat/organizations' into feat/org-app-install

This commit is contained in:
Joe Au-Yeung 2023-05-30 05:41:01 +09:00 committed by GitHub
commit 7a7571d688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
198 changed files with 8776 additions and 1614 deletions

70
.github/CODEOWNERS vendored
View File

@ -1,70 +0,0 @@
# This is a comment.
# Each line is a file pattern followed by one or more owners.
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
# ⬇️ Current Teams
# @calcom/app-store # https://github.com/orgs/calcom/teams/app-store
# @calcom/authentication # https://github.com/orgs/calcom/teams/authentication
# @calcom/automated-testing # https://github.com/orgs/calcom/teams/automated-testing
# @calcom/billing # https://github.com/orgs/calcom/teams/billing
# @calcom/bookings # https://github.com/orgs/calcom/teams/bookings
# @calcom/caldav # https://github.com/orgs/calcom/teams/caldav
# @calcom/calendar-apps # https://github.com/orgs/calcom/teams/calendar-apps
# @calcom/ci-dx # https://github.com/orgs/calcom/teams/ci-dx
# @calcom/console # https://github.com/orgs/calcom/teams/console
# @calcom/crm-apps # https://github.com/orgs/calcom/teams/crm-apps
# @calcom/desktop-app # https://github.com/orgs/calcom/teams/desktop-app
# @calcom/docker # https://github.com/orgs/calcom/teams/docker
# @calcom/docs # https://github.com/orgs/calcom/teams/docs
# @calcom/documentation # https://github.com/orgs/calcom/teams/documentation
# @calcom/embeds # https://github.com/orgs/calcom/teams/embeds
# @calcom/event-types # https://github.com/orgs/calcom/teams/event-types
# @calcom/impersonation # https://github.com/orgs/calcom/teams/impersonation
# @calcom/ops-stack # https://github.com/orgs/calcom/teams/ops-stack
# @calcom/public-api # https://github.com/orgs/calcom/teams/public-api
# @calcom/routing-forms # https://github.com/orgs/calcom/teams/routing-forms
# @calcom/seats # https://github.com/orgs/calcom/teams/seats
# @calcom/teams # https://github.com/orgs/calcom/teams/teams
# @calcom/ui # https://github.com/orgs/calcom/teams/ui
# @calcom/webhooks # https://github.com/orgs/calcom/teams/webhooks
# @calcom/workflows # https://github.com/orgs/calcom/teams/workflows
# @calcom/zapier # https://github.com/orgs/calcom/teams/zapier
.github/ @calcom/ci-dx
.husky/ @calcom/ci-dx
.vcode/ @calcom/ci-dx
deploy/ @calcom/ci-dx
scripts/ @calcom/ci-dx
apps/swagger @calcom/docs
apps/web/playwright @calcom/automated-testing
packages/app-store-cli @calcom/app-store
packages/app-store/routing-forms @calcom/routing-forms
packages/app-store/applecalendar @calcom/caldav
packages/app-store/caldavcalendar @calcom/caldav
packages/app-store/googlecalendar @calcom/calendar-apps
packages/app-store/exchange2013calendar @calcom/calendar-apps
packages/app-store/exchange2016calendar @calcom/calendar-apps
packages/app-store/larkcalendar @calcom/calendar-apps
packages/app-store/office365calendar @calcom/calendar-apps
packages/config @calcom/leads
packages/core @calcom/bookings
packages/dayjs @calcom/leads
packages/emails @calcom/leads
packages/embeds @calcom/embeds
packages/eslint-plugin @calcom/leads
packages/features @calcom/leads
packages/features/bookings @calcom/bookings
packages/features/ee/impersonation @calcom/impersonation
packages/features/ee/payments @calcom/billing
packages/features/ee/workflows @calcom/workflows
packages/features/kbar @alishaz-polymath
packages/features/tips @PeerRich
packages/features/webhooks @calcom/webhooks
packages/ui @calcom/ui
playwright.config.ts @calcom/automated-testing

1
.gitignore vendored
View File

@ -84,7 +84,6 @@ apps/storybook/build-storybook.log
# Submodules
.gitmodules
apps/api
apps/website
apps/console
apps/auth

6
apps/api/.env.example Normal file
View File

@ -0,0 +1,6 @@
API_KEY_PREFIX=cal_
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
# Get it in console.cal.com
CALCOM_LICENSE_KEY=""

1
apps/api/.eslintrc.js Normal file
View File

@ -0,0 +1 @@
module.exports = require("@calcom/config/eslint-preset");

81
apps/api/.gitignore vendored Normal file
View File

@ -0,0 +1,81 @@
# .env file
.env
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
/test-results/
playwright/videos
playwright/screenshots
playwright/artifacts
playwright/results
playwright/reports/*
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*
!.env.example
# vercel
.vercel
# Webstorm
.idea
### VisualStudioCode template
.vscode/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Typescript
tsconfig.tsbuildinfo
# turbo
.turbo
# Prisma-Zod
packages/prisma/zod/*.ts
# Builds
dist
# Linting
lint-results
# Yarn
yarn-error.log
.turbo
.next
.husky
.vscode
.env

0
apps/api/.gitkeep Normal file
View File

5
apps/api/.prettierignore Normal file
View File

@ -0,0 +1,5 @@
.next/
coverage/
node_modules/
tests/
templates/

225
apps/api/README.md Normal file
View File

@ -0,0 +1,225 @@
<!-- PROJECT LOGO -->
<div align="center">
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
</a>
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
</div>
# Cal.com Public API
Welcome to the Public API ("/apps/api") of the Cal.com.
This is the public REST api for cal.com.
It exposes CRUD Endpoints of all our most important resources.
And it makes it easy for anyone to integrate with Cal.com at the application programming level.
## Stack
- NextJS
- TypeScript
- Prisma
## Development
### Setup
1. Clone the main repo (NOT THIS ONE)
```sh
git clone --recurse-submodules -j8 https://github.com/calcom/cal.com.git
```
1. Go to the project folder
```sh
cd cal.com
```
1. Copy `apps/api/.env.example` to `apps/api/.env`
```sh
cp apps/api/.env.example apps/api/.env
cp .env.example .env
```
1. Install packages with yarn
```sh
yarn
```
1. Start developing
```sh
yarn workspace @calcom/api dev
```
1. Open [http://localhost:3002](http://localhost:3002) with your browser to see the result.
## API Authentication (API Keys)
The API requires a valid apiKey query param to be passed:
You can generate them at <https://app.cal.com/settings/security>
For example:
```sh
GET https://api.cal.com/v1/users?apiKey={INSERT_YOUR_CAL.COM_API_KEY_HERE}
```
API Keys optionally may have expiry dates, if they are expired they won't work. If you create an apiKey without a userId relation, it won't work either for now as it relies on it to establish the current authenticated user.
In the future we might add support for header Bearer Auth if we need to or if our customers require it.
## Middlewares
We don't use the new NextJS 12 Beta Middlewares, mainly because they run on the edge, and are not able to call prisma from api endpoints. We use instead a very nifty library called next-api-middleware that let's us use a similar approach building our own middlewares and applying them as we see fit.
- withMiddleware() requires some default middlewares (verifyApiKey, etc...)
## Next.config.js
### Redirects
Since this is an API only project, we don't want to have to type /api/ in all the routes, and so redirect all traffic to api, so a call to `api.cal.com/v1` will resolve to `api.cal.com/api/v1`
Likewise, v1 is added as param query called version to final /api call so we don't duplicate endpoints in the future for versioning if needed.
### Transpiling locally shared monorepo modules
We're calling several packages from monorepo, this need to be transpiled before building since are not available as regular npm packages. That's what withTM does.
```js
"@calcom/app-store",
"@calcom/prisma",
"@calcom/lib",
"@calcom/features",
```
## API Endpoint Validation
We validate that only the supported methods are accepted at each endpoint, so in
- **/endpoint**: you can only [GET] (all) and [POST] (create new)
- **/endpoint/id**: you can read create and edit [GET, PATCH, DELETE]
### Zod Validations
The API uses `zod` library like our main web repo. It validates that either GET query parameters or POST body content's are valid and up to our spec. It gives errors when parsing result's with schemas and failing validation.
We use it in several ways, but mainly, we first import the auto-generated schema from @calcom/prisma for each model, which lives in `lib/validations/`
We have some shared validations which several resources require, like baseApiParams which parses apiKey in all requests, or querIdAsString or TransformParseInt which deal with the id's coming from req.query.
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elswhere)
- **[*]Public** that also omits any values that we don't want to expose when returning the model as a response, which we parse against before returning all resources.
- **[*]BodyParams** which merges both `[*]BaseBodyParams.merge([*]RequiredParams);`
### Next Validations
[Next-Validations Docs](https://next-validations.productsway.com/)
[Next-Validations Repo](https://github.com/jellydn/next-validations)
We also use this useful helper library that let us wrap our endpoints in a validate HOC that checks the req against our validation schema built out with zod for either query and / or body's requests.
## Testing with Jest + node-mocks-http
We aim to provide a fully tested API for our peace of mind, this is accomplished by using jest + node-mocks-http
## Endpoints matrix
| resource | get [id] | get all | create | edit | delete |
| --------------------- | -------- | ------- | ------ | ---- | ------ |
| attendees | ✅ | ✅ | ✅ | ✅ | ✅ |
| availabilities | ✅ | ✅ | ✅ | ✅ | ✅ |
| booking-references | ✅ | ✅ | ✅ | ✅ | ✅ |
| event-references | ✅ | ✅ | ✅ | ✅ | ✅ |
| destination-calendars | ✅ | ✅ | ✅ | ✅ | ✅ |
| custom-inputs | ✅ | ✅ | ✅ | ✅ | ✅ |
| event-types | ✅ | ✅ | ✅ | ✅ | ✅ |
| memberships | ✅ | ✅ | ✅ | ✅ | ✅ |
| payments | ✅ | ✅ | ❌ | ❌ | ❌ |
| schedules | ✅ | ✅ | ✅ | ✅ | ✅ |
| selected-calendars | ✅ | ✅ | ✅ | ✅ | ✅ |
| teams | ✅ | ✅ | ✅ | ✅ | ✅ |
| users | ✅ | 👤[1] | ✅ | ✅ | ✅ |
## Models from database that are not exposed
mostly because they're deemed too sensitive can be revisited if needed. Also they are expected to be used via cal's webapp.
- [ ] Api Keys
- [ ] Credentials
- [ ] Webhooks
- [ ] ResetPasswordRequest
- [ ] VerificationToken
- [ ] ReminderMail
## Documentation (OpenAPI)
You will see that each endpoint has a comment at the top with the annotation `@swagger` with the documentation of the endpoint, **please update it if you change the code!** This is what auto-generates the OpenAPI spec by collecting the YAML in each endpoint and parsing it in /docs alongside the json-schema (auto-generated from prisma package, not added to code but manually for now, need to fix later)
### @calcom/apps/swagger
The documentation of the API lives inside the code, and it's auto-generated, the only endpoints that return without a valid apiKey are the homepage, with a JSON message redirecting you to the docs. and the /docs endpoint, which returns the OpenAPI 3.0 JSON Spec. Which SwaggerUi then consumes and generates the docs on.
## Deployment
`scripts/vercel-deploy.sh`
The API is deployed to vercel.com, it uses a similar deployment script to website or webapp, and requires transpilation of several shared packages that are part of our turborepo ["app-store", "prisma", "lib", "ee"]
in order to build and deploy properly.
## Envirorment variables
### Required
DATABASE_URL=DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
## Optional
API*KEY_PREFIX=cal*# This can be changed per envirorment so cal*test* for staging for example.
> If you're self-hosting under our commercial license, you can use any prefix you want for api keys. either leave the default cal\_ (not providing any envirorment variable) or modify it
**Ensure that while testing swagger, API project should be run in production mode**
We make sure of this by not using next in dev, but next build && next start, if you want hot module reloading and such when developing, please use yarn run next directly on apps/api.
See <https://github.com/vercel/next.js/blob/canary/packages/next/server/dev/hot-reloader.ts#L79>. Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only.
To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1
## Hosted api through cal.com
> _❗ WARNING: This is still experimental and not fully implemented yet❗_
Go to console.cal.com
Add a deployment or go to an existing one.
Activate API or Admin addon
Provide your `DATABASE_URL`
Now you can call api.cal.com?key=CALCOM_LICENSE_KEY, which will connect to your own databaseUrl.
## How to deploy
We recommend deploying API in vercel.
There's some settings that you'll need to setup.
Under Vercel > Your API Project > Settings
In General > Build & Development Settings
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
OUTPUT DIRECTORY: `apps/api/.next`
In Git > Ignored Build Step
Add this command: `./scripts/vercel-deploy.sh`
See `scripts/vercel-deploy.sh` for more info on how the deployment is done.
> _❗ IMORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
## Environment variables
Lastly API requires an env var for `DATABASE_URL` and `CALCOM_LICENSE_KEY`

View File

@ -0,0 +1 @@
export const PRISMA_CLIENT_CACHING_TIME = 1000 * 60 * 60 * 24; // one day in ms

View File

@ -0,0 +1,24 @@
import { nanoid } from "nanoid";
import type { NextMiddleware } from "next-api-middleware";
export const addRequestId: NextMiddleware = async (_req, res, next) => {
// Apply header with unique ID to every request
res.setHeader("Calcom-Response-ID", nanoid());
// Add all headers here instead of next.config.js as it is throwing error( Cannot set headers after they are sent to the client) for OPTIONS method
// It is known to happen only in Dev Mode.
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, PATCH, DELETE, POST, PUT");
res.setHeader(
"Access-Control-Allow-Headers",
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization"
);
// Ensure all OPTIONS request are automatically successful. Headers are already set above.
if (_req.method === "OPTIONS") {
res.status(200).end();
return;
}
// Let remaining middleware and API route execute
await next();
};

View File

@ -0,0 +1,14 @@
import * as Sentry from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
export const captureErrors: NextMiddleware = async (_req, res, next) => {
try {
// Catch any errors that are thrown in remaining
// middleware and the API route handler
await next();
} catch (error) {
Sentry.captureException(error);
console.log(error);
res.status(400).json({ message: "Something went wrong", error });
}
};

View File

@ -0,0 +1,43 @@
import { PrismaClient } from "@prisma/client";
import type { NextMiddleware } from "next-api-middleware";
import { CONSOLE_URL } from "@calcom/lib/constants";
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
// This replaces the prisma client for the custom one if the key is valid
export const customPrismaClient: NextMiddleware = async (req, res, next) => {
const {
query: { key },
} = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!key) {
req.prisma = new PrismaClient();
await next();
return;
}
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
.then((res) => res.json())
.then((res) => res.databaseUrl);
if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return;
}
req.prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
/* @note:
In order to skip verifyApiKey for customPrisma requests,
we pass isAdmin true, and userId 0, if we detect them later,
we skip verifyApiKey logic and pass onto next middleware instead.
*/
req.isAdmin = true;
req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key;
await next();
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
};

View File

@ -0,0 +1,9 @@
import type { NextMiddleware } from "next-api-middleware";
export const extendRequest: NextMiddleware = async (req, res, next) => {
req.pagination = {
take: 100,
skip: 0,
};
await next();
};

View File

@ -0,0 +1,32 @@
import type { NextMiddleware } from "next-api-middleware";
export const httpMethod = (allowedHttpMethod: "GET" | "POST" | "PATCH" | "DELETE"): NextMiddleware => {
return async function (req, res, next) {
if (req.method === allowedHttpMethod || req.method == "OPTIONS") {
await next();
} else {
res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` });
res.end();
}
};
};
// Made this so we can support several HTTP Methods in one route and use it there.
// Could be further extracted into a third function or refactored into one.
// that checks if it's just a string or an array and apply the correct logic to both cases.
export const httpMethods = (allowedHttpMethod: string[]): NextMiddleware => {
return async function (req, res, next) {
if (allowedHttpMethod.some((method) => method === req.method || req.method == "OPTIONS")) {
await next();
} else {
res.status(405).json({ message: `Only ${allowedHttpMethod} Method allowed` });
res.end();
}
};
};
export const HTTP_POST = httpMethod("POST");
export const HTTP_GET = httpMethod("GET");
export const HTTP_PATCH = httpMethod("PATCH");
export const HTTP_DELETE = httpMethod("DELETE");
export const HTTP_GET_DELETE_PATCH = httpMethods(["GET", "DELETE", "PATCH"]);
export const HTTP_GET_OR_POST = httpMethods(["GET", "POST"]);

View File

@ -0,0 +1,14 @@
export default function parseJSONSafely(str: string) {
try {
return JSON.parse(str);
} catch (e) {
console.error((e as Error).message);
if ((e as Error).message.includes("Unexpected token")) {
return {
success: false,
message: `Invalid JSON in the body: ${(e as Error).message}`,
};
}
return {};
}
}

View File

@ -0,0 +1,47 @@
import type { NextMiddleware } from "next-api-middleware";
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import { isAdminGuard } from "~/lib/utils/isAdmin";
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
export const dateNotInPast = function (date: Date) {
const now = new Date();
if (now.setHours(0, 0, 0, 0) > date.setHours(0, 0, 0, 0)) {
return true;
}
};
// This verifies the apiKey and sets the user if it is valid.
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
const { prisma, isCustomPrisma, isAdmin } = req;
const hasValidLicense = await checkLicense(prisma);
if (!hasValidLicense)
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
// If the user is an admin and using a license key (from customPrisma), skip the apiKey check.
if (isCustomPrisma && isAdmin) {
await next();
return;
}
// Check if the apiKey query param is provided.
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
// remove the prefix from the user provided api_key. If no env set default to "cal_"
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
// Hash the key again before matching against the database records.
const hashedKey = hashAPIKey(strippedApiKey);
// Check if the hashed api key exists in database.
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
// If cannot find any api key. Throw a 401 Unauthorized.
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
return res.status(401).json({ error: "This apiKey is expired" });
}
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
// save the user id in the request for later use
req.userId = apiKey.userId;
// save the isAdmin boolean here for later use
req.isAdmin = await isAdminGuard(req);
req.isCustomPrisma = false;
await next();
};

View File

@ -0,0 +1,37 @@
import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { customPrismaClient } from "./customPrisma";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
HTTP_DELETE,
HTTP_PATCH,
HTTP_GET,
HTTP_GET_OR_POST,
HTTP_GET_DELETE_PATCH,
} from "./httpMethods";
import { verifyApiKey } from "./verifyApiKey";
import { withPagination } from "./withPagination";
const withMiddleware = label(
{
HTTP_GET_OR_POST,
HTTP_GET_DELETE_PATCH,
HTTP_GET,
HTTP_PATCH,
HTTP_POST,
HTTP_DELETE,
addRequestId,
verifyApiKey,
customPrismaClient,
extendRequest,
pagination: withPagination,
sentry: captureErrors,
},
// The order here, determines the order of execution, put customPrismaClient before verifyApiKey always.
["extendRequest", "sentry", "customPrismaClient", "verifyApiKey", "addRequestId"] // <-- Provide a list of middleware to call automatically
);
export { withMiddleware };

View File

@ -0,0 +1,17 @@
import type { NextMiddleware } from "next-api-middleware";
import z from "zod";
const withPage = z.object({
page: z.coerce.number().min(1).optional().default(1),
take: z.coerce.number().min(1).optional().default(10),
});
export const withPagination: NextMiddleware = async (req, _, next) => {
const { page, take } = withPage.parse(req.query);
const skip = (page - 1) * take;
req.pagination = {
take,
skip,
};
await next();
};

187
apps/api/lib/types.ts Normal file
View File

@ -0,0 +1,187 @@
import type { EventLocationType } from "@calcom/app-store/locations";
import type {
Attendee,
Availability,
Booking,
BookingReference,
Credential,
DestinationCalendar,
EventType,
EventTypeCustomInput,
Membership,
Payment,
ReminderMail,
Schedule,
SelectedCalendar,
Team,
User,
Webhook,
} from "@calcom/prisma/client";
// Base response, used for all responses
export type BaseResponse = {
message?: string;
error?: Error;
};
// User
export type UserResponse = BaseResponse & {
user?: Partial<User>;
};
export type UsersResponse = BaseResponse & {
users?: Partial<User>[];
};
// Team
export type TeamResponse = BaseResponse & {
team?: Partial<Team>;
owner?: Partial<Membership>;
};
export type TeamsResponse = BaseResponse & {
teams?: Partial<Team>[];
};
// SelectedCalendar
export type SelectedCalendarResponse = BaseResponse & {
selected_calendar?: Partial<SelectedCalendar>;
};
export type SelectedCalendarsResponse = BaseResponse & {
selected_calendars?: Partial<SelectedCalendar>[];
};
// Attendee
export type AttendeeResponse = BaseResponse & {
attendee?: Partial<Attendee>;
};
// Grouping attendees in booking arrays for now,
// later might remove endpoint and move to booking endpoint altogether.
export type AttendeesResponse = BaseResponse & {
attendees?: Partial<Attendee>[];
};
// Availability
export type AvailabilityResponse = BaseResponse & {
availability?: Partial<Availability>;
};
export type AvailabilitiesResponse = BaseResponse & {
availabilities?: Partial<Availability>[];
};
// BookingReference
export type BookingReferenceResponse = BaseResponse & {
booking_reference?: Partial<BookingReference>;
};
export type BookingReferencesResponse = BaseResponse & {
booking_references?: Partial<BookingReference>[];
};
// Booking
export type BookingResponse = BaseResponse & {
booking?: Partial<Booking>;
};
export type BookingsResponse = BaseResponse & {
bookings?: Partial<Booking>[];
};
// Credential
export type CredentialResponse = BaseResponse & {
credential?: Partial<Credential>;
};
export type CredentialsResponse = BaseResponse & {
credentials?: Partial<Credential>[];
};
// DestinationCalendar
export type DestinationCalendarResponse = BaseResponse & {
destination_calendar?: Partial<DestinationCalendar>;
};
export type DestinationCalendarsResponse = BaseResponse & {
destination_calendars?: Partial<DestinationCalendar>[];
};
// Membership
export type MembershipResponse = BaseResponse & {
membership?: Partial<Membership>;
};
export type MembershipsResponse = BaseResponse & {
memberships?: Partial<Membership>[];
};
// EventTypeCustomInput
export type EventTypeCustomInputResponse = BaseResponse & {
event_type_custom_input?: Partial<EventTypeCustomInput>;
};
export type EventTypeCustomInputsResponse = BaseResponse & {
event_type_custom_inputs?: Partial<EventTypeCustomInput>[];
};
// From rrule https://jakubroztocil.github.io/rrule freq
export enum Frequency {
"YEARLY",
"MONTHLY",
"WEEKLY",
"DAILY",
"HOURLY",
"MINUTELY",
"SECONDLY",
}
interface EventTypeExtended extends Omit<EventType, "recurringEvent" | "locations"> {
recurringEvent: {
dtstart?: Date | undefined;
interval?: number | undefined;
count?: number | undefined;
freq?: Frequency | undefined;
until?: Date | undefined;
tzid?: string | undefined;
} | null;
locations:
| {
link?: string | undefined;
address?: string | undefined;
hostPhoneNumber?: string | undefined;
type: EventLocationType;
}[]
| null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| any;
}
// EventType
export type EventTypeResponse = BaseResponse & {
event_type?: Partial<EventType | EventTypeExtended>;
};
export type EventTypesResponse = BaseResponse & {
event_types?: Partial<EventType | EventTypeExtended>[];
};
// Payment
export type PaymentResponse = BaseResponse & {
payment?: Partial<Payment>;
};
export type PaymentsResponse = BaseResponse & {
payments?: Partial<Payment>[];
};
// Schedule
export type ScheduleResponse = BaseResponse & {
schedule?: Partial<Schedule>;
};
export type SchedulesResponse = BaseResponse & {
schedules?: Partial<Schedule>[];
};
// Webhook
export type WebhookResponse = BaseResponse & {
webhook?: Partial<Webhook> | null;
};
export type WebhooksResponse = BaseResponse & {
webhooks?: Partial<Webhook>[];
};
// ReminderMail
export type ReminderMailResponse = BaseResponse & {
reminder_mail?: Partial<ReminderMail>;
};
export type ReminderMailsResponse = BaseResponse & {
reminder_mails?: Partial<ReminderMail>[];
};

View File

@ -0,0 +1,9 @@
import type { NextApiRequest } from "next";
import { UserPermissionRole } from "@calcom/prisma/enums";
export const isAdminGuard = async (req: NextApiRequest) => {
const { userId, prisma } = req;
const user = await prisma.user.findUnique({ where: { id: userId } });
return user?.role === UserPermissionRole.ADMIN;
};

View File

@ -0,0 +1,4 @@
export const stringifyISODate = (date: Date | undefined): string => {
return `${date?.toISOString()}`;
};
// TODO: create a function that takes an object and returns a stringified version of dates of it.

View File

@ -0,0 +1,29 @@
import { z } from "zod";
import { _ApiKeyModel as ApiKey } from "@calcom/prisma/zod";
export const apiKeyCreateBodySchema = ApiKey.pick({
note: true,
expiresAt: true,
userId: true,
})
.partial({ userId: true })
.merge(z.object({ neverExpires: z.boolean().optional() }))
.strict();
export const apiKeyEditBodySchema = ApiKey.pick({
note: true,
})
.partial()
.strict();
export const apiKeyPublicSchema = ApiKey.pick({
id: true,
userId: true,
note: true,
createdAt: true,
expiresAt: true,
lastUsedAt: true,
/** We might never want to expose these. Leaving this a as reminder. */
// hashedKey: true,
});

View File

@ -0,0 +1,39 @@
import { z } from "zod";
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
import { timeZone } from "~/lib/validations/shared/timeZone";
export const schemaAttendeeBaseBodyParams = Attendee.pick({
bookingId: true,
email: true,
name: true,
timeZone: true,
});
const schemaAttendeeCreateParams = z
.object({
bookingId: z.number().int(),
email: z.string().email(),
name: z.string(),
timeZone: timeZone,
})
.strict();
const schemaAttendeeEditParams = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
timeZone: timeZone.optional(),
})
.strict();
export const schemaAttendeeEditBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeEditParams);
export const schemaAttendeeCreateBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeCreateParams);
export const schemaAttendeeReadPublic = Attendee.pick({
id: true,
bookingId: true,
name: true,
email: true,
timeZone: true,
});

View File

@ -0,0 +1,54 @@
import { z } from "zod";
import { _AvailabilityModel as Availability, _ScheduleModel as Schedule } from "@calcom/prisma/zod";
import { denullishShape } from "@calcom/prisma/zod-utils";
export const schemaAvailabilityBaseBodyParams = /** We make all these properties required */ denullishShape(
Availability.pick({
/** We need to pass the schedule where this availability belongs to */
scheduleId: true,
})
);
export const schemaAvailabilityReadPublic = Availability.pick({
id: true,
startTime: true,
endTime: true,
date: true,
scheduleId: true,
days: true,
// eventTypeId: true /** @deprecated */,
// userId: true /** @deprecated */,
}).merge(z.object({ success: z.boolean().optional(), Schedule: Schedule.partial() }).partial());
const schemaAvailabilityCreateParams = z
.object({
startTime: z.date().or(z.string()),
endTime: z.date().or(z.string()),
days: z.array(z.number()).optional(),
})
.strict();
const schemaAvailabilityEditParams = z
.object({
startTime: z.date().or(z.string()).optional(),
endTime: z.date().or(z.string()).optional(),
days: z.array(z.number()).optional(),
})
.strict();
export const schemaAvailabilityEditBodyParams = schemaAvailabilityEditParams;
export const schemaAvailabilityCreateBodyParams = schemaAvailabilityBaseBodyParams.merge(
schemaAvailabilityCreateParams
);
export const schemaAvailabilityReadBodyParams = z
.object({
userId: z.union([z.number(), z.array(z.number())]),
})
.partial();
export const schemaSingleAvailabilityReadBodyParams = z.object({
userId: z.number(),
});

View File

@ -0,0 +1,28 @@
import { _BookingReferenceModel as BookingReference } from "@calcom/prisma/zod";
import { denullishShape } from "@calcom/prisma/zod-utils";
export const schemaBookingReferenceBaseBodyParams = BookingReference.pick({
type: true,
bookingId: true,
uid: true,
meetingId: true,
meetingPassword: true,
meetingUrl: true,
deleted: true,
}).partial();
export const schemaBookingReferenceReadPublic = BookingReference.pick({
id: true,
type: true,
bookingId: true,
uid: true,
meetingId: true,
meetingPassword: true,
meetingUrl: true,
deleted: true,
});
export const schemaBookingCreateBodyParams = BookingReference.omit({ id: true, bookingId: true })
.merge(denullishShape(BookingReference.pick({ bookingId: true })))
.strict();
export const schemaBookingEditBodyParams = schemaBookingCreateBodyParams.partial();

View File

@ -0,0 +1,67 @@
import { z } from "zod";
import { _BookingModel as Booking, _AttendeeModel, _UserModel } from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { schemaQueryUserId } from "./shared/queryUserId";
const schemaBookingBaseBodyParams = Booking.pick({
uid: true,
userId: true,
eventTypeId: true,
title: true,
description: true,
startTime: true,
endTime: true,
status: true,
}).partial();
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
const schemaBookingEditParams = z
.object({
title: z.string().optional(),
startTime: iso8601.optional(),
endTime: iso8601.optional(),
// Not supporting responses in edit as that might require re-triggering emails
// responses
})
.strict();
export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams.merge(schemaBookingEditParams);
export const schemaBookingReadPublic = Booking.extend({
attendees: z
.array(
_AttendeeModel.pick({
email: true,
name: true,
timeZone: true,
locale: true,
})
)
.optional(),
user: _UserModel
.pick({
email: true,
name: true,
timeZone: true,
locale: true,
})
.optional(),
}).pick({
id: true,
userId: true,
description: true,
eventTypeId: true,
uid: true,
title: true,
startTime: true,
endTime: true,
timeZone: true,
attendees: true,
user: true,
metadata: true,
status: true,
responses: true,
});

View File

@ -0,0 +1,48 @@
import { z } from "zod";
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
}).partial();
const schemaDestinationCalendarCreateParams = z
.object({
integration: z.string(),
externalId: z.string(),
eventTypeId: z.number(),
bookingId: z.number(),
userId: z.number(),
})
.strict();
export const schemaDestinationCalendarCreateBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
schemaDestinationCalendarCreateParams
);
const schemaDestinationCalendarEditParams = z
.object({
integration: z.string().optional(),
externalId: z.string().optional(),
eventTypeId: z.number().optional(),
bookingId: z.number().optional(),
userId: z.number().optional(),
})
.strict();
export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
schemaDestinationCalendarEditParams
);
export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
id: true,
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
});

View File

@ -0,0 +1,13 @@
import { _EventTypeCustomInputModel as EventTypeCustomInput } from "@calcom/prisma/zod";
export const schemaEventTypeCustomInputBaseBodyParams = EventTypeCustomInput.omit({
id: true,
});
export const schemaEventTypeCustomInputPublic = EventTypeCustomInput.omit({});
export const schemaEventTypeCustomInputBodyParams = schemaEventTypeCustomInputBaseBodyParams.strict();
export const schemaEventTypeCustomInputEditBodyParams = schemaEventTypeCustomInputBaseBodyParams
.partial()
.strict();

View File

@ -0,0 +1,139 @@
import { z } from "zod";
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { Frequency } from "~/lib/types";
import { jsonSchema } from "./shared/jsonSchema";
import { schemaQueryUserId } from "./shared/queryUserId";
import { timeZone } from "./shared/timeZone";
const recurringEventInputSchema = z.object({
dtstart: z.string().optional(),
interval: z.number().int().optional(),
count: z.number().int().optional(),
freq: z.nativeEnum(Frequency).optional(),
until: z.string().optional(),
tzid: timeZone.optional(),
});
const hostSchema = _HostModel.pick({
isFixed: true,
userId: true,
});
export const schemaEventTypeBaseBodyParams = EventType.pick({
title: true,
description: true,
slug: true,
length: true,
hidden: true,
position: true,
eventName: true,
timeZone: true,
periodType: true,
periodStartDate: true,
schedulingType: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
teamId: true,
price: true,
currency: true,
slotInterval: true,
successRedirectUrl: true,
locations: true,
})
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.partial()
.strict();
const schemaEventTypeCreateParams = z
.object({
title: z.string(),
slug: z.string(),
description: z.string().optional().nullable(),
length: z.number().int(),
metadata: z.any().optional(),
recurringEvent: recurringEventInputSchema.optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
})
.strict();
export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams
.merge(schemaEventTypeCreateParams)
.merge(schemaQueryUserId.partial());
const schemaEventTypeEditParams = z
.object({
title: z.string().optional(),
slug: z.string().optional(),
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
})
.strict();
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
export const schemaEventTypeReadPublic = EventType.pick({
id: true,
title: true,
slug: true,
length: true,
hidden: true,
position: true,
userId: true,
teamId: true,
eventName: true,
timeZone: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
recurringEvent: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
schedulingType: true,
price: true,
currency: true,
slotInterval: true,
successRedirectUrl: true,
description: true,
locations: true,
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
bookingFields: true,
}).merge(
z.object({
locations: z
.array(
z.object({
link: z.string().optional(),
address: z.string().optional(),
hostPhoneNumber: z.string().optional(),
type: z.any().optional(),
})
)
.nullable(),
metadata: jsonSchema.nullable(),
customInputs: customInputSchema.array().optional(),
link: z.string().optional(),
bookingFields: eventTypeBookingFields.optional().nullable(),
})
);

View File

@ -0,0 +1,65 @@
import { z } from "zod";
import { MembershipRole } from "@calcom/prisma/enums";
import { _MembershipModel as Membership, _TeamModel } from "@calcom/prisma/zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
export const schemaMembershipBaseBodyParams = Membership.omit({});
const schemaMembershipRequiredParams = z.object({
teamId: z.number(),
});
export const membershipCreateBodySchema = Membership.partial({
accepted: true,
role: true,
disableImpersonation: true,
}).transform((v) => ({
accepted: false,
role: MembershipRole.MEMBER,
disableImpersonation: false,
...v,
}));
export const membershipEditBodySchema = Membership.omit({
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
teamId: true,
userId: true,
})
.partial({
accepted: true,
role: true,
disableImpersonation: true,
})
.strict();
export const schemaMembershipBodyParams = schemaMembershipBaseBodyParams.merge(
schemaMembershipRequiredParams
);
export const schemaMembershipPublic = Membership.merge(z.object({ team: _TeamModel }).partial());
/** We extract userId and teamId from compound ID string */
export const membershipIdSchema = schemaQueryIdAsString
// So we can query additional team data in memberships
.merge(z.object({ teamId: z.union([stringOrNumber, z.array(stringOrNumber)]) }).partial())
.transform((v, ctx) => {
const [userIdStr, teamIdStr] = v.id.split("_");
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
const teamIdInt = schemaQueryIdParseInt.safeParse({ id: teamIdStr });
if (!userIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
return z.NEVER;
}
if (!teamIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "teamId is not a number " });
return z.NEVER;
}
return {
userId: userIdInt.data.id,
teamId: teamIdInt.data.id,
};
});

View File

@ -0,0 +1,5 @@
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
export const schemaPaymentBodyParams = Payment.omit({ id: true });
export const schemaPaymentPublic = Payment.omit({ externalId: true });

View File

@ -0,0 +1,17 @@
import { z } from "zod";
import { _ReminderMailModel as ReminderMail } from "@calcom/prisma/zod";
export const schemaReminderMailBaseBodyParams = ReminderMail.omit({ id: true }).partial();
export const schemaReminderMailPublic = ReminderMail.omit({});
const schemaReminderMailRequiredParams = z.object({
referenceId: z.number().int(),
reminderType: z.enum(["PENDING_BOOKING_CONFIRMATION"]),
elapsedMinutes: z.number().int(),
});
export const schemaReminderMailBodyParams = schemaReminderMailBaseBodyParams.merge(
schemaReminderMailRequiredParams
);

View File

@ -0,0 +1,34 @@
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { _ScheduleModel as Schedule, _AvailabilityModel as Availability } from "@calcom/prisma/zod";
import { timeZone } from "./shared/timeZone";
const schemaScheduleBaseBodyParams = Schedule.omit({ id: true, timeZone: true }).partial();
export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional(), timeZone: timeZone.optional() })
);
export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional(), name: z.string(), timeZone })
);
export const schemaSchedulePublic = z
.object({ id: z.number() })
.merge(Schedule)
.merge(
z.object({
availability: z
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true }))
.transform((v) =>
v.map((item) => ({
...item,
startTime: dayjs.utc(item.startTime).format("HH:mm:ss"),
endTime: dayjs.utc(item.endTime).format("HH:mm:ss"),
}))
)
.optional(),
})
);

View File

@ -0,0 +1,48 @@
import z from "zod";
import { _SelectedCalendarModel as SelectedCalendar } from "@calcom/prisma/zod";
import { schemaQueryIdAsString } from "./shared/queryIdString";
import { schemaQueryIdParseInt } from "./shared/queryIdTransformParseInt";
export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
userId: true,
});
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
/** We can assume the first part is the userId since it's an integer */
const [userIdStr, ...rest] = v.id.split("_");
/** We can assume that the remainder is both the integration type and external id combined */
const integration_externalId = rest.join("_");
/**
* Since we only handle calendars here we can split by `_calendar_` and re add it later on.
* This handle special cases like `google_calendar_c_blabla@group.calendar.google.com` and
* `hubspot_other_calendar`.
**/
const [_integration, externalId] = integration_externalId.split("_calendar_");
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
if (!userIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
return z.NEVER;
}
if (!_integration) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing integration" });
return z.NEVER;
}
if (!externalId) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing externalId" });
return z.NEVER;
}
return {
userId: userIdInt.data.id,
/** We re-add the split `_calendar` string */
integration: `${_integration}_calendar`,
externalId,
};
});

View File

@ -0,0 +1,11 @@
import { z } from "zod";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const baseApiParams = z.object({
// since we added apiKey as query param this is required by next-validations helper
// for query params to work properly and not fail.
apiKey: z.string().optional(),
// version required for supporting /v1/ redirect to query in api as *?version=1
version: z.string().optional(),
});

View File

@ -0,0 +1,9 @@
import { z } from "zod";
// Helper schema for JSON fields
type Literal = boolean | number | string;
type Json = Literal | { [key: string]: Json } | Json[];
const literalSchema = z.union([z.string(), z.number(), z.boolean()]);
export const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);

View File

@ -0,0 +1,19 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
/** Used for UUID style id queries */
export const schemaQueryIdAsString = baseApiParams
.extend({
id: z.string(),
})
.strict();
export const withValidQueryIdString = withValidation({
schema: schemaQueryIdAsString,
type: "Zod",
mode: "query",
});

View File

@ -0,0 +1,16 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryIdParseInt = baseApiParams.extend({
id: z.coerce.number(),
});
export const withValidQueryIdTransformParseInt = withValidation({
schema: schemaQueryIdParseInt,
type: "Zod",
mode: "query",
});

View File

@ -0,0 +1,21 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryTeamId = baseApiParams
.extend({
teamId: z
.string()
.regex(/^\d+$/)
.transform((id) => parseInt(id)),
})
.strict();
export const withValidQueryTeamId = withValidation({
schema: schemaQueryTeamId,
type: "Zod",
mode: "query",
});

View File

@ -0,0 +1,26 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserId = baseApiParams.extend({
userId: stringOrNumber,
});
export const schemaQuerySingleOrMultipleUserIds = z.object({
userId: z.union([stringOrNumber, z.array(stringOrNumber)]),
});
export const schemaQuerySingleOrMultipleTeamIds = z.object({
teamId: z.union([stringOrNumber, z.array(stringOrNumber)]),
});
export const withValidQueryUserId = withValidation({
schema: schemaQueryUserId,
type: "Zod",
mode: "query",
});

View File

@ -0,0 +1,7 @@
import tzdata from "tzdata";
import * as z from "zod";
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
message: `Expected one of the following: ${Object.keys(tzdata.zones).join(", ")}`,
});

View File

@ -0,0 +1,18 @@
import { z } from "zod";
import { _TeamModel as Team } from "@calcom/prisma/zod";
export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true }).partial({
hideBranding: true,
metadata: true,
});
const schemaTeamRequiredParams = z.object({});
export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict();
export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial();
export const schemaTeamReadPublic = Team.omit({});
export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic);

View File

@ -0,0 +1,170 @@
import { z } from "zod";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { timeZone } from "~/lib/validations/shared/timeZone";
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
enum weekdays {
MONDAY = "Monday",
TUESDAY = "Tuesday",
WEDNESDAY = "Wednesday",
THURSDAY = "Thursday",
FRIDAY = "Friday",
SATURDAY = "Saturday",
SUNDAY = "Sunday",
}
// @note: extracted from apps/web/next-i18next.config.js, update if new locales.
enum locales {
EN = "en",
FR = "fr",
IT = "it",
RU = "ru",
ES = "es",
DE = "de",
PT = "pt",
RO = "ro",
NL = "nl",
PT_BR = "pt-BR",
ES_419 = "es-419",
KO = "ko",
JA = "ja",
PL = "pl",
AR = "ar",
IW = "iw",
ZH_CN = "zh-CN",
ZH_TW = "zh-TW",
CS = "cs",
SR = "sr",
SV = "sv",
VI = "vi",
}
enum theme {
DARK = "dark",
LIGHT = "light",
}
enum timeFormat {
TWELVE = 12,
TWENTY_FOUR = 24,
}
const usernameSchema = z
.string()
.transform((v) => v.toLowerCase())
// .refine(() => {})
.superRefine(async (val, ctx) => {
if (val) {
const result = await checkUsername(val);
if (!result.available) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "already_in_use_error" });
if (result.premium) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "premium_username" });
}
});
// @note: These are the values that are editable via PATCH method on the user Model
export const schemaUserBaseBodyParams = User.pick({
name: true,
email: true,
username: true,
bio: true,
timeZone: true,
weekStart: true,
theme: true,
defaultScheduleId: true,
locale: true,
timeFormat: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
role: true,
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
// avatar: true,
}).partial();
// @note: partial() is used to allow for the user to edit only the fields they want to edit making all optional,
// if want to make any required do it in the schemaRequiredParams
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
// for example making weekStart only accept weekdays as input
const schemaUserEditParams = z.object({
email: z.string().email(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
.refine((id: number) => id > 0)
.optional()
.nullable(),
locale: z.nativeEnum(locales).optional().nullable(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: z.string().email(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
.refine((id: number) => id > 0)
.optional()
.nullable(),
locale: z.nativeEnum(locales).optional(),
createdDate: iso8601.optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
.merge(schemaUserEditParams)
.omit({})
.partial()
.strict();
export const schemaUserCreateBodyParams = schemaUserBaseBodyParams
.merge(schemaUserCreateParams)
.omit({})
.strict();
// @note: These are the values that are always returned when reading a user
export const schemaUserReadPublic = User.pick({
id: true,
username: true,
name: true,
email: true,
emailVerified: true,
bio: true,
avatar: true,
timeZone: true,
weekStart: true,
endTime: true,
bufferTime: true,
theme: true,
defaultScheduleId: true,
locale: true,
timeFormat: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
createdDate: true,
verified: true,
invitedTo: true,
role: true,
});
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);

View File

@ -0,0 +1,55 @@
import { z } from "zod";
import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
import { _WebhookModel as Webhook } from "@calcom/prisma/zod";
const schemaWebhookBaseBodyParams = Webhook.pick({
userId: true,
eventTypeId: true,
eventTriggers: true,
active: true,
subscriberUrl: true,
payloadTemplate: true,
});
export const schemaWebhookCreateParams = z
.object({
// subscriberUrl: z.string().url(),
// eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(),
// active: z.boolean(),
payloadTemplate: z.string().optional().nullable(),
eventTypeId: z.number().optional(),
userId: z.number().optional(),
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
// appId: z.string().optional().nullable(),
})
.strict();
export const schemaWebhookCreateBodyParams = schemaWebhookBaseBodyParams.merge(schemaWebhookCreateParams);
export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams
.merge(
z.object({
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
})
)
.partial()
.strict();
export const schemaWebhookReadPublic = Webhook.pick({
id: true,
userId: true,
eventTypeId: true,
payloadTemplate: true,
eventTriggers: true,
// FIXME: We have some invalid urls saved in the DB
// subscriberUrl: true,
/** @todo: find out how to properly add back and validate those. */
// eventType: true,
// app: true,
appId: true,
}).merge(
z.object({
subscriberUrl: z.string(),
})
);

5
apps/api/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,10 @@
const path = require("path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("../web/public/static/locales"),
};
module.exports = config;

69
apps/api/next.config.js Normal file
View File

@ -0,0 +1,69 @@
const { withAxiom } = require("next-axiom");
module.exports = withAxiom({
transpilePackages: [
"@calcom/app-store",
"@calcom/core",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/features",
"@calcom/lib",
"@calcom/prisma",
"@calcom/trpc",
],
async headers() {
return [
{
source: "/docs",
headers: [
{
key: "Access-Control-Allow-Credentials",
value: "true",
},
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, OPTIONS, PATCH, DELETE, POST, PUT",
},
{
key: "Access-Control-Allow-Headers",
value:
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Content-Type, api_key, Authorization",
},
],
},
];
},
async rewrites() {
return {
afterFiles: [
// This redirects requests recieved at / the root to the /api/ folder.
{
source: "/v:version/:rest*",
destination: "/api/v:version/:rest*",
},
// This redirects requests to api/v*/ to /api/ passing version as a query parameter.
{
source: "/api/v:version/:rest*",
destination: "/api/:rest*?version=:version",
},
// Keeps backwards compatibility with old webhook URLs
{
source: "/api/hooks/:rest*",
destination: "/api/webhooks/:rest*",
},
],
fallback: [
// These rewrites are checked after both pages/public files
// and dynamic routes are checked
{
source: "/:path*",
destination: `/api/:path*`,
},
],
};
},
});

21
apps/api/next.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import type { Session } from "next-auth";
import type { NextApiRequest as BaseNextApiRequest } from "next/types";
import type { PrismaClient } from "@calcom/prisma/client";
export type * from "next/types";
export declare module "next" {
interface NextApiRequest extends BaseNextApiRequest {
session?: Session | null;
userId: number;
method: string;
prisma: PrismaClient;
// session: { user: { id: number } };
// query: Partial<{ [key: string]: string | string[] }>;
isAdmin: boolean;
isCustomPrisma: boolean;
pagination: { take: number; skip: number };
}
}

47
apps/api/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "@calcom/api",
"version": "1.0.0",
"description": "Public API for Cal.com",
"main": "index.ts",
"repository": "git@github.com:calcom/api.git",
"author": "Cal.com Inc.",
"private": true,
"scripts": {
"build": "next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "PORT=3002 next dev",
"lint": "eslint . --ignore-path .gitignore",
"lint:report": "eslint . --format json --output-file ../../lint-results/api.json",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"start": "PORT=3002 next start",
"test": "echo 'No tests yet' && exit 0",
"type-check": "tsc --pretty --noEmit"
},
"devDependencies": {
"@calcom/tsconfig": "*",
"@calcom/types": "*",
"node-mocks-http": "^1.11.0"
},
"dependencies": {
"@calcom/app-store": "*",
"@calcom/core": "*",
"@calcom/dayjs": "*",
"@calcom/emails": "*",
"@calcom/features": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/trpc": "*",
"@sentry/nextjs": "^7.20.0",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"next": "~13.2.1",
"next-api-middleware": "^1.0.1",
"next-axiom": "^0.16.0",
"next-swagger-doc": "^0.3.6",
"next-validations": "^0.2.0",
"typescript": "^4.9.4",
"tzdata": "^1.0.30",
"uuid": "^8.3.2",
"zod": "^3.20.2"
}
}

View File

@ -0,0 +1,17 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
export async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const { id } = schemaQueryIdAsString.parse(req.query);
// Admin can check any api key
if (isAdmin) 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" });
}

View File

@ -0,0 +1,14 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
async function deleteHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdAsString.parse(query);
await prisma.apiKey.delete({ where: { id } });
return { message: `ApiKey with id: ${id} deleted` };
}
export default defaultResponder(deleteHandler);

View File

@ -0,0 +1,15 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { apiKeyPublicSchema } from "~/lib/validations/api-key";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
async function getHandler(req: NextApiRequest) {
const { prisma, 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);

View File

@ -0,0 +1,16 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { apiKeyEditBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
async function patchHandler(req: NextApiRequest) {
const { prisma, 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);

View 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);
})
);

View File

@ -0,0 +1,40 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin } = req;
if (isAdmin && 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, isAdmin, prisma } = req;
req.args = isAdmin ? {} : { 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);

View File

@ -0,0 +1,44 @@
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 { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = 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 (!isAdmin && bodyUserId) throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
if (isAdmin && 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);

View 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"),
})
);

View File

@ -0,0 +1,20 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin, prisma } = 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 (isAdmin) 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;

View File

@ -0,0 +1,43 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,44 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const attendee = await prisma.attendee.findUnique({ where: { id } });
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,76 @@
import type { NextApiRequest } from "next";
import type { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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 { isAdmin, prisma } = req;
if (isAdmin) 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);

View 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);
})
);

View File

@ -0,0 +1,41 @@
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 { 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, isAdmin, prisma } = req;
const args: Prisma.AttendeeFindManyArgs = isAdmin ? {} : { 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);

View File

@ -0,0 +1,81 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin, prisma } = req;
const body = schemaAttendeeCreateBodyParams.parse(req.body);
if (!isAdmin) {
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);

View 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"),
})
);

View File

@ -0,0 +1,19 @@
import type { NextApiRequest } from "next";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, prisma, isAdmin, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
/** Admins can skip the ownership verification */
if (isAdmin) 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;

View File

@ -0,0 +1,45 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,49 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,86 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View 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);
})
);

View File

@ -0,0 +1,99 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma } = req;
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, prisma, isAdmin } = req;
if (isAdmin) 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);

View File

@ -0,0 +1,9 @@
import { defaultHandler } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
export default withMiddleware()(
defaultHandler({
POST: import("./_post"),
})
);

View File

@ -0,0 +1,158 @@
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 { availabilityUserSelect } from "@calcom/prisma";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
/**
* @swagger
* /availability:
* get:
* summary: Find user or team 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: teamId
* schema:
* type: integer
* example: 123
* description: ID of the team 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: 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 | Team not found | Team has no members
*/
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 { prisma, isAdmin } = req;
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
if (!teamId)
return getUserAvailability({
username,
dateFrom,
dateTo,
eventTypeId,
userId,
});
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.map((membership) => membership.userId);
const members = await prisma.user.findMany({
where: { id: { in: allMemberIds } },
select: availabilityUserSelect,
});
if (!isAdmin) 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,
}),
};
});
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);

View File

@ -0,0 +1,9 @@
import { defaultHandler } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
export default withMiddleware()(
defaultHandler({
GET: import("./_get"),
})
);

View File

@ -0,0 +1,19 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin, prisma } = 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 (isAdmin) 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;

View File

@ -0,0 +1,43 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
await prisma.bookingReference.delete({ where: { id } });
return { message: `BookingReference with id: ${id} deleted` };
}
export default defaultResponder(deleteHandler);

View File

@ -0,0 +1,44 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,78 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, query, body, isAdmin, 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 = isAdmin
? /* 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);

View 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);
})
);

View File

@ -0,0 +1,38 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin, prisma } = req;
const args: Prisma.BookingReferenceFindManyArgs = isAdmin ? {} : { where: { booking: { userId } } };
const data = await prisma.bookingReference.findMany(args);
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,86 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin, prisma } = req;
const body = schemaBookingCreateBodyParams.parse(req.body);
const args: Prisma.BookingFindFirstOrThrowArgs = isAdmin
? /* 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);

View 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"),
})
);

View File

@ -0,0 +1,24 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, prisma, isAdmin, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const userWithBookings = await prisma.user.findUnique({
where: { id: userId },
include: { bookings: true },
});
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
if (!isAdmin && !userBookingIds.includes(id)) {
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
}
}
export default authMiddleware;

View File

@ -0,0 +1,82 @@
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: reason
* 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>Cannot cancel past events</td>
* <td>The provided id matched an existing booking with a past startDate.</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);

View File

@ -0,0 +1,73 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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/ArrayOfBookings"
* examples:
* bookings:
* value: [
* {
* "id": 1,
* "description": "Meeting with John",
* "eventTypeId": 2,
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
* "title": "Business Meeting",
* "startTime": "2023-04-20T10:00:00.000Z",
* "endTime": "2023-04-20T11:00:00.000Z",
* "timeZone": "Europe/London",
* "attendees": [
* {
* "email": "example@cal.com",
* "name": "John Doe",
* "timeZone": "Europe/London",
* "locale": "en"
* }
* ]
* }
* ]
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Booking was not found
*/
export async function getHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const booking = await prisma.booking.findUnique({
where: { id },
include: { attendees: true, user: true },
});
return { booking: schemaBookingReadPublic.parse(booking) };
}
export default defaultResponder(getHandler);

View File

@ -0,0 +1,110 @@
import type { NextApiRequest } from "next";
import type { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
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"]'
* 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"
* }
* }
* 400:
* description: Bad request. Booking body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
export async function patchHandler(req: NextApiRequest) {
const { prisma, 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 { isAdmin } = req;
if (body.userId && !isAdmin) {
// 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",
});
}
}
export default defaultResponder(patchHandler);

View 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);
});

View 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);
})
);

View File

@ -0,0 +1,130 @@
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 { schemaBookingReadPublic } from "~/lib/validations/booking";
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]
* operationId: listBookings
* tags:
* - bookings
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/ArrayOfBookings"
* examples:
* bookings:
* value: [
* {
* "id": 1,
* "description": "Meeting with John",
* "eventTypeId": 2,
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
* "title": "Business Meeting",
* "startTime": "2023-04-20T10:00:00.000Z",
* "endTime": "2023-04-20T11:00:00.000Z",
* "timeZone": "Europe/London",
* "attendees": [
* {
* "email": "example@cal.com",
* "name": "John Doe",
* "timeZone": "Europe/London",
* "locale": "en"
* }
* ]
* }
* ]
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: No bookings were found
*/
async function handler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const args: Prisma.BookingFindManyArgs = {};
args.include = {
attendees: true,
user: true,
};
/** Only admins can query other users */
if (isAdmin && req.query.userId) {
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { email: true },
});
const userEmails = users.map((u) => u.email);
args.where = {
OR: [
{ userId: { in: userIds } },
{
attendees: {
some: {
email: { in: userEmails },
},
},
},
],
};
} else if (!isAdmin) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
email: true,
},
});
if (!user) {
throw new HttpError({ message: "User not found", statusCode: 500 });
}
args.where = {
OR: [
{
userId,
},
{
attendees: {
some: {
email: user.email,
},
},
},
],
};
}
const data = await prisma.booking.findMany(args);
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
}
export default defaultResponder(handler);

View File

@ -0,0 +1,204 @@
import type { NextApiRequest } from "next";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { defaultResponder } from "@calcom/lib/server";
/**
* @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
* - end
* - name
* - email
* - timeZone
* - language
* - metadata
* - customInputs
* - location
* 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'
* name:
* type: string
* description: 'Name of the Attendee'
* email:
* type: string
* format: email
* description: 'Email ID of the Attendee'
* timeZone:
* type: string
* description: 'TimeZone of the Attendee'
* language:
* type: string
* description: 'Language of the Attendee'
* metadata:
* type: object
* properties: {}
* description: 'Any metadata associated with the booking'
* customInputs:
* type: array
* items: {}
* location:
* type: string
* description: 'Meeting location'
* 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'
* 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",
* "name": "Hello Hello",
* "email": "hello@gmail.com",
* "timeZone": "Europe/London",
* "language": "en",
* "metadata": {},
* "customInputs": [],
* "location": "Calcom HQ",
* "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:
* bookings:
* value:
* {
* "id": 11223344,
* "uid": "5yUjmAYTDF6MXo98re8SkX",
* "userId": 123,
* "eventTypeId": 2323232,
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
* "description": null,
* "customInputs": {},
* "responses": null,
* "startTime": "2023-05-24T13:00:00.000Z",
* "endTime": "2023-05-24T13:30:00.000Z",
* "location": "Calcom HQ",
* "createdAt": "2023-04-19T10:17:58.580Z",
* "updatedAt": null,
* "status": "PENDING",
* "paid": false,
* "destinationCalendarId": 2180,
* "cancellationReason": null,
* "rejectionReason": null,
* "dynamicEventSlugRef": null,
* "dynamicGroupSlugRef": null,
* "rescheduled": null,
* "fromReschedule": null,
* "recurringEventId": null,
* "smsReminderNumber": null,
* "scheduledJobs": [],
* "metadata": {},
* "isRecorded": false,
* "user": {
* "email": "test@cal.com",
* "name": "Syed Ali Shahbaz",
* "timeZone": "Asia/Calcutta"
* },
* "attendees": [
* {
* "id": 12345,
* "email": "hello@gmail.com",
* "name": "Hello Hello",
* "timeZone": "Europe/London",
* "locale": "en",
* "bookingId": 11223344
* }
* ],
* "payment": [],
* "references": []
* }
* 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, isAdmin } = req;
if (isAdmin) req.userId = req.body.userId || userId;
const booking = await handleNewBooking(req);
return booking;
}
export default defaultResponder(handler);

View 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"),
})
);

View File

@ -0,0 +1,19 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
async function authMiddleware(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const { id } = schemaQueryIdParseInt.parse(req.query);
// Admins can just skip this check
if (isAdmin) 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;

View File

@ -0,0 +1,42 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,43 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View File

@ -0,0 +1,86 @@
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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 { prisma, 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);

View 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);
})
);

View File

@ -0,0 +1,37 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin, prisma } = req;
const args: Prisma.EventTypeCustomInputFindManyArgs = isAdmin ? {} : { 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);

View File

@ -0,0 +1,103 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
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, isAdmin, prisma } = req;
const { eventTypeId, ...body } = schemaEventTypeCustomInputBodyParams.parse(req.body);
if (!isAdmin) {
/* 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);

View 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"),
})
);

View File

@ -0,0 +1,240 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import type { DestinationCalendarResponse } from "~/lib/types";
import {
schemaDestinationCalendarEditBodyParams,
schemaDestinationCalendarReadPublic,
} from "~/lib/validations/destination-calendar";
import {
schemaQueryIdParseInt,
withValidQueryIdTransformParseInt,
} from "~/lib/validations/shared/queryIdTransformParseInt";
export async function destionationCalendarById(
{ method, query, body, userId, prisma }: NextApiRequest,
res: NextApiResponse<DestinationCalendarResponse>
) {
const safeQuery = schemaQueryIdParseInt.safeParse(query);
const safeBody = schemaDestinationCalendarEditBodyParams.safeParse(body);
if (!safeQuery.success) {
res.status(400).json({ message: "Your query was invalid" });
return;
}
const data = await prisma.destinationCalendar.findMany({ where: { userId } });
const userDestinationCalendars = data.map((destinationCalendar) => destinationCalendar.id);
// FIXME: Should we also check ownership of bokingId and eventTypeId to avoid users cross-pollinating other users calendars.
// On a related note, moving from sequential integer IDs to UUIDs would be a good idea. and maybe help avoid having this problem.
if (userDestinationCalendars.includes(safeQuery.data.id)) res.status(401).json({ message: "Unauthorized" });
else {
switch (method) {
/**
* @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: DestinationCalendar was not found
* 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:
* 201:
* description: OK, destinationCalendar edited successfuly
* 400:
* description: Bad request. DestinationCalendar body is invalid.
* 401:
* description: Authorization information is missing or invalid.
* 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:
* 201:
* description: OK, destinationCalendar removed successfuly
* 400:
* description: Bad request. DestinationCalendar id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "GET":
await prisma.destinationCalendar
.findUnique({ where: { id: safeQuery.data.id } })
.then((data) => schemaDestinationCalendarReadPublic.parse(data))
.then((destination_calendar) => res.status(200).json({ destination_calendar }))
.catch((error: Error) =>
res.status(404).json({
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
/**
* @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
* tags:
* - destination-calendars
* responses:
* 201:
* description: OK, destinationCalendar edited successfuly
* 400:
* description: Bad request. DestinationCalendar body is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "PATCH":
if (!safeBody.success) {
{
res.status(400).json({ message: "Invalid request body" });
return;
}
}
await prisma.destinationCalendar
.update({ where: { id: safeQuery.data.id }, data: safeBody.data })
.then((data) => schemaDestinationCalendarReadPublic.parse(data))
.then((destination_calendar) => res.status(200).json({ destination_calendar }))
.catch((error: Error) =>
res.status(404).json({
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
/**
* @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:
* 201:
* description: OK, destinationCalendar removed successfuly
* 400:
* description: Bad request. DestinationCalendar id is invalid.
* 401:
* description: Authorization information is missing or invalid.
*/
case "DELETE":
await prisma.destinationCalendar
.delete({
where: { id: safeQuery.data.id },
})
.then(() =>
res.status(200).json({
message: `DestinationCalendar with id: ${safeQuery.data.id} deleted`,
})
)
.catch((error: Error) =>
res.status(404).json({
message: `DestinationCalendar with id: ${safeQuery.data.id} not found`,
error,
})
);
break;
default:
res.status(405).json({ message: "Method not allowed" });
break;
}
}
}
export default withMiddleware("HTTP_GET_DELETE_PATCH")(
withValidQueryIdTransformParseInt(destionationCalendarById)
);

Some files were not shown because too many files have changed in this diff Show More