Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5ef3e1d72 | ||
|
|
0c19606e19 | ||
|
|
b8219c273d | ||
|
|
f75415e414 | ||
|
|
4bf5e3cdf5 | ||
|
|
4181b5988a | ||
|
|
c41ba9d984 | ||
|
|
2a578ea9d5 | ||
|
|
c96827e1c0 | ||
|
|
3f5363038c | ||
|
|
e2de28b933 | ||
|
|
5d88858995 | ||
|
|
82a500aeaa | ||
|
|
2408337c78 | ||
|
|
119edb5389 | ||
|
|
ae4474cd42 | ||
|
|
3290ae526a | ||
|
|
16a7cf6d04 | ||
|
|
ab2ab31eef | ||
|
|
a08ae43533 | ||
|
|
d60b6c6a6d | ||
|
|
9085287fc6 | ||
|
|
7e2b5b12b4 | ||
|
|
3211c5bcff | ||
|
|
a75a5b72b2 | ||
|
|
509fc4b289 | ||
|
|
f63c06d68a | ||
|
|
3dff77fe59 | ||
|
|
a6f9952d75 | ||
|
|
03704b2e73 | ||
|
|
982a715c4b | ||
|
|
eaff9e8c89 | ||
|
|
e2938ac39a | ||
|
|
b92db55380 | ||
|
|
3015c5a15e | ||
|
|
2444598e38 | ||
|
|
6eadadec73 | ||
|
|
145449e7b4 | ||
|
|
1d8326ed83 | ||
|
|
59993891b8 | ||
|
|
a31d140761 | ||
|
|
b055fe182a | ||
|
|
392757fb55 | ||
|
|
3222be9171 | ||
|
|
6378729962 | ||
|
|
1c5786b66f | ||
|
|
e61a0d8081 | ||
|
|
0cb955cc0b | ||
|
|
00689fda0a | ||
|
|
bd83b36b75 | ||
|
|
05e2029bc9 | ||
|
|
9e3d369ac3 | ||
|
|
356c603e3f | ||
|
|
5afe3aa54d | ||
|
|
985f216a43 | ||
|
|
3a472997ff | ||
|
|
6dcb6aece4 | ||
|
|
dc7ee1aba2 | ||
|
|
2f9f29511b | ||
|
|
0aa58c1095 | ||
|
|
58b77b2a16 | ||
|
|
caeaf9053b | ||
|
|
fe209df2be | ||
|
|
2e7a5a84a6 | ||
|
|
410558f822 | ||
|
|
a3ceefc00e | ||
|
|
c524a3c1ef | ||
|
|
a5ba2682f6 | ||
|
|
51c91e41da | ||
|
|
cf6654dd1f | ||
|
|
d057d7d37f | ||
|
|
262738f288 | ||
|
|
27cd2457e8 | ||
|
|
b33ca2f937 | ||
|
|
82f64f7d6a | ||
|
|
8052d27d0b | ||
|
|
5720ec8038 | ||
|
|
01b1f5bcc8 | ||
|
|
953a8119db | ||
|
|
e8daa4f92f | ||
|
|
f631abfdc4 | ||
|
|
b5c6d7af91 | ||
|
|
249bcaca68 | ||
|
|
f2efd6dc28 | ||
|
|
be2f0f986e | ||
|
|
589c1dccb9 | ||
|
|
4eac6c755d | ||
|
|
131b3d0984 | ||
|
|
693356d3c4 | ||
|
|
4b717108b0 | ||
|
|
ad8c888b77 | ||
|
|
d6fe955ea8 | ||
|
|
ab9b6024bb | ||
|
|
a7e99f3674 | ||
|
|
999019c6f0 | ||
|
|
240ab60c5f | ||
|
|
e8f84779f5 | ||
|
|
08a92b46f9 | ||
|
|
877e4199e7 | ||
|
|
afd730e1c8 | ||
|
|
471f5bd7eb | ||
|
|
c676d47b7e | ||
|
|
b3536728e0 | ||
|
|
e22fec93b1 | ||
|
|
f81716fbb2 | ||
|
|
0b66ddc6db | ||
|
|
382a27f134 | ||
|
|
ac5ee225d5 | ||
|
|
d8fd6f5c25 | ||
|
|
0e333aa71c | ||
|
|
ae70786b2b | ||
|
|
2df553b58b | ||
|
|
d4afa1436d | ||
|
|
88efa0a2b2 | ||
|
|
b563e41623 | ||
|
|
f097f852ad | ||
|
|
aea8588a21 | ||
|
|
7e0c684348 | ||
|
|
21956f096d | ||
|
|
d1adfa3bf9 | ||
|
|
62976a4c77 | ||
|
|
93489709a8 | ||
|
|
050102fde2 | ||
|
|
c1112ab2ab | ||
|
|
34d4c3d775 | ||
|
|
e477e5892c | ||
|
|
ad44ca9f2a | ||
|
|
868cf91c35 | ||
|
|
8b9693ea60 | ||
|
|
935115d7c1 | ||
|
|
eae74c568c | ||
|
|
9e9dd6070d | ||
|
|
5af10a2f61 | ||
|
|
3936a0ae94 | ||
|
|
c2a3d8b8df |
+1
-4
@@ -119,7 +119,7 @@ NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` below
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
@@ -347,8 +347,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_DEVELOPER_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_ORG_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
@@ -359,7 +357,6 @@ APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKING_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_WORKFLOWS_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
|
||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_VIDEO_ENABLED=0
|
||||
|
||||
@@ -17,6 +17,7 @@ env:
|
||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||
NODE_OPTIONS: --max-old-space-size=29000
|
||||
REDIS_URL: "redis://localhost:6379"
|
||||
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Unit
|
||||
on:
|
||||
workflow_call:
|
||||
env:
|
||||
REPLEXICA_API_KEY: ${{ secrets.CI_REPLEXICA_API_KEY }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
|
||||
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"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
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"
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
undecided:
|
||||
- "@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
Vendored
@@ -6,6 +6,7 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { isAdminGuard } from "../utils/isAdmin";
|
||||
import { isLockedOrBlocked } from "../utils/isLockedOrBlocked";
|
||||
import { ScopeOfAdmin } from "../utils/scopeOfAdmin";
|
||||
|
||||
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
|
||||
@@ -24,23 +25,35 @@ export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
if (!hasValidLicense && IS_PRODUCTION) {
|
||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||
}
|
||||
// Check if the apiKey query param is provided.
|
||||
|
||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||
// remove the prefix from the user provided api_key. If no env set default to "cal_"
|
||||
|
||||
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
|
||||
// Hash the key again before matching against the database records.
|
||||
const hashedKey = hashAPIKey(strippedApiKey);
|
||||
// Check if the hashed api key exists in database.
|
||||
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
|
||||
// If cannot find any api key. Throw a 401 Unauthorized.
|
||||
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { hashedKey },
|
||||
include: {
|
||||
user: {
|
||||
select: { role: true, locked: true, email: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!apiKey) return res.status(401).json({ error: "Your API key is not valid." });
|
||||
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
|
||||
return res.status(401).json({ error: "This apiKey is expired" });
|
||||
return res.status(401).json({ error: "This API key is expired." });
|
||||
}
|
||||
if (!apiKey.userId) return res.status(404).json({ error: "No user found for this apiKey" });
|
||||
if (!apiKey.userId || !apiKey.user)
|
||||
return res.status(404).json({ error: "No user found for this API key." });
|
||||
|
||||
// save the user id in the request for later use
|
||||
req.userId = apiKey.userId;
|
||||
req.user = apiKey.user;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
const userIsLockedOrBlocked = await isLockedOrBlocked(req);
|
||||
|
||||
if (userIsLockedOrBlocked)
|
||||
return res.status(403).json({ error: "You are not authorized to perform this request." });
|
||||
|
||||
req.isSystemWideAdmin = isAdmin && scope === ScopeOfAdmin.SystemWide;
|
||||
req.isOrganizationOwnerOrAdmin = isAdmin && scope === ScopeOfAdmin.OrgOwnerOrAdmin;
|
||||
|
||||
@@ -6,8 +6,7 @@ import { UserPermissionRole, MembershipRole } from "@calcom/prisma/enums";
|
||||
import { ScopeOfAdmin } from "./scopeOfAdmin";
|
||||
|
||||
export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
const { userId } = req;
|
||||
const user = await prisma.user.findUnique({ where: { id: userId }, select: { role: true } });
|
||||
const { user, userId } = req;
|
||||
if (!user) return { isAdmin: false, scope: null };
|
||||
|
||||
const { role: userRole } = user;
|
||||
@@ -34,7 +33,7 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!orgOwnerOrAdminMemberships.length) return { isAdmin: false, scope: null };
|
||||
if (orgOwnerOrAdminMemberships.length > 0) return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
|
||||
return { isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin };
|
||||
return { isAdmin: false, scope: null };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
|
||||
export async function isLockedOrBlocked(req: NextApiRequest) {
|
||||
const user = req.user;
|
||||
if (!user?.email) return false;
|
||||
return user.locked || (await checkIfEmailInWatchlistController(user.email));
|
||||
}
|
||||
Vendored
+1
@@ -8,6 +8,7 @@ export declare module "next" {
|
||||
session?: Session | null;
|
||||
|
||||
userId: number;
|
||||
user?: { role: string; locked: boolean; email: string } | null;
|
||||
method: string;
|
||||
// session: { user: { id: number } };
|
||||
// query: Partial<{ [key: string]: string | string[] }>;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||
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, vi, it, expect, afterEach, beforeEach } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { isAdminGuard } from "~/lib/utils/isAdmin";
|
||||
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
|
||||
|
||||
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
|
||||
import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
@@ -20,11 +20,6 @@ afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
vi.mock("@calcom/prisma");
|
||||
vi.mock("~/lib/utils/isAdmin", () => ({
|
||||
isAdminGuard: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Verify API key", () => {
|
||||
let service: LicenseKeyService;
|
||||
|
||||
@@ -45,7 +40,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(false);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -69,7 +63,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: false, scope: null });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -91,10 +84,18 @@ describe("Verify API key", () => {
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.apiKey.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
userId: 2,
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "admin@example.com",
|
||||
role: UserPermissionRole.ADMIN,
|
||||
locked: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const middleware = {
|
||||
@@ -102,7 +103,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.SystemWide });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -125,10 +125,36 @@ describe("Verify API key", () => {
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
|
||||
prismaMock.apiKey.findUnique.mockResolvedValue({
|
||||
id: 1,
|
||||
userId: 2,
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "org-admin@acme.com",
|
||||
role: UserPermissionRole.USER,
|
||||
locked: false,
|
||||
teams: {
|
||||
create: {
|
||||
accepted: true,
|
||||
role: MembershipRole.OWNER,
|
||||
team: {
|
||||
create: {
|
||||
name: "ACME",
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
isAdminAPIEnabled: true,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const middleware = {
|
||||
@@ -136,7 +162,6 @@ describe("Verify API key", () => {
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
vi.mocked(isAdminGuard).mockResolvedValue({ isAdmin: true, scope: ScopeOfAdmin.OrgOwnerOrAdmin });
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
|
||||
@@ -149,4 +174,44 @@ describe("Verify API key", () => {
|
||||
expect(req.isSystemWideAdmin).toBe(false);
|
||||
expect(req.isOrganizationOwnerOrAdmin).toBe(true);
|
||||
});
|
||||
|
||||
it("should return 403 if user is locked or blocked", async () => {
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
body: {},
|
||||
query: {
|
||||
apiKey: "cal_test_key",
|
||||
},
|
||||
prisma,
|
||||
});
|
||||
const hashedKey = hashAPIKey("test_key");
|
||||
await prismock.apiKey.create({
|
||||
data: {
|
||||
hashedKey,
|
||||
user: {
|
||||
create: {
|
||||
email: "locked@example.com",
|
||||
role: UserPermissionRole.USER,
|
||||
locked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const middleware = {
|
||||
fn: verifyApiKey,
|
||||
};
|
||||
|
||||
vi.mocked(service.checkLicense).mockResolvedValue(true);
|
||||
|
||||
const serverNext = vi.fn((next: void) => Promise.resolve(next));
|
||||
const middlewareSpy = vi.spyOn(middleware, "fn");
|
||||
|
||||
await middleware.fn(req, res, serverNext);
|
||||
|
||||
expect(middlewareSpy).toBeCalled();
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(JSON.parse(res._getData())).toEqual({ error: "You are not authorized to perform this request." });
|
||||
expect(serverNext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("isAdmin guard", () => {
|
||||
});
|
||||
|
||||
req.userId = 0;
|
||||
req.user = undefined;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
@@ -35,6 +36,7 @@ describe("isAdmin guard", () => {
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
|
||||
req.userId = memberUser.id;
|
||||
req.user = memberUser;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
@@ -51,6 +53,7 @@ describe("isAdmin guard", () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
req.user = adminUser;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
|
||||
@@ -67,6 +70,8 @@ describe("isAdmin guard", () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
req.user = adminUser;
|
||||
|
||||
const { isAdmin, scope } = await isAdminGuard(req);
|
||||
expect(isAdmin).toBe(true);
|
||||
expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin);
|
||||
@@ -81,6 +86,8 @@ describe("isAdmin guard", () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } });
|
||||
|
||||
req.userId = adminUser.id;
|
||||
req.user = adminUser;
|
||||
|
||||
const { isAdmin } = await isAdminGuard(req);
|
||||
expect(isAdmin).toBe(false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
||||
|
||||
describe("isLockedOrBlocked", () => {
|
||||
beforeEach(async () => {
|
||||
await prismock.watchlist.createMany({
|
||||
data: [
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "spam.com",
|
||||
createdById: 1,
|
||||
},
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "blocked.com",
|
||||
createdById: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false if no user in request", async () => {
|
||||
const req = { userId: null, user: null } as any;
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if user has no email", async () => {
|
||||
const req = { userId: 123, user: { email: null } } as any;
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if user is locked", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: true,
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if user email domain is watchlisted", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: false,
|
||||
email: "test@blocked.com",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if user is not locked and email domain is not watchlisted", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: false,
|
||||
email: "test@example.com",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle email domains case-insensitively", async () => {
|
||||
const req = {
|
||||
userId: 123,
|
||||
user: {
|
||||
locked: false,
|
||||
email: "test@BLOCKED.COM",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await isLockedOrBlocked(req);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"./next.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../../../packages/types/*.d.ts",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.63",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.69",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -160,10 +160,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/")
|
||||
async createBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput_2024_04_15
|
||||
@Body() body: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<Partial<BookingResponse>>> {
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const { orgSlug, locationUrl } = body;
|
||||
req.headers["x-cal-force-slug"] = orgSlug;
|
||||
try {
|
||||
@@ -191,10 +191,10 @@ export class BookingsController_2024_04_15 {
|
||||
async cancelBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
const oAuthClientId = clientId?.toString();
|
||||
if (bookingId) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
@@ -245,10 +245,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[]
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
const oAuthClientId = clientId?.toString();
|
||||
try {
|
||||
const recurringEventId = uuidv4();
|
||||
for (const recurringEvent of req.body) {
|
||||
@@ -283,10 +283,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/instant")
|
||||
async createInstantBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput_2024_04_15
|
||||
@Body() _: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
const oAuthClientId = clientId?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
@@ -333,7 +333,7 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
|
||||
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
const res = { ...DEFAULT_PLATFORM_PARAMS };
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
// fetch oAuthClient from db and use data stored in db to set these values
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.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 { randomNumber } from "test/utils/randomNumber";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking, PlatformOAuthClient, Team, User } from "@calcom/prisma/client";
|
||||
|
||||
const suffix = randomNumber();
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Booking fields", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = `alice-${suffix}@api.com`;
|
||||
let user: User;
|
||||
|
||||
let bookingWithSplitName: Booking;
|
||||
const splitName = {
|
||||
firstName: "Oldie",
|
||||
lastName: "Goldie",
|
||||
};
|
||||
|
||||
let seatedBookingWithSplitName: Booking;
|
||||
|
||||
let eventTypeWithBookingFieldsId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: `booking fields ${suffix}` });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: `normal-booking-${randomNumber()}`, length: 60 },
|
||||
user.id
|
||||
);
|
||||
|
||||
bookingWithSplitName = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: "booking",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: event.id,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: splitName,
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie Goldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const seatedEvent = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: `seated-${randomNumber()}`, length: 60, seatsPerTimeSlot: 3 },
|
||||
user.id
|
||||
);
|
||||
|
||||
seatedBookingWithSplitName = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 15, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: "seated-booking",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: seatedEvent.id,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: splitName,
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie Goldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
bookingSeat: {
|
||||
create: {
|
||||
referenceUid: "unique-seat-uid",
|
||||
data: {
|
||||
responses: {
|
||||
email: "oldie@gmail.com",
|
||||
name: {
|
||||
firstName: "Oldie",
|
||||
lastName: "Goldie",
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
booking: {
|
||||
connect: {
|
||||
uid: "seated-booking",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypeWithBookingFields = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "peer coding with booking fields",
|
||||
slug: `with-custom-booking-fields-${randomNumber()}`,
|
||||
length: 60,
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
label: "",
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
variant: "fullName",
|
||||
editable: "system",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
defaultLabel: "your_name",
|
||||
variantsConfig: {
|
||||
variants: {
|
||||
fullName: {
|
||||
fields: [
|
||||
{
|
||||
name: "fullName",
|
||||
type: "text",
|
||||
label: "your_name",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
firstAndLastName: {
|
||||
fields: [
|
||||
{
|
||||
name: "firstName",
|
||||
type: "text",
|
||||
label: "name",
|
||||
required: true,
|
||||
placeholder: "lauris",
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
type: "text",
|
||||
label: "surname",
|
||||
required: true,
|
||||
placeholder: "skraucis",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system",
|
||||
required: true,
|
||||
defaultLabel: "email_address",
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
type: "radioInput",
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system",
|
||||
required: false,
|
||||
defaultLabel: "location",
|
||||
getOptionsAt: "locations",
|
||||
optionsInputs: {
|
||||
phone: {
|
||||
type: "phone",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
somewhereElse: {
|
||||
type: "text",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
attendeeInPerson: {
|
||||
type: "address",
|
||||
required: true,
|
||||
placeholder: "",
|
||||
},
|
||||
},
|
||||
hideWhenJustOneOption: true,
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: true,
|
||||
defaultLabel: "what_is_this_meeting_about",
|
||||
defaultPlaceholder: "",
|
||||
},
|
||||
{
|
||||
name: "notes",
|
||||
type: "textarea",
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_notes",
|
||||
defaultPlaceholder: "share_additional_notes",
|
||||
},
|
||||
{
|
||||
name: "guests",
|
||||
type: "multiemail",
|
||||
hidden: false,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "additional_guests",
|
||||
defaultPlaceholder: "email",
|
||||
},
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
views: [
|
||||
{
|
||||
id: "reschedule",
|
||||
label: "Reschedule View",
|
||||
},
|
||||
],
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
{
|
||||
name: "favorite-movie",
|
||||
type: "text",
|
||||
label: "favorite movie",
|
||||
sources: [
|
||||
{
|
||||
id: "user",
|
||||
type: "user",
|
||||
label: "User",
|
||||
fieldRequired: true,
|
||||
},
|
||||
],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "matrix",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
eventTypeWithBookingFieldsId = eventTypeWithBookingFields.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("get individual booking", () => {
|
||||
it("should should get a seated booking with split name responses", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${bookingWithSplitName.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.attendees[0].name).toEqual(`${splitName.firstName} ${splitName.lastName}`);
|
||||
expect(data.bookingFieldsResponses.name).toEqual(splitName);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get a seated booking with split name responses", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${seatedBookingWithSplitName.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)) {
|
||||
// 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 {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("make booking", () => {
|
||||
it("should not be able to book an event type with custom required booking fields if they are missing in bookingFieldsResponses", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithBookingFieldsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should be able to book an event type with custom required booking fields", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithBookingFieldsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
"favorite-movie": "lord of the rings",
|
||||
},
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_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.bookingFieldsResponses["favorite-movie"]).toEqual("lord of the rings");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
+8
-1
@@ -34,6 +34,7 @@ import {
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerReassignedEmail,
|
||||
AttendeeUpdatedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -61,10 +62,12 @@ jest
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
jest
|
||||
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeUpdatedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type EmailSetup = {
|
||||
team: Team;
|
||||
@@ -681,6 +684,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
@@ -697,6 +701,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -898,6 +903,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
@@ -914,6 +920,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,6 +96,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
@@ -351,6 +352,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
|
||||
@@ -508,6 +510,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.id).toEqual(createdBooking.id);
|
||||
expect(data.uid).toEqual(createdBooking.uid);
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.status).toEqual(createdBooking.status);
|
||||
expect(data.start).toEqual(createdBooking.start);
|
||||
expect(data.end).toEqual(createdBooking.end);
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
@ApiExtraModels(
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13
|
||||
)
|
||||
export class CancelBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
@ApiExtraModels(
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13
|
||||
)
|
||||
export class CreateBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@@ -21,6 +26,8 @@ export class CreateBookingOutput_2024_08_13 {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
|
||||
{ $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) } },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects",
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
@ApiExtraModels(
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13
|
||||
)
|
||||
export class RescheduleBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
|
||||
@@ -10,6 +10,7 @@ import { UsersRepository, UserWithProfile } from "@/modules/users/users.reposito
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
handleNewBooking,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
CancelBookingInput,
|
||||
} from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type CreatedBooking = {
|
||||
hosts: { id: number }[];
|
||||
@@ -45,6 +47,14 @@ type CreatedBooking = {
|
||||
start: string;
|
||||
};
|
||||
|
||||
const eventTypeBookingFieldSchema = z.object({
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
editable: z.string(),
|
||||
});
|
||||
|
||||
const eventTypeBookingFieldsSchema = z.array(eventTypeBookingFieldSchema);
|
||||
|
||||
@Injectable()
|
||||
export class BookingsService_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingsService");
|
||||
@@ -70,6 +80,8 @@ export class BookingsService_2024_08_13 {
|
||||
const isRecurring = !!eventType?.recurringEvent;
|
||||
const isSeated = !!eventType?.seatsPerTimeSlot;
|
||||
|
||||
await this.hasRequiredBookingFieldsResponses(body, eventType);
|
||||
|
||||
if (isRecurring && isSeated) {
|
||||
return await this.createRecurringSeatedBooking(request, body);
|
||||
}
|
||||
@@ -91,6 +103,28 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async hasRequiredBookingFieldsResponses(body: CreateBookingInput, eventType: EventType | null) {
|
||||
const bookingFields = body.bookingFieldsResponses;
|
||||
if (!bookingFields || !eventType || !eventType.bookingFields) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// note(Lauris): we filter out system fields, because some of them are set by default and name and email are passed in the body.attendee
|
||||
const eventTypeBookingFields = eventTypeBookingFieldsSchema
|
||||
.parse(eventType.bookingFields)
|
||||
.filter((field) => !field.editable.startsWith("system"));
|
||||
|
||||
for (const field of eventTypeBookingFields) {
|
||||
if (field.required && !(field.name in bookingFields)) {
|
||||
throw new BadRequestException(`
|
||||
Missing required booking field response: ${field.name} - it is required by the event type booking fields, but missing in the bookingFieldsResponses.
|
||||
You can fetch the event type with ID ${eventType.id} to see the required fields.`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleInstantMeeting(bookingRequest);
|
||||
@@ -329,12 +363,17 @@ export class BookingsService_2024_08_13 {
|
||||
|
||||
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
|
||||
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
|
||||
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
|
||||
const platformClientParams = bookingBefore?.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
await handleMarkNoShow({
|
||||
bookingUid,
|
||||
attendees: bodyTransformed.attendees,
|
||||
noShowHost: bodyTransformed.noShowHost,
|
||||
userId: bookingOwnerId,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
@@ -389,7 +428,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(requestUser);
|
||||
|
||||
@@ -397,6 +440,7 @@ export class BookingsService_2024_08_13 {
|
||||
bookingId: booking.id,
|
||||
orgId: profile?.organizationId || null,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||
@@ -407,12 +451,6 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async getEmailsEnabled(eventTypeId: number) {
|
||||
const oAuthParams = await this.inputService.getOAuthClientParams(eventTypeId);
|
||||
const emailsEnabled = oAuthParams ? oAuthParams.arePlatformEmailsEnabled : true;
|
||||
return emailsEnabled;
|
||||
}
|
||||
|
||||
async reassignBookingToUser(
|
||||
bookingUid: string,
|
||||
newUserId: number,
|
||||
@@ -429,7 +467,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
@@ -440,6 +482,7 @@ export class BookingsService_2024_08_13 {
|
||||
reassignReason: body.reason,
|
||||
reassignedById,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
@@ -451,7 +494,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
@@ -462,6 +509,7 @@ export class BookingsService_2024_08_13 {
|
||||
confirmed: true,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -474,7 +522,11 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
@@ -486,6 +538,7 @@ export class BookingsService_2024_08_13 {
|
||||
recurringEventId: booking.recurringEventId,
|
||||
reason,
|
||||
emailsEnabled,
|
||||
platformClientParams,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,13 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
}),
|
||||
]),
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
@@ -31,12 +37,20 @@ export const seatedBookingResponsesSchema = z
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
|
||||
|
||||
type DatabaseBooking = Booking & {
|
||||
eventType: {
|
||||
id: number;
|
||||
@@ -50,18 +64,13 @@ type DatabaseBooking = Booking & {
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
user: { id: number; name: string | null; email: string } | null;
|
||||
user: DatabaseUser | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
|
||||
type BookingWithUser = Booking & { user: DatabaseUser | null };
|
||||
|
||||
const bookingUserMetadataSchema = bookingMetadataSchema.transform((data) => {
|
||||
if (data === null) return {};
|
||||
// note(Lauris): return only user defined metadata
|
||||
const { videoCallUrl, ...rest } = data;
|
||||
return rest;
|
||||
});
|
||||
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
|
||||
|
||||
@@ -73,16 +82,16 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
@@ -101,9 +110,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
absent: !!attendee.noShow,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location: databaseBooking.location,
|
||||
location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
meetingUrl: location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
@@ -111,10 +120,34 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
bookingTransformed.metadata = metadata;
|
||||
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
getUserDefinedMetadata(databaseMetadata: DatabaseMetadata) {
|
||||
if (databaseMetadata === null) return {};
|
||||
|
||||
const { videoCallUrl, ...userDefinedMetadata } = databaseMetadata;
|
||||
|
||||
return userDefinedMetadata;
|
||||
}
|
||||
|
||||
getHost(user: DatabaseUser | null) {
|
||||
if (!user) {
|
||||
return {
|
||||
id: "unknown",
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
username: "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
username: user.username || "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
async getOutputRecurringBookings(bookingsIds: number[]) {
|
||||
const transformed = [];
|
||||
|
||||
@@ -134,16 +167,16 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
reschedulingReason: bookingResponses?.rescheduledReason,
|
||||
@@ -162,9 +195,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
absent: !!attendee.noShow,
|
||||
})),
|
||||
guests: bookingResponses.guests,
|
||||
location: databaseBooking.location,
|
||||
location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
meetingUrl: location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
bookingFieldsResponses: databaseBooking.responses,
|
||||
@@ -176,7 +209,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
});
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
bookingTransformed.metadata = metadata;
|
||||
bookingTransformed.metadata = this.getUserDefinedMetadata(metadata);
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
@@ -192,13 +225,15 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
@@ -208,9 +243,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
meetingUrl: location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
@@ -285,13 +320,15 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
hosts: [this.getHost(databaseBooking.user)],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
@@ -302,9 +339,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
meetingUrl: location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
|
||||
@@ -10,7 +10,8 @@ import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SUCCESS_STATUS, GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
|
||||
@@ -78,7 +79,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
}
|
||||
|
||||
@@ -134,8 +135,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
|
||||
oAuth2Client.setCredentials(key);
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const calendar = new calendar_v3.Calendar({
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
|
||||
@@ -78,4 +78,11 @@ export class EventTypesRepository_2024_04_15 {
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
select: { length: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+120
-6
@@ -10,10 +10,12 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Schedule } from "@prisma/client";
|
||||
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
@@ -75,18 +77,22 @@ describe("Event types Endpoints", () => {
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const falseTestEmail = "false-event-types@api.com";
|
||||
const name = "bob-the-builder";
|
||||
const username = name;
|
||||
let eventType: EventTypeOutput_2024_06_14;
|
||||
let user: User;
|
||||
let orgUser: User;
|
||||
let falseTestUser: User;
|
||||
let firstSchedule: Schedule;
|
||||
let secondSchedule: Schedule;
|
||||
let falseTestSchedule: Schedule;
|
||||
|
||||
let orgUserEventType1: EventType;
|
||||
let orgUserEventType2: EventType;
|
||||
let orgUserEventType3: EventType;
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
@@ -109,8 +115,12 @@ describe("Event types Endpoints", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepostoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: "organization",
|
||||
slug: "event-type-2024-06-14-org-slug",
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
@@ -118,6 +128,49 @@ describe("Event types Endpoints", () => {
|
||||
username,
|
||||
});
|
||||
|
||||
orgUser = await userRepositoryFixture.create({
|
||||
email: "event-types-2024-06-14-org-user@example.com",
|
||||
name: "event-types-2024-06-14-org-user",
|
||||
username: "event-types-2024-06-14-org-user",
|
||||
});
|
||||
|
||||
profileRepositoryFixture.create({
|
||||
uid: `usr-${orgUser.id}`,
|
||||
username: orgUser.username as string,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: orgUser.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
orgUserEventType1 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType1", slug: "org-event-type-1", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType2 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType2", slug: "org-event-type-2", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
orgUserEventType3 = await eventTypesRepositoryFixture.create(
|
||||
{ title: "orgUserEventType3", slug: "org-event-type-3", length: 60, locations: [] },
|
||||
orgUser.id
|
||||
);
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: orgUser.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestEmail,
|
||||
name: "false-test",
|
||||
@@ -342,7 +395,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/even-types by username`, async () => {
|
||||
it(`/GET/event-types by username`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${username}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
@@ -386,6 +439,61 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug`, async () => {
|
||||
console.log(organization);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgSlug and eventSlug`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v2/event-types?username=${orgUser.username}&orgSlug=${organization.slug}&eventSlug=${orgUserEventType1.slug}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(1);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it(`/GET/event-types by username and orgId`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types?username=${orgUser.username}&orgId=${organization.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
|
||||
.set("Authorization", `Bearer whatever`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data?.length).toEqual(3);
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType1.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType2.id)?.id).toBeDefined();
|
||||
expect(responseBody.data?.find((e) => e.id === orgUserEventType3.id)?.id).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class 2",
|
||||
@@ -919,6 +1027,12 @@ describe("Event types Endpoints", () => {
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
|
||||
try {
|
||||
await userRepositoryFixture.delete(orgUser.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+15
-11
@@ -74,8 +74,13 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(username: string, eventTypeSlug: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
orgSlug?: string,
|
||||
orgId?: number
|
||||
) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,8 +97,8 @@ export class EventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
async getEventTypesByUsername(username: string) {
|
||||
const user = await this.usersRepository.findByUsername(username);
|
||||
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
@@ -152,27 +157,26 @@ export class EventTypesService_2024_06_14 {
|
||||
}
|
||||
|
||||
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
|
||||
const { username, eventSlug, usernames } = queryParams;
|
||||
|
||||
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
|
||||
if (username && eventSlug) {
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug);
|
||||
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
|
||||
return eventType ? [eventType] : [];
|
||||
}
|
||||
|
||||
if (username) {
|
||||
return await this.getEventTypesByUsername(username);
|
||||
return await this.getEventTypesByUsername(username, orgSlug, orgId);
|
||||
}
|
||||
|
||||
if (usernames) {
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames);
|
||||
const dynamicEventType = await this.getDynamicEventType(usernames, orgSlug, orgId);
|
||||
return [dynamicEventType];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async getDynamicEventType(usernames: string[]) {
|
||||
const users = await this.usersService.getByUsernames(usernames);
|
||||
async getDynamicEventType(usernames: string[], orgSlug?: string, orgId?: number) {
|
||||
const users = await this.usersService.getByUsernames(usernames, orgSlug, orgId);
|
||||
const usersFiltered: UserWithProfile[] = [];
|
||||
for (const user of users) {
|
||||
if (user) {
|
||||
|
||||
@@ -27,10 +27,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { google } from "googleapis";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APPS_READ, GOOGLE_CALENDAR_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WinstonTransport as AxiomTransport } from "@axiomhq/winston";
|
||||
import type { LoggerOptions } from "winston";
|
||||
import { format, transports as Transports } from "winston";
|
||||
import { format, transports as Transports, config } from "winston";
|
||||
import type Transport from "winston-transport";
|
||||
|
||||
const formattedTimestamp = format.timestamp({
|
||||
@@ -8,14 +8,7 @@ const formattedTimestamp = format.timestamp({
|
||||
});
|
||||
|
||||
const colorizer = format.colorize({
|
||||
colors: {
|
||||
fatal: "red",
|
||||
error: "red",
|
||||
warn: "yellow",
|
||||
info: "blue",
|
||||
debug: "white",
|
||||
trace: "grey",
|
||||
},
|
||||
colors: config.npm.colors,
|
||||
});
|
||||
|
||||
const WINSTON_DEV_FORMAT = format.combine(
|
||||
@@ -26,14 +19,7 @@ const WINSTON_DEV_FORMAT = format.combine(
|
||||
);
|
||||
const WINSTON_PROD_FORMAT = format.combine(format.errors({ stack: true }), formattedTimestamp, format.json());
|
||||
|
||||
export const logLevels = {
|
||||
fatal: 0,
|
||||
error: 1,
|
||||
warn: 2,
|
||||
info: 3,
|
||||
debug: 4,
|
||||
trace: 5,
|
||||
} as const;
|
||||
export const logLevels = config.npm.levels;
|
||||
|
||||
export const loggerConfig = (): LoggerOptions => {
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
@@ -56,7 +56,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
const tracker = await this.getTracker(request);
|
||||
this.logger.log(
|
||||
this.logger.verbose(
|
||||
`Tracker "${tracker}" generated based on: Bearer token "${request.get(
|
||||
"Authorization"
|
||||
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${request.ip}"`
|
||||
@@ -89,7 +89,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
|
||||
private async handleNonApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
||||
const rateLimit = this.getDefaultRateLimit(tracker);
|
||||
this.logger.log(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
|
||||
this.logger.verbose(`Tracker "${tracker}" uses default rate limits because it is not tracking api key:
|
||||
${JSON.stringify(rateLimit, null, 2)}
|
||||
`);
|
||||
|
||||
@@ -135,7 +135,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
|
||||
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
|
||||
if (cachedRateLimits) {
|
||||
this.logger.log(`Tracker "${tracker}" rate limits retrieved from redis cache:
|
||||
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from redis cache:
|
||||
${cachedRateLimits}
|
||||
`);
|
||||
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
|
||||
@@ -158,13 +158,13 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
});
|
||||
|
||||
if (rateLimits) {
|
||||
this.logger.log(`Tracker "${tracker}" rate limits retrieved from database:
|
||||
this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from database:
|
||||
${JSON.stringify(rateLimits, null, 2)}`);
|
||||
}
|
||||
|
||||
if (!rateLimits || rateLimits.length === 0) {
|
||||
rateLimits = [this.getDefaultRateLimit(tracker)];
|
||||
this.logger.log(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
|
||||
this.logger.verbose(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
|
||||
${JSON.stringify(rateLimits, null, 2)}`);
|
||||
}
|
||||
|
||||
@@ -194,10 +194,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
);
|
||||
response.setHeader(`X-RateLimit-Reset-${nameFirstUpper}`, timeToBlockExpire || timeToExpire);
|
||||
|
||||
this.logger.log(
|
||||
this.logger.verbose(
|
||||
`Tracker "${tracker}" rate limit "${name}" incremented. isBlocked ${isBlocked}, totalHits ${totalHits}, timeToExpire ${timeToExpire}, timeToBlockExpire ${timeToBlockExpire}`
|
||||
);
|
||||
this.logger.log(
|
||||
this.logger.verbose(
|
||||
`Tracker "${tracker}" rate limit "${name}" response headers:
|
||||
X-RateLimit-Limit-${nameFirstUpper}: ${limit},
|
||||
X-RateLimit-Remaining-${nameFirstUpper}: ${timeToBlockExpire ? 0 : Math.max(0, limit - totalHits)},
|
||||
@@ -227,7 +227,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
return `ip_${request.ip}`;
|
||||
}
|
||||
|
||||
this.logger.log(`no tracker found: ${request.url}`);
|
||||
this.logger.verbose(`no tracker found: ${request.url}`);
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { google } from "googleapis";
|
||||
import { OAuth2Client } from "googleapis-common";
|
||||
import { z } from "zod";
|
||||
|
||||
@Injectable()
|
||||
@@ -21,7 +21,7 @@ export class GCalService {
|
||||
|
||||
const { client_id, client_secret } = this.gcalResponseSchema.parse(app.keys);
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirectUri);
|
||||
const oAuth2Client = new OAuth2Client(client_id, client_secret, redirectUri);
|
||||
return oAuth2Client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class PlatformPlanGuard implements CanActivate {
|
||||
canAccess = hasMinimumPlan({
|
||||
currentPlan: team.platformBilling?.plan as PlatformPlanType,
|
||||
minimumPlan: minimumPlan,
|
||||
plans: ["STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
|
||||
plans: ["FREE", "STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+26
-9
@@ -16,6 +16,7 @@ import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, EventType } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
@@ -79,6 +80,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let postResponseData: CreateManagedUserOutput["data"];
|
||||
|
||||
@@ -103,26 +105,33 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: "organization",
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
uid: "asd1qwwqeqw-asddsadasd",
|
||||
username: platformAdminEmail,
|
||||
organization: { connect: { id: organization.id } },
|
||||
movedFromUser: {
|
||||
connect: {
|
||||
id: platformAdmin.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: { id: platformAdmin.id },
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: platformAdmin.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -370,7 +379,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let postResponseData: CreateManagedUserOutput["data"];
|
||||
|
||||
const userEmail = "oauth-client-users-user@gmail.com";
|
||||
@@ -390,10 +399,11 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: "Testy Organization",
|
||||
isOrganization: true,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
owner = await userRepositoryFixture.create({
|
||||
@@ -417,6 +427,13 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: owner.id } },
|
||||
team: { connect: { id: organization.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
oAuthClient1 = await createOAuthClient(organization.id);
|
||||
oAuthClient2 = await createOAuthClient(organization.id);
|
||||
|
||||
|
||||
+15
-3
@@ -1,6 +1,8 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
|
||||
import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output";
|
||||
import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output";
|
||||
@@ -29,7 +31,7 @@ import {
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { User } from "@prisma/client";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { Pagination } from "@calcom/platform-types";
|
||||
@@ -38,7 +40,7 @@ import { Pagination } from "@calcom/platform-types";
|
||||
path: "/v2/oauth-clients/:clientId/users",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, OAuthClientGuard)
|
||||
@UseGuards(ApiAuthGuard, OAuthClientGuard, OrganizationRolesGuard)
|
||||
@DocsTags("Platform / Managed Users")
|
||||
export class OAuthClientUsersController {
|
||||
private readonly logger = new Logger("UserController");
|
||||
@@ -52,6 +54,7 @@ export class OAuthClientUsersController {
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all managed users" })
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async getManagedUsers(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Query() queryParams: Pagination
|
||||
@@ -73,6 +76,7 @@ export class OAuthClientUsersController {
|
||||
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Create a managed user" })
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async createUser(
|
||||
@Param("clientId") oAuthClientId: string,
|
||||
@Body() body: CreateManagedUserInput
|
||||
@@ -104,6 +108,7 @@ export class OAuthClientUsersController {
|
||||
@Get("/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Get a managed user" })
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async getUserById(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number
|
||||
@@ -119,6 +124,7 @@ export class OAuthClientUsersController {
|
||||
@Patch("/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Update a managed user" })
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async updateUser(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number,
|
||||
@@ -138,6 +144,7 @@ export class OAuthClientUsersController {
|
||||
@Delete("/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Delete a managed user" })
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async deleteUser(
|
||||
@Param("clientId") clientId: string,
|
||||
@Param("userId") userId: number
|
||||
@@ -155,7 +162,12 @@ export class OAuthClientUsersController {
|
||||
|
||||
@Post("/:userId/force-refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: "Force refresh tokens" })
|
||||
@ApiOperation({
|
||||
summary: "Force refresh tokens",
|
||||
description: `If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.
|
||||
Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`,
|
||||
})
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
async forceRefresh(
|
||||
@Param("userId") userId: number,
|
||||
@Param("clientId") oAuthClientId: string
|
||||
|
||||
+3
-3
@@ -24,10 +24,10 @@ import { withNextAuth } from "test/utils/withNextAuth";
|
||||
|
||||
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
|
||||
|
||||
describe("EventTypes WebhooksController (e2e)", () => {
|
||||
describe("OAuth client WebhooksController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "event-types-webhook-controller-e2e@api.com";
|
||||
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
|
||||
const userEmail = "oauth-client-webhook-controller-e2e@api.com";
|
||||
const otherUserEmail = "other-oauth-client-webhook-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let otherUser: UserWithProfile;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
|
||||
+8
-3
@@ -30,6 +30,7 @@ import {
|
||||
ApiExcludeEndpoint as DocsExcludeEndpoint,
|
||||
ApiBadRequestResponse as DocsBadRequestResponse,
|
||||
ApiHeader as DocsHeader,
|
||||
ApiOperation,
|
||||
} from "@nestjs/swagger";
|
||||
import { Response as ExpressResponse } from "express";
|
||||
|
||||
@@ -39,7 +40,6 @@ import { SUCCESS_STATUS, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
path: "/v2/oauth/:clientId",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("OAuth")
|
||||
export class OAuthFlowController {
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
@@ -115,13 +115,18 @@ export class OAuthFlowController {
|
||||
@Post("/refresh")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@DocsTags("Managed users")
|
||||
@DocsTags("Platform / Managed Users")
|
||||
@DocsHeader({
|
||||
name: X_CAL_SECRET_KEY,
|
||||
description: "OAuth client secret key.",
|
||||
required: true,
|
||||
})
|
||||
async refreshAccessToken(
|
||||
@ApiOperation({
|
||||
summary: "Refresh managed user tokens",
|
||||
description: `If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and
|
||||
each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have \`calAccessToken\` and \`calRefreshToken\` columns.`,
|
||||
})
|
||||
async refreshTokens(
|
||||
@Param("clientId") clientId: string,
|
||||
@Headers(X_CAL_SECRET_KEY) secretKey: string,
|
||||
@Body() body: RefreshTokenInput
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiTags as DocsTags, ApiCreatedResponse, ApiOkResponse, ApiOperation }
|
||||
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { getAvailableSlots } from "@calcom/platform-libraries";
|
||||
import type { AvailableSlotsType } from "@calcom/platform-libraries";
|
||||
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
|
||||
@@ -89,9 +90,31 @@ export class SlotsController {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time", example: "2024-09-25T08:00:00.000Z" },
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
properties: {
|
||||
time: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
startTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:00:00.000Z",
|
||||
},
|
||||
endTime: {
|
||||
type: "string",
|
||||
format: "date-time",
|
||||
example: "2024-09-25T08:30:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,11 +125,18 @@ export class SlotsController {
|
||||
status: "success",
|
||||
data: {
|
||||
slots: {
|
||||
// Default format (when slotFormat is 'time' or not provided)
|
||||
"2024-09-25": [{ time: "2024-09-25T08:00:00.000Z" }, { time: "2024-09-25T08:15:00.000Z" }],
|
||||
// Alternative format (when slotFormat is 'range')
|
||||
"2024-09-26": [
|
||||
{ time: "2024-09-26T08:00:00.000Z" },
|
||||
{ time: "2024-09-26T08:15:00.000Z" },
|
||||
{ time: "2024-09-26T08:30:00.000Z" },
|
||||
{
|
||||
startTime: "2024-09-26T08:00:00.000Z",
|
||||
endTime: "2024-09-26T08:30:00.000Z",
|
||||
},
|
||||
{
|
||||
startTime: "2024-09-26T08:15:00.000Z",
|
||||
endTime: "2024-09-26T08:45:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -129,8 +159,20 @@ export class SlotsController {
|
||||
},
|
||||
});
|
||||
|
||||
const transformedSlots =
|
||||
query.slotFormat === SlotFormat.Range
|
||||
? await this.slotsService.formatSlots(
|
||||
availableSlots,
|
||||
query.duration,
|
||||
query.eventTypeId,
|
||||
query.slotFormat
|
||||
)
|
||||
: availableSlots.slots;
|
||||
|
||||
return {
|
||||
data: availableSlots,
|
||||
data: {
|
||||
slots: transformedSlots,
|
||||
},
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
|
||||
import { SlotsRepository } from "@/modules/slots/slots.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { SlotFormat } from "@calcom/platform-enums";
|
||||
import { ReserveSlotInput } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
@@ -54,4 +55,51 @@ export class SlotsService {
|
||||
const event = await this.eventTypeRepo.getEventTypeById(eventTypeId);
|
||||
return !!event?.teamId;
|
||||
}
|
||||
|
||||
async getEventTypeWithDuration(eventTypeId: number) {
|
||||
return await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
}
|
||||
|
||||
async getDuration(duration?: number, eventTypeId?: number): Promise<number> {
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (eventTypeId) {
|
||||
const eventType = await this.eventTypeRepo.getEventTypeWithDuration(eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
return eventType.length;
|
||||
}
|
||||
|
||||
throw new Error("duration or eventTypeId is required");
|
||||
}
|
||||
|
||||
async formatSlots(
|
||||
availableSlots: { slots: Record<string, { time: string }[]> },
|
||||
duration?: number,
|
||||
eventTypeId?: number,
|
||||
slotFormat?: SlotFormat
|
||||
): Promise<Record<string, { startTime: string; endTime: string }[]>> {
|
||||
if (slotFormat && !Object.values(SlotFormat).includes(slotFormat)) {
|
||||
throw new BadRequestException("Invalid slot format. Must be either 'range' or 'time'");
|
||||
}
|
||||
|
||||
const slotDuration = await this.getDuration(duration, eventTypeId);
|
||||
|
||||
return Object.entries(availableSlots.slots).reduce<
|
||||
Record<string, { startTime: string; endTime: string }[]>
|
||||
>((acc, [date, slots]) => {
|
||||
acc[date] = (slots as { time: string }[]).map((slot) => {
|
||||
const startTime = new Date(slot.time);
|
||||
const endTime = new Date(startTime.getTime() + slotDuration * 60000);
|
||||
return {
|
||||
startTime: startTime.toISOString(),
|
||||
endTime: endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class UpdateManagedUserInput {
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(["12", "24"])
|
||||
@IsIn([12, 24])
|
||||
@ApiProperty({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
||||
timeFormat?: TimeFormat;
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { User } from "@calcom/prisma/client";
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
|
||||
async getByUsernames(usernames: string[]) {
|
||||
async getByUsernames(usernames: string[], orgSlug?: string, orgId?: number) {
|
||||
const users = await Promise.all(
|
||||
usernames.map((username) => this.usersRepository.findByUsername(username))
|
||||
usernames.map((username) => this.usersRepository.findByUsername(username, orgSlug, orgId))
|
||||
);
|
||||
const usersFiltered: User[] = [];
|
||||
|
||||
|
||||
@@ -142,11 +142,21 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string) {
|
||||
async findByUsername(username: string, orgSlug?: string, orgId?: number) {
|
||||
return this.dbRead.prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
where:
|
||||
orgId || orgSlug
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
organization: orgSlug ? { slug: orgSlug } : { id: orgId },
|
||||
username: username,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"post": {
|
||||
"operationId": "OAuthClientUsersController_forceRefresh",
|
||||
"summary": "Force refresh tokens",
|
||||
"description": "If you have lost managed user access or refresh token, then you can get new ones by using OAuth credentials.\n Each access token is valid for 60 minutes and each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
@@ -397,6 +398,57 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_refreshTokens",
|
||||
"summary": "Refresh managed user tokens",
|
||||
"description": "If managed user access token is expired then get a new one using this endpoint. Each access token is valid for 60 minutes and \n each refresh token for 1 year. Make sure to store them later in your database, for example, by updating the User model to have `calAccessToken` and `calRefreshToken` columns.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-secret-key",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"description": "OAuth client secret key.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshTokenInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KeysResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Platform / Managed Users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth-clients/{clientId}/webhooks": {
|
||||
"post": {
|
||||
"operationId": "OAuthClientWebhooksController_createOAuthClientWebhook",
|
||||
@@ -4007,6 +4059,15 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Id of the event-type, if this is provided no other parameter is required",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "usernames",
|
||||
"required": false,
|
||||
@@ -4015,6 +4076,33 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the user you want the get the event-types of, username is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgSlug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "slug of the user's organization if he is in one, orgId is not required if using this parameter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "ID of the organization of the user you want the get the event-types of, orgSlug is not needed when using this parameter",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4439,56 +4527,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_refreshAccessToken",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-cal-secret-key",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"description": "OAuth client secret key.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshTokenInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KeysResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuth",
|
||||
"Managed users"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/schedules": {
|
||||
"post": {
|
||||
"operationId": "SchedulesController_2024_06_11_createSchedule",
|
||||
@@ -5003,6 +5041,20 @@
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slotFormat",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Format of slot times in response. Use 'range' to get start and end times.",
|
||||
"example": "range",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"range",
|
||||
"time"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5026,13 +5078,31 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"startTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:00:00.000Z"
|
||||
},
|
||||
"endTime": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2024-09-25T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5053,13 +5123,12 @@
|
||||
],
|
||||
"2024-09-26": [
|
||||
{
|
||||
"time": "2024-09-26T08:00:00.000Z"
|
||||
"startTime": "2024-09-26T08:00:00.000Z",
|
||||
"endTime": "2024-09-26T08:30:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:15:00.000Z"
|
||||
},
|
||||
{
|
||||
"time": "2024-09-26T08:30:00.000Z"
|
||||
"startTime": "2024-09-26T08:15:00.000Z",
|
||||
"endTime": "2024-09-26T08:45:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6532,7 +6601,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date."
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -6560,7 +6629,7 @@
|
||||
"rolling": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date."
|
||||
"description": "\n Determines the behavior of the booking window:\n - If **true**, the window is rolling. This means the number of available days will always be equal the specified 'value' \n and adjust dynamically as bookings are made. For example, if 'value' is 3 and availability is only on Mondays, \n a booker attempting to schedule on November 10 will see slots on November 11, 18, and 25. As one of these days \n becomes fully booked, a new day (e.g., December 2) will open up to ensure 3 available days are always visible.\n - If **false**, the window is fixed. This means the booking window only considers the next 'value' days from the\n moment someone is trying to book. For example, if 'value' is 3, availability is only on Mondays, and the current \n date is November 10, the booker will only see slots on November 11 because the window is restricted to the next \n 3 calendar days (November 10–12).\n "
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13023,6 +13092,33 @@
|
||||
"attendee"
|
||||
]
|
||||
},
|
||||
"BookingHost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Jane Doe"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "jane100"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "America/Los_Angeles"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"username",
|
||||
"timeZone"
|
||||
]
|
||||
},
|
||||
"EventType": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13062,7 +13158,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13195,7 +13291,7 @@
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Host"
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -13311,6 +13407,341 @@
|
||||
"recurringBookingUid"
|
||||
]
|
||||
},
|
||||
"SeatedAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ar",
|
||||
"ca",
|
||||
"de",
|
||||
"es",
|
||||
"eu",
|
||||
"he",
|
||||
"id",
|
||||
"ja",
|
||||
"lv",
|
||||
"pl",
|
||||
"ro",
|
||||
"sr",
|
||||
"th",
|
||||
"vi",
|
||||
"az",
|
||||
"cs",
|
||||
"el",
|
||||
"es-419",
|
||||
"fi",
|
||||
"hr",
|
||||
"it",
|
||||
"km",
|
||||
"nl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh-CN",
|
||||
"bg",
|
||||
"da",
|
||||
"en",
|
||||
"et",
|
||||
"fr",
|
||||
"hu",
|
||||
"iw",
|
||||
"ko",
|
||||
"no",
|
||||
"pt-BR",
|
||||
"sk",
|
||||
"ta",
|
||||
"uk",
|
||||
"zh-TW"
|
||||
],
|
||||
"example": "en"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
"example": {
|
||||
"customField": "customValue"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"timeZone",
|
||||
"absent",
|
||||
"seatUid"
|
||||
]
|
||||
},
|
||||
"CreateSeatedBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 123
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"example": "booking_uid_123"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Consultation"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Learn how to integrate scheduling into marketplace."
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cancelled",
|
||||
"accepted",
|
||||
"rejected",
|
||||
"pending"
|
||||
],
|
||||
"example": "accepted"
|
||||
},
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "User requested cancellation"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "User rescheduled the event"
|
||||
},
|
||||
"rescheduledFromUid": {
|
||||
"type": "string",
|
||||
"example": "previous_uid_123"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T16:30:00Z"
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"example": 50,
|
||||
"deprecated": true,
|
||||
"description": "Deprecated - rely on 'eventType' object containing the id instead."
|
||||
},
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SeatedAttendee"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"uid",
|
||||
"title",
|
||||
"description",
|
||||
"hosts",
|
||||
"status",
|
||||
"start",
|
||||
"end",
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"seatUid",
|
||||
"attendees"
|
||||
]
|
||||
},
|
||||
"CreateRecurringSeatedBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 123
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"example": "booking_uid_123"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Consultation"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Learn how to integrate scheduling into marketplace."
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cancelled",
|
||||
"accepted",
|
||||
"rejected",
|
||||
"pending"
|
||||
],
|
||||
"example": "accepted"
|
||||
},
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "User requested cancellation"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "User rescheduled the event"
|
||||
},
|
||||
"rescheduledFromUid": {
|
||||
"type": "string",
|
||||
"example": "previous_uid_123"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T16:30:00Z"
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"example": 50,
|
||||
"deprecated": true,
|
||||
"description": "Deprecated - rely on 'eventType' object containing the id instead."
|
||||
},
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"seatUid": {
|
||||
"type": "string",
|
||||
"example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1"
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SeatedAttendee"
|
||||
}
|
||||
},
|
||||
"recurringBookingUid": {
|
||||
"type": "string",
|
||||
"example": "recurring_uid_987"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"uid",
|
||||
"title",
|
||||
"description",
|
||||
"hosts",
|
||||
"status",
|
||||
"start",
|
||||
"end",
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"seatUid",
|
||||
"attendees",
|
||||
"recurringBookingUid"
|
||||
]
|
||||
},
|
||||
"CreateBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13332,6 +13763,15 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CreateSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects"
|
||||
@@ -13342,6 +13782,243 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"GetSeatedBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 123
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"example": "booking_uid_123"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Consultation"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Learn how to integrate scheduling into marketplace."
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cancelled",
|
||||
"accepted",
|
||||
"rejected",
|
||||
"pending"
|
||||
],
|
||||
"example": "accepted"
|
||||
},
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "User requested cancellation"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "User rescheduled the event"
|
||||
},
|
||||
"rescheduledFromUid": {
|
||||
"type": "string",
|
||||
"example": "previous_uid_123"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T16:30:00Z"
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"example": 50,
|
||||
"deprecated": true,
|
||||
"description": "Deprecated - rely on 'eventType' object containing the id instead."
|
||||
},
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SeatedAttendee"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"uid",
|
||||
"title",
|
||||
"description",
|
||||
"hosts",
|
||||
"status",
|
||||
"start",
|
||||
"end",
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"attendees"
|
||||
]
|
||||
},
|
||||
"GetRecurringSeatedBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 123
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"example": "booking_uid_123"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Consultation"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Learn how to integrate scheduling into marketplace."
|
||||
},
|
||||
"hosts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingHost"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cancelled",
|
||||
"accepted",
|
||||
"rejected",
|
||||
"pending"
|
||||
],
|
||||
"example": "accepted"
|
||||
},
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "User requested cancellation"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "User rescheduled the event"
|
||||
},
|
||||
"rescheduledFromUid": {
|
||||
"type": "string",
|
||||
"example": "previous_uid_123"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T16:30:00Z"
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"example": 50,
|
||||
"deprecated": true,
|
||||
"description": "Deprecated - rely on 'eventType' object containing the id instead."
|
||||
},
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SeatedAttendee"
|
||||
}
|
||||
},
|
||||
"recurringBookingUid": {
|
||||
"type": "string",
|
||||
"example": "recurring_uid_987"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"uid",
|
||||
"title",
|
||||
"description",
|
||||
"hosts",
|
||||
"status",
|
||||
"start",
|
||||
"end",
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"attendees",
|
||||
"recurringBookingUid"
|
||||
]
|
||||
},
|
||||
"GetBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function randomNumber(): number {
|
||||
return Math.floor(Math.random() * 100000);
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME }
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/developer/:path*", process.env.APP_ROUTER_SETTINGS_DEVELOPER_ENABLED === "1"] as const,
|
||||
["/settings/organizations/:path*", process.env.APP_ROUTER_SETTINGS_ORG_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
@@ -23,7 +21,6 @@ const ROUTES: [URLPattern, boolean][] = [
|
||||
["/auth/platform/:path*", process.env.APP_ROUTER_AUTH_PLATFORM_ENABLED === "1"] as const,
|
||||
["/auth/oauth2/:path*", process.env.APP_ROUTER_AUTH_OAUTH2_ENABLED === "1"] as const,
|
||||
["/workflows/:path*", process.env.APP_ROUTER_WORKFLOWS_ENABLED === "1"] as const,
|
||||
["/settings/teams/:path*", process.env.APP_ROUTER_SETTINGS_TEAMS_ENABLED === "1"] as const,
|
||||
["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const,
|
||||
["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const,
|
||||
["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
|
||||
|
||||
export default WithLayout({ getServerLayout: getLayout })<"L">;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Attribute",
|
||||
(t) => t("edit_attribute_description")
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Attribute",
|
||||
() => "Create an attribute for your team members"
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -1,11 +0,0 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("attributes"),
|
||||
(t) => t("attribute_meta_description")
|
||||
);
|
||||
|
||||
export default Page;
|
||||
@@ -1,5 +0,0 @@
|
||||
import BillingPage, { generateMetadata } from "../../billing/page";
|
||||
|
||||
export { generateMetadata };
|
||||
|
||||
export default BillingPage;
|
||||
@@ -1,4 +0,0 @@
|
||||
import BillingPage, { generateMetadata } from "../../../billing/page";
|
||||
|
||||
export { generateMetadata };
|
||||
export default BillingPage;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("set_up_your_platform_organization"),
|
||||
(t) => t("platform_organization_description")
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: LayoutWrapper,
|
||||
Page: LegacyPage,
|
||||
getData: withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps),
|
||||
requiresLicense: true,
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import CreateNewView from "~/settings/platform/oauth-clients/create-new-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const p = { ...params, ...searchParams };
|
||||
const clientId = p?.clientId ?? "";
|
||||
return await _generateMetadata(
|
||||
() => `OAuth client ${!!clientId ? "updation" : "creation"} form`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: CreateNewView,
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import PlatformView from "~/settings/platform/platform-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("platform"),
|
||||
(t) => t("platform_description")
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: PlatformView,
|
||||
});
|
||||
@@ -74,6 +74,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_HEAD_SCRIPTS,
|
||||
}}
|
||||
@@ -105,6 +106,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_BODY_SCRIPTS,
|
||||
}}
|
||||
|
||||
-2
@@ -13,9 +13,7 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
// FIXME: Refactor me once next-auth endpoint is migrated to App Router
|
||||
const session = await getServerSessionForAppDir();
|
||||
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
return (
|
||||
+2
-3
@@ -4,8 +4,7 @@ import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-s
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import ApiKeysView from "~/settings/developer/api-keys-view";
|
||||
import NewApiKeyButton from "~/settings/developer/components/CreateApiKeyButton";
|
||||
import ApiKeysView, { NewApiKeyButton } from "~/settings/developer/api-keys-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -25,7 +24,7 @@ const Page = async () => {
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
CTA={<NewApiKeyButton />}
|
||||
borderInShellHeader={true}>
|
||||
<ApiKeysView isAppDir={true} />
|
||||
<ApiKeysView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -20,7 +20,7 @@ const Page = async ({ params }: PageProps) => {
|
||||
const id = typeof params?.id === "string" ? params.id : undefined;
|
||||
|
||||
const webhook = await WebhookRepository.findByWebhookId(id);
|
||||
console.log("This is the webhook", webhook);
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("edit_webhook")}
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import { WebhooksViewAppDir } from "@calcom/features/webhooks/pages/webhooks-view";
|
||||
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -9,8 +9,4 @@ export const generateMetadata = async () =>
|
||||
(t) => t("add_webhook_description", { appName: APP_NAME })
|
||||
);
|
||||
|
||||
const Page = () => {
|
||||
return <WebhooksViewAppDir />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default WebhooksView;
|
||||
@@ -1,11 +1,8 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getFixedT } from "app/_utils";
|
||||
|
||||
import { ConferencingAppsViewWebWrapper } from "@calcom/atoms/monorepo";
|
||||
import { getServerSessionForAppDir } from "@calcom/feature-auth/lib/get-server-session-for-app-dir";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import ConferencingView from "~/settings/my-account/conferencing-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -17,22 +14,12 @@ const Page = async () => {
|
||||
const session = await getServerSessionForAppDir();
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
const AddConferencingButton = () => {
|
||||
return (
|
||||
<Button color="secondary" StartIcon="plus" href="/apps/categories/conferencing">
|
||||
{t("add")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
<ConferencingAppsViewWebWrapper
|
||||
title={t("conferencing")}
|
||||
description={t("conferencing_description")}
|
||||
CTA={<AddConferencingButton />}
|
||||
borderInShellHeader={true}>
|
||||
<ConferencingView />
|
||||
</SettingsHeader>
|
||||
add={t("add")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("attribute"),
|
||||
(t) => t("edit_attribute_description")
|
||||
);
|
||||
|
||||
export default OrgAttributesEditPage;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("attribute"),
|
||||
(t) => t("create_attribute_description")
|
||||
);
|
||||
|
||||
export default OrgAttributesCreatePage;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { _generateMetadata, getFixedT } from "app/_utils";
|
||||
|
||||
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("attributes"),
|
||||
(t) => t("attribute_meta_description")
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const session = await getServerSessionForAppDir();
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("attributes")} description={t("attribute_meta_description")}>
|
||||
<OrgSettingsAttributesPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,5 @@
|
||||
import BillingPage, { generateMetadata } from "app/settings/(settings-layout)/billing/page";
|
||||
|
||||
export { generateMetadata };
|
||||
|
||||
export default BillingPage;
|
||||
+1
-1
@@ -16,7 +16,7 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -19,7 +19,7 @@ const Page = async () => {
|
||||
title={t("profile")}
|
||||
description={t("profile_org_description")}
|
||||
borderInShellHeader={true}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -15,8 +15,8 @@ const Page = async () => {
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("profile")} description={t("profile_org_description")}>
|
||||
<OrgSSOView isAppDir={true} />
|
||||
<SettingsHeader title={t("sso_configuration")} description={t("sso_configuration_description_orgs")}>
|
||||
<OrgSSOView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -19,7 +19,7 @@ const Page = async () => {
|
||||
title={t("booking_appearance")}
|
||||
description={t("appearance_team_description")}
|
||||
borderInShellHeader={false}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+8
-4
@@ -1,7 +1,9 @@
|
||||
import { _generateMetadata, getFixedT } from "app/_utils";
|
||||
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
|
||||
import LegacyPage, {
|
||||
TeamMembersCTA,
|
||||
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -15,9 +17,11 @@ const Page = async () => {
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
return (
|
||||
// TODO: Add CTA Button
|
||||
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<SettingsHeader
|
||||
title={t("team_members")}
|
||||
description={t("members_team_description")}
|
||||
CTA={<TeamMembersCTA />}>
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -16,7 +16,7 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("profile")} description={t("profile_team_description")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -16,7 +16,7 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("org_admin_other_teams")} description={t("org_admin_other_teams_description")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -19,7 +19,7 @@ const Page = async () => {
|
||||
title={t("booking_appearance")}
|
||||
description={t("appearance_team_description")}
|
||||
borderInShellHeader={false}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import BillingPage, { generateMetadata } from "app/settings/(settings-layout)/billing/page";
|
||||
|
||||
export { generateMetadata };
|
||||
export default BillingPage;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { _generateMetadata, getFixedT } from "app/_utils";
|
||||
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
import TeamBookingLimitsView from "@calcom/features/ee/teams/pages/team-booking-limits-view";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("booking_limits"),
|
||||
(t) => t("booking_limits_team_description")
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const session = await getServerSessionForAppDir();
|
||||
const t = await getFixedT(session?.user.locale || "en");
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("booking_limits")}
|
||||
description={t("booking_limits_team_description")}
|
||||
borderInShellHeader={false}>
|
||||
<TeamBookingLimitsView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
+1
-1
@@ -16,7 +16,7 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("team_members")} description={t("members_team_description")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -19,7 +19,7 @@ const Page = async () => {
|
||||
title={t("profile")}
|
||||
description={t("profile_team_description")}
|
||||
borderInShellHeader={true}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -16,7 +16,7 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("teams")} description={t("create_manage_teams_collaborative")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
+4
-5
@@ -1,15 +1,14 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import EditView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-view";
|
||||
import MembersPage from "~/members/members-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("oAuth_client_updation_form"),
|
||||
() => ""
|
||||
(t) => t("organization_members"),
|
||||
(t) => t("organization_description")
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: EditView,
|
||||
Page: MembersPage,
|
||||
});
|
||||
+8
-3
@@ -1,8 +1,10 @@
|
||||
import { _generateMetadata, getFixedT } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir";
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/members";
|
||||
import LegacyPage from "@calcom/features/ee/organizations/pages/members";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import SettingsLayoutAppDir from "@calcom/features/settings/appDir/SettingsLayoutAppDir";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -16,9 +18,12 @@ const Page = async () => {
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("organization_members")} description={t("organization_description")}>
|
||||
<LegacyPage isAppDir={true} />
|
||||
<LegacyPage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
const getLayout = async (page: React.ReactElement) =>
|
||||
await SettingsLayoutAppDir({ children: page, containerClassName: "lg:max-w-screen-2xl" });
|
||||
|
||||
export default WithLayout({ ServerPage: Page, getServerLayout: getLayout })<"P">;
|
||||
+4
-5
@@ -1,15 +1,14 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import EditWebhooksView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view";
|
||||
import PlatformMembersView from "@calcom/features/ee/platform/pages/settings/members";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("oAuth_client_updation_form"),
|
||||
() => ""
|
||||
(t) => t("platform_members"),
|
||||
(t) => t("platform_members_description")
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: EditWebhooksView,
|
||||
Page: PlatformMembersView,
|
||||
});
|
||||
@@ -81,6 +81,9 @@ function PageWrapper(props: AppProps) {
|
||||
<Script
|
||||
nonce={nonce}
|
||||
id="page-status"
|
||||
// It is strictly not necessary to disable, but in a future update of react/no-danger this will error.
|
||||
// And we don't want it to error here anyways
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user