Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af524d9e93 | ||
|
|
6b7ced262d | ||
|
|
7a797490c8 | ||
|
|
3c367dd2c8 | ||
|
|
00f50a9449 | ||
|
|
381e22ae7e | ||
|
|
3850b519fd | ||
|
|
78473bab1b | ||
|
|
e437dfa000 | ||
|
|
07789ba697 | ||
|
|
a6c6a46076 | ||
|
|
d44d74d2fc | ||
|
|
52662b3375 | ||
|
|
8612fa71c7 | ||
|
|
caaecf2f94 | ||
|
|
8741714402 | ||
|
|
271d9a5f61 | ||
|
|
3d3acba82a | ||
|
|
ca1f84c827 | ||
|
|
a44bcafcf2 | ||
|
|
e4ab8d87c4 | ||
|
|
a4519e705d | ||
|
|
a3113607ff | ||
|
|
acf270bc61 | ||
|
|
d1a9af84b2 | ||
|
|
6ea879f265 | ||
|
|
307b2946a2 | ||
|
|
23d9788dce | ||
|
|
caade0850a | ||
|
|
f6af6e34bd | ||
|
|
5f5cfaf7c7 | ||
|
|
07a4aa6c7d | ||
|
|
641193bd61 | ||
|
|
2d372746d3 | ||
|
|
4b2265c35d | ||
|
|
4dc2d3236d | ||
|
+20 |
7bff15cc47 | ||
|
|
97ef32f7ca | ||
|
|
16a94fe712 | ||
|
|
8f29811d5a | ||
|
|
a47cd5b106 | ||
|
|
64aa19fcea | ||
|
|
7c71b179c8 | ||
|
|
d485e5f621 | ||
|
|
aa8d0f78d6 | ||
|
|
564497ec2a | ||
|
|
2089ad40ea | ||
|
|
42e94cd5b7 | ||
|
|
4546bd8653 | ||
|
|
2a89a5e249 | ||
|
|
aba7c6df7f | ||
|
|
97348e2052 | ||
|
|
30149dd100 | ||
|
|
d91ccd37e0 | ||
|
|
0bd90d1673 | ||
|
|
6899b853ee | ||
|
|
b07d81d0b9 | ||
|
|
22104f8d91 | ||
|
|
26d2decea4 | ||
|
|
b6d8dc0bd6 | ||
|
|
7c0cdcfc6d | ||
|
|
ad23613599 | ||
|
|
71598a4389 | ||
|
|
2ce1828841 | ||
|
|
b287f7e047 | ||
|
|
c2be796a5b | ||
|
|
829bb63eb9 | ||
|
|
07accf700e | ||
|
|
46f73e79c1 | ||
|
|
b0921fe90a | ||
|
|
3c956dd8e8 | ||
|
|
197a7ba6e9 | ||
|
|
e6daed762b | ||
|
|
0b3aa4fd34 | ||
|
|
4a6704c934 | ||
|
|
fbcfc35621 | ||
|
|
0d2ec20f66 | ||
|
|
01fc501cb9 | ||
|
|
1be002c33c | ||
|
|
8a4b03833d | ||
|
|
8714dda047 | ||
|
|
5b89123307 | ||
|
|
3221379584 | ||
|
|
578f5dc16b | ||
|
|
3f042a1077 | ||
|
|
a44e7b2371 | ||
|
|
b06a94ab57 | ||
|
|
ec55454e43 | ||
|
|
9ec0217858 | ||
|
|
66d69f5a06 | ||
|
|
b7b58c2dc7 | ||
|
|
de1e19ee6d | ||
|
|
a366cb7c89 | ||
|
|
dac336894c | ||
|
|
373c17a46d | ||
|
|
4c00c1ac2b | ||
|
|
050a030ff0 | ||
|
|
dfb2232a59 | ||
|
|
984ccf7c80 | ||
|
|
e87168322c | ||
|
|
bf9e9b4ae1 | ||
|
|
6d4b05cf1d | ||
|
|
bd1af4027f | ||
|
|
3872175393 | ||
|
|
5ca063d4e8 | ||
|
|
112aa8201f | ||
|
|
17fcc2c213 | ||
|
|
d3f1dc1b32 | ||
|
|
25f3368fa1 | ||
|
|
9a271f397b | ||
|
|
6847781a7f | ||
|
|
2d9a3bd741 | ||
|
|
649faf4ff6 | ||
|
|
03346f04ed | ||
|
|
99260974f4 | ||
|
|
4a493c0a0e | ||
|
|
47435c36ff | ||
|
|
599744b31c | ||
|
|
d2c4c45137 | ||
|
|
43ba47a122 | ||
|
|
0048b2d92c | ||
|
|
9dc0570d38 | ||
|
|
1fbfca958a | ||
|
|
7a6c93795a | ||
|
|
4481b232cc | ||
|
|
84b265cd54 | ||
|
|
eb86064995 | ||
|
|
39fdf5a74e | ||
|
|
f28c605b50 | ||
|
|
63d26c4fba | ||
|
|
14bba343a4 | ||
|
|
e441f3308f | ||
|
|
b4aad453cd | ||
|
|
dc7a1c4680 | ||
|
|
fd651133c2 | ||
|
|
f0d2a1a051 | ||
|
|
62c7627775 | ||
|
|
be53a20429 | ||
|
|
5c6f52f1d4 | ||
|
|
0e9e3e5012 | ||
|
|
5d709cf63c | ||
|
|
0f67faeef3 | ||
|
|
1ff4f557f6 | ||
|
|
7e2c0bd29e | ||
|
|
5b59cf9d9c | ||
|
|
d5420eda00 | ||
|
|
c17ac67b3f | ||
|
|
b2763a6274 | ||
|
|
08b6f60407 | ||
|
|
466b3e4c08 | ||
|
|
401209810a | ||
|
|
9f34a840d0 | ||
|
|
37f1535e1a | ||
|
|
5dc81e8677 | ||
|
|
5a166ee430 | ||
|
|
f6f964d495 | ||
|
|
6364e85293 | ||
|
|
46add2bebc | ||
|
|
8ada940dc7 | ||
|
|
d027f8004a | ||
|
|
cc2f16411b | ||
|
|
15b58c1c13 | ||
|
|
2d12ea7372 | ||
|
|
06bf9cfbfe | ||
|
|
5dbc6d9ca6 | ||
|
|
486e3a386e | ||
|
|
3a29327761 | ||
|
|
4f226cd653 | ||
|
|
207804bd97 | ||
|
|
7969789d60 | ||
|
|
67fb0e6719 | ||
|
|
7f390b147a | ||
|
|
1dc869971e | ||
|
|
3fe83bb45c | ||
|
|
4fbeff31e7 | ||
|
|
e800341485 | ||
|
|
f5dc22f145 | ||
|
|
4fd31db6ee | ||
|
|
f58d3cf837 | ||
|
|
fc79ea898a | ||
|
|
5a602f10e6 | ||
|
|
e29b42a02b | ||
|
|
cdb57eaa59 | ||
|
|
662ba8dfc6 | ||
|
|
552d88ad6d | ||
|
|
08bcc77e2c | ||
|
|
c211f181fc | ||
|
|
cc6016aef3 | ||
|
|
77d0fa9ff8 | ||
|
|
720136fcf9 | ||
|
|
9bf3732717 |
@@ -388,6 +388,12 @@ BLACKLISTED_GUEST_EMAILS=
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
|
||||
# Mintlify chat api
|
||||
# Power AI chat in for docs
|
||||
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
|
||||
NEXT_PUBLIC_CHAT_API_URL=
|
||||
NEXT_PUBLIC_DOCS_URL=
|
||||
|
||||
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
|
||||
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
|
||||
NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
@@ -406,3 +412,13 @@ DIRECTORY_IDS_TO_LOG=
|
||||
# Set this when Cal.com is used to serve only one organization's booking pages
|
||||
# Read more about it in the README.md
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||
|
||||
## Env variables related to avoiding booking failures
|
||||
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
||||
NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS=
|
||||
# Request for checking reservation of same slot would be sent only after this time has passed. It must be lower than PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS
|
||||
NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS=
|
||||
# Query available slots interval - Should be kept high in minutes as it could cause significant load on the system
|
||||
NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS=
|
||||
# Used to invalidate available slots when navigating to booking form
|
||||
NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM=0
|
||||
+4
-1
@@ -12,8 +12,11 @@
|
||||
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||
/packages/core/getAggregatedAvailability/index.ts @calcom/Foundation
|
||||
/packages/core/getAggregateWorkingHours.ts @calcom/Foundation
|
||||
/packages/core/getUserAvailability.ts @calcom/Foundation
|
||||
/packages/features/bookings/lib/handleNewBooking/**/* @calcom/Foundation
|
||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
|
||||
/packages/lib/slots.ts @calcom/Foundation
|
||||
/packages/platform/**/* @calcom/Platform
|
||||
/packages/prisma/**/* @calcom/Foundation
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
- Fixes #XXXX (GitHub issue number)
|
||||
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)
|
||||
|
||||
<!-- Please provide a loom video for visual changes to speed up reviews
|
||||
Loom Video: https://www.loom.com/
|
||||
-->
|
||||
## Visual Demo (For contributors especially)
|
||||
|
||||
A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**.
|
||||
|
||||
#### Video Demo (if applicable):
|
||||
|
||||
- Show screen recordings of the issue or feature.
|
||||
- Demonstrate how to reproduce the issue, the behavior before and after the change.
|
||||
|
||||
#### Image Demo (if applicable):
|
||||
|
||||
- Add side-by-side screenshots of the original and updated change.
|
||||
- Highlight any significant change(s).
|
||||
|
||||
## Mandatory Tasks (DO NOT REMOVE)
|
||||
|
||||
|
||||
@@ -96,3 +96,4 @@ apps/auth
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
i18n.cache
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -2,6 +2,7 @@ API_KEY_PREFIX=cal_
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
|
||||
# Get it in console.cal.com
|
||||
# Get CALCOM_LICENSE_KEY here: https://go.cal.com/get-license
|
||||
CALCOM_LICENSE_KEY=""
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555
|
||||
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL=http://localhost:5555
|
||||
|
||||
@@ -190,16 +190,6 @@ We make sure of this by not using next in dev, but next build && next start, if
|
||||
See <https://github.com/vercel/next.js/blob/canary/packages/next/server/dev/hot-reloader.ts#L79>. Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only.
|
||||
To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1
|
||||
|
||||
## Hosted api through cal.com
|
||||
|
||||
> _❗ WARNING: This is still experimental and not fully implemented yet❗_
|
||||
|
||||
Go to console.cal.com
|
||||
Add a deployment or go to an existing one.
|
||||
Activate API or Admin addon
|
||||
Provide your `DATABASE_URL`
|
||||
Now you can call api.cal.com?key=CALCOM_LICENSE_KEY, which will connect to your own databaseUrl.
|
||||
|
||||
## How to deploy
|
||||
|
||||
We recommend deploying API in vercel.
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserCreationService } from "@calcom/lib/server/service/userCreationService";
|
||||
import { CreationSource } from "@calcom/prisma/enums";
|
||||
|
||||
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
|
||||
@@ -93,7 +93,9 @@ async function postHandler(req: NextApiRequest) {
|
||||
// If user is not ADMIN, return unauthorized.
|
||||
if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
const data = await schemaUserCreateBodyParams.parseAsync(req.body);
|
||||
const user = await prisma.user.create({ data: { ...data, creationSource: CreationSource.API_V1 } });
|
||||
const user = await UserCreationService.createUser({
|
||||
data: { ...data, creationSource: CreationSource.API_V1 },
|
||||
});
|
||||
req.statusCode = 201;
|
||||
return { user };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
|
||||
import handler from "../../../pages/api/users/_post";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: (key: string) => {
|
||||
return () => key;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.stubEnv("CALCOM_LICENSE_KEY", undefined);
|
||||
|
||||
describe("POST /api/users", () => {
|
||||
test("should throw 401 if not system-wide admin", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
email: "test@example.com",
|
||||
username: "test",
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
test("should throw a 400 if no email is provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
username: "test",
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
test("should throw a 400 if no username is provided", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
email: "test@example.com",
|
||||
},
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
test("should create user successfully", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
email: "test@example.com",
|
||||
username: "test",
|
||||
},
|
||||
prisma: prismock,
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const userQuery = await prismock.user.findFirst({
|
||||
where: {
|
||||
email: "test@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(userQuery).toEqual(
|
||||
expect.objectContaining({
|
||||
email: "test@example.com",
|
||||
username: "test",
|
||||
locked: false,
|
||||
organizationId: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should auto lock user if email is in watchlist", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {
|
||||
email: "test@example.com",
|
||||
username: "test",
|
||||
},
|
||||
prisma: prismock,
|
||||
});
|
||||
req.isSystemWideAdmin = true;
|
||||
|
||||
await prismock.watchlist.create({
|
||||
data: {
|
||||
type: "EMAIL",
|
||||
value: "test@example.com",
|
||||
severity: "CRITICAL",
|
||||
createdById: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const userQuery = await prismock.user.findFirst({
|
||||
where: {
|
||||
email: "test@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
expect(userQuery).toEqual(
|
||||
expect.objectContaining({
|
||||
email: "test@example.com",
|
||||
username: "test",
|
||||
locked: true,
|
||||
organizationId: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -30,7 +30,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.94",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.116",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -61,6 +61,7 @@
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsforce": "^1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"nest-winston": "^1.9.4",
|
||||
"next-auth": "^4.22.1",
|
||||
@@ -90,6 +91,7 @@
|
||||
"@types/supertest": "^2.0.12",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"prettier": "^2.8.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
|
||||
@@ -8,13 +8,13 @@ used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cal.com Subscription Terms available
|
||||
at https://cal.com/terms, or other agreements governing
|
||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
|
||||
and otherwise have a valid Cal.com Commercial License ("Commercial License")
|
||||
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||
and to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||
exploited with a valid Commercial License for the correct number of hosts.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
|
||||
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
|
||||
</a>
|
||||
|
||||
<a href="https://console.cal.com/">Get a License Key</a>
|
||||
<a href="https://go.cal.com/get-license">Get a License Key</a>
|
||||
</div>
|
||||
|
||||
# Enterprise Edition of API
|
||||
# Commercial License of API
|
||||
|
||||
Welcome to the Enterprise Edition ("/ee") of the Cal.com API.
|
||||
Welcome to the Commercial License of the Cal.com API.
|
||||
|
||||
Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All "Multiplayer APIs" are under a commercial license.
|
||||
|
||||
The [/ee](https://github.com/calcom/cal.com/tree/main/apps/api/v2/ee) subfolder is the place for all the **Enterprise Edition** features from our [hosted](https://cal.com/pricing) plan and enterprise-grade features for [Enterprise](https://cal.com/enterprise) such as SSO, SAML, OIDC, SCIM, SIEM and much more or [Platform](https://cal.com/platform) plan to build a marketplace.
|
||||
The [/ee](https://github.com/calcom/cal.com/tree/main/apps/api/v2/ee) subfolder is the place for all the **Commercial License** features from our [hosted](https://cal.com/pricing) plan.
|
||||
|
||||
> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://console.cal.com/) first❗_
|
||||
> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://go.cal.com/get-license) first❗_
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -11,7 +11,7 @@ import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeyRepository],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
export class BookingsModule_2024_04_15 {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/g
|
||||
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
@@ -98,7 +99,7 @@ export class BookingsController_2024_04_15 {
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository
|
||||
private readonly apiKeyRepository: ApiKeysRepository
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@@ -189,18 +190,24 @@ export class BookingsController_2024_04_15 {
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
@Post("/:bookingUid/cancel")
|
||||
async cancelBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string,
|
||||
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
const isUidNumber = !isNaN(Number(bookingUid));
|
||||
|
||||
if (isUidNumber) {
|
||||
throw new BadRequestException("Please provide booking uid instead of booking id.");
|
||||
}
|
||||
|
||||
if (bookingUid) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
req.body.uid = bookingUid;
|
||||
const res = await handleCancelBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed)
|
||||
);
|
||||
|
||||
@@ -169,4 +169,19 @@ export class CreateBookingInput_2024_04_15 {
|
||||
label?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
teamMemberEmail?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
crmAppSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
crmOwnerRecordType?: string;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
@@ -29,7 +29,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingsRepository_2024_08_13,
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
ApiKeyRepository,
|
||||
ApiKeysRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -309,6 +309,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
placeholder: "matrix",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
name: "video-url",
|
||||
type: "url",
|
||||
label: "video url",
|
||||
sources: [
|
||||
{
|
||||
id: "user",
|
||||
type: "user",
|
||||
label: "User",
|
||||
fieldRequired: true,
|
||||
},
|
||||
],
|
||||
editable: "user",
|
||||
required: false,
|
||||
placeholder: "add video url",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
user.id
|
||||
@@ -455,7 +472,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
console.log("asap data", JSON.stringify(data, null, 2));
|
||||
expect(data.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
|
||||
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
|
||||
} else {
|
||||
@@ -500,6 +516,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
"favorite-movie": "lord of the rings",
|
||||
"video-url": "http://video-url.com",
|
||||
},
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
@@ -516,6 +533,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.bookingFieldsResponses["favorite-movie"]).toEqual("lord of the rings");
|
||||
expect(data.bookingFieldsResponses["video-url"]).toEqual("http://video-url.com");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
|
||||
@@ -167,6 +167,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
rating: 10,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -396,6 +397,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
// Check updatedAt date is between the time of the request and after the request
|
||||
expect(data.updatedAt).toBeDefined();
|
||||
const updatedAtDate = data.updatedAt ? new Date(data.updatedAt) : null;
|
||||
expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
expect(data.metadata).toEqual(body.metadata);
|
||||
createdBooking = data;
|
||||
} else {
|
||||
@@ -546,6 +553,30 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get a booking with rating", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${bookingInThePast.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toEqual(bookingInThePast.id);
|
||||
expect(data.uid).toEqual(bookingInThePast.uid);
|
||||
expect(data.rating).toEqual(bookingInThePast.rating);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get 1 recurrence of a recurring booking", async () => {
|
||||
const recurrenceUid = createdRecurringBooking[0].uid;
|
||||
return request(app.getHttpServer())
|
||||
@@ -869,7 +900,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
it("should should get bookings by before specified end time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterStart=${createdRecurringBooking[0].start}`)
|
||||
.get(`/v2/bookings?beforeEnd=${createdRecurringBooking[0].end}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings after specified update time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[1].updatedAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings before specified update time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeUpdatedAt=${createdRecurringBooking[0].updatedAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings in update range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[0].updatedAt}&beforeUpdatedAt=${createdRecurringBooking[2].updatedAt}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -1004,6 +1091,46 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by updated in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].updatedAt).toEqual(createdBooking.updatedAt);
|
||||
expect(data[1].updatedAt).toEqual(bookingInThePast.updatedAt?.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by updated in ascending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].updatedAt).toEqual(bookingInThePast.updatedAt?.toISOString());
|
||||
expect(data[1].updatedAt).toEqual(createdBooking.updatedAt);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule bookings", () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
@@ -74,7 +74,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository
|
||||
) {}
|
||||
|
||||
@@ -463,11 +463,18 @@ export class InputBookingsService_2024_08_13 {
|
||||
teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined),
|
||||
eventTypeIds:
|
||||
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
|
||||
afterUpdatedDate: queryParams.afterUpdatedAt,
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
transformGetBookingsSort(queryParams: GetBookingsInput_2024_08_13) {
|
||||
if (!queryParams.sortStart && !queryParams.sortEnd && !queryParams.sortCreated) {
|
||||
if (
|
||||
!queryParams.sortStart &&
|
||||
!queryParams.sortEnd &&
|
||||
!queryParams.sortCreated &&
|
||||
!queryParams.sortUpdatedAt
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -475,6 +482,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
sortStart: queryParams.sortStart,
|
||||
sortEnd: queryParams.sortEnd,
|
||||
sortCreated: queryParams.sortCreated,
|
||||
sortUpdated: queryParams.sortUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
meetingUrl: location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
updatedAt: databaseBooking.updatedAt,
|
||||
rating: databaseBooking.rating,
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
@@ -225,6 +227,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
bookingFieldsResponses: databaseBooking.responses,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
updatedAt: databaseBooking.updatedAt,
|
||||
rating: databaseBooking.rating,
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
|
||||
@@ -271,6 +275,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
meetingUrl: location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
updatedAt: databaseBooking.updatedAt,
|
||||
rating: databaseBooking.rating,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
@@ -280,7 +286,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
@@ -297,7 +304,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
@@ -376,6 +384,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
updatedAt: databaseBooking.updatedAt,
|
||||
rating: databaseBooking.rating,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
|
||||
@@ -387,7 +397,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
defaultSeatedBookingData,
|
||||
false
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
@@ -404,7 +415,8 @@ export class OutputBookingsService_2024_08_13 {
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
defaultSeatedBookingMetadata,
|
||||
false
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { User } from "@prisma/client";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
@@ -123,10 +123,15 @@ export class CalendarsController {
|
||||
};
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
|
||||
type: String,
|
||||
name: "calendar",
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Get("/:calendar/connect")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Get connect URL" })
|
||||
@ApiOperation({ summary: "Get oAuth connect URL" })
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@@ -146,10 +151,15 @@ export class CalendarsController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
|
||||
type: String,
|
||||
name: "calendar",
|
||||
})
|
||||
@Get("/:calendar/save")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "Save a calendar" })
|
||||
@ApiOperation({ summary: "Save an oAuth calendar credentials" })
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Query("code") code: string,
|
||||
@@ -177,6 +187,11 @@ export class CalendarsController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
enum: [APPLE_CALENDAR],
|
||||
type: String,
|
||||
name: "calendar",
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Post("/:calendar/credentials")
|
||||
@ApiOperation({ summary: "Sync credentials" })
|
||||
@@ -198,6 +213,11 @@ export class CalendarsController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
|
||||
type: String,
|
||||
name: "calendar",
|
||||
})
|
||||
@Get("/:calendar/check")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@@ -219,6 +239,11 @@ export class CalendarsController {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiParam({
|
||||
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
|
||||
type: String,
|
||||
name: "calendar",
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Post("/:calendar/disconnect")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
+19
@@ -377,6 +377,15 @@ describe("Event types Endpoints", () => {
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
type: "url",
|
||||
label: "Video Url",
|
||||
slug: "video-url",
|
||||
required: true,
|
||||
placeholder: "add video url",
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
bookingLimitsCount: {
|
||||
@@ -478,6 +487,16 @@ describe("Event types Endpoints", () => {
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
type: "url",
|
||||
label: "Video Url",
|
||||
slug: "video-url",
|
||||
required: true,
|
||||
placeholder: "add video url",
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
isDefault: false,
|
||||
},
|
||||
{ ...defaultResponseBookingFieldTitle },
|
||||
{ ...defaultResponseBookingFieldNotes },
|
||||
{ ...defaultResponseBookingFieldGuests },
|
||||
|
||||
@@ -7,11 +7,7 @@ import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesRepository_2024_06_14 {
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private usersService: UsersService
|
||||
) {}
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserEventType(
|
||||
userId: number,
|
||||
@@ -47,6 +43,13 @@ export class EventTypesRepository_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeWithHosts(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
include: { hosts: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserEventType(userId: number, eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findFirst({
|
||||
where: {
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
import { IsEnum, IsString, ValidateNested, IsOptional } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@@ -10,8 +10,9 @@ class Data {
|
||||
callId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiProperty()
|
||||
agentId!: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export class CreatePhoneCallOutput {
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
Headers,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiExcludeController } from "@nestjs/swagger";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
|
||||
import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -41,7 +42,7 @@ const CALENDAR_SCOPES = [
|
||||
path: "/v2/gcal",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Platform / Google Calendar")
|
||||
@ApiExcludeController(true)
|
||||
export class GcalController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import { MeModule } from "@/ee/me/me.module";
|
||||
import { ProviderModule } from "@/ee/provider/provider.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
||||
import { SlotsModule_2024_04_15 } from "@/modules/slots/slots-2024-04-15/slots.module";
|
||||
import { SlotsModule_2024_09_04 } from "@/modules/slots/slots-2024-09-04/slots.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
@@ -29,7 +30,8 @@ import { Module } from "@nestjs/common";
|
||||
BookingsModule_2024_04_15,
|
||||
BookingsModule_2024_08_13,
|
||||
TeamsMembershipsModule,
|
||||
SlotsModule,
|
||||
SlotsModule_2024_04_15,
|
||||
SlotsModule_2024_09_04,
|
||||
TeamsModule,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
VERSION_2024_06_11,
|
||||
VERSION_2024_06_14,
|
||||
VERSION_2024_08_13,
|
||||
VERSION_2024_09_04,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue;
|
||||
@@ -13,8 +14,10 @@ export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unkn
|
||||
export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue;
|
||||
export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue;
|
||||
export const VERSION_2024_08_13_VALUE: VersionValue = VERSION_2024_08_13 as unknown as VersionValue;
|
||||
export const VERSION_2024_09_04_VALUE: VersionValue = VERSION_2024_09_04 as unknown as VersionValue;
|
||||
|
||||
export { VERSION_2024_04_15 };
|
||||
export { VERSION_2024_06_11 };
|
||||
export { VERSION_2024_06_14 };
|
||||
export { VERSION_2024_08_13 };
|
||||
export { VERSION_2024_09_04 };
|
||||
|
||||
@@ -3,20 +3,22 @@ import { ZodSchema } from "zod";
|
||||
|
||||
const logger = new Logger("safeParse");
|
||||
|
||||
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T): T {
|
||||
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T, logError = true): T {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
const errorStack = new Error().stack;
|
||||
|
||||
logger.error(
|
||||
`Zod parsing failed.\n` +
|
||||
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
|
||||
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
|
||||
`3. Zod Error: ${result.error}\n` +
|
||||
`4. Call Stack: ${errorStack}`
|
||||
);
|
||||
if (logError) {
|
||||
logger.error(
|
||||
`Zod parsing failed.\n` +
|
||||
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
|
||||
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
|
||||
`3. Zod Error: ${result.error}\n` +
|
||||
`4. Call Stack: ${errorStack}`
|
||||
);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ApiKeyRepository],
|
||||
exports: [ApiKeyRepository],
|
||||
})
|
||||
export class ApiKeyModule {}
|
||||
+17
-1
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository {
|
||||
export class ApiKeysRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getApiKeyFromHash(hashedKey: string) {
|
||||
@@ -13,4 +13,20 @@ export class ApiKeyRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamApiKeys(teamId: number) {
|
||||
return this.dbRead.prisma.apiKey.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
return this.dbWrite.prisma.apiKey.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { ApiKeysController } from "@/modules/api-keys/controllers/api-keys.controller";
|
||||
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ApiKeysRepository, ApiKeysService],
|
||||
controllers: [ApiKeysController],
|
||||
exports: [ApiKeysRepository, ApiKeysService],
|
||||
})
|
||||
export class ApiKeysModule {}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { RefreshApiKeyOutput } from "@/modules/api-keys/outputs/refresh-api-key.output";
|
||||
import { ApiKeysService } from "@/modules/api-keys/services/api-keys.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { ApiAuthGuardRequest } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards } from "@nestjs/common";
|
||||
import { ApiTags, ApiHeader, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/api-keys",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiTags("Api Keys")
|
||||
export class ApiKeysController {
|
||||
constructor(private readonly apiKeysService: ApiKeysService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Refresh API Key",
|
||||
description: `Generate a new API key and delete the current one. Provide API key to refresh as a Bearer token in the Authorization header (e.g. "Authorization: Bearer <apiKey>").`,
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description: "Bearer <apiKey>",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
})
|
||||
@Post("/refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async refresh(
|
||||
@Body() body: RefreshApiKeyInput,
|
||||
@GetUser("id") userId: number,
|
||||
@Req() request: ApiAuthGuardRequest
|
||||
): Promise<RefreshApiKeyOutput> {
|
||||
const currentApiKey = await this.apiKeysService.getRequestApiKey(request);
|
||||
const newApiKey = await this.apiKeysService.refreshApiKey(userId, currentApiKey, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
apiKey: newApiKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateApiKeyInput extends RefreshApiKeyInput {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({
|
||||
description: "For which team or organization is the api key.",
|
||||
})
|
||||
readonly teamId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
description: "Store additional note about this api key.",
|
||||
example: "This is an api key for development purposes.",
|
||||
})
|
||||
readonly note?: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsInt, IsOptional, Min } from "class-validator";
|
||||
|
||||
export class RefreshApiKeyInput {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "For how many days is managed organization api key valid. Defaults to 30 days.",
|
||||
example: 60,
|
||||
default: 30,
|
||||
minimum: 1,
|
||||
})
|
||||
readonly apiKeyDaysValid?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ description: "If true, organization api key never expires.", example: true })
|
||||
readonly apiKeyNeverExpires?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class ApiKeyOutput {
|
||||
@IsString()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly apiKey!: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiKeyOutput } from "@/modules/api-keys/outputs/api-key.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class RefreshApiKeyOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({ type: ApiKeyOutput })
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ApiKeyOutput)
|
||||
data!: ApiKeyOutput;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { hashAPIKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { CreateApiKeyInput } from "@/modules/api-keys/inputs/create-api-key.input";
|
||||
import { RefreshApiKeyInput } from "@/modules/api-keys/inputs/refresh-api-key.input";
|
||||
import { ApiAuthGuardRequest } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { createApiKeyHandler } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeysService {
|
||||
constructor(
|
||||
private readonly apiKeysRepository: ApiKeysRepository,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
async getRequestApiKey(request: ApiAuthGuardRequest) {
|
||||
if (request.authMethod !== AuthMethods.API_KEY) {
|
||||
throw new UnauthorizedException(
|
||||
"ApiKeysService - This endpoint can only be accessed using an API key by providing 'Authorization: Bearer <apiKey>' header"
|
||||
);
|
||||
}
|
||||
const apiKey = request.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!apiKey) {
|
||||
throw new UnauthorizedException("ApiKeysService - No API key provided");
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput) {
|
||||
if (createApiKeyInput.apiKeyDaysValid && createApiKeyInput.apiKeyNeverExpires) {
|
||||
throw new BadRequestException(
|
||||
"ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNeverExpires. It has to be either or none of them."
|
||||
);
|
||||
}
|
||||
|
||||
const defaultApiKeyDaysValid = 30;
|
||||
const apiKeyExpiresAfterDays = createApiKeyInput.apiKeyDaysValid
|
||||
? createApiKeyInput.apiKeyDaysValid
|
||||
: defaultApiKeyDaysValid;
|
||||
const apiKeyExpiresAt = DateTime.utc().plus({ days: apiKeyExpiresAfterDays }).toJSDate();
|
||||
const apiKey = await createApiKeyHandler({
|
||||
ctx: {
|
||||
user: {
|
||||
id: authUserId,
|
||||
},
|
||||
},
|
||||
input: {
|
||||
note: createApiKeyInput.note,
|
||||
neverExpires: !!createApiKeyInput.apiKeyNeverExpires,
|
||||
expiresAt: apiKeyExpiresAt,
|
||||
teamId: createApiKeyInput.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
async refreshApiKey(authUserId: number, apiKey: string, refreshApiKeyInput: RefreshApiKeyInput) {
|
||||
const strippedApiKey = stripApiKey(apiKey, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = hashAPIKey(strippedApiKey);
|
||||
const apiKeyInDb = await this.apiKeysRepository.getApiKeyFromHash(apiKeyHash);
|
||||
if (!apiKeyInDb) {
|
||||
throw new UnauthorizedException("ApiKeysService - provided api key is not valid.");
|
||||
}
|
||||
|
||||
const newApiKey = await this.createApiKey(authUserId, {
|
||||
...refreshApiKeyInput,
|
||||
note: apiKeyInDb.note || undefined,
|
||||
teamId: apiKeyInDb.teamId || undefined,
|
||||
});
|
||||
|
||||
await this.apiKeysRepository.deleteById(apiKeyInDb.id);
|
||||
|
||||
return newApiKey;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
@@ -122,9 +122,10 @@ export class EventTypesAtomService {
|
||||
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
|
||||
await this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
|
||||
const bookingFields = [...(body.bookingFields || [])];
|
||||
const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined;
|
||||
|
||||
if (
|
||||
bookingFields?.length &&
|
||||
!bookingFields.find((field) => field.type === "email") &&
|
||||
!bookingFields.find((field) => field.type === "phone")
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||
import { ApiKeysModule } from "@/modules/api-keys/api-keys.module";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
@@ -17,7 +17,7 @@ import { PassportModule } from "@nestjs/passport";
|
||||
imports: [
|
||||
PassportModule,
|
||||
RedisModule,
|
||||
ApiKeyModule,
|
||||
ApiKeysModule,
|
||||
UsersModule,
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
export const GetOptionalUser = createParamDecorator<
|
||||
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user as ApiAuthGuardUser;
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
...prev,
|
||||
[curr]: user[curr],
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (data) {
|
||||
return user[data];
|
||||
}
|
||||
|
||||
return user;
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createParamDecorator, ExecutionContext, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
export const GetOrgId = createParamDecorator((_: unknown, ctx: ExecutionContext): number => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const organizationId = request.organizationId;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException("GetOrgId decorator: Organization ID not found");
|
||||
}
|
||||
|
||||
return organizationId;
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NO_AUTH_PROVIDED_MESSAGE } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
|
||||
export class OptionalApiAuthGuard extends ApiAuthGuard {
|
||||
handleRequest(error: Error, user: any) {
|
||||
// note(Lauris): optional means that auth is not required but if it is invalid then still throw error.
|
||||
const noAuthProvided = error && error.message === NO_AUTH_PROVIDED_MESSAGE;
|
||||
if (user || noAuthProvided || !error) {
|
||||
return user || null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
@@ -20,10 +20,10 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const organizationId = this.getOrganizationId(context);
|
||||
|
||||
if (!user || !organizationId) {
|
||||
throw new ForbiddenException("No organization associated with the user.");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - No organization associated with the user.");
|
||||
}
|
||||
|
||||
await this.isPlatform(organizationId);
|
||||
@@ -40,13 +40,25 @@ 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 (SHP).");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - Organization is not a platform (SHP).");
|
||||
}
|
||||
}
|
||||
|
||||
getOrganizationId(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const authMethodOrganizationId = request.organizationId;
|
||||
if (authMethodOrganizationId) return authMethodOrganizationId;
|
||||
|
||||
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
return userOrganizationId;
|
||||
}
|
||||
|
||||
isMembershipAccepted(accepted: boolean) {
|
||||
if (!accepted) {
|
||||
throw new ForbiddenException(`User has not accepted membership in the organization.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User has not accepted membership in the organization.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +69,9 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
|
||||
const hasRequiredRole = allowedRoles.includes(membershipRole);
|
||||
if (!hasRequiredRole) {
|
||||
throw new ForbiddenException(`User must have one of the roles: ${allowedRoles.join(", ")}.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User must have one of the roles: ${allowedRoles.join(", ")}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsManagedOrgInManagerOrg implements CanActivate {
|
||||
constructor(private managedOrganizationsRepository: ManagedOrganizationsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { managedOrganization: Team }>();
|
||||
const managedOrgId: string = request.params.managedOrganizationId;
|
||||
const managerOrgId: string = request.params.orgId;
|
||||
|
||||
if (!managerOrgId) {
|
||||
throw new ForbiddenException("No manager org id found in request params.");
|
||||
}
|
||||
|
||||
if (!managedOrgId) {
|
||||
throw new ForbiddenException("No managed org id found in request params.");
|
||||
}
|
||||
|
||||
const managedOrganization = await this.managedOrganizationsRepository.getByManagerManagedIds(
|
||||
Number(managerOrgId),
|
||||
Number(managedOrgId)
|
||||
);
|
||||
|
||||
if (!managedOrganization) {
|
||||
throw new NotFoundException(
|
||||
`Managed organization with id=${managedOrgId} within manager organization with id=${managerOrgId} not found.`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsRoutingFormInTeam implements CanActivate {
|
||||
constructor(private routingFormsRepository: RoutingFormsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
|
||||
const teamId: string = request.params.teamId;
|
||||
const routingFormId: string = request.params.routingFormId;
|
||||
|
||||
if (!routingFormId) {
|
||||
throw new ForbiddenException("No routing form id found in request params.");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new ForbiddenException("No team id found in request params.");
|
||||
}
|
||||
|
||||
const routingForm = await this.routingFormsRepository.getTeamRoutingForm(Number(teamId), routingFormId);
|
||||
|
||||
if (!routingForm) {
|
||||
throw new NotFoundException(
|
||||
`Team with id=(${teamId}) routing form with id=(${routingFormId}) not found.`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import appConfig from "@/config/app";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
@@ -9,6 +10,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -16,6 +18,7 @@ import { ConfigModule } from "@nestjs/config";
|
||||
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import { createRequest } from "node-mocks-http";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -27,6 +30,7 @@ import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { ApiAuthGuardRequest } from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -35,7 +39,9 @@ describe("ApiAuthStrategy", () => {
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let organization: Team;
|
||||
let organizationTwo: Team;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let oAuthClientTwo: PlatformOAuthClient;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
@@ -65,7 +71,8 @@ describe("ApiAuthStrategy", () => {
|
||||
ConfigService,
|
||||
OAuthFlowService,
|
||||
UsersRepository,
|
||||
ApiKeyRepository,
|
||||
UsersService,
|
||||
ApiKeysRepository,
|
||||
DeploymentsService,
|
||||
OAuthClientRepository,
|
||||
PrismaReadService,
|
||||
@@ -84,7 +91,11 @@ describe("ApiAuthStrategy", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-${randomString()}` });
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||
organizationTwo = await teamRepositoryFixture.create({
|
||||
name: `api-auth-organization-2-${randomString()}`,
|
||||
});
|
||||
|
||||
validApiKeyUser = await userRepositoryFixture.create({
|
||||
email: validApiKeyEmail,
|
||||
});
|
||||
@@ -103,6 +114,13 @@ describe("ApiAuthStrategy", () => {
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: validOAuthEmail,
|
||||
user: { connect: { id: validOAuthUser.id } },
|
||||
organization: { connect: { id: organizationTwo.id } },
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
@@ -110,6 +128,7 @@ describe("ApiAuthStrategy", () => {
|
||||
permissions: 32,
|
||||
};
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||
oAuthClientTwo = await oAuthClientRepositoryFixture.create(organizationTwo.id, data, "secret");
|
||||
});
|
||||
|
||||
describe("authenticate with strategy", () => {
|
||||
@@ -119,25 +138,68 @@ describe("ApiAuthStrategy", () => {
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const user = await strategy.accessTokenStrategy(accessToken);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.ACCESS_TOKEN;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.accessTokenStrategy(accessToken, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validAccessTokenUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key", async () => {
|
||||
it("should return user associated with valid api key and correctly set organizationId for org1", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organization.id
|
||||
);
|
||||
|
||||
const user = await strategy.apiKeyStrategy(keyString);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key and correctly set organizationId for org2", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organizationTwo.id
|
||||
);
|
||||
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client", async () => {
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client for org2", async () => {
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClientTwo.id, oAuthClientTwo.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should throw 401 if api key is invalid", async () => {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -18,7 +19,13 @@ import { getToken } from "next-auth/jwt";
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
export type ApiAuthGuardRequest = Request & {
|
||||
authMethod: AuthMethods;
|
||||
organizationId: number | null;
|
||||
user: ApiAuthGuardUser;
|
||||
};
|
||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
constructor(
|
||||
@@ -27,14 +34,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
private readonly oauthFlowService: OAuthFlowService,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly oauthRepository: OAuthClientRepository,
|
||||
private readonly profilesRepository: ProfilesRepository
|
||||
private readonly profilesRepository: ProfilesRepository,
|
||||
private readonly usersService: UsersService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request & { authMethod: AuthMethods }) {
|
||||
async authenticate(request: ApiAuthGuardRequest) {
|
||||
try {
|
||||
const { params } = request;
|
||||
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||
@@ -43,7 +51,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (oAuthClientId && oAuthClientSecret) {
|
||||
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
||||
}
|
||||
|
||||
if (bearerToken) {
|
||||
@@ -51,7 +59,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? AuthMethods["API_KEY"]
|
||||
: AuthMethods["ACCESS_TOKEN"];
|
||||
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
||||
}
|
||||
|
||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||
@@ -59,12 +67,10 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (nextAuthToken) {
|
||||
request.authMethod = AuthMethods["NEXT_AUTH"];
|
||||
return await this.authenticateNextAuth(nextAuthToken);
|
||||
return await this.authenticateNextAuth(nextAuthToken, request);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers"
|
||||
);
|
||||
throw new UnauthorizedException(NO_AUTH_PROVIDED_MESSAGE);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return this.error(err);
|
||||
@@ -75,8 +81,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateNextAuth(token: { email?: string | null }) {
|
||||
const user = await this.nextAuthStrategy(token);
|
||||
async authenticateNextAuth(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
const user = await this.nextAuthStrategy(token, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
@@ -87,12 +93,16 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
};
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
async authenticateOAuthClient(
|
||||
oAuthClientId: string,
|
||||
oAuthClientSecret: string,
|
||||
request: ApiAuthGuardRequest
|
||||
) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string, request: ApiAuthGuardRequest) {
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!client) {
|
||||
@@ -115,14 +125,20 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException("No user associated with the provided OAuth client");
|
||||
}
|
||||
|
||||
request.organizationId = client.organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async authenticateBearerToken(authString: string, requestOrigin: string | undefined) {
|
||||
async authenticateBearerToken(
|
||||
authString: string,
|
||||
request: ApiAuthGuardRequest,
|
||||
requestOrigin: string | undefined
|
||||
) {
|
||||
try {
|
||||
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? await this.apiKeyStrategy(authString)
|
||||
: await this.accessTokenStrategy(authString, requestOrigin);
|
||||
? await this.apiKeyStrategy(authString, request)
|
||||
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
||||
|
||||
if (!user) {
|
||||
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||
@@ -139,7 +155,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async apiKeyStrategy(apiKey: string) {
|
||||
async apiKeyStrategy(apiKey: string, request: ApiAuthGuardRequest) {
|
||||
const isLicenseValid = await this.deploymentsService.checkLicense();
|
||||
if (!isLicenseValid) {
|
||||
throw new UnauthorizedException("Invalid or missing CALCOM_LICENSE_KEY environment variable");
|
||||
@@ -163,10 +179,12 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(apiKeyOwnerId);
|
||||
request.organizationId = keyData.teamId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async accessTokenStrategy(accessToken: string, origin?: string) {
|
||||
async accessTokenStrategy(accessToken: string, request: ApiAuthGuardRequest, origin?: string) {
|
||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||
if (!accessTokenValid) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
@@ -190,10 +208,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication api key not found.");
|
||||
}
|
||||
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async nextAuthStrategy(token: { email?: string | null }) {
|
||||
async nextAuthStrategy(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
if (!token.email) {
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
@@ -202,6 +227,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { ManagedOrganizationsBillingService } from "@/modules/billing/services/managed-organizations.billing.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -26,8 +27,14 @@ import { Module } from "@nestjs/common";
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
||||
exports: [BillingService, BillingRepository],
|
||||
providers: [
|
||||
BillingConfigService,
|
||||
BillingService,
|
||||
BillingRepository,
|
||||
BillingProcessor,
|
||||
ManagedOrganizationsBillingService,
|
||||
],
|
||||
exports: [BillingService, BillingRepository, ManagedOrganizationsBillingService],
|
||||
controllers: [BillingController],
|
||||
})
|
||||
export class BillingModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Process, Processor } from "@nestjs/bull";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
@@ -38,7 +38,7 @@ export class BillingRepository {
|
||||
|
||||
async updateBillingOverdue(subId: string, cusId: string, overdue: boolean) {
|
||||
try {
|
||||
return this.dbWrite.prisma.platformBilling.update({
|
||||
return this.dbWrite.prisma.platformBilling.updateMany({
|
||||
where: {
|
||||
subscriptionId: subId,
|
||||
customerId: cusId,
|
||||
|
||||
@@ -141,7 +141,6 @@ describe("Platform Billing Controller (e2e)", () => {
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BILLING_QUEUE, INCREMENT_JOB, IncrementJobDataType } from "@/modules/bi
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { InjectQueue } from "@nestjs/bull";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class ManagedOrganizationsBillingService {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createManagedOrganizationBilling(managerOrgId: number, managedOrgId: number) {
|
||||
const managerOrgBilling = await this.dbRead.prisma.platformBilling.findUnique({
|
||||
where: { id: managerOrgId },
|
||||
});
|
||||
if (!managerOrgBilling) {
|
||||
throw new NotFoundException("Manager organization billing not found.");
|
||||
}
|
||||
if (managerOrgBilling.plan !== PlatformPlan.SCALE) {
|
||||
throw new NotFoundException("Manager organization billing plan is not SCALE.");
|
||||
}
|
||||
|
||||
return this.dbWrite.prisma.platformBilling.create({
|
||||
data: {
|
||||
id: managedOrgId,
|
||||
customerId: managerOrgBilling.customerId,
|
||||
subscriptionId: managerOrgBilling.subscriptionId,
|
||||
plan: managerOrgBilling.plan,
|
||||
billingCycleStart: managerOrgBilling.billingCycleStart,
|
||||
billingCycleEnd: managerOrgBilling.billingCycleEnd,
|
||||
overdue: managerOrgBilling.overdue,
|
||||
managerBillingId: managerOrgBilling.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/controllers/teams/bookings/organizations-teams-bookings.module";
|
||||
import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [MembershipsRepository],
|
||||
exports: [MembershipsRepository],
|
||||
providers: [MembershipsRepository, MembershipsService],
|
||||
exports: [MembershipsRepository, MembershipsService],
|
||||
})
|
||||
export class MembershipsModule {}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { intersectionBy } from "lodash";
|
||||
|
||||
@Injectable()
|
||||
export class MembershipsService {
|
||||
constructor(private readonly membershipsRepository: MembershipsRepository) {}
|
||||
|
||||
async haveMembershipsInCommon(firstUserId: number, secondUserId: number) {
|
||||
const memberships = await this.membershipsInCommon(firstUserId, secondUserId);
|
||||
return memberships.length > 0;
|
||||
}
|
||||
|
||||
async membershipsInCommon(firstUserId: number, secondUserId: number) {
|
||||
const firstUserMemberships = await this.membershipsRepository.findUserMemberships(firstUserId);
|
||||
const secondUserMemberships = await this.membershipsRepository.findUserMemberships(secondUserId);
|
||||
|
||||
return intersectionBy(
|
||||
firstUserMemberships.filter((m) => m.accepted),
|
||||
secondUserMemberships.filter((m) => m.accepted),
|
||||
"teamId"
|
||||
);
|
||||
}
|
||||
}
|
||||
+64
-5
@@ -74,6 +74,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let oAuthClientEventTypesDisabled: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
@@ -84,6 +85,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let postResponseData: CreateManagedUserOutput["data"];
|
||||
let postResponseData2: CreateManagedUserOutput["data"];
|
||||
|
||||
const platformAdminEmail = `oauth-client-users-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
@@ -91,6 +93,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
|
||||
const userTimeZone = "Europe/Rome";
|
||||
|
||||
const userEmail2 = `oauth-client-users-user2-${randomString()}@api.com`;
|
||||
const userTimeZone2 = "America/New_York";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
@@ -116,6 +121,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
oAuthClientEventTypesDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
@@ -136,12 +142,13 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
async function createOAuthClient(organizationId: number, areDefaultEventTypesEnabled?: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 32,
|
||||
areDefaultEventTypesEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
@@ -185,7 +192,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it(`/POST`, async () => {
|
||||
it(`/POST with default event types`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmail,
|
||||
timeZone: userTimeZone,
|
||||
@@ -218,14 +225,55 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(responseBody.data.user.email);
|
||||
await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email);
|
||||
await userHasDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(userEmail: string) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClient.id);
|
||||
it(`/POST without default event types`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmail2,
|
||||
timeZone: userTimeZone2,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Alice Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClientEventTypesDisabled.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClientEventTypesDisabled.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
|
||||
postResponseData2 = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
getOAuthUserEmail(oAuthClientEventTypesDisabled.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart);
|
||||
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
|
||||
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
|
||||
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
await userConnectedToOAuth(oAuthClientEventTypesDisabled.id, responseBody.data.user.email);
|
||||
await userDoesNotHaveDefaultEventTypes(responseBody.data.user.id);
|
||||
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
|
||||
await userHasOnlyOneSchedule(responseBody.data.user.id);
|
||||
});
|
||||
|
||||
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string) {
|
||||
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
|
||||
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
|
||||
|
||||
expect(oAuthUsers?.length).toEqual(1);
|
||||
@@ -251,6 +299,11 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
async function userDoesNotHaveDefaultEventTypes(userId: number) {
|
||||
const defaultEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
|
||||
expect(defaultEventTypes?.length).toEqual(0);
|
||||
}
|
||||
|
||||
async function userHasDefaultSchedule(userId: number, scheduleId: number | null) {
|
||||
expect(scheduleId).toBeDefined();
|
||||
expect(scheduleId).not.toBeNull();
|
||||
@@ -358,12 +411,18 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await userRepositoryFixture.delete(postResponseData.user.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(postResponseData2.user.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
} catch (e) {
|
||||
|
||||
+4
-7
@@ -85,14 +85,11 @@ export class OAuthClientUsersController {
|
||||
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
|
||||
);
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth Client with ID ${oAuthClientId} not found`);
|
||||
}
|
||||
|
||||
const isPlatformManaged = true;
|
||||
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
|
||||
oAuthClientId,
|
||||
body,
|
||||
isPlatformManaged,
|
||||
client?.organizationId
|
||||
);
|
||||
const { user, tokens } = await this.oAuthClientUsersService.createOAuthClientUser(client, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
+60
-18
@@ -3,8 +3,7 @@ import { AppModule } from "@/app.module";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.strategy";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -15,14 +14,27 @@ import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { NextAuthMockStrategy } from "test/mocks/next-auth-mock.strategy";
|
||||
import { ApiAuthMockStrategy } from "test/mocks/api-auth-mock.strategy";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withNextAuth } from "test/utils/withNextAuth";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import {
|
||||
APPS_READ,
|
||||
APPS_WRITE,
|
||||
BOOKING_READ,
|
||||
BOOKING_WRITE,
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
PROFILE_READ,
|
||||
PROFILE_WRITE,
|
||||
SCHEDULE_READ,
|
||||
SCHEDULE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import type { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("OAuth Clients Endpoints", () => {
|
||||
@@ -72,15 +84,15 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
|
||||
})
|
||||
).compile();
|
||||
const strategy = moduleRef.get(NextAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(NextAuthMockStrategy);
|
||||
const strategy = moduleRef.get(ApiAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(ApiAuthMockStrategy);
|
||||
usersFixtures = new UserRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
teamFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
@@ -123,6 +135,7 @@ describe("OAuth Clients Endpoints", () => {
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let teamFixtures: TeamRepositoryFixture;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
|
||||
let user: User;
|
||||
let org: Team;
|
||||
@@ -130,19 +143,20 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const userEmail = `oauth-clients-user-${randomString()}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withNextAuth(
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, OAuthClientModule, UsersModule, AuthModule, PrismaModule],
|
||||
})
|
||||
).compile();
|
||||
const strategy = moduleRef.get(NextAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(NextAuthMockStrategy);
|
||||
const strategy = moduleRef.get(ApiAuthStrategy);
|
||||
expect(strategy).toBeInstanceOf(ApiAuthMockStrategy);
|
||||
usersFixtures = new UserRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
teamFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
|
||||
user = await usersFixtures.create({
|
||||
email: userEmail,
|
||||
@@ -218,17 +232,17 @@ describe("OAuth Clients Endpoints", () => {
|
||||
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||
});
|
||||
|
||||
it(`/POST`, () => {
|
||||
it(`/POST`, async () => {
|
||||
const body: CreateOAuthClientInput = {
|
||||
name: oAuthClientName,
|
||||
redirectUris: ["http://test-oauth-client.com"],
|
||||
permissions: 32,
|
||||
permissions: ["BOOKING_READ", "PROFILE_WRITE"],
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/oauth-clients")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
@@ -239,6 +253,9 @@ describe("OAuth Clients Endpoints", () => {
|
||||
clientId: responseBody.data.clientId,
|
||||
clientSecret: responseBody.data.clientSecret,
|
||||
};
|
||||
const dbOAuthClient = await oAuthClientsRepositoryFixture.get(client.clientId);
|
||||
expect(dbOAuthClient).toBeDefined();
|
||||
expect(dbOAuthClient?.permissions).toEqual(BOOKING_READ + PROFILE_WRITE);
|
||||
});
|
||||
});
|
||||
it(`/GET`, () => {
|
||||
@@ -291,7 +308,18 @@ describe("OAuth Clients Endpoints", () => {
|
||||
let membership: Membership;
|
||||
let client: { clientId: string; clientSecret: string };
|
||||
const oAuthClientName = `oauth-clients-owner-${randomString()}`;
|
||||
const oAuthClientPermissions = 32;
|
||||
const oAuthClientPermissions: CreateOAuthClientInput["permissions"] = [
|
||||
"EVENT_TYPE_READ",
|
||||
"EVENT_TYPE_WRITE",
|
||||
"BOOKING_READ",
|
||||
"BOOKING_WRITE",
|
||||
"SCHEDULE_READ",
|
||||
"SCHEDULE_WRITE",
|
||||
"APPS_READ",
|
||||
"APPS_WRITE",
|
||||
"PROFILE_READ",
|
||||
"PROFILE_WRITE",
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
membership = await membershipFixtures.addUserToOrg(user, org, "OWNER", true);
|
||||
@@ -301,13 +329,13 @@ describe("OAuth Clients Endpoints", () => {
|
||||
const body: CreateOAuthClientInput = {
|
||||
name: oAuthClientName,
|
||||
redirectUris: ["http://test-oauth-client.com"],
|
||||
permissions: 32,
|
||||
permissions: ["*"],
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/oauth-clients")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<{ clientId: string; clientSecret: string }> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
@@ -318,6 +346,20 @@ describe("OAuth Clients Endpoints", () => {
|
||||
clientId: responseBody.data.clientId,
|
||||
clientSecret: responseBody.data.clientSecret,
|
||||
};
|
||||
const dbOAuthClient = await oAuthClientsRepositoryFixture.get(client.clientId);
|
||||
expect(dbOAuthClient).toBeDefined();
|
||||
expect(dbOAuthClient?.permissions).toEqual(
|
||||
EVENT_TYPE_READ +
|
||||
EVENT_TYPE_WRITE +
|
||||
BOOKING_READ +
|
||||
BOOKING_WRITE +
|
||||
SCHEDULE_READ +
|
||||
SCHEDULE_WRITE +
|
||||
APPS_READ +
|
||||
APPS_WRITE +
|
||||
PROFILE_READ +
|
||||
PROFILE_WRITE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+18
-33
@@ -1,8 +1,7 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { GetOrgId } from "@/modules/auth/decorators/get-org-id/get-org-id.decorator";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output";
|
||||
@@ -10,11 +9,9 @@ import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controller
|
||||
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-guard";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Body,
|
||||
@@ -33,16 +30,14 @@ import {
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags as DocsTags,
|
||||
ApiExcludeController as DocsExcludeController,
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import { Pagination } from "@calcom/platform-types";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
|
||||
|
||||
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
||||
@@ -51,15 +46,16 @@ Second, make sure that the logged in user has organizationId set to pass the Org
|
||||
path: "/v2/oauth-clients",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@ApiTags("OAuth Clients")
|
||||
@UseGuards(ApiAuthGuard, OrganizationRolesGuard)
|
||||
export class OAuthClientsController {
|
||||
private readonly logger = new Logger("OAuthClientController");
|
||||
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
private readonly oAuthClientsService: OAuthClientsService,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly teamsRepository: OrganizationsRepository,
|
||||
private usersService: UsersService
|
||||
private readonly teamsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@@ -71,10 +67,9 @@ export class OAuthClientsController {
|
||||
type: CreateOAuthClientResponseDto,
|
||||
})
|
||||
async createOAuthClient(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@GetOrgId() organizationId: number,
|
||||
@Body() body: CreateOAuthClientInput
|
||||
): Promise<CreateOAuthClientResponseDto> {
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
this.logger.log(
|
||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||
);
|
||||
@@ -84,14 +79,11 @@ export class OAuthClientsController {
|
||||
throw new BadRequestException("Team is not subscribed, cannot create an OAuth Client.");
|
||||
}
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
|
||||
const oAuthClientCredentials = await this.oAuthClientsService.createOAuthClient(organizationId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
clientId: id,
|
||||
clientSecret: secret,
|
||||
},
|
||||
data: oAuthClientCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,10 +91,8 @@ export class OAuthClientsController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
|
||||
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
}
|
||||
|
||||
@@ -112,11 +102,7 @@ export class OAuthClientsController {
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
const client = await this.oauthClientRepository.getOAuthClient(clientId);
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
|
||||
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -149,8 +135,7 @@ export class OAuthClientsController {
|
||||
@Body() body: UpdateOAuthClientInput
|
||||
): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`For client ${clientId} updating OAuth Client with data: ${JSON.stringify(body)}`);
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, body);
|
||||
|
||||
const client = await this.oAuthClientsService.updateOAuthClient(clientId, body);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
@@ -161,7 +146,7 @@ export class OAuthClientsController {
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
|
||||
const client = await this.oauthClientRepository.deleteOAuthClient(clientId);
|
||||
const client = await this.oAuthClientsService.deleteOAuthClient(clientId);
|
||||
return { status: SUCCESS_STATUS, data: client };
|
||||
}
|
||||
|
||||
|
||||
+4
-17
@@ -1,22 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, ValidateNested, IsNotEmptyObject, IsString } from "class-validator";
|
||||
import { IsIn, ValidateNested, IsNotEmptyObject } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS, REDIRECT_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class DataDto {
|
||||
@ApiProperty({
|
||||
example: "clsx38nbl0001vkhlwin9fmt0",
|
||||
})
|
||||
@IsString()
|
||||
clientId!: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi",
|
||||
})
|
||||
@IsString()
|
||||
clientSecret!: string;
|
||||
}
|
||||
import { CreateOAuthClientOutput } from "@calcom/platform-types";
|
||||
|
||||
export class CreateOAuthClientResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@@ -31,8 +18,8 @@ export class CreateOAuthClientResponseDto {
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => DataDto)
|
||||
data!: DataDto;
|
||||
@Type(() => CreateOAuthClientOutput)
|
||||
data!: CreateOAuthClientOutput;
|
||||
}
|
||||
|
||||
export class CreateOauthClientRedirect {
|
||||
|
||||
+3
-47
@@ -1,53 +1,9 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsDate,
|
||||
IsNotEmptyObject,
|
||||
} from "class-validator";
|
||||
import { ValidateNested, IsEnum, IsNotEmptyObject } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class PlatformOAuthClientDto {
|
||||
@ApiProperty({ example: "clsx38nbl0001vkhlwin9fmt0" })
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: "MyClient" })
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: "secretValue" })
|
||||
@IsString()
|
||||
secret!: string;
|
||||
|
||||
@ApiProperty({ example: 3 })
|
||||
@IsNumber()
|
||||
permissions!: number;
|
||||
|
||||
@ApiPropertyOptional({ example: "https://example.com/logo.png" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
logo!: string | null;
|
||||
|
||||
@ApiProperty({ example: ["https://example.com/callback"] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
redirectUris!: string[];
|
||||
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsNumber()
|
||||
organizationId!: number;
|
||||
|
||||
@ApiProperty({ example: "2024-03-23T08:33:21.851Z", type: Date })
|
||||
@IsDate()
|
||||
createdAt!: Date;
|
||||
}
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetOAuthClientResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { PlatformOAuthClientDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetOAuthClientsResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { ApiAuthGuardRequest, ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
@@ -15,25 +16,35 @@ export class OAuthClientGuard implements CanActivate {
|
||||
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { user: ApiAuthGuardUser }>();
|
||||
const request = context.switchToHttp().getRequest<ApiAuthGuardRequest>();
|
||||
const organizationId = this.getOrganizationId(context);
|
||||
const user: ApiAuthGuardUser = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const oAuthClientId = request.params.clientId;
|
||||
|
||||
if (!oAuthClientId) {
|
||||
throw new ForbiddenException("No OAuth client associated with the request.");
|
||||
throw new ForbiddenException("OAuthClientGuard - No OAuth client associated with the request.");
|
||||
}
|
||||
|
||||
if (!user || !organizationId) {
|
||||
throw new ForbiddenException("No organization associated with the user.");
|
||||
throw new ForbiddenException("OAuthClientGuard - No organization associated with the user.");
|
||||
}
|
||||
|
||||
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!oAuthClient) {
|
||||
throw new NotFoundException("OAuth client not found.");
|
||||
throw new NotFoundException("OAuthClientGuard - OAuth client not found.");
|
||||
}
|
||||
|
||||
return Boolean(user.isSystemAdmin || oAuthClient.organizationId === organizationId);
|
||||
}
|
||||
|
||||
getOrganizationId(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const authMethodOrganizationId = request.organizationId;
|
||||
if (authMethodOrganizationId) return authMethodOrganizationId;
|
||||
|
||||
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
return userOrganizationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oaut
|
||||
import { OAuthFlowController } from "@/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthClientsInputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-input.service";
|
||||
import { OAuthClientsOutputService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients-output.service";
|
||||
import { OAuthClientsService } from "@/modules/oauth-clients/services/oauth-clients/oauth-clients.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
@@ -18,6 +21,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -40,6 +44,10 @@ import { Global, Module } from "@nestjs/common";
|
||||
OAuthFlowService,
|
||||
OAuthClientUsersService,
|
||||
OrganizationsTeamsService,
|
||||
OAuthClientsService,
|
||||
OAuthClientsInputService,
|
||||
OAuthClientsOutputService,
|
||||
JwtService,
|
||||
],
|
||||
controllers: [OAuthClientUsersController, OAuthClientsController, OAuthFlowController],
|
||||
exports: [OAuthClientRepository],
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import type { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
import type { PlatformOAuthClient, Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientRepository {
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private jwtService: JwtService
|
||||
) {}
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createOAuthClient(organizationId: number, data: CreateOAuthClientInput) {
|
||||
async createOAuthClient(
|
||||
organizationId: number,
|
||||
data: Omit<Prisma.PlatformOAuthClientCreateInput, "organization">
|
||||
) {
|
||||
return this.dbWrite.prisma.platformOAuthClient.create({
|
||||
data: {
|
||||
...data,
|
||||
secret: this.jwtService.sign(data),
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
@@ -86,7 +81,7 @@ export class OAuthClientRepository {
|
||||
|
||||
async updateOAuthClient(
|
||||
clientId: string,
|
||||
updateData: Partial<CreateOAuthClientInput>
|
||||
updateData: Prisma.PlatformOAuthClientUpdateInput
|
||||
): Promise<PlatformOAuthClient> {
|
||||
return this.dbWrite.prisma.platformOAuthClient.update({
|
||||
where: { id: clientId },
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
import { User, CreationSource } from "@prisma/client";
|
||||
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@@ -16,16 +16,13 @@ export class OAuthClientUsersService {
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly organizationsTeamsService: OrganizationsTeamsService
|
||||
private readonly schedulesService: SchedulesService_2024_04_15
|
||||
) {}
|
||||
|
||||
async createOauthClientUser(
|
||||
oAuthClientId: string,
|
||||
body: CreateManagedUserInput,
|
||||
isPlatformManaged: boolean,
|
||||
organizationId?: number
|
||||
) {
|
||||
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
|
||||
const oAuthClientId = oAuthClient.id;
|
||||
const organizationId = oAuthClient.organizationId;
|
||||
|
||||
const existingUser = await this.getExistingUserByEmail(oAuthClientId, body.email);
|
||||
if (existingUser) {
|
||||
throw new ConflictException(
|
||||
@@ -35,7 +32,9 @@ export class OAuthClientUsersService {
|
||||
|
||||
let user: User;
|
||||
if (!organizationId) {
|
||||
throw new BadRequestException("You cannot create a managed user outside of an organization");
|
||||
throw new BadRequestException(
|
||||
"You cannot create a managed user outside of an organization - the OAuth client does not belong to any organization."
|
||||
);
|
||||
} else {
|
||||
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
user = (
|
||||
@@ -57,7 +56,7 @@ export class OAuthClientUsersService {
|
||||
autoAccept: true,
|
||||
},
|
||||
},
|
||||
isPlatformManaged,
|
||||
isPlatformManaged: true,
|
||||
timeFormat: body.timeFormat,
|
||||
weekStart: body.weekStart,
|
||||
timeZone: body.timeZone,
|
||||
@@ -79,7 +78,9 @@ export class OAuthClientUsersService {
|
||||
user.id
|
||||
);
|
||||
|
||||
await this.eventTypesService.createUserDefaultEventTypes(user.id);
|
||||
if (oAuthClient.areDefaultEventTypesEnabled) {
|
||||
await this.eventTypesService.createUserDefaultEventTypes(user.id);
|
||||
}
|
||||
|
||||
if (body.timeZone) {
|
||||
const defaultSchedule = await this.schedulesService.createUserDefaultSchedule(user.id, body.timeZone);
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { PERMISSION_MAP } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsInputService {
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
transformCreateOAuthClientInput(input: CreateOAuthClientInput) {
|
||||
const transformed = {
|
||||
...input,
|
||||
permissions: this.transformPermissions(input.permissions),
|
||||
};
|
||||
|
||||
return {
|
||||
...transformed,
|
||||
secret: this.jwtService.sign(transformed),
|
||||
};
|
||||
}
|
||||
|
||||
transformPermissions(permissions: Array<keyof typeof PERMISSION_MAP | "*">): number {
|
||||
const values = permissions.includes("*")
|
||||
? Object.values(PERMISSION_MAP)
|
||||
: permissions.map((p) => PERMISSION_MAP[p as keyof typeof PERMISSION_MAP]);
|
||||
return values.reduce((acc, val) => acc | val, 0);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { PERMISSIONS, PERMISSION_MAP } from "@calcom/platform-constants";
|
||||
import { PlatformOAuthClientDto } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsOutputService {
|
||||
transformPermissions(permissionsNumber: number): Array<keyof typeof PERMISSION_MAP> {
|
||||
const permissionNumbers = PERMISSIONS.filter(
|
||||
(permission) => (permissionsNumber & permission) === permission
|
||||
);
|
||||
|
||||
return permissionNumbers.map(
|
||||
(permission) =>
|
||||
Object.entries(PERMISSION_MAP).find(
|
||||
([_, value]) => value === permission
|
||||
)?.[0] as keyof typeof PERMISSION_MAP
|
||||
);
|
||||
}
|
||||
|
||||
transformOAuthClient(client: PlatformOAuthClient): PlatformOAuthClientDto {
|
||||
return {
|
||||
id: client.id,
|
||||
name: client.name,
|
||||
secret: client.secret,
|
||||
permissions: this.transformPermissions(client.permissions),
|
||||
logo: client.logo,
|
||||
redirectUris: client.redirectUris,
|
||||
organizationId: client.organizationId,
|
||||
createdAt: client.createdAt,
|
||||
bookingRedirectUri: client.bookingRedirectUri ?? undefined,
|
||||
bookingCancelRedirectUri: client.bookingCancelRedirectUri ?? undefined,
|
||||
bookingRescheduleRedirectUri: client.bookingRescheduleRedirectUri ?? undefined,
|
||||
areEmailsEnabled: client.areEmailsEnabled,
|
||||
areDefaultEventTypesEnabled: client.areDefaultEventTypesEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
transformOAuthClients(clients: PlatformOAuthClient[]): PlatformOAuthClientDto[] {
|
||||
return clients.map((client) => this.transformOAuthClient(client));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
|
||||
|
||||
import { OAuthClientRepository } from "../../oauth-client.repository";
|
||||
import { OAuthClientsInputService } from "./oauth-clients-input.service";
|
||||
import { OAuthClientsOutputService } from "./oauth-clients-output.service";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientsService {
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
private readonly oauthClientsInputService: OAuthClientsInputService,
|
||||
private readonly oauthClientsOutputService: OAuthClientsOutputService
|
||||
) {}
|
||||
|
||||
async createOAuthClient(organizationId: number, input: CreateOAuthClientInput) {
|
||||
const transformedInput = this.oauthClientsInputService.transformCreateOAuthClientInput(input);
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(
|
||||
organizationId,
|
||||
transformedInput
|
||||
);
|
||||
|
||||
return {
|
||||
clientId: id,
|
||||
clientSecret: secret,
|
||||
};
|
||||
}
|
||||
|
||||
async getOAuthClients(organizationId: number) {
|
||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||
return this.oauthClientsOutputService.transformOAuthClients(clients);
|
||||
}
|
||||
|
||||
async getOAuthClientById(clientId: string) {
|
||||
const client = await this.oauthClientRepository.getOAuthClient(clientId);
|
||||
if (!client) {
|
||||
throw new NotFoundException(`OAuth client with ID ${clientId} not found`);
|
||||
}
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
|
||||
async updateOAuthClient(clientId: string, input: UpdateOAuthClientInput) {
|
||||
const client = await this.oauthClientRepository.updateOAuthClient(clientId, input);
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
|
||||
async deleteOAuthClient(clientId: string) {
|
||||
const client = await this.oauthClientRepository.deleteOAuthClient(clientId);
|
||||
return this.oauthClientsOutputService.transformOAuthClient(client);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
+7
-7
@@ -6,16 +6,16 @@ import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.g
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/create-organization-attributes.output";
|
||||
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/delete-organization-attributes.output";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/create-organization-attributes.output";
|
||||
import { DeleteOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/delete-organization-attributes.output";
|
||||
import {
|
||||
GetOrganizationAttributesOutput,
|
||||
GetSingleAttributeOutput,
|
||||
} from "@/modules/organizations/outputs/attributes/get-organization-attributes.output";
|
||||
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/outputs/attributes/update-organization-attributes.output";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/services/attributes/organization-attributes.service";
|
||||
} from "@/modules/organizations/attributes/index/outputs/get-organization-attributes.output";
|
||||
import { UpdateOrganizationAttributesOutput } from "@/modules/organizations/attributes/index/outputs/update-organization-attributes.output";
|
||||
import { OrganizationAttributesService } from "@/modules/organizations/attributes/index/services/organization-attributes.service";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/inputs/attributes/update-organization-attribute.input";
|
||||
import { CreateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/create-organization-attribute.input";
|
||||
import { UpdateOrganizationAttributeInput } from "@/modules/organizations/attributes/index/inputs/update-organization-attribute.input";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { ValidateNested } from "class-validator";
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { Attribute } from "@/modules/organizations/attributes/index/outputs/attribute.output";
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsOptional, ValidateNested } from "class-validator";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user