Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4335c9ab92 | ||
|
|
20df7156cf | ||
|
|
9c35425e57 | ||
|
|
65df5ac0e4 | ||
|
|
84fdd03603 | ||
|
|
e6ec8bdf4d | ||
|
|
86c8197104 | ||
|
|
bb13badcf4 | ||
|
|
64b2397fa2 | ||
|
|
90130457e2 | ||
|
|
260521fb28 | ||
|
|
6390d595f5 | ||
|
|
74dc46f8b2 | ||
|
|
67262910cd | ||
|
|
d59e68de6d | ||
|
|
b50318fbca | ||
|
|
1ce7df324d | ||
|
|
e3b7264208 | ||
|
|
5e3f6e6c2c | ||
|
|
5b6c43118e | ||
|
|
beee545568 | ||
|
|
a597defd03 | ||
|
|
e3562cdfb5 | ||
|
|
86a7824ec9 | ||
|
|
7001515a44 | ||
|
|
36044c4ab3 | ||
|
|
4933295810 | ||
|
|
ed0cd30092 | ||
|
|
1132d9d0dc | ||
|
|
9bd3a362a2 | ||
|
|
844076badb | ||
|
|
c0523a31a7 | ||
|
|
1fff642860 | ||
|
|
89fe383c9f | ||
|
|
4a173c48b8 | ||
|
|
a72a3e45e9 | ||
|
|
a8d8c992a4 | ||
|
|
99f9a121cb | ||
|
|
6a0e2392fe | ||
|
|
addec46feb | ||
|
|
a4fe9a51b5 | ||
|
|
78b8bbb5aa | ||
|
|
d7ece4f583 | ||
|
|
8fc9b9ae9d | ||
|
|
d1175bf7de | ||
|
|
3900453482 | ||
|
|
68e7e76b31 | ||
|
|
986d196a37 | ||
|
|
d30fb213d2 | ||
|
|
3f02de8214 | ||
|
|
7421cea0da | ||
|
|
507daa6a0c | ||
|
|
61f68af435 | ||
|
|
b3f4cfab32 | ||
|
|
c3b1de498d | ||
|
|
cdf059361e | ||
|
|
2aada9a7da | ||
|
|
b96786125c | ||
|
|
9451f38cec | ||
|
|
306f53dec7 | ||
|
|
db865a680f | ||
|
|
aca552c9f7 | ||
|
|
b202c4e79f | ||
|
|
972ce6911a | ||
|
|
751530b460 | ||
|
|
3b263e9282 | ||
|
|
2cbf9e1ac8 | ||
|
|
6283535d6a | ||
|
|
a03ae81509 | ||
|
|
a7e05aa016 | ||
|
|
f8cbf8b1c4 | ||
|
|
ccc294e567 | ||
|
|
0be344b1ba | ||
|
|
7a525e476a | ||
|
|
fe62f9c6de | ||
|
|
9608f6518f | ||
|
|
2f8fa20ec6 | ||
|
|
6f097b0a74 | ||
|
|
159bbb53b6 | ||
|
|
622160ea17 | ||
|
|
5d543c864f | ||
|
|
ed745623eb | ||
|
|
29bca76e91 | ||
|
|
33d0fee43e | ||
|
|
bb13de4176 | ||
|
|
c854dd092c | ||
|
|
26d64bd93f | ||
|
|
1025238eee | ||
|
|
eec76ecfe2 | ||
|
|
f5876ae6d6 | ||
|
|
ccc8cdc1ff | ||
|
|
a1840c9362 | ||
|
|
22212e7edb | ||
|
|
6ffe7005dd | ||
|
|
4890d72e30 | ||
|
|
a531541273 | ||
|
|
2bced59b47 | ||
|
|
e9554604e7 | ||
|
|
48c51da853 | ||
|
|
cc2164657c | ||
|
|
a35683667b | ||
|
|
cf403bebf1 | ||
|
|
f3e4bbc753 | ||
|
|
4bfbac6649 | ||
|
|
3d7fd7eb01 | ||
|
|
ec42900e25 | ||
|
|
032b243248 | ||
|
|
b70b5a5449 | ||
|
|
cc5dfb06bd |
+11
-3
@@ -169,8 +169,7 @@ STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
PAYMENT_FEE_PERCENTAGE=
|
||||
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
@@ -213,6 +212,10 @@ E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Cloudflare Turnstile
|
||||
NEXT_PUBLIC_CLOUDFLARE_SITEKEY=
|
||||
CLOUDFLARE_TURNSTILE_SECRET=
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
@@ -345,4 +348,9 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||
|
||||
# Ratelimiting via unkey
|
||||
UNKEY_ROOT_KEY=
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
|
||||
# Used for Cal.ai Enterprise Voice AI Agents
|
||||
# https://retellai.com
|
||||
RETELL_AI_KEY=
|
||||
|
||||
@@ -4,3 +4,4 @@ node_modules
|
||||
**/**/public
|
||||
packages/prisma/zod
|
||||
apps/web/public/embed
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
@@ -3,7 +3,7 @@ description: "Cache or restore if necessary"
|
||||
inputs:
|
||||
DATABASE_URL:
|
||||
required: false
|
||||
default: "postgresql://postgres:@localhost:5432/calendso"
|
||||
default: "postgresql://postgres:postgres@localhost:5432/calendso"
|
||||
path:
|
||||
required: false
|
||||
default: "backups/backup.sql"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Production Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_EMAIL: ${{ secrets.E2E_TEST_CALCOM_QA_EMAIL }}
|
||||
E2E_TEST_CALCOM_QA_PASSWORD: ${{ secrets.E2E_TEST_CALCOM_QA_PASSWORD }}
|
||||
E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS: ${{ secrets.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }}
|
||||
E2E_TEST_CALCOM_GCAL_KEYS: ${{ secrets.E2E_TEST_CALCOM_GCAL_KEYS }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ vars.CI_NEXT_PUBLIC_IS_E2E }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ vars.CI_NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ vars.CI_NEXT_PUBLIC_WEBSITE_URL }}
|
||||
PAYMENT_FEE_FIXED: ${{ vars.CI_PAYMENT_FEE_FIXED }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ vars.CI_PAYMENT_FEE_PERCENTAGE }}
|
||||
SAML_ADMINS: ${{ secrets.CI_SAML_ADMINS }}
|
||||
SAML_DATABASE_URL: ${{ secrets.CI_SAML_DATABASE_URL }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build API V1
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
strategy:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -10,13 +10,19 @@ jobs:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
mailhog:
|
||||
|
||||
@@ -56,6 +56,13 @@ jobs:
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-api-v1:
|
||||
name: Production build
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/api-v1-production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-without-database:
|
||||
name: Production build (without database)
|
||||
needs: [changes]
|
||||
@@ -99,7 +106,7 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
required:
|
||||
needs: [changes, lint, type-check, test, build, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
needs: [changes, lint, type-check, test, build, build-api-v1, e2e, e2e-embed, e2e-embed-react, e2e-app-store]
|
||||
if: always()
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
|
||||
@@ -30,6 +30,8 @@ env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -34,6 +34,8 @@ env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
NEXT_PUBLIC_API_V2_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_URL }}
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -42,13 +44,19 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.13
|
||||
image: postgres:13
|
||||
credentials:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: calendso
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
|
||||
@@ -17,3 +17,4 @@ packages/prisma/zod
|
||||
packages/prisma/enums
|
||||
apps/web/public/embed
|
||||
apps/api/v2/swagger/documentation.json
|
||||
packages/ui/components/icon/dynamicIconImports.tsx
|
||||
|
||||
+13
-2
@@ -13,8 +13,6 @@ Contributions are what makes the open source community such an amazing place to
|
||||
- Issues from non-core members automatically receive the `🚨 needs approval` label.
|
||||
- We greatly value new feature ideas. To ensure consistency in the product's direction, they undergo review and approval.
|
||||
|
||||
|
||||
|
||||
## Priorities
|
||||
|
||||
<table>
|
||||
@@ -178,11 +176,15 @@ If you get errors, be sure to fix them before committing.
|
||||
Do not commit your `yarn.lock` unless you've made changes to the `package.json`. If you've already committed `yarn.lock` unintentionally, follow these steps to undo:
|
||||
|
||||
If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout HEAD~1 yarn.lock
|
||||
git commit -m "Revert yarn.lock changes"
|
||||
```
|
||||
|
||||
_NB_: You may have to bypass the pre-commit hook with by appending `--no-verify` to the git commit
|
||||
If you've pushed the commit with the `yarn.lock`:
|
||||
|
||||
1. Correct the commit locally using the above method.
|
||||
2. Carefully force push:
|
||||
|
||||
@@ -194,26 +196,35 @@ If `yarn.lock` was committed a while ago and there have been several commits sin
|
||||
|
||||
1. **Checkout a Previous Version**:
|
||||
- Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log:
|
||||
|
||||
```bash
|
||||
git log yarn.lock
|
||||
```
|
||||
|
||||
- Once you have identified the commit hash, use it to checkout the previous version of `yarn.lock`:
|
||||
|
||||
```bash
|
||||
git checkout <commit_hash> yarn.lock
|
||||
```
|
||||
|
||||
2. **Commit the Reverted Version**:
|
||||
- After checking out the previous version of the `yarn.lock`, commit this change:
|
||||
|
||||
```bash
|
||||
git commit -m "Revert yarn.lock to its state before unintended changes"
|
||||
```
|
||||
|
||||
3. **Proceed with Caution**:
|
||||
- If you need to push this change, first pull the latest changes from your remote branch to ensure you're not overwriting other recent changes:
|
||||
|
||||
```bash
|
||||
git pull origin <your-branch-name>
|
||||
```
|
||||
|
||||
- Then push the updated branch:
|
||||
|
||||
```bash
|
||||
git push origin <your-branch-name>
|
||||
```
|
||||
|
||||
Lastly, make sure to keep the branches updated (e.g. click the `Update branch` button on GitHub PR).
|
||||
|
||||
@@ -113,7 +113,7 @@ To get a local copy up and running, please follow these simple steps.
|
||||
Here is what you need to be able to run Cal.com.
|
||||
|
||||
- Node.js (Version: >=18.x)
|
||||
- PostgreSQL
|
||||
- PostgreSQL (Version: >=13.x)
|
||||
- Yarn _(recommended)_
|
||||
|
||||
> If you want to enable any of the available integrations, you may want to obtain additional credentials for each one. More details on this can be found below under the [integrations section](#integrations).
|
||||
@@ -210,7 +210,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||

|
||||
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`. The port is configurable and does not have to be 5432.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ test.describe("Org", () => {
|
||||
];
|
||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Group Meeting");
|
||||
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||
|
||||
@@ -4,3 +4,4 @@ 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
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { API_KEY_RATE_LIMIT } from "@calcom/lib/rateLimit";
|
||||
|
||||
export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
@@ -11,7 +10,7 @@ export const rateLimitApiKey: NextMiddleware = async (req, res, next) => {
|
||||
identifier: req.query.apiKey as string,
|
||||
rateLimitingType: "api",
|
||||
onRateLimiterResponse: (response) => {
|
||||
res.setHeader("X-RateLimit-Limit", API_KEY_RATE_LIMIT);
|
||||
res.setHeader("X-RateLimit-Limit", response.limit);
|
||||
res.setHeader("X-RateLimit-Remaining", response.remaining);
|
||||
res.setHeader("X-RateLimit-Reset", response.reset);
|
||||
},
|
||||
|
||||
@@ -47,6 +47,22 @@ const nextConfig = {
|
||||
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*",
|
||||
|
||||
@@ -93,6 +93,7 @@ async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||
await canAccessTeamEventOrThrow(req, {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (eventType?.userId === req.userId) return true; // is owner.
|
||||
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { purchaseTeamSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -105,10 +105,11 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
};
|
||||
delete data.slug;
|
||||
if (IS_TEAM_BILLING_ENABLED) {
|
||||
const checkoutSession = await purchaseTeamSubscription({
|
||||
const checkoutSession = await purchaseTeamOrOrgSubscription({
|
||||
teamId: _team.id,
|
||||
seats: _team.members.length,
|
||||
userId,
|
||||
pricePerSeat: null,
|
||||
});
|
||||
if (!checkoutSession.url)
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
@@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
|
||||
customer_update: {
|
||||
address: "auto",
|
||||
},
|
||||
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
enabled: IS_PRODUCTION,
|
||||
},
|
||||
metadata: {
|
||||
pendingPaymentTeamId,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"paths": {
|
||||
"~/*": ["*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
@@ -7,7 +7,7 @@ LOG_LEVEL=
|
||||
NEXTAUTH_SECRET=
|
||||
DATABASE_URL=
|
||||
JWT_SECRET=
|
||||
SENTRY_DNS=
|
||||
SENTRY_DSN=
|
||||
|
||||
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
@@ -7,7 +7,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@nestjs/swagger",
|
||||
"options": { "dtoFileNameSuffix": ".input.ts", "classValidatorShim": true }
|
||||
"options": { "dtoFileNameSuffix": [".input.ts", ".output.ts", ".dto.ts"], "classValidatorShim": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export class AppLoggerMiddleware implements NestMiddleware {
|
||||
private logger = new Logger("HTTP");
|
||||
|
||||
use(request: Request, response: Response, next: NextFunction): void {
|
||||
const { ip, method, protocol, originalUrl, path: url } = request;
|
||||
const { ip, method, originalUrl } = request;
|
||||
const userAgent = request.get("user-agent") || "";
|
||||
|
||||
response.on("close", () => {
|
||||
|
||||
@@ -44,9 +44,9 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
})
|
||||
);
|
||||
|
||||
if (process.env.SENTRY_DNS) {
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: getEnv("SENTRY_DNS"),
|
||||
dsn: getEnv("SENTRY_DSN"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ const loadConfig = (): AppConfig => {
|
||||
port: Number(getEnv("API_PORT", "5555")),
|
||||
path: getEnv("API_URL", "http://localhost"),
|
||||
url: `${getEnv("API_URL", "http://localhost")}${
|
||||
process.env.API_PORT ? `:${Number(getEnv("API_PORT", "5555"))}` : ""
|
||||
process.env.API_PORT && getEnv("NODE_ENV", "development") === "development"
|
||||
? `:${Number(getEnv("API_PORT", "5555"))}`
|
||||
: ""
|
||||
}/api/v2`,
|
||||
},
|
||||
db: {
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
handleNewBooking,
|
||||
getBookingInfo,
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
describe("User Authenticated", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {};
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings")
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
||||
response.body;
|
||||
const fetchedBooking = responseBody.data.bookings[0];
|
||||
|
||||
expect(responseBody.data.bookings.length).toEqual(1);
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(fetchedBooking).toBeDefined();
|
||||
|
||||
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
||||
const { bookingInfo } = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(bookingInfo?.id).toBeDefined();
|
||||
expect(bookingInfo?.uid).toBeDefined();
|
||||
expect(bookingInfo?.id).toEqual(createdBooking.id);
|
||||
expect(bookingInfo?.uid).toEqual(createdBooking.uid);
|
||||
expect(bookingInfo?.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(bookingInfo?.startTime).toEqual(createdBooking.startTime);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a recurring booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingRecurringCount = 5;
|
||||
const currentBookingRecurringIndex = 0;
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
recurringCount: bookingRecurringCount,
|
||||
currentRecurringIndex: currentBookingRecurringIndex,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/reccuring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||
response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("recurring");
|
||||
});
|
||||
});
|
||||
|
||||
it("should create an instant booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/instant")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("instant");
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel a booking", async () => {
|
||||
const bookingId = createdBooking.id;
|
||||
|
||||
const body = {
|
||||
allRemainingBookings: false,
|
||||
cancellationReason: "Was fighting some unforseen rescheduling demons",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> =
|
||||
response.body;
|
||||
|
||||
expect(bookingId).toBeDefined();
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -11,12 +14,23 @@ import {
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
handleNewBooking,
|
||||
BookingResponse,
|
||||
@@ -24,7 +38,9 @@ import {
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
@@ -34,7 +50,65 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
export class BookingsController {
|
||||
private readonly logger = new Logger("ee bookings controller");
|
||||
|
||||
constructor(private readonly oAuthFlowService: OAuthFlowService) {}
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
) {}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getBookings(
|
||||
@GetUser() user: User,
|
||||
@Query() queryParams: GetBookingsInput
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: filters.status,
|
||||
skip: cursor ?? 0,
|
||||
take: limit ?? 10,
|
||||
filters,
|
||||
ctx: {
|
||||
user: { email: user.email, id: user.id },
|
||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookings,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid")
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||
|
||||
if (!bookingInfo) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookingInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid/reschedule")
|
||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@@ -56,6 +130,30 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
): Promise<ApiResponse> {
|
||||
if (bookingId) {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException("Booking ID is required.");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createReccuringBooking(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { IsBoolean, IsNumber, IsString, IsOptional, IsArray } from "class-validator";
|
||||
import { IsBoolean, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, Get, Logger, UseGuards, Query } from "@nestjs/common";
|
||||
import { Controller, Get, UseGuards, Query } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ConnectedDestinationCalendars } from "@calcom/platform-libraries";
|
||||
@@ -16,8 +16,6 @@ import { EventBusyDate } from "@calcom/types/Calendar";
|
||||
})
|
||||
@UseGuards(AccessTokenGuard)
|
||||
export class CalendarsController {
|
||||
private readonly logger = new Logger("ee overlay calendars controller");
|
||||
|
||||
constructor(private readonly calendarsService: CalendarsService) {}
|
||||
|
||||
@Get("/busy-times")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
@@ -61,7 +63,7 @@ describe("Event types Endpoints", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const name = "bob";
|
||||
const name = "bob the builder";
|
||||
const username = name;
|
||||
let eventType: EventType;
|
||||
let user: User;
|
||||
@@ -86,6 +88,7 @@ describe("Event types Endpoints", () => {
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
@@ -95,17 +98,6 @@ describe("Event types Endpoints", () => {
|
||||
username,
|
||||
});
|
||||
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
length: 60,
|
||||
title: "peer coding session",
|
||||
slug: "peer-coding",
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -129,6 +121,50 @@ describe("Event types Endpoints", () => {
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const body: CreateEventTypeInput = {
|
||||
title: "Test Event Type",
|
||||
slug: "test-event-type",
|
||||
description: "A description of the test event type.",
|
||||
length: 60,
|
||||
hidden: false,
|
||||
locations: [
|
||||
{
|
||||
type: "Online",
|
||||
link: "https://example.com/meet",
|
||||
displayLocationPublicly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||
expect(responseBody.data).toHaveProperty("id");
|
||||
expect(responseBody.data.title).toEqual(body.title);
|
||||
eventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const newTitle = "Updated title";
|
||||
|
||||
const body: UpdateEventTypeInput = {
|
||||
title: newTitle,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async () => {
|
||||
eventType.title = newTitle;
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
@@ -154,10 +190,8 @@ describe("Event types Endpoints", () => {
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypesByViewer> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
console.log("asap responseBody.data", responseBody.data);
|
||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||
expect(responseBody.data.eventTypeGroups[0]).toBeDefined();
|
||||
@@ -178,7 +212,6 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
console.log("asap responseBody.data", responseBody.data);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(1);
|
||||
expect(responseBody.data[0].id).toEqual(eventType.id);
|
||||
@@ -192,10 +225,18 @@ describe("Event types Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should delete schedule", async () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||
import { DeleteEventTypeOutput } from "@/ee/event-types/outputs/delete-event-type.output";
|
||||
import { GetEventTypeOutput } from "@/ee/event-types/outputs/get-event-type.output";
|
||||
import { GetEventTypesPublicOutput } from "@/ee/event-types/outputs/get-event-types-public.output";
|
||||
import { GetEventTypesOutput } from "@/ee/event-types/outputs/get-event-types.output";
|
||||
import { UpdateEventTypeOutput } from "@/ee/event-types/outputs/update-event-type.output";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Param, Post, Body, NotFoundException } from "@nestjs/common";
|
||||
import { EventType } from "@prisma/client";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Body,
|
||||
NotFoundException,
|
||||
Patch,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EventTypesByViewer } from "@calcom/lib";
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { EventType as AtomEventType, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "event-types",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Event types")
|
||||
export class EventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||
|
||||
@@ -29,8 +45,8 @@ export class EventTypesController {
|
||||
async createEventType(
|
||||
@Body() body: CreateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<EventType>> {
|
||||
const eventType = await this.eventTypesService.createUserEventType(user.id, body);
|
||||
): Promise<CreateEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -43,12 +59,9 @@ export class EventTypesController {
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventType(
|
||||
@Param("eventTypeId") eventTypeId: string,
|
||||
@ForAtom() forAtom: boolean,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiSuccessResponse<EventType | AtomEventType>> {
|
||||
const eventType = forAtom
|
||||
? await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId))
|
||||
: await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
|
||||
): Promise<GetEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.getUserEventTypeForAtom(user, Number(eventTypeId));
|
||||
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
@@ -63,7 +76,7 @@ export class EventTypesController {
|
||||
@Get("/")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<ApiSuccessResponse<EventTypesByViewer>> {
|
||||
async getEventTypes(@GetUser() user: UserWithProfile): Promise<GetEventTypesOutput> {
|
||||
const eventTypes = await getEventTypesByViewer({
|
||||
id: user.id,
|
||||
profile: {
|
||||
@@ -79,9 +92,7 @@ export class EventTypesController {
|
||||
|
||||
@Get("/:username/public")
|
||||
@Permissions([EVENT_TYPE_READ])
|
||||
async getPublicEventTypes(
|
||||
@Param("username") username: string
|
||||
): Promise<ApiSuccessResponse<EventTypesPublic>> {
|
||||
async getPublicEventTypes(@Param("username") username: string): Promise<GetEventTypesPublicOutput> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypesPublicByUsername(username);
|
||||
|
||||
return {
|
||||
@@ -89,4 +100,41 @@ export class EventTypesController {
|
||||
data: eventTypes,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<UpdateEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async deleteEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@GetUser("id") userId: number
|
||||
): Promise<DeleteEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
id: eventType.id,
|
||||
length: eventType.length,
|
||||
slug: eventType.slug,
|
||||
title: eventType.title,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule],
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
||||
providers: [EventTypesRepository, EventTypesService],
|
||||
controllers: [EventTypesController],
|
||||
exports: [EventTypesService, EventTypesRepository],
|
||||
|
||||
@@ -10,7 +10,10 @@ import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
export class EventTypesRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
||||
async createUserEventType(
|
||||
userId: number,
|
||||
body: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">
|
||||
) {
|
||||
return this.dbWrite.prisma.eventType.create({
|
||||
data: {
|
||||
...body,
|
||||
@@ -59,4 +62,19 @@ export class EventTypesRepository {
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getUserEventTypeBySlug(userId: number, slug: string) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: {
|
||||
userId_slug: {
|
||||
userId: userId,
|
||||
slug: slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,53 @@
|
||||
import { IsNumber, IsString, Min } from "class-validator";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, ValidateNested, Min, IsArray } from "class-validator";
|
||||
|
||||
export const CREATE_EVENT_LENGTH_EXAMPLE = 60;
|
||||
export const CREATE_EVENT_SLUG_EXAMPLE = "cooking-class";
|
||||
export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!";
|
||||
export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
|
||||
"Discover the culinary wonders of the Argentina by making the best flan ever!";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested.
|
||||
export class CreateEventTypeInput {
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsArray()
|
||||
locations?: EventTypeLocation[];
|
||||
|
||||
// @ApiHideProperty()
|
||||
// @IsOptional()
|
||||
// @IsNumber()
|
||||
// teamId?: number;
|
||||
|
||||
// @ApiHideProperty()
|
||||
// @IsOptional()
|
||||
// @IsEnum(SchedulingType)
|
||||
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum Editable {
|
||||
system = "system",
|
||||
systemButOptional = "system-but-optional",
|
||||
systemButHidden = "system-but-hidden",
|
||||
user = "user",
|
||||
userReadonly = "user-readonly",
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export enum BaseField {
|
||||
number = "number",
|
||||
boolean = "boolean",
|
||||
address = "address",
|
||||
name = "name",
|
||||
text = "text",
|
||||
textarea = "textarea",
|
||||
email = "email",
|
||||
phone = "phone",
|
||||
multiemail = "multiemail",
|
||||
select = "select",
|
||||
multiselect = "multiselect",
|
||||
checkbox = "checkbox",
|
||||
radio = "radio",
|
||||
radioInput = "radioInput",
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
SECONDLY = 6,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum PeriodType {
|
||||
UNLIMITED = "UNLIMITED",
|
||||
ROLLING = "ROLLING",
|
||||
RANGE = "RANGE",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum SchedulingType {
|
||||
ROUND_ROBIN = "ROUND_ROBIN",
|
||||
COLLECTIVE = "COLLECTIVE",
|
||||
MANAGED = "MANAGED",
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested.
|
||||
|
||||
export class EventTypeLocation {
|
||||
@IsString()
|
||||
@DocsProperty({ example: "link" })
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
link?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
displayLocationPublicly?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
hostPhoneNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiHideProperty()
|
||||
credentialId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
teamName?: string;
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
||||
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
||||
import { Frequency } from "@/ee/event-types/inputs/enums/frequency";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
Min,
|
||||
IsInt,
|
||||
IsEnum,
|
||||
IsArray,
|
||||
IsDate,
|
||||
IsNumber,
|
||||
} from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested. Go to bottom of file to see UpdateEventTypeInput.
|
||||
|
||||
class Option {
|
||||
@IsString()
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
editUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
fieldRequired?: boolean;
|
||||
}
|
||||
|
||||
class View {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class OptionsInput {
|
||||
@IsString()
|
||||
type!: "address" | "text" | "phone";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class VariantField {
|
||||
@IsString()
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
class Variant {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => VariantField)
|
||||
fields!: VariantField[];
|
||||
}
|
||||
|
||||
class VariantsConfig {
|
||||
variants!: Record<string, Variant>;
|
||||
}
|
||||
|
||||
export class BookingField {
|
||||
@IsEnum(BaseField)
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Option)
|
||||
options?: Option[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsInput)
|
||||
optionsInputs?: Record<string, OptionsInput>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
variant?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VariantsConfig)
|
||||
variantsConfig?: VariantsConfig;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => View)
|
||||
views?: View[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Editable)
|
||||
editable?: Editable;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
sources?: Source[];
|
||||
}
|
||||
|
||||
export class RecurringEvent {
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
dtstart?: Date;
|
||||
|
||||
@IsInt()
|
||||
interval!: number;
|
||||
|
||||
@IsInt()
|
||||
count!: number;
|
||||
|
||||
@IsEnum(Frequency)
|
||||
freq!: Frequency;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
until?: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
tzid?: string;
|
||||
}
|
||||
|
||||
export class IntervalLimits {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_DAY?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_WEEK?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_MONTH?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_YEAR?: number;
|
||||
}
|
||||
|
||||
export class UpdateEventTypeInput {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
length?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsOptional()
|
||||
locations?: EventTypeLocation[];
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// position?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// offsetStart?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// userId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// profileId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// teamId?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// eventName?: string;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// parentId?: number;
|
||||
|
||||
// @IsOptional()
|
||||
// @IsArray()
|
||||
// @ValidateNested({ each: true })
|
||||
// @Type(() => BookingField)
|
||||
// bookingFields?: BookingField[];
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// timeZone?: string;
|
||||
|
||||
// @IsEnum(PeriodType)
|
||||
// @IsOptional()
|
||||
// periodType?: PeriodType; -> import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||
|
||||
// @IsDate()
|
||||
// @IsOptional()
|
||||
// periodStartDate?: Date;
|
||||
|
||||
// @IsDate()
|
||||
// @IsOptional()
|
||||
// periodEndDate?: Date;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// periodDays?: number;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// periodCountCalendarDays?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// lockTimeZoneToggleOnBookingPage?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// requiresConfirmation?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// requiresBookerEmailVerification?: boolean;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => RecurringEvent)
|
||||
// @IsOptional()
|
||||
// recurringEvent?: RecurringEvent;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// disableGuests?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// hideCalendarNotes?: boolean;
|
||||
|
||||
// @IsInt()
|
||||
// @Min(0)
|
||||
// @IsOptional()
|
||||
// minimumBookingNotice?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// beforeEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// afterEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// seatsPerTimeSlot?: number;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// onlyShowFirstAvailableSlot?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// seatsShowAttendees?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// seatsShowAvailabilityCount?: boolean;
|
||||
|
||||
// @IsEnum(SchedulingType)
|
||||
// @IsOptional()
|
||||
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// scheduleId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// price?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// currency?: string;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// slotInterval?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// @IsUrl()
|
||||
// successRedirectUrl?: string;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => IntervalLimits)
|
||||
// @IsOptional()
|
||||
// bookingLimits?: IntervalLimits;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => IntervalLimits)
|
||||
// @IsOptional()
|
||||
// durationLimits?: IntervalLimits;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// isInstantEvent?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// assignAllTeamMembers?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// useEventTypeDestinationCalendarEmail?: boolean;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// secondaryEmailId?: number;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CreateEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
data!: EventTypeOutput;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsInt, IsString } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class DeleteData {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
}
|
||||
|
||||
export class DeleteEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Type(() => DeleteData)
|
||||
data!: DeleteData;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||
import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import {
|
||||
BookingField,
|
||||
IntervalLimits,
|
||||
RecurringEvent,
|
||||
} from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsJSON,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
export class EventTypeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
description!: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hidden!: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsArray()
|
||||
locations!: EventTypeLocation | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
position!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
offsetStart!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
userId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
profileId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
teamId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
eventName!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
parentId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
@ApiHideProperty()
|
||||
bookingFields!: BookingField[] | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
timeZone!: string | null;
|
||||
|
||||
@IsEnum(PeriodType)
|
||||
@ApiHideProperty()
|
||||
periodType!: PeriodType | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiHideProperty()
|
||||
periodStartDate!: Date | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiHideProperty()
|
||||
periodEndDate!: Date | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
periodDays!: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
periodCountCalendarDays!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => RecurringEvent)
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
recurringEvent!: RecurringEvent | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hideCalendarNotes!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
minimumBookingNotice!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
beforeEventBuffer!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
afterEventBuffer!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
seatsPerTimeSlot!: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
onlyShowFirstAvailableSlot!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
seatsShowAttendees!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
seatsShowAvailabilityCount!: boolean;
|
||||
|
||||
@IsEnum(SchedulingType)
|
||||
@ApiHideProperty()
|
||||
schedulingType!: SchedulingType | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
scheduleId!: number | null;
|
||||
|
||||
@IsNumber()
|
||||
@ApiHideProperty()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
slotInterval!: number | null;
|
||||
|
||||
@IsJSON()
|
||||
@ApiHideProperty()
|
||||
metadata!: Record<string, any> | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
successRedirectUrl!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => IntervalLimits)
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
bookingLimits!: IntervalLimits;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => IntervalLimits)
|
||||
@ApiHideProperty()
|
||||
durationLimits!: IntervalLimits;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
useEventTypeDestinationCalendarEmail!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
secondaryEmailId!: number | null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Data {
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
eventType!: EventTypeOutput;
|
||||
}
|
||||
|
||||
export class GetEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: Data,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Data)
|
||||
data!: Data;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class PublicEventType {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export class GetEventTypesPublicOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PublicEventType)
|
||||
@IsArray()
|
||||
data!: PublicEventType[];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class EventTypeGroup {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeOutput)
|
||||
@IsArray()
|
||||
eventTypes!: EventTypeOutput[];
|
||||
}
|
||||
|
||||
class GetEventTypesData {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeGroup)
|
||||
@IsArray()
|
||||
eventTypeGroups!: EventTypeGroup[];
|
||||
}
|
||||
|
||||
export class GetEventTypesOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => GetEventTypesData)
|
||||
@IsArray()
|
||||
data!: GetEventTypesData;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UpdateEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
data!: EventTypeOutput;
|
||||
}
|
||||
@@ -1,26 +1,73 @@
|
||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesService {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository,
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
||||
return this.eventTypesRepository.createUserEventType(userId, body);
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput) {
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { eventType } = await createEventType({
|
||||
input: body,
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
|
||||
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||
if (existsWithSlug) {
|
||||
throw new BadRequestException("User already has an event type with this slug.");
|
||||
}
|
||||
}
|
||||
|
||||
async getUserToCreateEvent(user: UserWithProfile) {
|
||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||
const isOrgAdmin = organizationId
|
||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||
: false;
|
||||
const profileId = user.movedToProfile?.id || null;
|
||||
return {
|
||||
id: user.id,
|
||||
organizationId: user.organizationId,
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserEventType(userId: number, eventTypeId: number) {
|
||||
return this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(userId, eventType);
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||
@@ -30,7 +77,18 @@ export class EventTypesService {
|
||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||
: false;
|
||||
|
||||
return this.eventTypesRepository.getUserEventTypeForAtom(user, isUserOrganizationAdmin, eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
|
||||
user,
|
||||
isUserOrganizationAdmin,
|
||||
eventTypeId
|
||||
);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||
@@ -43,11 +101,7 @@ export class EventTypesService {
|
||||
}
|
||||
|
||||
async createUserDefaultEventTypes(userId: number) {
|
||||
const thirtyMinutes = DEFAULT_EVENT_TYPES.thirtyMinutes;
|
||||
const thirtyMinutesVideo = DEFAULT_EVENT_TYPES.thirtyMinutesVideo;
|
||||
|
||||
const sixtyMinutes = DEFAULT_EVENT_TYPES.sixtyMinutes;
|
||||
const sixtyMinutesVideo = DEFAULT_EVENT_TYPES.sixtyMinutesVideo;
|
||||
const { sixtyMinutes, sixtyMinutesVideo, thirtyMinutes, thirtyMinutesVideo } = DEFAULT_EVENT_TYPES;
|
||||
|
||||
const defaultEventTypes = await Promise.all([
|
||||
this.eventTypesRepository.createUserEventType(userId, thirtyMinutes),
|
||||
@@ -58,4 +112,53 @@ export class EventTypesService {
|
||||
|
||||
return defaultEventTypes;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
await updateEventType({
|
||||
input: { id: eventTypeId, ...body },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
|
||||
const { eventType } = await this.getUserEventTypeForAtom(user, eventTypeId);
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
|
||||
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
this.checkUserOwnsEventType(userId, existingEventType);
|
||||
}
|
||||
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = user.movedToProfile?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(userId, existingEventType);
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
checkUserOwnsEventType(userId: number, eventType: Pick<EventType, "id" | "userId">) {
|
||||
if (userId !== eventType.userId) {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
let gcalCredentials: Credential;
|
||||
let accessTokenSecret: string;
|
||||
let refreshTokenSecret: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
@@ -82,16 +83,16 @@ describe("Platform Gcal Endpoints", () => {
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should respond 401 with invalid access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/oauth/auth-url`)
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer invalid_access_token`)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
it(`/GET/ee/gcal/oauth/auth-url: it should auth-url to google oauth with valid access token `, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/oauth/auth-url`)
|
||||
.get(`/api/v2/ee/gcal/oauth/auth-url`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
@@ -99,50 +100,50 @@ describe("Platform Gcal Endpoints", () => {
|
||||
expect(data.authUrl).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without oauth code`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without oauth code`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&origin%3D${CLIENT_REDIRECT_URI}&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without access token`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without access token`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/api/v2/ee/gcal/oauth/save?state=origin%3D${CLIENT_REDIRECT_URI}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/oauth/save: without origin`, async () => {
|
||||
it(`/GET/ee/gcal/oauth/save: without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/platform/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
`/api/v2/ee/gcal/oauth/save?state=accessToken=${accessTokenSecret}&code=4/0AfJohXmBuT7QVrEPlAJLBu4ZcSnyj5jtDoJqSW_riPUhPXQ70RPGkOEbVO3xs-OzQwpPQw&scope=https://www.googleapis.com/auth/calendar.readonly%20https://www.googleapis.com/auth/calendar.events`
|
||||
)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token but without origin`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token but without origin`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/platform/gcal/check`).expect(401);
|
||||
it(`/GET/ee/gcal/check without access token`, async () => {
|
||||
await request(app.getHttpServer()).get(`/api/v2/ee/gcal/check`).expect(401);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token and origin but no credentials`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token and origin but no credentials`, async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/GET/platform/gcal/check with access token and origin and gcal credentials`, async () => {
|
||||
it(`/GET/ee/gcal/check with access token and origin and gcal credentials`, async () => {
|
||||
gcalCredentials = await credentialsRepositoryFixture.create(
|
||||
"google_calendar",
|
||||
{},
|
||||
@@ -150,7 +151,7 @@ describe("Platform Gcal Endpoints", () => {
|
||||
"google-calendar"
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.get(`/api/v2/platform/gcal/check`)
|
||||
.get(`/api/v2/ee/gcal/check`)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.set("Origin", CLIENT_REDIRECT_URI)
|
||||
.expect(200);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
@@ -40,22 +39,21 @@ const CALENDAR_SCOPES = [
|
||||
];
|
||||
|
||||
@Controller({
|
||||
path: "platform/gcal",
|
||||
path: "ee/gcal",
|
||||
version: "2",
|
||||
})
|
||||
export class GcalController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
constructor(
|
||||
private readonly appRepository: AppsRepository,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly config: ConfigService,
|
||||
private readonly gcalService: GcalService
|
||||
private readonly gcalService: GCalService
|
||||
) {}
|
||||
|
||||
private redirectUri = `${this.config.get("api.url")}/platform/gcal/oauth/save`;
|
||||
private redirectUri = `${this.config.get("api.url")}/ee/gcal/oauth/save`;
|
||||
|
||||
@Get("/oauth/auth-url")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GcalController } from "@/ee/gcal/gcal.controller";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { GcalService } from "@/modules/apps/services/gcal.service";
|
||||
import { GCalService } from "@/modules/apps/services/gcal.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -11,7 +11,7 @@ import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TokensModule, OAuthClientModule],
|
||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GcalService],
|
||||
providers: [AppsRepository, ConfigService, CredentialsRepository, SelectedCalendarsRepository, GCalService],
|
||||
controllers: [GcalController],
|
||||
})
|
||||
export class GcalModule {}
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("Me Endpoints", () => {
|
||||
|
||||
it("should get user associated with access token", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/me")
|
||||
.get("/api/v2/ee/me")
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<UserResponse> = response.body;
|
||||
@@ -84,7 +84,7 @@ describe("Me Endpoints", () => {
|
||||
const body: UpdateManagedPlatformUserInput = { timeZone: "Europe/Rome" };
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/me")
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -108,19 +108,22 @@ describe("Me Endpoints", () => {
|
||||
it("should not update user associated with access token given invalid timezone", async () => {
|
||||
const bodyWithIncorrectTimeZone: UpdateManagedPlatformUserInput = { timeZone: "Narnia/Woods" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectTimeZone).expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid time format", async () => {
|
||||
const bodyWithIncorrectTimeFormat: UpdateManagedPlatformUserInput = { timeFormat: 100 };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectTimeFormat).expect(400);
|
||||
return request(app.getHttpServer())
|
||||
.patch("/api/v2/ee/me")
|
||||
.send(bodyWithIncorrectTimeFormat)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should not update user associated with access token given invalid week start", async () => {
|
||||
const bodyWithIncorrectWeekStart: UpdateManagedPlatformUserInput = { weekStart: "waba luba dub dub" };
|
||||
|
||||
return request(app.getHttpServer()).patch("/api/v2/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
return request(app.getHttpServer()).patch("/api/v2/ee/me").send(bodyWithIncorrectWeekStart).expect(400);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UserResponse, userSchemaResponse } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "me",
|
||||
path: "ee/me",
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(AccessTokenGuard, PermissionsGuard)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
BadRequestException,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Param,
|
||||
UnauthorizedException,
|
||||
@@ -20,16 +18,11 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "platform/provider",
|
||||
path: "ee/provider",
|
||||
version: "2",
|
||||
})
|
||||
export class CalProviderController {
|
||||
private readonly logger = new Logger("Platform Provider Controller");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly oauthClientRepository: OAuthClientRepository
|
||||
) {}
|
||||
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
|
||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ScheduleWithAvailabilitiesForWeb } from "@calcom/platform-libraries";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { updateScheduleHandler } from "@calcom/platform-libraries";
|
||||
import type { UpdateScheduleOutputType } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ScheduleResponse, UpdateScheduleInput } from "@calcom/platform-types";
|
||||
@@ -73,16 +72,6 @@ export class SchedulesController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/time-zones")
|
||||
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
||||
const timeZones = await this.schedulesService.getSchedulePossibleTimeZones();
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: timeZones,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:scheduleId")
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getSchedule(
|
||||
|
||||
@@ -6,8 +6,6 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Schedule } from "@prisma/client";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class SchedulesService {
|
||||
constructor(
|
||||
@@ -96,8 +94,4 @@ export class SchedulesService {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getSchedulePossibleTimeZones() {
|
||||
return cityTimezonesHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type Environment = {
|
||||
NEXTAUTH_SECRET: string;
|
||||
DATABASE_URL: string;
|
||||
JWT_SECRET: string;
|
||||
SENTRY_DNS: string;
|
||||
SENTRY_DSN: string;
|
||||
LOG_LEVEL: keyof typeof logLevels;
|
||||
REDIS_URL: string;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -10,10 +11,14 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const statusCode = exception.getStatus();
|
||||
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PrismaClientUnknownRequestError,
|
||||
PrismaClientValidationError,
|
||||
} from "@prisma/client/runtime/library";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -31,9 +32,13 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
catch(error: PrismaError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
this.logger.error(`PrismaError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
status: ERROR_STATUS,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArgumentsHost, Catch, Logger, HttpStatus } from "@nestjs/common";
|
||||
import { BaseExceptionFilter } from "@nestjs/core";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
@@ -12,10 +13,14 @@ export class SentryFilter extends BaseExceptionFilter {
|
||||
handleUnknownError(exception: any, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
this.logger.error(`Sentry Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
// capture if client has been init
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { TRPCError } from "@calcom/platform-libraries";
|
||||
@@ -11,7 +12,8 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: TRPCError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let statusCode = 500;
|
||||
switch (exception.code) {
|
||||
case "UNAUTHORIZED":
|
||||
@@ -38,9 +40,15 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
statusCode = 500;
|
||||
break;
|
||||
}
|
||||
|
||||
this.logger.error(`TRPC Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ArgumentsHost, ExceptionFilter } from "@nestjs/common";
|
||||
import { Catch, HttpStatus, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { BAD_REQUEST, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
@@ -12,10 +13,16 @@ export class ZodExceptionFilter implements ExceptionFilter {
|
||||
catch(error: ZodError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
this.logger.error(`ZodError: ${error.message}`, {
|
||||
error,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
});
|
||||
|
||||
response.status(HttpStatus.BAD_REQUEST).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
|
||||
export const ForAtom = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
||||
export const ForAtom = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.query.for === "atom";
|
||||
});
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
|
||||
export class BaseStrategy {
|
||||
success!: (user: unknown) => void;
|
||||
error!: (error: Error) => void;
|
||||
}
|
||||
|
||||
export class NextAuthPassportStrategy {
|
||||
success!: (user: UserWithProfile) => void;
|
||||
error!: (error: Error) => void;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import { google } from "googleapis";
|
||||
import { z } from "zod";
|
||||
|
||||
@Injectable()
|
||||
export class GcalService {
|
||||
export class GCalService {
|
||||
private logger = new Logger("GcalService");
|
||||
|
||||
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||
|
||||
constructor(private readonly appsRepository: AppsRepository) {}
|
||||
|
||||
async getOAuthClient(redirectUri: string) {
|
||||
@@ -17,9 +19,7 @@ export class GcalService {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
const { client_id, client_secret } = z
|
||||
.object({ client_id: z.string(), client_secret: z.string() })
|
||||
.parse(app.keys);
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async isPlatform(organizationId: number) {
|
||||
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
||||
if (!isPlatform) {
|
||||
throw new ForbiddenException("Organization is not a platform.");
|
||||
throw new ForbiddenException("Organization is not a platform (SHP).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -7,7 +7,7 @@ import type { Request } from "express";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
@Injectable()
|
||||
export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
||||
export class NextAuthStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||
constructor(private readonly userRepository: UsersRepository, private readonly config: ConfigService) {
|
||||
super();
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class NextAuthStrategy extends PassportStrategy(BaseStrategy, "next-auth"
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findByEmail(payload.email);
|
||||
const user = await this.userRepository.findByEmailWithProfile(payload.email);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { EventsModule } from "@/modules/events/events.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule],
|
||||
imports: [EventsModule, OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -22,7 +22,10 @@ export class EventsController {
|
||||
queryParams.eventSlug,
|
||||
queryParams.isTeamEvent,
|
||||
queryParams.org || null,
|
||||
this.prismaReadService.prisma as unknown as PrismaClient
|
||||
this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
// We should be fine allowing unpublished orgs events to be servable through platform because Platform access is behind license
|
||||
// If there is ever a need to restrict this, we can introduce a new query param `fromRedirectOfNonOrgLink`
|
||||
true
|
||||
);
|
||||
return {
|
||||
data: event,
|
||||
|
||||
+15
@@ -179,6 +179,21 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
it(`/GET: return list of managed users`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users?limit=10&offset=0`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.set("Origin", `${CLIENT_REDIRECT_URI}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<UserReturned[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toBeGreaterThan(0);
|
||||
expect(responseBody.data[0].email).toEqual(postResponseData.user.email);
|
||||
});
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/oauth-clients/${oAuthClient.id}/users/${postResponseData.user.id}`)
|
||||
|
||||
+23
-1
@@ -20,11 +20,12 @@ import {
|
||||
Patch,
|
||||
BadRequestException,
|
||||
Delete,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { ApiResponse, Pagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "oauth-clients/:clientId/users",
|
||||
@@ -39,6 +40,27 @@ export class OAuthClientUsersController {
|
||||
private readonly oauthRepository: OAuthClientRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
async getManagedUsers(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() queryParams: Pagination
|
||||
): Promise<ApiResponse<User[]>> {
|
||||
this.logger.log(`getting managed users with data for OAuth Client with ID ${oAuthClientId}`);
|
||||
const { offset, limit } = queryParams;
|
||||
|
||||
const existingUsers = await this.userRepository.findManagedUsersByOAuthClientId(
|
||||
oAuthClientId,
|
||||
offset ?? 0,
|
||||
limit ?? 50
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: existingUsers,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(OAuthClientCredentialsGuard)
|
||||
async createUser(
|
||||
|
||||
+6
-4
@@ -8,6 +8,7 @@ import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/o
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -57,9 +58,10 @@ export class OAuthClientsController {
|
||||
type: CreateOAuthClientResponseDto,
|
||||
})
|
||||
async createOAuthClient(
|
||||
@GetUser("organizationId") organizationId: number,
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() body: CreateOAuthClientInput
|
||||
): Promise<CreateOAuthClientResponseDto> {
|
||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||
this.logger.log(
|
||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||
);
|
||||
@@ -77,9 +79,9 @@ export class OAuthClientsController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(
|
||||
@GetUser("organizationId") organizationId: number
|
||||
): Promise<GetOAuthClientsResponseDto> {
|
||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
||||
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EventTypesService } from "@/ee/event-types/services/event-types.service
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedPlatformUserInput } from "@/modules/users/inputs/create-managed-platform-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
@@ -24,24 +24,24 @@ export class OAuthClientUsersService {
|
||||
) {
|
||||
let user: User;
|
||||
if (!organizationId) {
|
||||
const username = generateShortHash(body.email, oAuthClientId);
|
||||
user = await this.userRepository.create(body, username, oAuthClientId, isPlatformManaged);
|
||||
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
||||
} else {
|
||||
const [_, emailDomain] = body.email.split("@");
|
||||
const [username, emailDomain] = body.email.split("@");
|
||||
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
user = (
|
||||
await createNewUsersConnectToOrgIfExists({
|
||||
usernamesOrEmails: [body.email],
|
||||
usernamesOrEmails: [email],
|
||||
input: {
|
||||
teamId: organizationId,
|
||||
role: "MEMBER",
|
||||
usernameOrEmail: [body.email],
|
||||
usernameOrEmail: [email],
|
||||
isOrg: true,
|
||||
language: "en",
|
||||
},
|
||||
parentId: null,
|
||||
autoAcceptEmailDomain: emailDomain,
|
||||
autoAcceptEmailDomain: "never-auto-accept-email-domain-for-managed-users",
|
||||
connectionInfoMap: {
|
||||
[body.email]: {
|
||||
[email]: {
|
||||
orgId: organizationId,
|
||||
autoAccept: true,
|
||||
},
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { TokenExpiredException } from "@/modules/auth/guards/access-token/token-expired.exception";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN } from "@calcom/platform-constants";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthFlowService {
|
||||
private logger = new Logger("OAuthFlowService");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
) //private readonly redisService: RedisIOService
|
||||
{}
|
||||
private readonly oAuthClientRepository: OAuthClientRepository //private readonly redisService: RedisIOService
|
||||
) {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async propagateAccessToken(accessToken: string) {
|
||||
// this.logger.log("Propagating access token to redis", accessToken);
|
||||
// TODO propagate
|
||||
@@ -48,6 +46,7 @@ export class OAuthFlowService {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
private async readFromCache(secret: string) {
|
||||
return { status: "CACHE_MISS" };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
||||
export class PrismaReadService implements OnModuleInit {
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client";
|
||||
export class PrismaWriteService implements OnModuleInit {
|
||||
public prisma: PrismaClient;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
constructor(readonly configService: ConfigService) {
|
||||
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||
|
||||
this.prisma = new PrismaClient({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [PrismaModule],
|
||||
providers: [SelectedCalendarsRepository],
|
||||
exports: [SelectedCalendarsRepository],
|
||||
})
|
||||
export class CredentialsModule {}
|
||||
export class SelectedCalendarsModule {}
|
||||
|
||||
@@ -16,4 +16,12 @@ export class SelectedCalendarsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getUserSelectedCalendars(userId: number) {
|
||||
return this.dbRead.prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CityTimezones } from "@calcom/platform-libraries";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "timezones",
|
||||
version: "2",
|
||||
})
|
||||
export class TimezonesController {
|
||||
constructor(private readonly timezonesService: TimezonesService) {}
|
||||
|
||||
@Get("/")
|
||||
async getTimeZones(): Promise<ApiResponse<CityTimezones>> {
|
||||
const timeZones = await this.timezonesService.getCityTimeZones();
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: timeZones,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { cityTimezonesHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TimezonesService {
|
||||
async getCityTimeZones() {
|
||||
return cityTimezonesHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TimezonesController } from "@/modules/timezones/controllers/timezones.controller";
|
||||
import { TimezonesService } from "@/modules/timezones/services/timezones.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [TimezonesService],
|
||||
controllers: [TimezonesController],
|
||||
exports: [TimezonesService],
|
||||
})
|
||||
export class TimezoneModule {}
|
||||
@@ -48,7 +48,7 @@ export class TokensRepository {
|
||||
}
|
||||
|
||||
async createOAuthTokens(clientId: string, ownerId: number) {
|
||||
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
|
||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||
const [accessToken, refreshToken] = await this.dbWrite.prisma.$transaction([
|
||||
this.dbWrite.prisma.accessToken.create({
|
||||
@@ -101,7 +101,7 @@ export class TokensRepository {
|
||||
}
|
||||
|
||||
async refreshOAuthTokens(clientId: string, refreshTokenSecret: string, tokenUserId: number) {
|
||||
const accessExpiry = DateTime.now().plus({ minute: 1 }).startOf("minute").toJSDate();
|
||||
const accessExpiry = DateTime.now().plus({ minute: 60 }).startOf("minute").toJSDate();
|
||||
const refreshExpiry = DateTime.now().plus({ year: 1 }).startOf("day").toJSDate();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -21,7 +21,7 @@ export class UsersRepository {
|
||||
) {
|
||||
this.formatInput(user);
|
||||
|
||||
return this.dbRead.prisma.user.create({
|
||||
return this.dbWrite.prisma.user.create({
|
||||
data: {
|
||||
...user,
|
||||
username,
|
||||
@@ -34,7 +34,7 @@ export class UsersRepository {
|
||||
}
|
||||
|
||||
async addToOAuthClient(userId: number, oAuthClientId: string) {
|
||||
return this.dbRead.prisma.user.update({
|
||||
return this.dbWrite.prisma.user.update({
|
||||
data: {
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClientId },
|
||||
@@ -83,6 +83,17 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmailWithProfile(email: string) {
|
||||
return this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
movedToProfile: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string) {
|
||||
return this.dbRead.prisma.user.findFirst({
|
||||
where: {
|
||||
@@ -91,6 +102,21 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findManagedUsersByOAuthClientId(oauthClientId: string, cursor: number, limit: number) {
|
||||
return this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
platformOAuthClients: {
|
||||
some: {
|
||||
id: oauthClientId,
|
||||
},
|
||||
},
|
||||
isPlatformManaged: true,
|
||||
},
|
||||
take: limit,
|
||||
skip: cursor,
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: number, updateData: UpdateManagedPlatformUserInput) {
|
||||
this.formatInput(updateData);
|
||||
|
||||
|
||||
@@ -22,7 +22,40 @@
|
||||
"/api/v2/events/public": {
|
||||
"get": {
|
||||
"operationId": "EventsController_getPublicEvent",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventSlug",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isTeamEvent",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -38,6 +71,31 @@
|
||||
}
|
||||
},
|
||||
"/api/v2/oauth-clients/{clientId}/users": {
|
||||
"get": {
|
||||
"operationId": "OAuthClientUsersController_getManagedUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OAuthClientUsersController_createUser",
|
||||
"parameters": [
|
||||
@@ -55,7 +113,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateUserInput"
|
||||
"$ref": "#/components/schemas/CreateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +191,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateUserInput"
|
||||
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,12 +558,15 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/CreateEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getEventTypes",
|
||||
@@ -516,12 +577,15 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypesOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{eventTypeId}": {
|
||||
@@ -543,12 +607,81 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "EventTypesController_updateEventType",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateEventTypeInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypesController_deleteEventType",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{username}/public": {
|
||||
@@ -570,15 +703,18 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/GetEventTypesPublicOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/oauth/auth-url": {
|
||||
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||
"get": {
|
||||
"operationId": "GcalController_redirect",
|
||||
"parameters": [
|
||||
@@ -605,7 +741,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/oauth/save": {
|
||||
"/api/v2/ee/gcal/oauth/save": {
|
||||
"get": {
|
||||
"operationId": "GcalController_save",
|
||||
"parameters": [
|
||||
@@ -640,7 +776,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/gcal/check": {
|
||||
"/api/v2/ee/gcal/check": {
|
||||
"get": {
|
||||
"operationId": "GcalController_check",
|
||||
"parameters": [],
|
||||
@@ -658,7 +794,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/provider/{clientId}": {
|
||||
"/api/v2/ee/provider/{clientId}": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyClientId",
|
||||
"parameters": [
|
||||
@@ -685,7 +821,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/platform/provider/{clientId}/access-token": {
|
||||
"/api/v2/ee/provider/{clientId}/access-token": {
|
||||
"get": {
|
||||
"operationId": "CalProviderController_verifyAccessToken",
|
||||
"parameters": [
|
||||
@@ -774,24 +910,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/time-zones": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getTimeZones",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/schedules/{scheduleId}": {
|
||||
"get": {
|
||||
"operationId": "SchedulesController_getSchedule",
|
||||
@@ -870,7 +988,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/me": {
|
||||
"/api/v2/ee/me": {
|
||||
"get": {
|
||||
"operationId": "MeController_getMe",
|
||||
"parameters": [],
|
||||
@@ -895,7 +1013,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateUserInput"
|
||||
"$ref": "#/components/schemas/UpdateManagedPlatformUserInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,6 +1069,22 @@
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookings",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingsController_createBooking",
|
||||
"parameters": [],
|
||||
@@ -978,6 +1112,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBooking",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingUid}/reschedule": {
|
||||
"get": {
|
||||
"operationId": "BookingsController_getBookingForReschedule",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/{bookingId}/cancel": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_cancelBooking",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bookingId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CancelBookingInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/bookings/reccuring": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_createReccuringBooking",
|
||||
@@ -1100,6 +1325,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/timezones": {
|
||||
"get": {
|
||||
"operationId": "TimezonesController_getTimeZones",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -1112,7 +1355,7 @@
|
||||
"servers": [],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"CreateUserInput": {
|
||||
"CreateManagedPlatformUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -1124,9 +1367,6 @@
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"defaultScheduleId": {
|
||||
"type": "number"
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1138,7 +1378,7 @@
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"UpdateUserInput": {
|
||||
"UpdateManagedPlatformUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
@@ -1187,11 +1427,11 @@
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
],
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"example": {
|
||||
@@ -1230,7 +1470,8 @@
|
||||
"example": 3
|
||||
},
|
||||
"logo": {
|
||||
"type": "object",
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "https://example.com/logo.png"
|
||||
},
|
||||
"redirectUris": {
|
||||
@@ -1393,7 +1634,251 @@
|
||||
"refreshToken"
|
||||
]
|
||||
},
|
||||
"EventTypeLocation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "link"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"example": "https://masterchief.com/argentina/flan/video/9129412"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"CreateEventTypeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"EventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||
},
|
||||
"locations": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title",
|
||||
"description",
|
||||
"locations"
|
||||
]
|
||||
},
|
||||
"CreateEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"Data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventType"
|
||||
]
|
||||
},
|
||||
"GetEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Data"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"EventTypeGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventTypes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventTypes"
|
||||
]
|
||||
},
|
||||
"GetEventTypesData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventTypeGroups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeGroup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"eventTypeGroups"
|
||||
]
|
||||
},
|
||||
"GetEventTypesOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/GetEventTypesData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"PublicEventType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"GetEventTypesPublicOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PublicEventType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateEventTypeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
@@ -1405,14 +1890,88 @@
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"DeleteData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"DeleteEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/DeleteData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateAvailabilityInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1526,6 +2085,10 @@
|
||||
"hashedLink"
|
||||
]
|
||||
},
|
||||
"CancelBookingInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"ReserveSlotInput": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { Booking, User } from "@prisma/client";
|
||||
|
||||
export class BookingsRepositoryFixture {
|
||||
private primaReadClient: PrismaReadService["prisma"];
|
||||
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||
|
||||
constructor(private readonly module: TestingModule) {
|
||||
this.primaReadClient = module.get(PrismaReadService).prisma;
|
||||
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||
}
|
||||
|
||||
async getById(bookingId: Booking["id"]) {
|
||||
return this.primaReadClient.booking.findFirst({ where: { id: bookingId } });
|
||||
}
|
||||
|
||||
async deleteById(bookingId: Booking["id"]) {
|
||||
return this.prismaWriteClient.booking.delete({ where: { id: bookingId } });
|
||||
}
|
||||
|
||||
async deleteAllBookings(userId: User["id"], userEmail: User["email"]) {
|
||||
return this.prismaWriteClient.booking.deleteMany({ where: { userId, userPrimaryEmail: userEmail } });
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export class EventTypesRepositoryFixture {
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateEventTypeInput, userId: number) {
|
||||
async create(data: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">, userId: number) {
|
||||
return this.prismaWriteClient.eventType.create({
|
||||
data: {
|
||||
...data,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { NextAuthPassportStrategy } from "@/lib/passport/strategies/types";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class NextAuthMockStrategy extends PassportStrategy(BaseStrategy, "next-auth") {
|
||||
export class NextAuthMockStrategy extends PassportStrategy(NextAuthPassportStrategy, "next-auth") {
|
||||
constructor(private readonly email: string, private readonly userRepository: UsersRepository) {
|
||||
super();
|
||||
}
|
||||
async authenticate() {
|
||||
try {
|
||||
const user = await this.userRepository.findByEmail(this.email);
|
||||
const user = await this.userRepository.findByEmailWithProfile(this.email);
|
||||
if (!user) {
|
||||
throw new Error("User with the provided email not found");
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
|
||||
LOG_LEVEL: "trace",
|
||||
REDIS_URL: "redis://localhost:9199",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
process.env = {
|
||||
...env,
|
||||
...process.env,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
## Cal.com Platform Example (https://platform.cal.com)
|
||||
@@ -1,11 +0,0 @@
|
||||
import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("set_a_password"),
|
||||
(t) => t("set_a_password_description")
|
||||
);
|
||||
|
||||
export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<X className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
<Icon name="x" className="text-inverted h-6 w-6 dark:text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Badge, ListItemText, Avatar } from "@calcom/ui";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||
|
||||
type ShouldHighlight =
|
||||
| {
|
||||
@@ -105,7 +104,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
|
||||
@@ -3,40 +3,39 @@
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
|
||||
import { Button, ButtonGroup, Icon } from "@calcom/ui";
|
||||
|
||||
export default function EnterprisePage() {
|
||||
const { t } = useLocale();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Globe className="h-5 w-5 text-red-500" />,
|
||||
icon: <Icon name="globe" className="h-5 w-5 text-red-500" />,
|
||||
title: t("branded_subdomain"),
|
||||
description: t("branded_subdomain_description"),
|
||||
},
|
||||
{
|
||||
icon: <BarChart className="h-5 w-5 text-blue-500" />,
|
||||
icon: <Icon name="bar-chart" className="h-5 w-5 text-blue-500" />,
|
||||
title: t("org_insights"),
|
||||
description: t("org_insights_description"),
|
||||
},
|
||||
{
|
||||
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
|
||||
icon: <Icon name="paintbrush" className="h-5 w-5 text-pink-500" />,
|
||||
title: t("extensive_whitelabeling"),
|
||||
description: t("extensive_whitelabeling_description"),
|
||||
},
|
||||
{
|
||||
icon: <Users className="h-5 w-5 text-orange-500" />,
|
||||
icon: <Icon name="users" className="h-5 w-5 text-orange-500" />,
|
||||
title: t("unlimited_teams"),
|
||||
description: t("unlimited_teams_description"),
|
||||
},
|
||||
{
|
||||
icon: <CreditCard className="h-5 w-5 text-green-500" />,
|
||||
icon: <Icon name="credit-card" className="h-5 w-5 text-green-500" />,
|
||||
title: t("unified_billing"),
|
||||
description: t("unified_billing_description"),
|
||||
},
|
||||
{
|
||||
icon: <Lock className="h-5 w-5 text-purple-500" />,
|
||||
icon: <Icon name="lock" className="h-5 w-5 text-purple-500" />,
|
||||
title: t("advanced_managed_events"),
|
||||
description: t("advanced_managed_events_description"),
|
||||
},
|
||||
|
||||
@@ -6,12 +6,11 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -42,7 +41,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
|
||||
<Button StartIcon="plus" color="secondary" {...(isPending && { loading: isPending })}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -50,7 +49,7 @@ const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorPro
|
||||
{options.map((data) => (
|
||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||
{data.slug === "add-new" ? (
|
||||
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
||||
<DropdownItem StartIcon="plus" color="minimal" href="/apps/categories/calendar">
|
||||
{t("install_new_calendar_app")}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
|
||||
@@ -11,17 +11,16 @@ import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
List,
|
||||
showToast,
|
||||
Button,
|
||||
DropdownMenuItem,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
|
||||
@@ -82,7 +81,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
<div className="flex justify-end">
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button StartIcon="ellipsis" variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
|
||||
@@ -90,7 +89,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon={Video}
|
||||
StartIcon="video"
|
||||
onClick={() => {
|
||||
const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
|
||||
if (locationType?.linkType === "static") {
|
||||
@@ -214,7 +213,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
color="destructive"
|
||||
onClick={() => handleDisconnect(credentialId, teamId)}
|
||||
disabled={isGlobal}
|
||||
StartIcon={Trash}>
|
||||
StartIcon="trash">
|
||||
{t("remove_app")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import type { IframeHTMLAttributes } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
||||
@@ -10,8 +10,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App as AppType } from "@calcom/types/App";
|
||||
import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
||||
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Icon, SkeletonButton, SkeletonText, showToast } from "@calcom/ui";
|
||||
|
||||
import { InstallAppButtonChild } from "./InstallAppButtonChild";
|
||||
|
||||
@@ -196,7 +195,7 @@ export const AppPage = ({
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Check} color="secondary" disabled>
|
||||
<Button StartIcon="check" color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("default")}
|
||||
@@ -323,7 +322,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
href={docs}>
|
||||
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
<Icon name="book-open" className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -335,7 +334,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={website}>
|
||||
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="external-link" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -347,7 +346,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={`mailto:${email}`}>
|
||||
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="mail" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -360,7 +359,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={tos}>
|
||||
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="file" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -372,7 +371,7 @@ export const AppPage = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
<Icon name="shield" className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -383,7 +382,7 @@ export const AppPage = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||
<Icon name="flag" className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,12 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
List,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
ShellSubHeading,
|
||||
Label,
|
||||
List,
|
||||
ShellSubHeading,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -267,7 +266,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={Calendar}
|
||||
Icon="calendar"
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user