first commit
This commit is contained in:
18
calcom/apps/api/index.js
Normal file
18
calcom/apps/api/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
const http = require("http");
|
||||
const connect = require("connect");
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
const apiProxyV1 = createProxyMiddleware({
|
||||
target: "http://localhost:3003",
|
||||
});
|
||||
|
||||
const apiProxyV2 = createProxyMiddleware({
|
||||
target: "http://localhost:3004",
|
||||
});
|
||||
|
||||
const app = connect();
|
||||
app.use("/", apiProxyV1);
|
||||
|
||||
app.use("/v2", apiProxyV2);
|
||||
|
||||
http.createServer(app).listen(3002);
|
16
calcom/apps/api/package.json
Normal file
16
calcom/apps/api/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@calcom/api-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "node ./index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"connect": "^3.7.0",
|
||||
"http": "^0.0.1-security",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
}
|
||||
}
|
7
calcom/apps/api/v1/.env.example
Normal file
7
calcom/apps/api/v1/.env.example
Normal file
@ -0,0 +1,7 @@
|
||||
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=""
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555
|
81
calcom/apps/api/v1/.gitignore
vendored
Normal file
81
calcom/apps/api/v1/.gitignore
vendored
Normal 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
calcom/apps/api/v1/.gitkeep
Normal file
0
calcom/apps/api/v1/.gitkeep
Normal file
5
calcom/apps/api/v1/.prettierignore
Normal file
5
calcom/apps/api/v1/.prettierignore
Normal file
@ -0,0 +1,5 @@
|
||||
.next/
|
||||
coverage/
|
||||
node_modules/
|
||||
tests/
|
||||
templates/
|
42
calcom/apps/api/v1/LICENSE
Normal file
42
calcom/apps/api/v1/LICENSE
Normal file
@ -0,0 +1,42 @@
|
||||
The Cal.com Commercial License (the “Commercial License”)
|
||||
Copyright (c) 2020-present Cal.com, Inc
|
||||
|
||||
With regard to the Cal.com Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cal.com Subscription Terms available
|
||||
at https://cal.com/terms, or other agreements governing
|
||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
|
||||
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||
and to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
|
||||
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
This Commercial License applies only to the part of this Software that is not distributed under
|
||||
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
|
||||
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
|
||||
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
|
||||
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
|
||||
be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cal.com Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
225
calcom/apps/api/v1/README.md
Normal file
225
calcom/apps/api/v1/README.md
Normal 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>
|
||||
|
||||
# Commercial 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`
|
15
calcom/apps/api/v1/instrumentation.ts
Normal file
15
calcom/apps/api/v1/instrumentation.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
||||
}
|
||||
}
|
1
calcom/apps/api/v1/lib/constants.ts
Normal file
1
calcom/apps/api/v1/lib/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const PRISMA_CLIENT_CACHING_TIME = 1000 * 60 * 60 * 24; // one day in ms
|
24
calcom/apps/api/v1/lib/helpers/addRequestid.ts
Normal file
24
calcom/apps/api/v1/lib/helpers/addRequestid.ts
Normal 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();
|
||||
};
|
20
calcom/apps/api/v1/lib/helpers/captureErrors.ts
Normal file
20
calcom/apps/api/v1/lib/helpers/captureErrors.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { captureException as SentryCaptureException } from "@sentry/nextjs";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
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) {
|
||||
SentryCaptureException(error);
|
||||
const redactedError = redactError(error);
|
||||
if (redactedError instanceof Error) {
|
||||
res.status(400).json({ message: redactedError.message, error: redactedError });
|
||||
return;
|
||||
}
|
||||
res.status(400).json({ message: "Something went wrong", error });
|
||||
}
|
||||
};
|
23
calcom/apps/api/v1/lib/helpers/checkIsInMaintenanceMode.ts
Normal file
23
calcom/apps/api/v1/lib/helpers/checkIsInMaintenanceMode.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
|
||||
try {
|
||||
return get<T>(key);
|
||||
} catch (error) {
|
||||
// Don't crash if EDGE_CONFIG env var is missing
|
||||
}
|
||||
};
|
||||
|
||||
export const config = { matcher: "/:path*" };
|
||||
|
||||
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
|
||||
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
|
||||
if (isInMaintenanceMode) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API is currently under maintenance. Please try again at a later time." });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
9
calcom/apps/api/v1/lib/helpers/extendRequest.ts
Normal file
9
calcom/apps/api/v1/lib/helpers/extendRequest.ts
Normal 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();
|
||||
};
|
32
calcom/apps/api/v1/lib/helpers/httpMethods.ts
Normal file
32
calcom/apps/api/v1/lib/helpers/httpMethods.ts
Normal 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"]);
|
90
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts
Normal file
90
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.test.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
|
||||
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
||||
checkRateLimitAndThrowError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("rateLimitApiKey middleware", () => {
|
||||
it("should return 401 if no apiKey is provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {},
|
||||
});
|
||||
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(401);
|
||||
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
|
||||
});
|
||||
|
||||
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
reset: Date.now(),
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
||||
identifier: "test-key",
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("should set rate limit headers correctly", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
const rateLimiterResponse = {
|
||||
limit: 100,
|
||||
remaining: 99,
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
||||
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
|
||||
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
|
||||
});
|
||||
|
||||
it("should return 429 if rate limit is exceeded", async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: "GET",
|
||||
query: { apiKey: "test-key" },
|
||||
});
|
||||
|
||||
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
||||
|
||||
// @ts-expect-error weird typing between middleware and createMocks
|
||||
await rateLimitApiKey(req, res, vi.fn() as any);
|
||||
|
||||
expect(res._getStatusCode()).toBe(429);
|
||||
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
||||
});
|
||||
});
|
24
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.ts
Normal file
24
calcom/apps/api/v1/lib/helpers/rateLimitApiKey.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
|
||||
// TODO: Add a way to add trusted api keys
|
||||
try {
|
||||
await checkRateLimitAndThrowError({
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(429).json({ message: "Rate limit exceeded" });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
14
calcom/apps/api/v1/lib/helpers/safeParseJSON.ts
Normal file
14
calcom/apps/api/v1/lib/helpers/safeParseJSON.ts
Normal 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 {};
|
||||
}
|
||||
}
|
46
calcom/apps/api/v1/lib/helpers/verifyApiKey.ts
Normal file
46
calcom/apps/api/v1/lib/helpers/verifyApiKey.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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 { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { isAdminGuard } from "../utils/isAdmin";
|
||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||
|
||||
// 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 hasValidLicense = await checkLicense(prisma);
|
||||
if (!hasValidLicense && IS_PRODUCTION)
|
||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||
// 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;
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||
|
||||
await next();
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
export const verifyCredentialSyncEnabled: NextMiddleware = async (req, res, next) => {
|
||||
const { isSystemWideAdmin } = req;
|
||||
|
||||
if (!isSystemWideAdmin) {
|
||||
return res.status(403).json({ error: "Only admin API keys can access credential syncing endpoints" });
|
||||
}
|
||||
|
||||
if (!APP_CREDENTIAL_SHARING_ENABLED) {
|
||||
return res.status(501).json({ error: "Credential syncing is not enabled" });
|
||||
}
|
||||
|
||||
if (
|
||||
req.headers[process.env.CALCOM_CREDENTIAL_SYNC_HEADER_NAME || "calcom-credential-sync-secret"] !==
|
||||
process.env.CALCOM_CREDENTIAL_SYNC_SECRET
|
||||
) {
|
||||
return res.status(401).json({ message: "Invalid credential sync secret" });
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
51
calcom/apps/api/v1/lib/helpers/withMiddleware.ts
Normal file
51
calcom/apps/api/v1/lib/helpers/withMiddleware.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { label } from "next-api-middleware";
|
||||
|
||||
import { addRequestId } from "./addRequestid";
|
||||
import { captureErrors } from "./captureErrors";
|
||||
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
|
||||
import { extendRequest } from "./extendRequest";
|
||||
import {
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
HTTP_PATCH,
|
||||
HTTP_GET,
|
||||
HTTP_GET_OR_POST,
|
||||
HTTP_GET_DELETE_PATCH,
|
||||
} from "./httpMethods";
|
||||
import { rateLimitApiKey } from "./rateLimitApiKey";
|
||||
import { verifyApiKey } from "./verifyApiKey";
|
||||
import { verifyCredentialSyncEnabled } from "./verifyCredentialSyncEnabled";
|
||||
import { withPagination } from "./withPagination";
|
||||
|
||||
const middleware = {
|
||||
HTTP_GET_OR_POST,
|
||||
HTTP_GET_DELETE_PATCH,
|
||||
HTTP_GET,
|
||||
HTTP_PATCH,
|
||||
HTTP_POST,
|
||||
HTTP_DELETE,
|
||||
addRequestId,
|
||||
checkIsInMaintenanceMode,
|
||||
verifyApiKey,
|
||||
rateLimitApiKey,
|
||||
extendRequest,
|
||||
pagination: withPagination,
|
||||
captureErrors,
|
||||
verifyCredentialSyncEnabled,
|
||||
};
|
||||
|
||||
type Middleware = keyof typeof middleware;
|
||||
|
||||
const middlewareOrder = [
|
||||
// The order here, determines the order of execution
|
||||
"checkIsInMaintenanceMode",
|
||||
"extendRequest",
|
||||
"captureErrors",
|
||||
"verifyApiKey",
|
||||
"rateLimitApiKey",
|
||||
"addRequestId",
|
||||
] as Middleware[]; // <-- Provide a list of middleware to call automatically
|
||||
|
||||
const withMiddleware = label(middleware, middlewareOrder);
|
||||
|
||||
export { withMiddleware, middleware, middlewareOrder };
|
17
calcom/apps/api/v1/lib/helpers/withPagination.ts
Normal file
17
calcom/apps/api/v1/lib/helpers/withPagination.ts
Normal 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
calcom/apps/api/v1/lib/types.ts
Normal file
187
calcom/apps/api/v1/lib/types.ts
Normal 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>[];
|
||||
};
|
14
calcom/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts
Normal file
14
calcom/apps/api/v1/lib/utils/extractUserIdsFromQuery.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
export function extractUserIdsFromQuery({ isSystemWideAdmin, query }: NextApiRequest) {
|
||||
/** Guard: Only admins can query other users */
|
||||
if (!isSystemWideAdmin) {
|
||||
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||
}
|
||||
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||
}
|
37
calcom/apps/api/v1/lib/utils/isAdmin.ts
Normal file
37
calcom/apps/api/v1/lib/utils/isAdmin.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
if (userRole === UserPermissionRole.ADMIN) return { isAdmin: true, scope: ScopeOfAdmin.SystemWide };
|
||||
|
||||
const orgOwnerOrAdminMemberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
};
|
4
calcom/apps/api/v1/lib/utils/isValidBase64Image.ts
Normal file
4
calcom/apps/api/v1/lib/utils/isValidBase64Image.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export function isValidBase64Image(input: string): boolean {
|
||||
const regex = /^data:image\/[^;]+;base64,(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
||||
return regex.test(input);
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
type AccessibleUsersType = {
|
||||
memberUserIds: number[];
|
||||
adminUserId: number;
|
||||
};
|
||||
|
||||
const getAllOrganizationMemberships = async (
|
||||
memberships: {
|
||||
userId: number;
|
||||
role: MembershipRole;
|
||||
teamId: number;
|
||||
}[],
|
||||
orgId: number
|
||||
) => {
|
||||
return memberships.reduce<number[]>((acc, membership) => {
|
||||
if (membership.teamId === orgId) {
|
||||
acc.push(membership.userId);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const getAllAdminMemberships = async (userId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getAllOrganizationMembers = async (organizationId: number) => {
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = memberships.find((membership) => membership.userId === adminUserId)?.teamId;
|
||||
if (!orgId) return [];
|
||||
|
||||
const allAccessibleMemberUserIds = await getAllOrganizationMemberships(memberships, orgId);
|
||||
const accessibleUserIds = allAccessibleMemberUserIds.filter((userId) => userId !== adminUserId);
|
||||
return accessibleUserIds;
|
||||
};
|
||||
|
||||
export const retrieveOrgScopedAccessibleUsers = async ({ adminId }: { adminId: number }) => {
|
||||
const adminMemberships = await getAllAdminMemberships(adminId);
|
||||
const organizationId = adminMemberships.find((membership) => membership.team.isOrganization)?.team.id;
|
||||
if (!organizationId) return [];
|
||||
|
||||
const allMemberships = await getAllOrganizationMembers(organizationId);
|
||||
return allMemberships.map((membership) => membership.userId);
|
||||
};
|
4
calcom/apps/api/v1/lib/utils/scopeOfAdmin.ts
Normal file
4
calcom/apps/api/v1/lib/utils/scopeOfAdmin.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const ScopeOfAdmin = {
|
||||
SystemWide: "SystemWide",
|
||||
OrgOwnerOrAdmin: "OrgOwnerOrAdmin",
|
||||
} as const;
|
4
calcom/apps/api/v1/lib/utils/stringifyISODate.ts
Normal file
4
calcom/apps/api/v1/lib/utils/stringifyISODate.ts
Normal 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.
|
29
calcom/apps/api/v1/lib/validations/api-key.ts
Normal file
29
calcom/apps/api/v1/lib/validations/api-key.ts
Normal 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,
|
||||
});
|
39
calcom/apps/api/v1/lib/validations/attendee.ts
Normal file
39
calcom/apps/api/v1/lib/validations/attendee.ts
Normal 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,
|
||||
});
|
56
calcom/apps/api/v1/lib/validations/availability.ts
Normal file
56
calcom/apps/api/v1/lib/validations/availability.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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(),
|
||||
date: z.date().or(z.string()).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(),
|
||||
date: z.date().or(z.string()).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(),
|
||||
});
|
28
calcom/apps/api/v1/lib/validations/booking-reference.ts
Normal file
28
calcom/apps/api/v1/lib/validations/booking-reference.ts
Normal 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();
|
88
calcom/apps/api/v1/lib/validations/booking.ts
Normal file
88
calcom/apps/api/v1/lib/validations/booking.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } 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());
|
||||
|
||||
export const schemaBookingGetParams = z.object({
|
||||
dateFrom: iso8601.optional(),
|
||||
dateTo: iso8601.optional(),
|
||||
order: z.enum(["asc", "desc"]).default("asc"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional(),
|
||||
});
|
||||
|
||||
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)
|
||||
.omit({ uid: true });
|
||||
|
||||
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,
|
||||
})
|
||||
.nullish(),
|
||||
payment: z
|
||||
.array(
|
||||
_PaymentModel.pick({
|
||||
id: true,
|
||||
success: true,
|
||||
paymentOption: true,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
responses: z.record(z.any()).nullable(),
|
||||
}).pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
description: true,
|
||||
eventTypeId: true,
|
||||
uid: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
timeZone: true,
|
||||
attendees: true,
|
||||
user: true,
|
||||
payment: true,
|
||||
metadata: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
fromReschedule: true,
|
||||
});
|
18
calcom/apps/api/v1/lib/validations/connected-calendar.ts
Normal file
18
calcom/apps/api/v1/lib/validations/connected-calendar.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const CalendarSchema = z.object({
|
||||
externalId: z.string(),
|
||||
name: z.string(),
|
||||
primary: z.boolean(),
|
||||
readOnly: z.boolean(),
|
||||
});
|
||||
|
||||
const IntegrationSchema = z.object({
|
||||
name: z.string(),
|
||||
appId: z.string(),
|
||||
userId: z.number(),
|
||||
integration: z.string(),
|
||||
calendars: z.array(CalendarSchema),
|
||||
});
|
||||
|
||||
export const schemaConnectedCalendarsReadPublic = z.array(IntegrationSchema);
|
64
calcom/apps/api/v1/lib/validations/credential-sync.ts
Normal file
64
calcom/apps/api/v1/lib/validations/credential-sync.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
const userId = z.string().transform((val) => {
|
||||
const userIdInt = parseInt(val);
|
||||
|
||||
if (isNaN(userIdInt)) {
|
||||
throw new HttpError({ message: "userId is not a valid number", statusCode: 400 });
|
||||
}
|
||||
|
||||
return userIdInt;
|
||||
});
|
||||
const appSlug = z.string();
|
||||
const credentialId = z.string().transform((val) => {
|
||||
const credentialIdInt = parseInt(val);
|
||||
|
||||
if (isNaN(credentialIdInt)) {
|
||||
throw new HttpError({ message: "credentialId is not a valid number", statusCode: 400 });
|
||||
}
|
||||
|
||||
return credentialIdInt;
|
||||
});
|
||||
const encryptedKey = z.string();
|
||||
|
||||
export const schemaCredentialGetParams = z.object({
|
||||
userId,
|
||||
appSlug: appSlug.optional(),
|
||||
});
|
||||
|
||||
export const schemaCredentialPostParams = z.object({
|
||||
userId,
|
||||
createSelectedCalendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
return val === "true";
|
||||
}),
|
||||
createDestinationCalendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
return val === "true";
|
||||
}),
|
||||
});
|
||||
|
||||
export const schemaCredentialPostBody = z.object({
|
||||
appSlug,
|
||||
encryptedKey,
|
||||
});
|
||||
|
||||
export const schemaCredentialPatchParams = z.object({
|
||||
userId,
|
||||
credentialId,
|
||||
});
|
||||
|
||||
export const schemaCredentialPatchBody = z.object({
|
||||
encryptedKey,
|
||||
});
|
||||
|
||||
export const schemaCredentialDeleteParams = z.object({
|
||||
userId,
|
||||
credentialId,
|
||||
});
|
48
calcom/apps/api/v1/lib/validations/destination-calendar.ts
Normal file
48
calcom/apps/api/v1/lib/validations/destination-calendar.ts
Normal 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().optional(),
|
||||
bookingId: z.number().optional(),
|
||||
userId: z.number().optional(),
|
||||
})
|
||||
.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,
|
||||
});
|
@ -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();
|
173
calcom/apps/api/v1/lib/validations/event-type.ts
Normal file
173
calcom/apps/api/v1/lib/validations/event-type.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
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 childrenSchema = z.object({
|
||||
id: z.number().int(),
|
||||
userId: z.number().int(),
|
||||
});
|
||||
|
||||
export const schemaEventTypeBaseBodyParams = EventType.pick({
|
||||
title: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
position: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
schedulingType: true,
|
||||
// START Limit future bookings
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
// END Limit future bookings
|
||||
requiresConfirmation: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
minimumBookingNotice: true,
|
||||
parentId: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
teamId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
assignAllTeamMembers: true,
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
})
|
||||
)
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
const schemaEventTypeCreateParams = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
description: z.string().optional().nullable(),
|
||||
length: z.number().int(),
|
||||
metadata: z.any().optional(),
|
||||
recurringEvent: recurringEventInputSchema.optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().optional(),
|
||||
parentId: z.number().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams
|
||||
.merge(schemaEventTypeCreateParams)
|
||||
.merge(schemaQueryUserId.partial());
|
||||
|
||||
const schemaEventTypeEditParams = z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
slug: z
|
||||
.string()
|
||||
.transform((s) => slugify(s))
|
||||
.optional(),
|
||||
length: z.number().int().optional(),
|
||||
seatsPerTimeSlot: z.number().optional(),
|
||||
seatsShowAttendees: z.boolean().optional(),
|
||||
seatsShowAvailabilityCount: z.boolean().optional(),
|
||||
bookingFields: eventTypeBookingFields.optional(),
|
||||
scheduleId: z.number().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,
|
||||
scheduleId: 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,
|
||||
parentId: true,
|
||||
successRedirectUrl: true,
|
||||
description: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
children: z.array(childrenSchema).optional().default([]),
|
||||
hosts: z.array(hostSchema).optional().default([]),
|
||||
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(),
|
||||
})
|
||||
);
|
68
calcom/apps/api/v1/lib/validations/membership.ts
Normal file
68
calcom/apps/api/v1/lib/validations/membership.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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.omit({ id: true })
|
||||
.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,
|
||||
id: 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,
|
||||
};
|
||||
});
|
5
calcom/apps/api/v1/lib/validations/payment.ts
Normal file
5
calcom/apps/api/v1/lib/validations/payment.ts
Normal 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 });
|
17
calcom/apps/api/v1/lib/validations/reminder-mail.ts
Normal file
17
calcom/apps/api/v1/lib/validations/reminder-mail.ts
Normal 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
|
||||
);
|
43
calcom/apps/api/v1/lib/validations/schedule.ts
Normal file
43
calcom/apps/api/v1/lib/validations/schedule.ts
Normal file
@ -0,0 +1,43 @@
|
||||
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,
|
||||
date: 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(),
|
||||
})
|
||||
);
|
48
calcom/apps/api/v1/lib/validations/selected-calendar.ts
Normal file
48
calcom/apps/api/v1/lib/validations/selected-calendar.ts
Normal 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,
|
||||
};
|
||||
});
|
11
calcom/apps/api/v1/lib/validations/shared/baseApiParams.ts
Normal file
11
calcom/apps/api/v1/lib/validations/shared/baseApiParams.ts
Normal 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(),
|
||||
});
|
9
calcom/apps/api/v1/lib/validations/shared/jsonSchema.ts
Normal file
9
calcom/apps/api/v1/lib/validations/shared/jsonSchema.ts
Normal 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)])
|
||||
);
|
@ -0,0 +1,20 @@
|
||||
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 schemaQueryAttendeeEmail = baseApiParams.extend({
|
||||
attendeeEmail: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
|
||||
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
|
||||
});
|
||||
|
||||
export const withValidQueryAttendeeEmail = withValidation({
|
||||
schema: schemaQueryAttendeeEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
19
calcom/apps/api/v1/lib/validations/shared/queryIdString.ts
Normal file
19
calcom/apps/api/v1/lib/validations/shared/queryIdString.ts
Normal 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",
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
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",
|
||||
});
|
||||
|
||||
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
|
||||
recordingId: z.string(),
|
||||
});
|
7
calcom/apps/api/v1/lib/validations/shared/querySlug.ts
Normal file
7
calcom/apps/api/v1/lib/validations/shared/querySlug.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
export const schemaQuerySlug = baseApiParams.extend({
|
||||
slug: z.string().optional(),
|
||||
});
|
21
calcom/apps/api/v1/lib/validations/shared/queryTeamId.ts
Normal file
21
calcom/apps/api/v1/lib/validations/shared/queryTeamId.ts
Normal 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",
|
||||
});
|
20
calcom/apps/api/v1/lib/validations/shared/queryUserEmail.ts
Normal file
20
calcom/apps/api/v1/lib/validations/shared/queryUserEmail.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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 schemaQueryUserEmail = baseApiParams.extend({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserEmails = z.object({
|
||||
email: z.union([z.string().email(), z.array(z.string().email())]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserEmail = withValidation({
|
||||
schema: schemaQueryUserEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
26
calcom/apps/api/v1/lib/validations/shared/queryUserId.ts
Normal file
26
calcom/apps/api/v1/lib/validations/shared/queryUserId.ts
Normal 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",
|
||||
});
|
7
calcom/apps/api/v1/lib/validations/shared/timeZone.ts
Normal file
7
calcom/apps/api/v1/lib/validations/shared/timeZone.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import tzdata from "tzdata";
|
||||
import { 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(", ")}`,
|
||||
});
|
30
calcom/apps/api/v1/lib/validations/team.ts
Normal file
30
calcom/apps/api/v1/lib/validations/team.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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,
|
||||
pendingPayment: true,
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
smsLockState: true,
|
||||
});
|
||||
|
||||
const schemaTeamRequiredParams = z.object({
|
||||
name: z.string().max(255),
|
||||
});
|
||||
|
||||
export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict();
|
||||
|
||||
export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial();
|
||||
|
||||
const schemaOwnerId = z.object({
|
||||
ownerId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const schemaTeamCreateBodyParams = schemaTeamBodyParams.merge(schemaOwnerId).strict();
|
||||
|
||||
export const schemaTeamReadPublic = Team.omit({});
|
||||
|
||||
export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic);
|
179
calcom/apps/api/v1/lib/validations/user.ts
Normal file
179
calcom/apps/api/v1/lib/validations/user.ts
Normal file
@ -0,0 +1,179 @@
|
||||
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 { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
|
||||
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", // Disabled until Crowdin reaches at least 80% completion
|
||||
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,
|
||||
appTheme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
hideBranding: true,
|
||||
timeFormat: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: 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().toLowerCase(),
|
||||
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(),
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: 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(),
|
||||
avatar: z.string().refine(isValidBase64Image).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.
|
||||
|
||||
const schemaUserCreateParams = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
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(),
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: 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(),
|
||||
avatar: z.string().refine(isValidBase64Image).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,
|
||||
appTheme: true,
|
||||
theme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
hideBranding: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
createdDate: true,
|
||||
verified: true,
|
||||
invitedTo: true,
|
||||
role: true,
|
||||
});
|
||||
|
||||
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);
|
57
calcom/apps/api/v1/lib/validations/webhook.ts
Normal file
57
calcom/apps/api/v1/lib/validations/webhook.ts
Normal file
@ -0,0 +1,57 @@
|
||||
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(),
|
||||
secret: z.string().optional().nullable(),
|
||||
// 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(),
|
||||
secret: z.string().optional().nullable(),
|
||||
})
|
||||
)
|
||||
.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
calcom/apps/api/v1/next-env.d.ts
vendored
Normal file
5
calcom/apps/api/v1/next-env.d.ts
vendored
Normal 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.
|
10
calcom/apps/api/v1/next-i18next.config.js
Normal file
10
calcom/apps/api/v1/next-i18next.config.js
Normal 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;
|
103
calcom/apps/api/v1/next.config.js
Normal file
103
calcom/apps/api/v1/next.config.js
Normal file
@ -0,0 +1,103 @@
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const plugins = [withAxiom];
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
instrumentationHook: true,
|
||||
},
|
||||
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*",
|
||||
},
|
||||
{
|
||||
source: "/api/v2",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/health",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/health`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/docs/:path*",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/docs/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/api/v2/:path*",
|
||||
destination: `${process.env.NEXT_PUBLIC_API_V2_ROOT_URL}/api/v2/:path*`,
|
||||
},
|
||||
// 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*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
plugins.push((nextConfig) =>
|
||||
withSentryConfig(nextConfig, {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
18
calcom/apps/api/v1/next.d.ts
vendored
Normal file
18
calcom/apps/api/v1/next.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import type { Session } from "next-auth";
|
||||
import type { NextApiRequest as BaseNextApiRequest } from "next/types";
|
||||
|
||||
export type * from "next/types";
|
||||
|
||||
export declare module "next" {
|
||||
interface NextApiRequest extends BaseNextApiRequest {
|
||||
session?: Session | null;
|
||||
|
||||
userId: number;
|
||||
method: string;
|
||||
// session: { user: { id: number } };
|
||||
// query: Partial<{ [key: string]: string | string[] }>;
|
||||
isSystemWideAdmin: boolean;
|
||||
isOrganizationOwnerOrAdmin: boolean;
|
||||
pagination: { take: number; skip: number };
|
||||
}
|
||||
}
|
46
calcom/apps/api/v1/package.json
Normal file
46
calcom/apps/api/v1/package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@calcom/api",
|
||||
"version": "1.0.0",
|
||||
"description": "Public API for BLS cal",
|
||||
"main": "index.ts",
|
||||
"repository": "git@github.com:calcom/api.git",
|
||||
"author": "BLS media",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "PORT=3003 next dev",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
"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": "^8.8.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "^13.5.4",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.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.22.4"
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
export async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
// Admin can check any api key
|
||||
if (isSystemWideAdmin) return;
|
||||
// Check if user can access the api key
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!apiKey) throw new HttpError({ statusCode: 404, message: "API key not found" });
|
||||
}
|
15
calcom/apps/api/v1/pages/api/api-keys/[id]/_delete.ts
Normal file
15
calcom/apps/api/v1/pages/api/api-keys/[id]/_delete.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
await prisma.apiKey.delete({ where: { id } });
|
||||
return { message: `ApiKey with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
16
calcom/apps/api/v1/pages/api/api-keys/[id]/_get.ts
Normal file
16
calcom/apps/api/v1/pages/api/api-keys/[id]/_get.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(query);
|
||||
const api_key = await prisma.apiKey.findUniqueOrThrow({ where: { id } });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
17
calcom/apps/api/v1/pages/api/api-keys/[id]/_patch.ts
Normal file
17
calcom/apps/api/v1/pages/api/api-keys/[id]/_patch.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyEditBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
|
||||
|
||||
async function patchHandler(req: NextApiRequest) {
|
||||
const { body } = req;
|
||||
const { id } = schemaQueryIdAsString.parse(req.query);
|
||||
const data = apiKeyEditBodySchema.parse(body);
|
||||
const api_key = await prisma.apiKey.update({ where: { id }, data });
|
||||
return { api_key: apiKeyPublicSchema.parse(api_key) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
18
calcom/apps/api/v1/pages/api/api-keys/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/api-keys/[id]/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import { authMiddleware } from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
41
calcom/apps/api/v1/pages/api/api-keys/_get.ts
Normal file
41
calcom/apps/api/v1/pages/api/api-keys/_get.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Ensure } from "@calcom/types/utils";
|
||||
|
||||
import { apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & {
|
||||
args?: Prisma.ApiKeyFindManyArgs;
|
||||
};
|
||||
|
||||
/** Admins can query other users' API keys */
|
||||
function handleAdminRequests(req: CustomNextApiRequest) {
|
||||
// To match type safety with runtime
|
||||
if (!hasReqArgs(req)) throw Error("Missing req.args");
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin && req.query.userId) {
|
||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
||||
req.args.where = { userId: { in: userIds } };
|
||||
if (Array.isArray(query.userId)) req.args.orderBy = { userId: "asc" };
|
||||
}
|
||||
}
|
||||
|
||||
function hasReqArgs(req: CustomNextApiRequest): req is Ensure<CustomNextApiRequest, "args"> {
|
||||
return "args" in req;
|
||||
}
|
||||
|
||||
async function getHandler(req: CustomNextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
req.args = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
// Proof of concept: allowing mutation in exchange of composability
|
||||
handleAdminRequests(req);
|
||||
const data = await prisma.apiKey.findMany(req.args);
|
||||
return { api_keys: data.map((v) => apiKeyPublicSchema.parse(v)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
46
calcom/apps/api/v1/pages/api/api-keys/_post.ts
Normal file
46
calcom/apps/api/v1/pages/api/api-keys/_post.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import { generateUniqueAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { apiKeyCreateBodySchema, apiKeyPublicSchema } from "~/lib/validations/api-key";
|
||||
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { neverExpires, userId: bodyUserId, ...input } = apiKeyCreateBodySchema.parse(req.body);
|
||||
const [hashedKey, apiKey] = generateUniqueAPIKey();
|
||||
const args: Prisma.ApiKeyCreateArgs = {
|
||||
data: {
|
||||
id: v4(),
|
||||
userId,
|
||||
...input,
|
||||
// And here we pass a null to expiresAt if never expires is true. otherwise just pass expiresAt from input
|
||||
expiresAt: neverExpires ? null : input.expiresAt,
|
||||
hashedKey,
|
||||
},
|
||||
};
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
|
||||
if (isSystemWideAdmin && bodyUserId) {
|
||||
const where: Prisma.UserWhereInput = { id: bodyUserId };
|
||||
await prisma.user.findFirstOrThrow({ where });
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.apiKey.create(args);
|
||||
return {
|
||||
api_key: {
|
||||
...apiKeyPublicSchema.parse(result),
|
||||
key: `${process.env.API_KEY_PREFIX ?? "cal_"}${apiKey}`,
|
||||
},
|
||||
message: "API key created successfully. Save the `key` value as it won't be displayed again.",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
10
calcom/apps/api/v1/pages/api/api-keys/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/api-keys/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware("HTTP_GET_OR_POST")(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const query = schemaQueryIdParseInt.parse(req.query);
|
||||
// @note: Here we make sure to only return attendee's of the user's own bookings if the user is not an admin.
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all user bookings, including attendees
|
||||
const attendee = await prisma.attendee.findFirst({
|
||||
where: { id: query.id, booking: { userId } },
|
||||
});
|
||||
// Flatten and merge all the attendees in one array
|
||||
if (!attendee) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
44
calcom/apps/api/v1/pages/api/attendees/[id]/_delete.ts
Normal file
44
calcom/apps/api/v1/pages/api/attendees/[id]/_delete.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* delete:
|
||||
* operationId: removeAttendeeById
|
||||
* summary: Remove an existing attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to delete
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee removed successfully
|
||||
* 400:
|
||||
* description: Bad request. Attendee id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.attendee.delete({ where: { id } });
|
||||
return { message: `Attendee with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
45
calcom/apps/api/v1/pages/api/attendees/[id]/_get.ts
Normal file
45
calcom/apps/api/v1/pages/api/attendees/[id]/_get.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* get:
|
||||
* operationId: getAttendeeById
|
||||
* summary: Find an attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to get
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Attendee was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const attendee = await prisma.attendee.findUnique({ where: { id } });
|
||||
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
77
calcom/apps/api/v1/pages/api/attendees/[id]/_patch.ts
Normal file
77
calcom/apps/api/v1/pages/api/attendees/[id]/_patch.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees/{id}:
|
||||
* patch:
|
||||
* operationId: editAttendeeById
|
||||
* summary: Edit an existing attendee
|
||||
* requestBody:
|
||||
* description: Edit an existing attendee related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* name:
|
||||
* type: string
|
||||
* timeZone:
|
||||
* type: string
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the attendee to get
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee edited successfully
|
||||
* 400:
|
||||
* description: Bad request. Attendee body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaAttendeeEditBodyParams.parse(body);
|
||||
await checkPermissions(req, data);
|
||||
const attendee = await prisma.attendee.update({ where: { id }, data });
|
||||
return { attendee: schemaAttendeeReadPublic.parse(attendee) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaAttendeeEditBodyParams>) {
|
||||
const { isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const { userId } = req;
|
||||
const { bookingId } = body;
|
||||
if (bookingId) {
|
||||
// Ensure that the booking the attendee is being added to belongs to the user
|
||||
const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId } });
|
||||
if (!booking) throw new HttpError({ statusCode: 403, message: "You don't have access to the booking" });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
18
calcom/apps/api/v1/pages/api/attendees/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/attendees/[id]/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
42
calcom/apps/api/v1/pages/api/attendees/_get.ts
Normal file
42
calcom/apps/api/v1/pages/api/attendees/_get.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees:
|
||||
* get:
|
||||
* operationId: listAttendees
|
||||
* summary: Find all attendees
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No attendees were found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.AttendeeFindManyArgs = isSystemWideAdmin ? {} : { where: { booking: { userId } } };
|
||||
const data = await prisma.attendee.findMany(args);
|
||||
const attendees = data.map((attendee) => schemaAttendeeReadPublic.parse(attendee));
|
||||
if (!attendees) throw new HttpError({ statusCode: 404, message: "No attendees were found" });
|
||||
return { attendees };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
82
calcom/apps/api/v1/pages/api/attendees/_post.ts
Normal file
82
calcom/apps/api/v1/pages/api/attendees/_post.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAttendeeCreateBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /attendees:
|
||||
* post:
|
||||
* operationId: addAttendee
|
||||
* summary: Creates a new attendee
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Create a new attendee related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - bookingId
|
||||
* - name
|
||||
* - email
|
||||
* - timeZone
|
||||
* properties:
|
||||
* bookingId:
|
||||
* type: number
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* name:
|
||||
* type: string
|
||||
* timeZone:
|
||||
* type: string
|
||||
* tags:
|
||||
* - attendees
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, attendee created
|
||||
* 400:
|
||||
* description: Bad request. Attendee body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaAttendeeCreateBodyParams.parse(req.body);
|
||||
|
||||
if (!isSystemWideAdmin) {
|
||||
const userBooking = await prisma.booking.findFirst({
|
||||
where: { userId, id: body.bookingId },
|
||||
select: { id: true },
|
||||
});
|
||||
// Here we make sure to only return attendee's of the user's own bookings.
|
||||
if (!userBooking) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
const data = await prisma.attendee.create({
|
||||
data: {
|
||||
email: body.email,
|
||||
name: body.name,
|
||||
timeZone: body.timeZone,
|
||||
booking: { connect: { id: body.bookingId } },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
attendee: schemaAttendeeReadPublic.parse(data),
|
||||
message: "Attendee created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
10
calcom/apps/api/v1/pages/api/attendees/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/attendees/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
/** Admins can skip the ownership verification */
|
||||
if (isSystemWideAdmin) return;
|
||||
/**
|
||||
* There's a caveat here. If the availability exists but the user doesn't own it,
|
||||
* the user will see a 404 error which may or not be the desired behavior.
|
||||
*/
|
||||
await prisma.availability.findFirstOrThrow({
|
||||
where: { id, Schedule: { userId } },
|
||||
});
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
46
calcom/apps/api/v1/pages/api/availabilities/[id]/_delete.ts
Normal file
46
calcom/apps/api/v1/pages/api/availabilities/[id]/_delete.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* delete:
|
||||
* operationId: removeAvailabilityById
|
||||
* summary: Remove an existing availability
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the availability to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability removed successfully
|
||||
* 400:
|
||||
* description: Bad request. Availability id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.availability.delete({ where: { id } });
|
||||
return { message: `Availability with id: ${id} deleted successfully` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
50
calcom/apps/api/v1/pages/api/availabilities/[id]/_get.ts
Normal file
50
calcom/apps/api/v1/pages/api/availabilities/[id]/_get.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaAvailabilityReadPublic } from "~/lib/validations/availability";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* get:
|
||||
* operationId: getAvailabilityById
|
||||
* summary: Find an availability
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the availability to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid
|
||||
* 404:
|
||||
* description: Availability not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const availability = await prisma.availability.findUnique({
|
||||
where: { id },
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
return { availability: schemaAvailabilityReadPublic.parse(availability) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
87
calcom/apps/api/v1/pages/api/availabilities/[id]/_patch.ts
Normal file
87
calcom/apps/api/v1/pages/api/availabilities/[id]/_patch.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaAvailabilityEditBodyParams,
|
||||
schemaAvailabilityReadPublic,
|
||||
} from "~/lib/validations/availability";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities/{id}:
|
||||
* patch:
|
||||
* operationId: editAvailabilityById
|
||||
* summary: Edit an existing availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* description: Your API key
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: ID of the availability to edit
|
||||
* requestBody:
|
||||
* description: Edit an existing availability related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* days:
|
||||
* type: array
|
||||
* description: Array of integers depicting weekdays
|
||||
* items:
|
||||
* type: integer
|
||||
* enum: [0, 1, 2, 3, 4, 5]
|
||||
* scheduleId:
|
||||
* type: integer
|
||||
* description: ID of schedule this availability is associated with
|
||||
* startTime:
|
||||
* type: string
|
||||
* description: Start time of the availability
|
||||
* endTime:
|
||||
* type: string
|
||||
* description: End time of the availability
|
||||
* examples:
|
||||
* availability:
|
||||
* summary: An example of availability
|
||||
* value:
|
||||
* scheduleId: 123
|
||||
* days: [1,2,3,5]
|
||||
* startTime: 1970-01-01T17:00:00.000Z
|
||||
* endTime: 1970-01-01T17:00:00.000Z
|
||||
*
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability edited successfully
|
||||
* 400:
|
||||
* description: Bad request. Availability body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaAvailabilityEditBodyParams.parse(body);
|
||||
const availability = await prisma.availability.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
return { availability: schemaAvailabilityReadPublic.parse(availability) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
18
calcom/apps/api/v1/pages/api/availabilities/[id]/index.ts
Normal file
18
calcom/apps/api/v1/pages/api/availabilities/[id]/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
99
calcom/apps/api/v1/pages/api/availabilities/_post.ts
Normal file
99
calcom/apps/api/v1/pages/api/availabilities/_post.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaAvailabilityCreateBodyParams,
|
||||
schemaAvailabilityReadPublic,
|
||||
} from "~/lib/validations/availability";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /availabilities:
|
||||
* post:
|
||||
* operationId: addAvailability
|
||||
* summary: Creates a new availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* requestBody:
|
||||
* description: Edit an existing availability related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - scheduleId
|
||||
* - startTime
|
||||
* - endTime
|
||||
* properties:
|
||||
* days:
|
||||
* type: array
|
||||
* description: Array of integers depicting weekdays
|
||||
* items:
|
||||
* type: integer
|
||||
* enum: [0, 1, 2, 3, 4, 5]
|
||||
* scheduleId:
|
||||
* type: integer
|
||||
* description: ID of schedule this availability is associated with
|
||||
* startTime:
|
||||
* type: string
|
||||
* description: Start time of the availability
|
||||
* endTime:
|
||||
* type: string
|
||||
* description: End time of the availability
|
||||
* examples:
|
||||
* availability:
|
||||
* summary: An example of availability
|
||||
* value:
|
||||
* scheduleId: 123
|
||||
* days: [1,2,3,5]
|
||||
* startTime: 1970-01-01T17:00:00.000Z
|
||||
* endTime: 1970-01-01T17:00:00.000Z
|
||||
*
|
||||
*
|
||||
* tags:
|
||||
* - availabilities
|
||||
* externalDocs:
|
||||
* url: https://docs.cal.com/availability
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, availability created
|
||||
* 400:
|
||||
* description: Bad request. Availability body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
await checkPermissions(req);
|
||||
const availability = await prisma.availability.create({
|
||||
data,
|
||||
include: { Schedule: { select: { userId: true } } },
|
||||
});
|
||||
req.statusCode = 201;
|
||||
return {
|
||||
availability: schemaAvailabilityReadPublic.parse(availability),
|
||||
message: "Availability created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
if (isSystemWideAdmin) return;
|
||||
const data = schemaAvailabilityCreateBodyParams.parse(req.body);
|
||||
const schedule = await prisma.schedule.findFirst({
|
||||
where: { userId, id: data.scheduleId },
|
||||
});
|
||||
if (!schedule)
|
||||
throw new HttpError({ statusCode: 401, message: "You can't add availabilities to this schedule" });
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
9
calcom/apps/api/v1/pages/api/availabilities/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/availabilities/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
250
calcom/apps/api/v1/pages/api/availability/_get.ts
Normal file
250
calcom/apps/api/v1/pages/api/availability/_get.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /teams/{teamId}/availability:
|
||||
* get:
|
||||
* summary: Find team availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "1234abcd5678efgh"
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: teamId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: ID of the team to fetch the availability for
|
||||
* - in: query
|
||||
* name: dateFrom
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-14 00:00:00"
|
||||
* description: Start Date of the availability query
|
||||
* - in: query
|
||||
* name: dateTo
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-20 00:00:00"
|
||||
* description: End Date of the availability query
|
||||
* - in: query
|
||||
* name: eventTypeId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: Event Type ID of the event type to fetch the availability for
|
||||
* operationId: team-availability
|
||||
* tags:
|
||||
* - availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* example:
|
||||
* busy:
|
||||
* - start: "2023-05-14T10:00:00.000Z"
|
||||
* end: "2023-05-14T11:00:00.000Z"
|
||||
* title: "Team meeting between Alice and Bob"
|
||||
* - start: "2023-05-15T14:00:00.000Z"
|
||||
* end: "2023-05-15T15:00:00.000Z"
|
||||
* title: "Project review between Carol and Dave"
|
||||
* - start: "2023-05-16T09:00:00.000Z"
|
||||
* end: "2023-05-16T10:00:00.000Z"
|
||||
* - start: "2023-05-17T13:00:00.000Z"
|
||||
* end: "2023-05-17T14:00:00.000Z"
|
||||
* timeZone: "America/New_York"
|
||||
* workingHours:
|
||||
* - days: [1, 2, 3, 4, 5]
|
||||
* startTime: 540
|
||||
* endTime: 1020
|
||||
* userId: 101
|
||||
* dateOverrides:
|
||||
* - date: "2023-05-15"
|
||||
* startTime: 600
|
||||
* endTime: 960
|
||||
* userId: 101
|
||||
* currentSeats: 4
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Team not found | Team has no members
|
||||
*
|
||||
* /availability:
|
||||
* get:
|
||||
* summary: Find user availability
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "1234abcd5678efgh"
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: userId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 101
|
||||
* description: ID of the user to fetch the availability for
|
||||
* - in: query
|
||||
* name: username
|
||||
* schema:
|
||||
* type: string
|
||||
* example: "alice"
|
||||
* description: username of the user to fetch the availability for
|
||||
* - in: query
|
||||
* name: dateFrom
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-14 00:00:00"
|
||||
* description: Start Date of the availability query
|
||||
* - in: query
|
||||
* name: dateTo
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* example: "2023-05-20 00:00:00"
|
||||
* description: End Date of the availability query
|
||||
* - in: query
|
||||
* name: eventTypeId
|
||||
* schema:
|
||||
* type: integer
|
||||
* example: 123
|
||||
* description: Event Type ID of the event type to fetch the availability for
|
||||
* operationId: user-availability
|
||||
* tags:
|
||||
* - availability
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* example:
|
||||
* busy:
|
||||
* - start: "2023-05-14T10:00:00.000Z"
|
||||
* end: "2023-05-14T11:00:00.000Z"
|
||||
* title: "Team meeting between Alice and Bob"
|
||||
* - start: "2023-05-15T14:00:00.000Z"
|
||||
* end: "2023-05-15T15:00:00.000Z"
|
||||
* title: "Project review between Carol and Dave"
|
||||
* - start: "2023-05-16T09:00:00.000Z"
|
||||
* end: "2023-05-16T10:00:00.000Z"
|
||||
* - start: "2023-05-17T13:00:00.000Z"
|
||||
* end: "2023-05-17T14:00:00.000Z"
|
||||
* timeZone: "America/New_York"
|
||||
* workingHours:
|
||||
* - days: [1, 2, 3, 4, 5]
|
||||
* startTime: 540
|
||||
* endTime: 1020
|
||||
* userId: 101
|
||||
* dateOverrides:
|
||||
* - date: "2023-05-15"
|
||||
* startTime: 600
|
||||
* endTime: 960
|
||||
* userId: 101
|
||||
* currentSeats: 4
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: User not found
|
||||
*/
|
||||
interface MemberRoles {
|
||||
[userId: number | string]: MembershipRole;
|
||||
}
|
||||
|
||||
const availabilitySchema = z
|
||||
.object({
|
||||
userId: stringOrNumber.optional(),
|
||||
teamId: stringOrNumber.optional(),
|
||||
username: z.string().optional(),
|
||||
dateFrom: z.string(),
|
||||
dateTo: z.string(),
|
||||
eventTypeId: stringOrNumber.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => !!data.username || !!data.userId || !!data.teamId,
|
||||
"Either username or userId or teamId should be filled in."
|
||||
);
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { isSystemWideAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
userId,
|
||||
returnDateOverrides: true,
|
||||
});
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: { members: true },
|
||||
});
|
||||
if (!team) throw new HttpError({ statusCode: 404, message: "teamId not found" });
|
||||
if (!team.members) throw new HttpError({ statusCode: 404, message: "team has no members" });
|
||||
const allMemberIds = team.members.reduce((allMemberIds: number[], member) => {
|
||||
if (member.accepted) {
|
||||
allMemberIds.push(member.userId);
|
||||
}
|
||||
return allMemberIds;
|
||||
}, []);
|
||||
const members = await prisma.user.findMany({
|
||||
where: { id: { in: allMemberIds } },
|
||||
select: availabilityUserSelect,
|
||||
});
|
||||
const memberRoles: MemberRoles = team.members.reduce((acc: MemberRoles, membership) => {
|
||||
acc[membership.userId] = membership.role;
|
||||
return acc;
|
||||
}, {} as MemberRoles);
|
||||
// check if the user is a team Admin or Owner, if it is a team request, or a system Admin
|
||||
const isUserAdminOrOwner =
|
||||
memberRoles[reqUserId] == MembershipRole.ADMIN ||
|
||||
memberRoles[reqUserId] == MembershipRole.OWNER ||
|
||||
isSystemWideAdmin;
|
||||
if (!isUserAdminOrOwner) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
const availabilities = members.map(async (user) => {
|
||||
return {
|
||||
userId: user.id,
|
||||
availability: await getUserAvailability({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
returnDateOverrides: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
const settled = await Promise.all(availabilities);
|
||||
if (!settled)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "We had an issue retrieving all your members availabilities",
|
||||
});
|
||||
return settled;
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
9
calcom/apps/api/v1/pages/api/availability/index.ts
Normal file
9
calcom/apps/api/v1/pages/api/availability/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
})
|
||||
);
|
@ -0,0 +1,20 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(req.query);
|
||||
// Here we make sure to only return references of the user's own bookings if the user is not an admin.
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId } },
|
||||
});
|
||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
@ -0,0 +1,44 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* delete:
|
||||
* operationId: removeBookingReferenceById
|
||||
* summary: Remove an existing booking reference
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to delete
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, bookingReference removed successfully
|
||||
* 400:
|
||||
* description: Bad request. BookingReference id is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.bookingReference.delete({ where: { id } });
|
||||
return { message: `BookingReference with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
export default defaultResponder(deleteHandler);
|
45
calcom/apps/api/v1/pages/api/booking-references/[id]/_get.ts
Normal file
45
calcom/apps/api/v1/pages/api/booking-references/[id]/_get.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* get:
|
||||
* operationId: getBookingReferenceById
|
||||
* summary: Find a booking reference
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: BookingReference was not found
|
||||
*/
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
@ -0,0 +1,79 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaBookingEditBodyParams,
|
||||
schemaBookingReferenceReadPublic,
|
||||
} from "~/lib/validations/booking-reference";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references/{id}:
|
||||
* patch:
|
||||
* operationId: editBookingReferenceById
|
||||
* summary: Edit an existing booking reference
|
||||
* requestBody:
|
||||
* description: Edit an existing booking reference related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* meetingId:
|
||||
* type: string
|
||||
* meetingPassword:
|
||||
* type: string
|
||||
* externalCalendarId:
|
||||
* type: string
|
||||
* deleted:
|
||||
* type: boolean
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking reference to edit
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, BookingReference edited successfully
|
||||
* 400:
|
||||
* description: Bad request. BookingReference body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body, isSystemWideAdmin, userId } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
/* If user tries to update bookingId, we run extra checks */
|
||||
if (data.bookingId) {
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: data.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
{ where: { id: data.bookingId, userId } };
|
||||
await prisma.booking.findFirstOrThrow(args);
|
||||
}
|
||||
const booking_reference = await prisma.bookingReference.update({ where: { id }, data });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
@ -0,0 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
GET: import("./_get"),
|
||||
PATCH: import("./_patch"),
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
})
|
||||
);
|
41
calcom/apps/api/v1/pages/api/booking-references/_get.ts
Normal file
41
calcom/apps/api/v1/pages/api/booking-references/_get.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-reference";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references:
|
||||
* get:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* operationId: listBookingReferences
|
||||
* summary: Find all booking references
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: No booking references were found
|
||||
*/
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
87
calcom/apps/api/v1/pages/api/booking-references/_post.ts
Normal file
87
calcom/apps/api/v1/pages/api/booking-references/_post.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
schemaBookingCreateBodyParams,
|
||||
schemaBookingReferenceReadPublic,
|
||||
} from "~/lib/validations/booking-reference";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /booking-references:
|
||||
* post:
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* operationId: addBookingReference
|
||||
* summary: Creates a new booking reference
|
||||
* requestBody:
|
||||
* description: Create a new booking reference related to one of your bookings
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - type
|
||||
* - uid
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* uid:
|
||||
* type: string
|
||||
* meetingId:
|
||||
* type: string
|
||||
* meetingPassword:
|
||||
* type: string
|
||||
* meetingUrl:
|
||||
* type: string
|
||||
* bookingId:
|
||||
* type: boolean
|
||||
* externalCalendarId:
|
||||
* type: string
|
||||
* deleted:
|
||||
* type: boolean
|
||||
* credentialId:
|
||||
* type: integer
|
||||
* tags:
|
||||
* - booking-references
|
||||
* responses:
|
||||
* 201:
|
||||
* description: OK, booking reference created
|
||||
* 400:
|
||||
* description: Bad request. BookingReference body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const body = schemaBookingCreateBodyParams.parse(req.body);
|
||||
const args: Prisma.BookingFindFirstOrThrowArgs = isSystemWideAdmin
|
||||
? /* If admin, we only check that the booking exists */
|
||||
{ where: { id: body.bookingId } }
|
||||
: /* For non-admins we make sure the booking belongs to the user */
|
||||
{ where: { id: body.bookingId, userId } };
|
||||
await prisma.booking.findFirstOrThrow(args);
|
||||
|
||||
const data = await prisma.bookingReference.create({
|
||||
data: {
|
||||
...body,
|
||||
bookingId: body.bookingId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
booking_reference: schemaBookingReferenceReadPublic.parse(data),
|
||||
message: "Booking reference created successfully",
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultResponder(postHandler);
|
10
calcom/apps/api/v1/pages/api/booking-references/index.ts
Normal file
10
calcom/apps/api/v1/pages/api/booking-references/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
export default withMiddleware()(
|
||||
defaultHandler({
|
||||
GET: import("./_get"),
|
||||
POST: import("./_post"),
|
||||
})
|
||||
);
|
@ -0,0 +1,69 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin, query } = req;
|
||||
if (isSystemWideAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
if (isOrganizationOwnerOrAdmin) {
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (booking) {
|
||||
const bookingUserId = booking.userId;
|
||||
if (bookingUserId) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [bookingUserId],
|
||||
});
|
||||
if (accessibleUsersIds.length > 0) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
bookings: true,
|
||||
teams: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||
|
||||
if (!userBookingIds.includes(id)) {
|
||||
const teamBookings = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
eventType: {
|
||||
team: {
|
||||
id: {
|
||||
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 403, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
78
calcom/apps/api/v1/pages/api/bookings/[id]/_delete.ts
Normal file
78
calcom/apps/api/v1/pages/api/bookings/[id]/_delete.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}/cancel:
|
||||
* delete:
|
||||
* summary: Booking cancellation
|
||||
* operationId: cancelBookingById
|
||||
*
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to cancel
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: allRemainingBookings
|
||||
* required: false
|
||||
* schema:
|
||||
* type: boolean
|
||||
* description: Delete all remaining bookings
|
||||
* - in: query
|
||||
* name: cancellationReason
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The reason for cancellation of the booking
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK, booking cancelled successfully
|
||||
* 400:
|
||||
* description: |
|
||||
* Bad request
|
||||
* <table>
|
||||
* <tr>
|
||||
* <td>Message</td>
|
||||
* <td>Cause</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Booking not found</td>
|
||||
* <td>The provided id didn't correspond to any existing booking.</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>User not found</td>
|
||||
* <td>The userId did not matched an existing user.</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
* 404:
|
||||
* description: User not found
|
||||
*/
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { id, allRemainingBookings, cancellationReason } = schemaQueryIdParseInt
|
||||
.merge(schemaBookingCancelParams.pick({ allRemainingBookings: true, cancellationReason: true }))
|
||||
.parse({
|
||||
...req.query,
|
||||
allRemainingBookings: req.query.allRemainingBookings === "true",
|
||||
});
|
||||
// Normalizing for universal handler
|
||||
req.body = { id, allRemainingBookings, cancellationReason };
|
||||
return await handleCancelBooking(req);
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
99
calcom/apps/api/v1/pages/api/bookings/[id]/_get.ts
Normal file
99
calcom/apps/api/v1/pages/api/bookings/[id]/_get.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}:
|
||||
* get:
|
||||
* summary: Find a booking
|
||||
* operationId: getBookingById
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to get
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Booking"
|
||||
* examples:
|
||||
* booking:
|
||||
* value:
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 91,
|
||||
* "userId": 5,
|
||||
* "description": "",
|
||||
* "eventTypeId": 7,
|
||||
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||
* "title": "60min between Pro Example and John Doe",
|
||||
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||
* "attendees": [
|
||||
* {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* }
|
||||
* ],
|
||||
* "user": {
|
||||
* "email": "pro@example.com",
|
||||
* "name": "Pro Example",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* },
|
||||
* "payment": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "success": true,
|
||||
* "paymentOption": "ON_BOOKING"
|
||||
* }
|
||||
* ],
|
||||
* "metadata": {},
|
||||
* "status": "ACCEPTED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
* 404:
|
||||
* description: Booking was not found
|
||||
*/
|
||||
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id },
|
||||
include: { attendees: true, user: true, payment: true },
|
||||
});
|
||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
136
calcom/apps/api/v1/pages/api/bookings/[id]/_patch.ts
Normal file
136
calcom/apps/api/v1/pages/api/bookings/[id]/_patch.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking";
|
||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bookings/{id}:
|
||||
* patch:
|
||||
* summary: Edit an existing booking
|
||||
* operationId: editBookingById
|
||||
* requestBody:
|
||||
* description: Edit an existing booking related to one of your event-types
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* description: 'Booking event title'
|
||||
* start:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'Start time of the Event'
|
||||
* end:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'End time of the Event'
|
||||
* status:
|
||||
* type: string
|
||||
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
||||
* description:
|
||||
* type: string
|
||||
* description: 'Description of the meeting'
|
||||
* examples:
|
||||
* editBooking:
|
||||
* value:
|
||||
* {
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "start": "2023-05-24T13:00:00.000Z",
|
||||
* "end": "2023-05-24T13:30:00.000Z",
|
||||
* "status": "CANCELLED"
|
||||
* }
|
||||
*
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: apiKey
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: path
|
||||
* name: id
|
||||
* schema:
|
||||
* type: integer
|
||||
* required: true
|
||||
* description: ID of the booking to edit
|
||||
* tags:
|
||||
* - bookings
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK, booking edited successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* examples:
|
||||
* bookings:
|
||||
* value:
|
||||
* {
|
||||
* "booking": {
|
||||
* "id": 11223344,
|
||||
* "userId": 182,
|
||||
* "description": null,
|
||||
* "eventTypeId": 2323232,
|
||||
* "uid": "stoSJtnh83PEL4rZmqdHe2",
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
||||
* "metadata": {},
|
||||
* "status": "CANCELLED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 400:
|
||||
* description: Bad request. Booking body is invalid.
|
||||
* 401:
|
||||
* description: Authorization information is missing or invalid.
|
||||
*/
|
||||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const data = schemaBookingEditBodyParams.parse(body);
|
||||
await checkPermissions(req, data);
|
||||
const booking = await prisma.booking.update({ where: { id }, data });
|
||||
return { booking: schemaBookingReadPublic.parse(booking) };
|
||||
}
|
||||
|
||||
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
|
||||
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
|
||||
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
|
||||
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
|
||||
if (body.userId && isOrganizationOwnerOrAdmin) {
|
||||
const accessibleUsersIds = await getAccessibleUsers({
|
||||
adminUserId: userId,
|
||||
memberUserIds: [body.userId],
|
||||
});
|
||||
if (accessibleUsersIds.length === 0) {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
message: "Only admin can change the organizer of a booking",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(patchHandler);
|
14
calcom/apps/api/v1/pages/api/bookings/[id]/cancel.ts
Normal file
14
calcom/apps/api/v1/pages/api/bookings/[id]/cancel.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
|
||||
import authMiddleware from "./_auth-middleware";
|
||||
|
||||
export default withMiddleware()(async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await authMiddleware(req);
|
||||
return defaultHandler({
|
||||
DELETE: import("./_delete"),
|
||||
})(req, res);
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user