Compare commits

...
Author SHA1 Message Date
github-actions[bot] 76bd90b228 chore: version packages (#23714)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-09 15:23:30 +00:00
Benny Joo d908403a99 refactor: circular deps between app store and lib [1] (#23653)
* move dailyApiFetcher.ts to app store package

* update imports

* bookingLocationService

* revert

* move findFieldValueByIdentifier

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* move to features package

* update imports

* update imports

* move

* fix typechecks

* move getCalendarLinks

* mv event test
2025-09-09 14:38:29 +00:00
Amit SharmaandUdit Takkar 0fe775962f fix: intercom showing on cal video page (#23713)
* feat: support auto create agent

* fix: intercom showing on cal.video page

* Revert "feat: support auto create agent"

This reverts commit 084319c419b3baa44a3a82c0a2d79a4242a64d4d.

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2025-09-09 14:28:59 +00:00
Calcom BotandLingo.dev 412a6992c7 feat: update translations via @LingoDotDev (#23715)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-09 11:04:39 -03:00
Rajiv Sahal cfd1992733 feat: add customReplyToEmail for EventTypeSettings atom (#23686)
* init: endpoint for fetching verified emails

* fix: enable custom reply to email in frontend

* init endpoint to add verified emails

* add verified emails option for platform in frontend

* fixup: move useGetVerifiedEmails hook to correct folder

* update atoms module

* fixup: teamId should be string

* add methond to fetch team member emails

* update logic to fetch and add emails

* fixup: append client id with email

* fixup: pass teamId for fetching verified emails

* fixup: simplify check for existing emails

* fix: cleanup comments

* fix: implement code rabbit feedback

* fix: add translations

* fixup: update transaltions

* fix: update logic for addVerifiedEmail

* add changesets

* fix: implement PR feedback
2025-09-09 16:52:28 +03:00
RomitandAnik Dhabal Babu e86d6169ba fix (#23705)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-09-09 19:05:17 +05:30
sean-brydonandcoderabbitai[bot] a379bd2b52 fix: org layout authentication (#23707)
* refactor layout to not check session

* add actions for orgs + org admins

* update types on actions to be correctly non nullable

* Add tests for utils

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* restore lock file

* rename _actions to actions

* update tests to be mocked

* restore lock

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-09 22:27:00 +09:00
Eunjae LeeandDevin AI 1137047606 feat: add PBAC permission checks to insights (#23592)
* feat: add PBAC permission checks to insights

- Add layout-level protection for insights pages with session.user.org.id check
- Add API-level protection for insights tRPC endpoints using insightsPbacProcedure
- Enhance routing form insights with PBAC team filtering
- Use insights.read permission with OWNER/ADMIN fallback roles
- Replace all userBelongsToTeamProcedure with insightsPbacProcedure in insights router
- Fix lint issues: remove unused variables and functions, fix non-null assertion

Implements Permission-Based Access Control (PBAC) for insights functionality following the same patterns as event type PBAC implementation from PR #22618.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: clean up unused imports in routing-forms

- Remove unused isFormCreateEditAllowed import
- Remove unused TRPCError import
- Change MembershipRole to type import for better tree-shaking

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* address feedback

* clean up permission check

* revert unnecessary changes

* fix procedure

* allow MEMBER for insights

* support teams not under orgs

* fix type error

* revert unnecessary changes

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-09 14:26:20 +01:00
Lauris Skraucis ad4cf0cc3a fix: v2 hidden event types (#23584)
* fix: return hidden event types only if owner auth provided

* chore: add tests

* chore: remove optional chaining when defined
2025-09-09 13:23:50 +02:00
RomitandKartik Saini e7d2e0dc2f fix: Prevent loading of dashboard GTM container on booking success page (#22620)
* moved bookings folder

* removed test

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-09-09 16:31:07 +05:30
Alex van Andel aa51218428 perf: move to disable prisma client extension inference (#23692)
* perf: move to disable prisma client extension inference

* Prisma doesn't like it when you pass Record<string, unknown>

* API v1 type fixes

* Missed one

* Fix unit test fail due to faulty expect

* Assigning to prisma InputJsonValue/Array must be done as object, not interface

* Fix @calcom/web ts error, teams not defined

* Run eslint formatter

* fixed the routingFormHelpers file causing a failing app store e2e test
2025-09-09 10:56:58 +00:00
Yadong (Adam) Zhang 38bc5fb71e chore(apps/web): add Scan for React performance issues and eliminate slow renders in web app (#23696)
* chore: add Scan for React performance issues and eliminate slow renders in web app

* chore(apps/web): always install latest `react-scan`.
2025-09-09 10:50:44 +00:00
411516ddb9 fix: duplicate event created in attendee calendar due to incorrect icalUid (#23658)
* chre: update reassigment to delete old events

* fix

* Update yarn.lock

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
2025-09-09 10:29:14 +00:00
Morgan c6c17ac388 fix: delegated credentials timezone outlook (#23700) 2025-09-09 11:25:48 +01:00
160 changed files with 5964 additions and 1466 deletions
@@ -3,7 +3,8 @@ import type { NextApiRequest } from "next";
import { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { schemaCredentialPatchParams, schemaCredentialPatchBody } from "~/lib/validations/credential-sync";
@@ -71,7 +72,7 @@ async function handler(req: NextApiRequest) {
userId,
},
data: {
key,
key: key as unknown as Prisma.InputJsonValue,
},
select: {
id: true,
@@ -6,7 +6,8 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { schemaCredentialPostBody, schemaCredentialPostParams } from "~/lib/validations/credential-sync";
@@ -92,7 +93,7 @@ async function handler(req: NextApiRequest) {
data: {
userId,
appId: appSlug,
key,
key: key as unknown as Prisma.InputJsonValue,
type: appMetadata.type,
},
select: credentialForCalendarServiceSelect,
+1 -1
View File
@@ -77,7 +77,7 @@ describe("POST /api/attendees", () => {
const userBooking = { id: 1 };
prismaMock.booking.findFirst.mockResolvedValue(userBooking);
prismaMock.booking.findFirst.mockResolvedValue(userBooking as any);
const attendeeData = {
id: 1,
@@ -25,7 +25,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
});
afterEach(() => {
@@ -20,7 +20,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
});
afterEach(() => {
+3 -2
View File
@@ -30,12 +30,13 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
name: "Test User",
});
} as any);
(getAccessibleUsers as any).mockResolvedValue([userId]);
(retrieveOrgScopedAccessibleUsers as any).mockResolvedValue([userId]);
prismaMock.membership.findMany.mockResolvedValue([
{
// @ts-expect-error Will be fixed by Prisma 6.7.0 upgrade mock changes - which uses vitest-mock-extended
team: {
id: 1,
isOrganization: true,
@@ -48,7 +49,7 @@ beforeEach(() => {
id: userId,
email: "test@example.com",
},
]);
] as any);
});
afterEach(() => {
+12 -4
View File
@@ -11,9 +11,9 @@ import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBook
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import { CreationSource, BookingStatus } from "@calcom/prisma/enums";
import handler from "../../../pages/api/bookings/_post";
@@ -303,6 +303,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -340,6 +341,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -376,6 +378,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -417,6 +420,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -468,7 +472,7 @@ describe("POST /api/bookings", () => {
prismaMock.booking.findUnique.mockResolvedValue({
...originalBooking,
status: "cancelled",
status: "CANCELLED",
});
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
@@ -492,6 +496,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -532,7 +537,7 @@ describe("POST /api/bookings", () => {
const previousBooking = await prisma.booking.findUnique({
where: { uid: "original-booking-uid" },
});
expect(previousBooking?.status).toBe("cancelled");
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
});
test("Creates source as api_v1", async () => {
@@ -556,6 +561,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -627,6 +633,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ profileId: null, length: 15 }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -684,6 +691,7 @@ describe("POST /api/bookings", () => {
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue({
...buildEventType({ recurringEvent: { freq: 2, count: 12, interval: 1 } }),
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
profile: { organizationId: null },
hosts: [],
users: [buildUser()],
@@ -50,6 +50,7 @@ describe("DELETE /api/event-types/[id]", () => {
// Mocking team.findUnique
prismaMock.team.findUnique.mockResolvedValue({
id: teamId,
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
members: [
{ userId: memberUser, role: MembershipRole.MEMBER, teamId: teamId },
{ userId: adminUser, role: MembershipRole.ADMIN, teamId: teamId },
@@ -84,6 +84,7 @@ describe("GET /api/event-types/[id]", () => {
prismaMock.team.findFirst.mockResolvedValue({
id: teamId,
// @ts-expect-error requires mockDeep which will be introduced in the Prisma 6.7.0 upgrade, ignore for now.
members: [
{
userId,
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.347",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.349",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
@@ -12,6 +12,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User, Schedule, EventType } from "@prisma/client";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
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";
@@ -88,11 +89,16 @@ describe("Event types Endpoints", () => {
let schedulesRepostoryFixture: SchedulesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let apiKeyString: string;
let apiKeyOrgUser: string;
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
const name = `event-types-2024-06-14-user-${randomString()}`;
const username = name;
let eventType: EventTypeOutput_2024_06_14;
let hiddenEventType: EventTypeOutput_2024_06_14;
let user: User;
let orgUser: User;
let falseTestUser: User;
@@ -165,13 +171,10 @@ describe("Event types Endpoints", () => {
};
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
)
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -198,12 +201,20 @@ describe("Event types Endpoints", () => {
name,
username,
});
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;
orgUser = await userRepositoryFixture.create({
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
name: `event-types-2024-06-14-org-user-${randomString()}`,
username: `event-types-2024-06-14-org-user-${randomString()}`,
});
const { keyString: orgUserApiKeyString } = await apiKeysRepositoryFixture.createApiKey(
orgUser.id,
null
);
apiKeyOrgUser = `cal_test_${orgUserApiKeyString}`;
profileRepositoryFixture.create({
uid: `usr-${orgUser.id}`,
@@ -334,6 +345,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(404);
});
@@ -370,6 +382,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
@@ -395,6 +408,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
});
@@ -504,6 +518,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201)
.then(async (response) => {
@@ -576,21 +591,52 @@ describe("Event types Endpoints", () => {
});
});
it("should create a hidden event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class hidden",
slug: "coding-class-hidden",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
hidden: true,
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.hidden).toEqual(body.hidden);
expect(createdEventType.ownerId).toEqual(user.id);
hiddenEventType = responseBody.data;
});
it(`/GET/event-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.get(`/api/v2/event-types?username=${user.username}`)
.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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.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?.length).toEqual(2);
const fetchedEventType = responseBody.data?.[0];
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
const fetchedHiddenEventType = responseBody.data?.find((et) => et.id === hiddenEventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
@@ -602,33 +648,149 @@ describe("Event types Endpoints", () => {
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType.customName).toEqual(eventType.customName);
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType?.customName).toEqual(eventType.customName);
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
expect(fetchedEventType?.color).toEqual(eventType.color);
expect(fetchedEventType?.hidden).toEqual(false);
expect(fetchedHiddenEventType?.id).toEqual(hiddenEventType.id);
expect(fetchedHiddenEventType?.hidden).toEqual(true);
});
it(`/GET/event-types by username should not return hidden event type if auth of non event type owner provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
.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);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.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);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username and eventSlug`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${eventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.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);
const fetchedEventType = responseBody.data?.find((et) => et.id === eventType.id);
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields.sort(orderBySlug)).toEqual(
eventType.bookingFields.sort(orderBySlug).filter((f) => ("hidden" in f ? !f?.hidden : true))
);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType?.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType?.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType?.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType?.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType?.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType?.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType?.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType?.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType?.customName).toEqual(eventType.customName);
expect(fetchedEventType?.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType?.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType?.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
expect(fetchedEventType?.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
expect(fetchedEventType?.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType?.color).toEqual(eventType.color);
expect(fetchedEventType?.hidden).toEqual(false);
});
it(`/GET/event-types by username and eventSlug should not return hidden event type if auth of non event type owner provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyOrgUser}`)
.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(0);
});
it(`/GET/event-types by username and eventSlug should not return hidden event type if no auth provided`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${user.username}&eventSlug=${hiddenEventType.slug}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.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(0);
});
it(`/GET/event-types by username and orgSlug`, async () => {
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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -647,8 +809,7 @@ describe("Event types Endpoints", () => {
`/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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -663,8 +824,7 @@ describe("Event types Endpoints", () => {
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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -705,6 +865,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(400);
});
@@ -732,6 +893,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
@@ -748,6 +910,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
seats: {
@@ -779,6 +942,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -799,6 +963,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
@@ -817,6 +982,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send({
seats: {
seatsPerTimeSlot: 4,
@@ -844,6 +1010,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(201);
@@ -860,6 +1027,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
locations: [
@@ -897,6 +1065,7 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -918,6 +1087,7 @@ describe("Event types Endpoints", () => {
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
@@ -935,6 +1105,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
confirmationPolicy: {
@@ -1034,6 +1205,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.send(body)
.expect(200)
.then(async (response) => {
@@ -1135,6 +1307,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(404);
@@ -1152,6 +1325,7 @@ describe("Event types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventType.id}`)
.set("Authorization", `Bearer ${apiKeyString}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
@@ -1161,8 +1335,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
@@ -1203,8 +1376,7 @@ describe("Event types Endpoints", () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
@@ -1245,13 +1417,16 @@ describe("Event types Endpoints", () => {
await request(app.getHttpServer())
.get(`/api/v2/event-types/1000`)
.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`)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(404);
});
it("should delete event type", async () => {
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
return request(app.getHttpServer())
.delete(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200);
});
afterAll(async () => {
@@ -6,11 +6,22 @@ import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import {
API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
} from "@/lib/docs/headers";
import {
AuthOptionalUser,
GetOptionalUser,
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
@@ -40,7 +51,6 @@ import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
@Controller({
@@ -62,7 +72,8 @@ export class EventTypesController_2024_06_14 {
constructor(
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
) {}
@Post("/")
@@ -109,30 +120,23 @@ export class EventTypesController_2024_06_14 {
}
@Get("/")
@ApiOperation({ summary: "Get all event types" })
@ApiOperation({
summary: "Get all event types",
description:
"Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
})
@UseGuards(OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
async getEventTypes(
@Query() queryParams: GetEventTypesQuery_2024_06_14
@Query() queryParams: GetEventTypesQuery_2024_06_14,
@GetOptionalUser() authUser: AuthOptionalUser
): Promise<GetEventTypesOutput_2024_06_14> {
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
if (!eventTypes || eventTypes.length === 0) {
throw new NotFoundException(`Event types not found`);
}
const eventTypes = await this.eventTypesService.getEventTypes(queryParams, authUser);
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
return {
...eventType,
bookingFields: Array.isArray(eventType?.bookingFields)
? eventType?.bookingFields
.map((field) => {
if ("hidden" in field) {
return field.hidden !== true ? field : null;
}
return field;
})
.filter((f) => f)
: [],
};
}) as EventTypeOutput_2024_06_14[];
const eventTypesWithoutHiddenFields =
this.outputEventTypesService.getResponseEventTypesWithoutHiddenFields(eventTypesFormatted);
return {
status: SUCCESS_STATUS,
@@ -75,6 +75,16 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypesPublic(userId: number) {
return this.dbRead.prisma.eventType.findMany({
where: {
userId,
hidden: false,
},
include: { users: true, schedule: true, destinationCalendar: true },
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -3,6 +3,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
@@ -38,6 +39,7 @@ export class EventTypesService_2024_06_14 {
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { destinationCalendar: _destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
@@ -93,34 +95,47 @@ export class EventTypesService_2024_06_14 {
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
orgSlug?: string,
orgId?: number
) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
async getEventTypeByUsernameAndSlug(params: {
username: string;
eventTypeSlug: string;
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return null;
}
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, eventTypeSlug);
const eventType = await this.eventTypesRepository.getUserEventTypeBySlug(user.id, params.eventTypeSlug);
if (!eventType) {
return null;
}
if (eventType.hidden && params.authUser?.id !== user.id) {
return null;
}
return {
ownerId: user.id,
...eventType,
};
}
async getEventTypesByUsername(username: string, orgSlug?: string, orgId?: number) {
const user = await this.usersRepository.findByUsername(username, orgSlug, orgId);
async getEventTypesByUsername(params: {
username: string;
orgSlug?: string;
orgId?: number;
authUser?: AuthOptionalUser;
}) {
const user = await this.usersRepository.findByUsername(params.username, params.orgSlug, params.orgId);
if (!user) {
return [];
}
if (params.authUser?.id !== user.id) {
return await this.getUserEventTypesPublic(user.id);
}
return await this.getUserEventTypes(user.id);
}
@@ -172,6 +187,14 @@ export class EventTypesService_2024_06_14 {
});
}
async getUserEventTypesPublic(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypesPublic(userId);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
});
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
@@ -181,15 +204,26 @@ export class EventTypesService_2024_06_14 {
return await getEventTypesPublic(user.id);
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14, authUser?: AuthOptionalUser) {
const { username, eventSlug, usernames, orgSlug, orgId } = queryParams;
if (username && eventSlug) {
const eventType = await this.getEventTypeByUsernameAndSlug(username, eventSlug, orgSlug, orgId);
const eventType = await this.getEventTypeByUsernameAndSlug({
username,
eventTypeSlug: eventSlug,
orgSlug,
orgId,
authUser,
});
return eventType ? [eventType] : [];
}
if (username) {
return await this.getEventTypesByUsername(username, orgSlug, orgId);
return await this.getEventTypesByUsername({
username,
orgSlug,
orgId,
authUser,
});
}
if (usernames) {
@@ -31,6 +31,7 @@ import {
EventTypeOutput_2024_06_14,
OutputUnknownLocation_2024_06_14,
OutputUnknownBookingField_2024_06_14,
OutputBookingField_2024_06_14,
} from "@calcom/platform-types";
type EventTypeRelations = {
@@ -253,7 +254,7 @@ export class OutputEventTypesService_2024_06_14 {
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(bookingField),
});
} satisfies OutputUnknownBookingField_2024_06_14);
}
}
@@ -344,4 +345,26 @@ export class OutputEventTypesService_2024_06_14 {
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
});
}
getResponseEventTypesWithoutHiddenFields(
eventTypes: EventTypeOutput_2024_06_14[]
): EventTypeOutput_2024_06_14[] {
return eventTypes.map((eventType) => this.getResponseEventTypeWithoutHiddenFields(eventType));
}
getResponseEventTypeWithoutHiddenFields(eventType: EventTypeOutput_2024_06_14): EventTypeOutput_2024_06_14 {
if (!Array.isArray(eventType?.bookingFields) || eventType.bookingFields.length === 0) return eventType;
const visibleBookingFields: OutputBookingField_2024_06_14[] = [];
for (const bookingField of eventType.bookingFields) {
if ("hidden" in bookingField && bookingField.hidden === true) {
continue;
}
visibleBookingFields.push(bookingField);
}
return {
...eventType,
bookingFields: visibleBookingFields,
};
}
}
@@ -17,6 +17,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisService } from "@/modules/redis/redis.service";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@@ -36,6 +37,7 @@ import { Module } from "@nestjs/common";
SchedulesAtomsService,
VerificationAtomsService,
RedisService,
TeamsRepository,
],
exports: [EventTypesAtomService],
controllers: [
@@ -64,4 +64,47 @@ export class AtomsRepository {
return userTeams;
}
async getSecondaryEmails(userId: number) {
return await this.dbRead.prisma.secondaryEmail.findMany({
where: {
userId,
emailVerified: {
not: null,
},
},
});
}
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
userId_email: { userId, email },
},
});
return existingSecondaryEmailRecord?.email;
}
async getExistingSecondaryEmail(email: string) {
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
where: {
email,
},
});
return existingSecondaryEmailRecord?.email;
}
async addSecondaryEmail(userId: number, email: string) {
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
data: {
userId,
email,
emailVerified: new Date(),
},
});
return existingSecondaryEmailRecord?.email;
}
}
@@ -1,8 +1,11 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Throttle } from "@/lib/endpoint-throttler-decorator";
import { AddVerifiedEmailInput } from "@/modules/atoms/inputs/add-verified-email.input";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { GetVerifiedEmailsParams } from "@/modules/atoms/inputs/get-verified-emails-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { GetVerifiedEmailsOutput } from "@/modules/atoms/outputs/get-verified-emails-output";
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
@@ -105,4 +108,44 @@ export class AtomsVerificationController {
status: SUCCESS_STATUS,
};
}
@Get("/emails/verified-emails")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async getVerifiedEmails(
@Query() query: GetVerifiedEmailsParams,
@GetUser() user: UserWithProfile
): Promise<GetVerifiedEmailsOutput> {
const verifiedEmails = await this.verificationService.getVerifiedEmails({
userId: user.id,
userEmail: user.email,
teamId: query.teamId ? Number(query.teamId) : undefined,
});
return {
data: verifiedEmails,
status: SUCCESS_STATUS,
};
}
@Post("/emails/verified-emails")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async addVerifiedEmails(
@Body() body: AddVerifiedEmailInput,
@GetUser() user: UserWithProfile
): Promise<ApiResponse<{ emailVerified: boolean }>> {
const emailVerified = await this.verificationService.addVerifiedEmail({
userId: user.id,
existingPrimaryEmail: user.email,
email: body.email,
});
return {
data: { emailVerified },
status: SUCCESS_STATUS,
};
}
}
@@ -0,0 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail } from "class-validator";
export class AddVerifiedEmailInput {
@ApiProperty({ example: "user@example.com" })
@IsEmail()
email!: string;
}
@@ -0,0 +1,25 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNumber, IsOptional, IsString, IsEmail } from "class-validator";
export class GetVerifiedEmailsParams {
@ApiPropertyOptional({ example: "12345" })
@IsOptional()
@IsString()
teamId?: string;
}
export class GetVerifiedEmailsInput {
@ApiProperty()
@IsNumber()
userId!: number;
@ApiProperty()
@IsEmail()
@IsString()
userEmail!: string;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
teamId?: number;
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsString, IsArray } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class GetVerifiedEmailsOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsString()
@Expose()
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
readonly data!: string[];
}
@@ -1,6 +1,9 @@
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
@@ -13,6 +16,11 @@ import {
@Injectable()
export class VerificationAtomsService {
constructor(
private readonly atomsRepository: AtomsRepository,
private readonly teamsRepository: TeamsRepository
) {}
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
return await checkEmailVerificationRequired(input);
}
@@ -61,4 +69,78 @@ export class VerificationAtomsService {
isVerifyingEmail: input.isVerifyingEmail,
});
}
async getVerifiedEmails(input: GetVerifiedEmailsInput): Promise<string[]> {
const { userId, userEmail, teamId } = input;
const userEmailWithoutOauthClientId = this.removeClientIdFromEmail(userEmail);
if (teamId) {
const verifiedEmails: string[] = [];
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
if (teamMembers.length === 0) {
return verifiedEmails;
}
teamMembers.forEach((member) => {
const memberEmailWithoutOauthClientId = this.removeClientIdFromEmail(member.email);
verifiedEmails.push(memberEmailWithoutOauthClientId);
member.secondaryEmails.forEach((secondaryEmail) => {
verifiedEmails.push(this.removeClientIdFromEmail(secondaryEmail.email));
});
});
return verifiedEmails;
}
let verifiedEmails = [userEmailWithoutOauthClientId];
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
verifiedEmails = verifiedEmails.concat(
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
);
return verifiedEmails;
}
async addVerifiedEmail({
userId,
existingPrimaryEmail,
email,
}: {
userId: number;
existingPrimaryEmail: string;
email: string;
}): Promise<boolean> {
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
userId,
email
);
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
if (alreadyExistingEmail) {
throw new BadRequestException("Email already exists");
}
if (existingPrimaryEmail === email || existingSecondaryEmail === email) {
return true;
}
await this.atomsRepository.addSecondaryEmail(userId, email);
return true;
}
removeClientIdFromEmail(email: string): string {
const [localPart, domain] = email.split("@");
const localPartSegments = localPart.split("+");
localPartSegments.pop();
const baseEmail = localPartSegments.join("+");
const normalizedEmail = `${baseEmail}@${domain}`;
return normalizedEmail;
}
}
@@ -120,4 +120,30 @@ export class TeamsRepository {
},
});
}
async getTeamMemberEmails(teamId: number) {
return this.dbRead.prisma.user.findMany({
where: {
teams: {
some: {
teamId,
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
emailVerified: {
not: null,
},
},
select: {
email: true,
},
},
},
});
}
}
+28
View File
@@ -9858,6 +9858,7 @@
"get": {
"operationId": "EventTypesController_2024_06_14_getEventTypes",
"summary": "Get all event types",
"description": "Hidden event types are returned only if authentication is provided and it belongs to the event type owner.",
"parameters": [
{
"name": "cal-api-version",
@@ -9913,6 +9914,33 @@
"schema": {
"type": "number"
}
},
{
"name": "Authorization",
"in": "header",
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-secret-key",
"in": "header",
"description": "For platform customers - OAuth client secret key",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "x-cal-client-id",
"in": "header",
"description": "For platform customers - OAuth client ID",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
@@ -0,0 +1,34 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { hasInsightsPermission } from "@calcom/features/insights/server/hasInsightsPermission";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export async function checkInsightsPagePermission() {
const featuresRepository = new FeaturesRepository(prisma);
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
if (!insightsEnabled) {
redirect("/");
}
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user?.id) {
redirect("/auth/login");
}
const hasPermission = await hasInsightsPermission({
userId: session.user.id,
organizationId: session.user.org?.id,
});
if (!hasPermission) {
redirect("/");
}
return session;
}
@@ -1,34 +1,22 @@
import { getTranslate } from "app/_utils";
import { notFound } from "next/navigation";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import Shell from "@calcom/features/shell/Shell";
import { prisma } from "@calcom/prisma";
import UpgradeTipWrapper from "./UpgradeTipWrapper";
export default async function InsightsLayout({ children }: { children: React.ReactNode }) {
const featuresRepository = new FeaturesRepository(prisma);
const insightsEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("insights");
if (!insightsEnabled) {
return notFound();
}
const t = await getTranslate();
return (
<>
<div>
<Shell
withoutMain={false}
heading={t("insights")}
subtitle={t("insights_subtitle")}
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
</Shell>
</div>
</>
<div>
<Shell
withoutMain={false}
heading={t("insights")}
subtitle={t("insights_subtitle")}
actions={<div className={`mb-2 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<UpgradeTipWrapper>{children}</UpgradeTipWrapper>
</Shell>
</div>
);
}
@@ -1,13 +1,11 @@
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import InsightsPage from "~/insights/insights-view";
import { checkInsightsPagePermission } from "./checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -18,7 +16,7 @@ export const generateMetadata = async () =>
);
const ServerPage = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await checkInsightsPagePermission();
const { timeZone } = await prisma.user.findUniqueOrThrow({
where: { id: session?.user.id ?? -1 },
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -12,5 +14,7 @@ export const generateMetadata = async () =>
);
export default async function Page() {
await checkInsightsPagePermission();
return <InsightsVirtualQueuesPage />;
}
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import InsightsRoutingPage from "~/insights/insights-routing-view";
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
@@ -12,5 +14,7 @@ export const generateMetadata = async () =>
);
export default async function Page() {
await checkInsightsPagePermission();
return <InsightsRoutingPage />;
}
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesEditPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-edit-view";
import { validateUserHasOrgAdmin } from "../../../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
);
const OrgAttributesEditPageWrapper = async () => {
await validateUserHasOrgAdmin();
return <OrgAttributesEditPage />;
};
@@ -2,6 +2,8 @@ import { _generateMetadata } from "app/_utils";
import OrgAttributesCreatePage from "@calcom/ee/organizations/pages/settings/attributes/attributes-create-view";
import { validateUserHasOrgAdmin } from "../../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("attribute"),
@@ -12,6 +14,8 @@ export const generateMetadata = async () =>
);
const OrgAttributesCreatePageWrapper = async () => {
await validateUserHasOrgAdmin();
return <OrgAttributesCreatePage />;
};
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import OrgSettingsAttributesPage from "@calcom/ee/organizations/pages/settings/attributes/attributes-list-view";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
}
const session = await validateUserHasOrgAdmin();
const { canRead, canEdit, canDelete, canCreate } = await getResourcePermissions({
userId: session.user.id,
@@ -1,5 +1,35 @@
import BillingPage, { generateMetadata } from "../../../billing/page";
import { _generateMetadata } from "app/_utils";
import { getTranslate } from "app/_utils";
export { generateMetadata };
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export default BillingPage;
import BillingView from "~/settings/billing/billing-view";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("manage_billing_description"),
undefined,
undefined,
"/settings/organizations/billing"
);
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrgAdmin();
// TODO(SEAN): Add PBAC to this page in the next PR
return (
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={true}>
<BillingView />
</SettingsHeader>
);
};
export default Page;
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import DelegationCredentialList from "@calcom/features/ee/organizations/pages/settings/delegationCredential";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("delegation_credential"),
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrgAdmin();
return (
<SettingsHeader
borderInShellHeader
@@ -1,15 +1,12 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,11 +19,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
}
const session = await validateUserHasOrgAdmin();
const { canEdit } = await getResourcePermissions({
userId: session.user.id,
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -21,9 +19,9 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const [t, _headers, _cookies] = await Promise.all([getTranslate(), headers(), cookies()]);
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(_headers, _cookies) });
const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
@@ -1,15 +1,13 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrgAdmin } from "../../actions/validateUserHasOrgAdmin";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,7 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrgAdmin();
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/organizations/general");
@@ -0,0 +1,229 @@
import type { Session } from "next-auth";
import { redirect } from "next/navigation";
import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrg, type ValidatedOrgSession } from "./validateUserHasOrg";
// Mock the dependencies
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("next/headers", () => ({
cookies: vi.fn(() => Promise.resolve({})),
headers: vi.fn(() => Promise.resolve({})),
}));
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn(() => ({})),
}));
const mockedGetServerSession = getServerSession as MockedFunction<typeof getServerSession>;
const mockedRedirect = vi.mocked(redirect);
describe("validateUserHasOrg", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockSession = (overrides: Partial<Session> = {}): Session => ({
expires: "2025-01-01",
hasValidLicense: true,
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
profileId: 789,
upId: "usr-123",
...overrides,
});
describe("when user has valid organization", () => {
it("should return session when user has org in user.org", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const result = await validateUserHasOrg();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(result.user.id).toBe(123);
expect(result.user.org?.id).toBe(456);
expect(result.user.profile?.organizationId).toBe(456);
});
it("should return session when user has organizationId but no org", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const result = await validateUserHasOrg();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
});
});
describe("when user does not have valid organization", () => {
it("should redirect when session is null", async () => {
mockedGetServerSession.mockResolvedValue(null);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no organization data", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: null,
organization: null,
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no user id", async () => {
const mockSession = createMockSession({
user: {
id: undefined as unknown as number,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrg()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
});
describe("return type validation", () => {
it("should return ValidatedOrgSession type with non-nullable properties", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const result: ValidatedOrgSession = await validateUserHasOrg();
// Type assertions to ensure proper typing
expect(typeof result.user.id).toBe("number");
expect(result.user.org).toBeTruthy();
expect(result.user.profile).toBeTruthy();
expect(typeof result.user.profile.organizationId).toBe("number");
// Runtime checks that the types guarantee these properties exist
expect(result.user.id).toBe(123);
expect(result.user.org.id).toBe(456);
expect(result.user.profile.organizationId).toBe(456);
});
});
describe("error handling", () => {
it("should propagate errors from getServerSession", async () => {
const error = new Error("Session error");
mockedGetServerSession.mockRejectedValue(error);
await expect(validateUserHasOrg()).rejects.toThrow("Session error");
});
});
});
@@ -0,0 +1,30 @@
import type { Session } from "next-auth";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export type ValidatedOrgSession = NonNullable<Session> & {
user: NonNullable<Session["user"]> & {
id: number;
org: NonNullable<NonNullable<Session["user"]>["org"]>;
profile: NonNullable<NonNullable<Session["user"]>["profile"]> & {
organizationId: number;
};
};
};
export async function validateUserHasOrg(): Promise<ValidatedOrgSession> {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
redirect("/settings/my-account/profile");
}
return session as ValidatedOrgSession;
}
@@ -0,0 +1,418 @@
import type { Session } from "next-auth";
import { redirect } from "next/navigation";
import { describe, it, vi, expect, beforeEach, type MockedFunction } from "vitest";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { MembershipRole } from "@calcom/prisma/enums";
import { validateUserHasOrgAdmin, type ValidatedOrgAdminSession } from "./validateUserHasOrgAdmin";
// Mock the dependencies
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("next/headers", () => ({
cookies: vi.fn(() => Promise.resolve({})),
headers: vi.fn(() => Promise.resolve({})),
}));
vi.mock("@calcom/features/auth/lib/getServerSession", () => ({
getServerSession: vi.fn(),
}));
vi.mock("@calcom/features/auth/lib/checkAdminOrOwner", () => ({
checkAdminOrOwner: vi.fn(),
}));
vi.mock("@lib/buildLegacyCtx", () => ({
buildLegacyRequest: vi.fn(() => ({})),
}));
const mockedGetServerSession = getServerSession as MockedFunction<typeof getServerSession>;
const mockedCheckAdminOrOwner = vi.mocked(checkAdminOrOwner);
const mockedRedirect = vi.mocked(redirect);
describe("validateUserHasOrgAdmin", () => {
beforeEach(() => {
vi.clearAllMocks();
});
const createMockSession = (overrides: Partial<Session> = {}): Session => ({
expires: "2025-01-01",
hasValidLicense: true,
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.ADMIN,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
profileId: 789,
upId: "usr-123",
...overrides,
});
describe("when user has valid organization and admin role", () => {
it("should return session when user is admin via user.org.role", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
expect(result.user.id).toBe(123);
expect(result.user.org?.id).toBe(456);
expect(result.user.profile?.organizationId).toBe(456);
});
it("should return session when user is owner", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.OWNER,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
});
it("should return session when role comes from organization members", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 123, role: MembershipRole.ADMIN },
{ userId: 124, role: MembershipRole.MEMBER },
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result = await validateUserHasOrgAdmin();
expect(result).toEqual(mockSession);
expect(mockedRedirect).not.toHaveBeenCalled();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
});
});
describe("when user does not have valid organization", () => {
it("should redirect when session is null", async () => {
mockedGetServerSession.mockResolvedValue(null);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
it("should redirect when user has no organization data", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: null,
organization: null,
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/my-account/profile");
});
});
describe("when user has organization but is not admin", () => {
it("should redirect when user is only a member", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.MEMBER,
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(false);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.MEMBER);
});
it("should redirect when role is undefined", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 999, role: MembershipRole.ADMIN }, // Different user
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(false);
const redirectError = new Error("NEXT_REDIRECT");
mockedRedirect.mockImplementation(() => {
throw redirectError;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("NEXT_REDIRECT");
expect(mockedRedirect).toHaveBeenCalledWith("/settings/organizations/profile");
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(undefined);
});
});
describe("return type validation", () => {
it("should return ValidatedOrgAdminSession type with non-nullable properties", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
const result: ValidatedOrgAdminSession = await validateUserHasOrgAdmin();
// Type assertions to ensure proper typing
expect(typeof result.user.id).toBe("number");
expect(result.user.org).toBeTruthy();
expect(result.user.profile).toBeTruthy();
expect(typeof result.user.profile.organizationId).toBe("number");
expect(result.user.org.role).toBeTruthy();
// Runtime checks that the types guarantee these properties exist
expect(result.user.id).toBe(123);
expect(result.user.org.id).toBe(456);
expect(result.user.org.role).toBe(MembershipRole.ADMIN);
expect(result.user.profile.organizationId).toBe(456);
});
});
describe("role resolution logic", () => {
it("should prefer org role over organization members role", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: {
id: 456,
name: "Test Org",
slug: "test-org",
logoUrl: null,
fullDomain: "test-org.cal.com",
domainSuffix: "cal.com",
role: MembershipRole.OWNER, // This should be preferred
},
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [
{ userId: 123, role: MembershipRole.MEMBER }, // Different role
],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
await validateUserHasOrgAdmin();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.OWNER);
});
it("should fall back to organization members role when org role is not available", async () => {
const mockSession = createMockSession({
user: {
id: 123,
email: "test@example.com",
name: "Test User",
org: undefined,
profile: {
id: 789,
upId: "usr-123",
username: "testuser",
organizationId: 456,
organization: {
id: 456,
name: "Test Org",
slug: "test-org",
calVideoLogo: null,
bannerUrl: null,
requestedSlug: null,
members: [{ userId: 123, role: MembershipRole.ADMIN }],
},
},
},
});
mockedGetServerSession.mockResolvedValue(mockSession);
mockedCheckAdminOrOwner.mockReturnValue(true);
await validateUserHasOrgAdmin();
expect(mockedCheckAdminOrOwner).toHaveBeenCalledWith(MembershipRole.ADMIN);
});
});
describe("error handling", () => {
it("should propagate errors from getServerSession", async () => {
const error = new Error("Session error");
mockedGetServerSession.mockRejectedValue(error);
await expect(validateUserHasOrgAdmin()).rejects.toThrow("Session error");
});
it("should propagate errors from checkAdminOrOwner", async () => {
const mockSession = createMockSession();
mockedGetServerSession.mockResolvedValue(mockSession);
const error = new Error("Permission check error");
mockedCheckAdminOrOwner.mockImplementation(() => {
throw error;
});
await expect(validateUserHasOrgAdmin()).rejects.toThrow("Permission check error");
});
});
});
@@ -0,0 +1,45 @@
import type { Session } from "next-auth";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
export type ValidatedOrgAdminSession = NonNullable<Session> & {
user: NonNullable<Session["user"]> & {
id: number;
org: NonNullable<NonNullable<Session["user"]>["org"]> & {
role: MembershipRole;
};
profile: NonNullable<NonNullable<Session["user"]>["profile"]> & {
organizationId: number;
};
};
};
export async function validateUserHasOrgAdmin(): Promise<ValidatedOrgAdminSession> {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists || !session?.user?.id || !session?.user?.profile?.organizationId) {
redirect("/settings/my-account/profile");
}
const userProfile = session?.user?.profile;
const userId = session?.user?.id;
const orgRole =
session?.user?.org?.role ??
userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
if (!isOrgAdminOrOwner) {
redirect("/settings/organizations/profile");
}
return session as ValidatedOrgAdminSession;
}
@@ -3,6 +3,8 @@ import { getTranslate, _generateMetadata } from "app/_utils";
import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => `${t("admin")} ${t("api_reference")}`,
@@ -15,6 +17,8 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={`${t("admin")} ${t("api_reference")}`}
@@ -1,15 +1,12 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { headers, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -23,11 +20,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) {
return redirect("/settings/profile");
}
const session = await validateUserHasOrg();
const { canRead, canEdit } = await getResourcePermissions({
userId: session.user.id,
@@ -1,19 +1,4 @@
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
const SettingsOrganizationsLayout = async ({ children }: { children: React.ReactNode }) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const orgExists =
session?.user?.org || session?.user?.profile?.organizationId || session?.user?.profile?.organization;
if (!orgExists) {
return redirect("/settings/my-account/profile");
}
return children;
};
@@ -1,8 +1,6 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile";
import { Resource } from "@calcom/features/pbac/domain/types/permission-registry";
import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions";
@@ -10,7 +8,7 @@ import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import type { Membership } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -22,20 +20,20 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
const t = await getTranslate();
const orgRole = session?.user.profile?.organization.members?.find(
(member: Membership) => member.userId === session?.user.id
const orgRole = session.user.profile.organization.members?.find(
(member: Membership) => member.userId === session.user.id
)?.role;
if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) {
if (!orgRole) {
return redirect("/settings/profile");
}
const { canRead, canEdit, canDelete } = await getResourcePermissions({
userId: session.user.id,
teamId: session?.user.profile?.organizationId,
teamId: session.user.profile.organizationId,
resource: Resource.Organization,
userRole: orgRole,
fallbackRoles: {
@@ -1,9 +1,7 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -13,8 +11,7 @@ import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../actions/validateUserHasOrg";
import { CreateRoleCTA } from "./_components/CreateRoleCta";
import { RolesList } from "./_components/RolesList";
import { roleSearchParamsCache } from "./_components/searchParams";
@@ -58,7 +55,7 @@ export const generateMetadata = async () =>
const Page = async ({ searchParams }: { searchParams: Record<string, string | string[] | undefined> }) => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
if (!session?.user?.org?.id || !session.user.id) {
return notFound();
@@ -1,13 +1,11 @@
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { OtherTeamsListing } from "@calcom/features/ee/organizations/pages/components/OtherTeamsListing";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validateUserHasOrg } from "../../../actions/validateUserHasOrg";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -20,7 +18,7 @@ export const generateMetadata = async () =>
const Page = async () => {
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const session = await validateUserHasOrg();
if (!session?.user?.id) {
redirect("/auth/login");
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("booking_appearance"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={t("booking_appearance")}
@@ -5,6 +5,8 @@ import LegacyPage, {
} from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("team_members"),
@@ -17,6 +19,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader
title={t("team_members")}
@@ -3,6 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { validateUserHasOrg } from "../../../../actions/validateUserHasOrg";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
(t) => t("profile"),
@@ -15,6 +17,8 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
const Page = async () => {
const t = await getTranslate();
await validateUserHasOrg();
return (
<SettingsHeader title={t("profile")} description={t("profile_team_description")}>
<LegacyPage />
@@ -26,12 +26,12 @@ import {
SystemField,
TITLE_FIELD,
} from "@calcom/features/bookings/lib/SystemField";
import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks";
import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating";
import { getCalendarLinks, CalendarLinkType } from "@calcom/lib/bookings/getCalendarLinks";
import type { nameObjectSchema } from "@calcom/features/eventtypes/lib/eventNaming";
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
import { APP_NAME } from "@calcom/lib/constants";
import { formatToLocalizedDate, formatToLocalizedTime, formatToLocalizedTimezone } from "@calcom/lib/dayjs";
import type { nameObjectSchema } from "@calcom/lib/event";
import { getEventName } from "@calcom/lib/event";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+1 -1
View File
@@ -35,7 +35,7 @@ vi.mock("@calcom/app-store/utils", () => ({
getEventTypeAppData: vi.fn(),
}));
vi.mock("@calcom/lib/event", () => ({
vi.mock("@calcom/features/eventtypes/lib/eventNaming", () => ({
getEventName: vi.fn(),
}));
+1
View File
@@ -8,6 +8,7 @@
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "yarn copy-static && next dev --turbopack",
"dev:scan": "yarn dev & npx --yes react-scan@latest localhost:3000",
"dev:cron": "npx tsx cron-tester.ts",
"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
"dx": "yarn dev",
@@ -29,8 +29,11 @@ interface RouteConfig {
value: string;
};
isFallback?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryValue?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributesQueryValue?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fallbackAttributesQueryValue?: any;
}
@@ -129,6 +132,7 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
const teamEventsConfig = config.attributeRouting.teamEvents;
// Create team events
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const teamEvents: { [key: string]: any } = {};
for (const eventConfig of teamEventsConfig) {
const eventType = await createRoundRobinTeamEventType({
@@ -149,10 +153,8 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
throw new Error("Attributes not found");
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const skillAttribute = attributes.find((attr) => attr.name === "Skills")!;
const locationAttribute = attributes.find((attr) => attr.name === "Location")!;
console.log("Attributes", JSON.stringify(attributes, null, 2));
const javascriptEventRoute = {
id: "8a898988-89ab-4cde-b012-31823f708642",
@@ -316,41 +318,44 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
queryValue: { id: "814899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: config.form?.fields || [
{
id: form.formFieldLocation.id,
type: "select",
label: "Location",
options: formFieldLocationOptions,
required: true,
},
{
id: form.formFieldSkills.id,
type: "multiselect",
label: "skills",
options: formFieldSkillsOptions,
required: true,
},
{
id: form.formFieldEmail.id,
type: "email",
label: "Email",
required: true,
},
{
id: form.formFieldName.id,
identifier: "name",
type: "text",
label: "Name",
required: false,
},
{
id: form.formFieldRating.id,
type: "number",
label: "Rating",
required: false,
},
],
fields: (
config.form?.fields ||
([
{
id: form.formFieldLocation.id,
type: "select",
label: "Location",
options: formFieldLocationOptions,
required: true,
},
{
id: form.formFieldSkills.id,
type: "multiselect",
label: "skills",
options: formFieldSkillsOptions,
required: true,
},
{
id: form.formFieldEmail.id,
type: "email",
label: "Email",
required: true,
},
{
id: form.formFieldName.id,
identifier: "name",
type: "text",
label: "Name",
required: false,
},
{
id: form.formFieldRating.id,
type: "number",
label: "Rating",
required: false,
},
] satisfies FieldConfig[])
).map((field) => ({ ...field })),
team: {
connect: {
id: teamId,
@@ -379,172 +384,178 @@ export async function createRoutingForm(config: CreateRoutingFormConfig) {
const form = await prisma.app_RoutingForms_Form.create({
data: {
routes: config.form?.routes || [
{
id: "8a898988-89ab-4cde-b012-31823f708642",
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
queryValue: {
routes: (
config.form?.routes ||
([
{
id: "8a898988-89ab-4cde-b012-31823f708642",
type: "group",
children1: {
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["event-routing"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "eventTypeRedirectUrl", value: "pro/30min" },
queryValue: {
id: "8a898988-89ab-4cde-b012-31823f708642",
type: "group",
children1: {
"8988bbb8-0123-4456-b89a-b1823f70c5ff": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["event-routing"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
action: { type: "customPageMessage", value: "Custom Page Result" },
queryValue: {
{
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
type: "group",
children1: {
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["custom-page"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "customPageMessage", value: "Custom Page Result" },
queryValue: {
id: "aa8aaba9-cdef-4012-b456-71823f70f7ef",
type: "group",
children1: {
"b99b8a89-89ab-4cde-b012-31823f718ff5": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["custom-page"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
action: { type: "externalRedirectUrl", value: `${WEBAPP_URL}/pro` },
queryValue: {
{
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
type: "group",
children1: {
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["external-redirect"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
action: { type: "externalRedirectUrl", value: `${WEBAPP_URL}/pro` },
queryValue: {
id: "a8ba9aab-4567-489a-bcde-f1823f71b4ad",
type: "group",
children1: {
"998b9b9a-0123-4456-b89a-b1823f7232b9": {
type: "rule",
properties: {
field: "c4296635-9f12-47b1-8153-c3a854649182",
value: ["external-redirect"],
operator: "equal",
valueSrc: ["value"],
valueType: ["text"],
},
},
},
},
},
},
{
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
queryValue: {
{
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectLegacyFieldUuid,
value: [["Option-2"]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectLegacyFieldUuid,
value: [["Option-2"]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
},
},
},
},
},
},
{
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectFieldUuid,
value: [[multiSelectOption2Uuid]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
{
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
action: { type: "customPageMessage", value: "Multiselect chosen" },
queryValue: {
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
type: "group",
children1: {
"b98a8abb-cdef-4012-b456-718262343d27": {
type: "rule",
properties: {
field: multiSelectFieldUuid,
value: [[multiSelectOption2Uuid]],
operator: "multiselect_equals",
valueSrc: ["value"],
valueType: ["multiselect"],
},
},
},
},
},
},
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: config.form?.fields || [
{
id: "c4296635-9f12-47b1-8153-c3a854649182",
type: "text",
label: "Test field",
required: true,
},
{
id: multiSelectLegacyFieldUuid,
type: "multiselect",
label: "Multi Select(with Legacy `selectText`)",
identifier: "multi",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: multiSelectFieldUuid,
type: "multiselect",
label: "Multi Select",
identifier: "multi-new-format",
options: [
{
id: multiSelectOption1Uuid,
label: "Option-1",
},
{
id: multiSelectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
{
id: legacySelectFieldUuid,
type: "select",
label: "Legacy Select",
identifier: "test-select",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: selectFieldUuid,
type: "select",
label: "Select",
identifier: "test-select-new-format",
options: [
{
id: selectOption1Uuid,
label: "Option-1",
},
{
id: selectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
],
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
] satisfies RouteConfig[])
).map((field) => ({ ...field })),
fields: (
config.form?.fields ||
([
{
id: "c4296635-9f12-47b1-8153-c3a854649182",
type: "text",
label: "Test field",
required: true,
},
{
id: multiSelectLegacyFieldUuid,
type: "multiselect",
label: "Multi Select(with Legacy `selectText`)",
identifier: "multi",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: multiSelectFieldUuid,
type: "multiselect",
label: "Multi Select",
identifier: "multi-new-format",
options: [
{
id: multiSelectOption1Uuid,
label: "Option-1",
},
{
id: multiSelectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
{
id: legacySelectFieldUuid,
type: "select",
label: "Legacy Select",
identifier: "test-select",
selectText: "Option-1\nOption-2",
required: false,
},
{
id: selectFieldUuid,
type: "select",
label: "Select",
identifier: "test-select-new-format",
options: [
{
id: selectOption1Uuid,
label: "Option-1",
},
{
id: selectOption2Uuid,
label: "Option-2",
},
],
required: false,
},
] satisfies FieldConfig[])
).map((field) => ({ ...field })),
user: {
connect: {
id: userId,
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "تمت ترقية خطة Organization بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا",
"there": "هناك",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
"delete_webhook_confirmation_message": "هل أنت متأكد أنك تريد حذف الويب هوك هذا؟ لن تتلقى بعد الآن بيانات اجتماعات {{appName}} على عنوان URL محدد، وفي الوقت الفعلي، عندما تتم جدولة حدث أو يتم إلغاؤه.",
"confirm_delete_webhook": "نعم، قم بحذف الويبهوك",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "إخفاء عنوان البريد الإلكتروني للمنظم من شاشة الحجز وإشعارات البريد الإلكتروني وأحداث التقويم",
"you_and_conjunction": "& أنت",
"select_verified_email": "اختر بريدًا إلكترونيًا تم التحقق منه",
"add_verified_email": "إضافة بريد إلكتروني موثق",
"add_verified_emails": "إضافة بريد إلكتروني موثق",
"custom_reply_to_email_title": "بريد إلكتروني مخصص للرد",
"custom_reply_to_email_description": "استخدم عنوان بريد إلكتروني مختلف كعنوان للرد على رسائل التأكيد بدلاً من بريد المنظم",
"email_survey_triggered_by_workflow": "تم تشغيل هذا الاستبيان بواسطة سير العمل في كال.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Təşkilatınız uğurla təkmilləşdirildi!",
"use_link_to_reset_password": "Şifrənizi sıfırlamaq üçün aşağıdakı linkdən istifadə edin",
"hey_there": "Salam",
"there": "orada",
"forgot_your_password_calcom": "Şifrənizi unutmusunuz? - {{appName}}",
"delete_webhook_confirmation_message": "Bu webhook-u silmək istədiyinizə əminsinizmi? Tədbir planlaşdırıldıqda və ya ləğv edildikdə, real vaxtda müəyyən edilmiş URL-də {{appName}} görüş məlumatlarını artıq almayacaqsınız.",
"confirm_delete_webhook": "Bəli, webhook-u silin",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Təşkilatçının e-poçt ünvanını rezervasiya ekranından, e-poçt bildirişlərindən və təqvim tədbirlərindən gizlədin",
"you_and_conjunction": "Siz &",
"select_verified_email": "Təsdiqlənmiş e-poçt seçin",
"add_verified_email": "Təsdiqlənmiş e-poçt əlavə et",
"add_verified_emails": "Təsdiqlənmiş e-poçtlar əlavə et",
"custom_reply_to_email_title": "Xüsusi 'Cavab-Ünvanı' e-poçtu",
"custom_reply_to_email_description": "Təsdiq e-poçtları üçün təşkilatçının e-poçtu əvəzinə fərqli bir e-poçt ünvanını cavab ünvanı kimi istifadə edin",
"email_survey_triggered_by_workflow": "Bu sorğu Cal-da Workflow tərəfindən başladıldı.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Вашата организация беше успешно ъпгрейдната!",
"use_link_to_reset_password": "Използвайте линка по-долу, за да възстановите паролата си",
"hey_there": "Здравейте",
"there": "там",
"forgot_your_password_calcom": "Забравена парола? - {{appName}}",
"delete_webhook_confirmation_message": "Сигурни ли сте, че искате да изтриете този уебхук? Вече няма да получавате данни за срещите от {{appName}} на посочения URL адрес в реално време, когато събитие бъде планирано или отменено.",
"confirm_delete_webhook": "Да, изтрий уебхука",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Скриване на имейл адреса на организатора от екрана за резервации, имейл известията и календарните събития",
"you_and_conjunction": "Вие &",
"select_verified_email": "Изберете потвърден имейл",
"add_verified_email": "Добави потвърден имейл",
"add_verified_emails": "Добави потвърдени имейли",
"custom_reply_to_email_title": "Персонализиран имейл за отговор",
"custom_reply_to_email_description": "Използвайте различен имейл адрес като адрес за отговор в имейлите за потвърждение вместо имейла на организатора",
"email_survey_triggered_by_workflow": "Тази анкета беше стартирана от Workflow в Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "আপনার সংস্থা সফলভাবে আপগ্রেড করা হয়েছিল!",
"use_link_to_reset_password": "আপনার পাসওয়ার্ডটি পুনরায় সেট করতে নীচের লিঙ্কটি ব্যবহার করুন",
"hey_there": "আরে ওখানে",
"there": "সেখানে",
"forgot_your_password_calcom": "আপনার পাসওয়ার্ড ভুলে গেছেন? {{appName}}",
"delete_webhook_confirmation_message": "আপনি কি নিশ্চিত যে আপনি এই ওয়েবহুকটি মুছতে চান? {{appName}} রিয়েল-টাইমে একটি নির্দিষ্ট ইউআরএলে ডেটা সভা করা, যখন কোনও ইভেন্ট নির্ধারিত বা বাতিল হয়।",
"confirm_delete_webhook": "হ্যাঁ, ওয়েবহুক মুছুন",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "বুকিং স্ক্রিন, ইমেইল বিজ্ঞপ্তি এবং ক্যালেন্ডার ইভেন্ট থেকে আয়োজকের ইমেইল ঠিকানা লুকান",
"you_and_conjunction": "আপনি এবং",
"select_verified_email": "একটি যাচাইকৃত ইমেইল নির্বাচন করুন",
"add_verified_email": "যাচাইকৃত ইমেইল যোগ করুন",
"add_verified_emails": "যাচাইকৃত ইমেইলগুলি যোগ করুন",
"custom_reply_to_email_title": "কাস্টম 'রিপ্লাই-টু' ইমেইল",
"custom_reply_to_email_description": "আয়োজকের ইমেইলের পরিবর্তে নিশ্চিতকরণ ইমেইলের জন্য রিপ্লাইটু হিসাবে একটি ভিন্ন ইমেইল ঠিকানা ব্যবহার করুন",
"email_survey_triggered_by_workflow": "এই সমীক্ষাটি Cal-এ একটি ওয়ার্কফ্লো দ্বারা ট্রিগার করা হয়েছিল।",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "La teva Organització s'ha actualitzat correctament!",
"use_link_to_reset_password": "Utilitza l'enllaç següent per restablir la teva contrasenya",
"hey_there": "Hola",
"there": "allà",
"forgot_your_password_calcom": "Has oblidat la teva contrasenya? - {{appName}}",
"delete_webhook_confirmation_message": "Estàs segur que vols eliminar aquest webhook? Ja no rebràs dades de reunions de {{appName}} a l'URL especificada en temps real quan es programi o cancel·li un esdeveniment.",
"confirm_delete_webhook": "Sí, elimina el webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Amaga l'adreça de correu electrònic de l'organitzador de la pantalla de reserva, les notificacions per correu electrònic i els esdeveniments del calendari",
"you_and_conjunction": "Vosaltres &",
"select_verified_email": "Selecciona un correu electrònic verificat",
"add_verified_email": "Afegir correu electrònic verificat",
"add_verified_emails": "Afegir correus electrònics verificats",
"custom_reply_to_email_title": "Correu electrònic personalitzat de 'Respondre a'",
"custom_reply_to_email_description": "Utilitza una adreça de correu electrònic diferent com a resposta per als correus de confirmació en lloc del correu de l'organitzador",
"email_survey_triggered_by_workflow": "Aquesta enquesta va ser activada per un Workflow a Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Váš upgrade na tarif Organization byl úspěšně dokončen!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Ahoj",
"there": "tam",
"forgot_your_password_calcom": "Zapomněli jste heslo? - {{appName}}",
"delete_webhook_confirmation_message": "Opravdu chcete smazat tento webhook? Přestanete v reálném čase dostávat {{appName}} data o schůzce na dané URL, pokud bude událost naplánována nebo zrušena.",
"confirm_delete_webhook": "Ano, smazat webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Skrýt e-mailovou adresu organizátora na obrazovce rezervace, v e-mailových oznámeních a událostech kalendáře",
"you_and_conjunction": "Vy &",
"select_verified_email": "Vyberte ověřený e-mail",
"add_verified_email": "Přidat ověřený e-mail",
"add_verified_emails": "Přidat ověřené e-maily",
"custom_reply_to_email_title": "Vlastní e-mail pro odpověď",
"custom_reply_to_email_description": "Použít jinou e-mailovou adresu jako adresu pro odpověď v potvrzovacích e-mailech místo e-mailu organizátora",
"email_survey_triggered_by_workflow": "Tento průzkum byl spuštěn pracovním postupem v Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Din organisation blev opgraderet med succes!",
"use_link_to_reset_password": "Brug nedenstående link for at nulstille din adgangskode",
"hey_there": "Hej der",
"there": "der",
"forgot_your_password_calcom": "Glemt din adgangskode? - {{appName}}",
"delete_webhook_confirmation_message": "Er du sikker på, at du vil slette denne webhook? Du vil ikke længere modtage {{appName}} mødedata på en specificeret URL, i realtid, når en begivenhed bliver planlagt eller annulleret.",
"confirm_delete_webhook": "Ja, slet webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Skjul arrangørens e-mailadresse fra bookingskærmen, e-mailnotifikationer og kalenderbegivenheder",
"you_and_conjunction": "Du &",
"select_verified_email": "Vælg en verificeret e-mail",
"add_verified_email": "Tilføj verificeret e-mail",
"add_verified_emails": "Tilføj verificerede e-mails",
"custom_reply_to_email_title": "Brugerdefineret 'Svar-til'-e-mail",
"custom_reply_to_email_description": "Brug en anden e-mailadresse som svar-til for bekræftelses-e-mails i stedet for arrangørens e-mail",
"email_survey_triggered_by_workflow": "Denne undersøgelse blev udløst af en Arbejdsgang i Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Das Upgrade für Ihre Organization war erfolgreich!",
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
"hey_there": "Hallo",
"there": "dort",
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}}-Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
"confirm_delete_webhook": "Ja, Webhook löschen",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "E-Mail-Adresse des Organisators im Buchungsbildschirm, in E-Mail-Benachrichtigungen und Kalenderereignissen verbergen",
"you_and_conjunction": "Sie &",
"select_verified_email": "Verifizierte E-Mail-Adresse auswählen",
"add_verified_email": "Verifizierte E-Mail hinzufügen",
"add_verified_emails": "Verifizierte E-Mails hinzufügen",
"custom_reply_to_email_title": "Benutzerdefinierte 'Antwort an'-E-Mail",
"custom_reply_to_email_description": "Verwende eine andere E-Mail-Adresse als 'Antwort an' für Bestätigungs-E-Mails anstelle der E-Mail-Adresse des Organisators",
"email_survey_triggered_by_workflow": "Diese Umfrage wurde durch einen Workflow in Cal ausgelöst.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Ο οργανισμός σας αναβαθμίστηκε με επιτυχία!",
"use_link_to_reset_password": "Χρησιμοποιήστε τον παρακάτω σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασης",
"hey_there": "Γεια σας,",
"there": "εκεί",
"forgot_your_password_calcom": "Ξεχάσατε τον κωδικό πρόσβασης? - {{appName}}",
"delete_webhook_confirmation_message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το webhook; Δεν θα λαμβάνετε πλέον δεδομένα συναντήσεων του {{appName}} σε συγκεκριμένο URL, σε πραγματικό χρόνο, όταν προγραμματίζεται ή ακυρώνεται μια εκδήλωση.",
"confirm_delete_webhook": "Ναι, διαγραφή webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Απόκρυψη της διεύθυνσης email του διοργανωτή από την οθόνη κράτησης, τις ειδοποιήσεις email και τα συμβάντα ημερολογίου",
"you_and_conjunction": "Εσείς &",
"select_verified_email": "Επιλογή επαληθευμένου email",
"add_verified_email": "Προσθήκη επαληθευμένου email",
"add_verified_emails": "Προσθήκη επαληθευμένων email",
"custom_reply_to_email_title": "Προσαρμοσμένο email 'Απάντησης-Προς'",
"custom_reply_to_email_description": "Χρησιμοποιήστε διαφορετική διεύθυνση email ως replyTo για τα email επιβεβαίωσης αντί για το email του διοργανωτή",
"email_survey_triggered_by_workflow": "Αυτή η έρευνα ενεργοποιήθηκε από μια ροή εργασίας στο Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Your Organization was upgraded successfully!",
"use_link_to_reset_password": "Use the link below to reset your password",
"hey_there": "Hey there",
"there": "there",
"forgot_your_password_calcom": "Forgot your password? - {{appName}}",
"delete_webhook_confirmation_message": "Are you sure you want to delete this webhook? You will no longer receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.",
"confirm_delete_webhook": "Yes, delete webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Hide organizer's email address from the booking screen, email notifications, and calendar events",
"you_and_conjunction": "You &",
"select_verified_email": "Select a verified email",
"add_verified_email": "Add verified email",
"add_verified_emails": "Add verified emails",
"custom_reply_to_email_title": "Custom 'Reply-To' email",
"custom_reply_to_email_description": "Use a different email address as the replyTo for confirmation emails instead of the organizer's email",
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "¡Tu Organización se actualizó con éxito!",
"use_link_to_reset_password": "Usa el enlace a continuación para restablecer tu contraseña",
"hey_there": "Hola",
"there": "ahí",
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
"delete_webhook_confirmation_message": "¿Estás seguro de que deseas eliminar este webhook? Ya no recibirás datos de reuniones de {{appName}} en una URL especificada, en tiempo real, cuando se programe o cancele un evento.",
"confirm_delete_webhook": "Sí, eliminar webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ocultar la dirección de correo electrónico del organizador en la pantalla de reserva, notificaciones por correo electrónico y eventos del calendario",
"you_and_conjunction": "Tú &",
"select_verified_email": "Seleccionar un correo electrónico verificado",
"add_verified_email": "Agregar correo electrónico verificado",
"add_verified_emails": "Agregar correos electrónicos verificados",
"custom_reply_to_email_title": "Correo 'Responder a' personalizado",
"custom_reply_to_email_description": "Usar una dirección de correo electrónico diferente como destinatario de respuesta para los correos de confirmación en lugar del correo del organizador",
"email_survey_triggered_by_workflow": "Esta encuesta se activó mediante un Workflow en Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Su Organization se actualizó con éxito.",
"use_link_to_reset_password": "Utilice el enlace de abajo para restablecer su contraseña",
"hey_there": "Hola",
"there": "ahí",
"forgot_your_password_calcom": "¿Olvidaste tu contraseña? - {{appName}}",
"delete_webhook_confirmation_message": "¿Está seguro de que desea eliminar este webhook? Ya no recibirá datos de reuniones de {{appName}} en una URL específica, en tiempo real, cuando se programe o cancele un evento.",
"confirm_delete_webhook": "Sí, elimina este webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ocultar la dirección de correo electrónico del organizador en la pantalla de reserva, notificaciones por correo electrónico y eventos del calendario",
"you_and_conjunction": "Tú &",
"select_verified_email": "Seleccionar un correo electrónico verificado",
"add_verified_email": "Añadir correo electrónico verificado",
"add_verified_emails": "Añadir correos electrónicos verificados",
"custom_reply_to_email_title": "Correo electrónico personalizado para 'Responder a'",
"custom_reply_to_email_description": "Usar una dirección de correo electrónico diferente como destinatario de respuesta para los correos de confirmación en lugar del correo del organizador",
"email_survey_triggered_by_workflow": "Esta encuesta fue activada por un Workflow en Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Teie organisatsiooni uuendamine õnnestus!",
"use_link_to_reset_password": "Kasutage oma parooli lähtestamiseks allolevat linki",
"hey_there": "Tere",
"there": "seal",
"forgot_your_password_calcom": "Unustasite oma parooli? - {{appName}}",
"delete_webhook_confirmation_message": "Kas soovite kindlasti selle veebihaagi kustutada? Kui üritus on ajastatud või tühistatud, ei saa te enam reaalajas {{appName}} koosoleku andmeid määratud URL-il.",
"confirm_delete_webhook": "Jah, kustuta veebihaak",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Peida korraldaja e-posti aadress broneerimisekraanil, e-posti teavitustes ja kalendrisündmustes.",
"you_and_conjunction": "Sina &",
"select_verified_email": "Vali kinnitatud e-posti aadress",
"add_verified_email": "Lisa kinnitatud e-post",
"add_verified_emails": "Lisa kinnitatud e-posti aadressid",
"custom_reply_to_email_title": "Kohandatud 'Vasta-aadress' e-post",
"custom_reply_to_email_description": "Kasutage kinnituskirjade jaoks vastuse aadressina teist e-posti aadressi, mitte korraldaja e-posti aadressi.",
"email_survey_triggered_by_workflow": "See küsitlus käivitati Cali töövooga.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Zure erakundea arrakastaz eguneratu da!",
"use_link_to_reset_password": "Erabili beheko esteka pasahitza berrezartzeko",
"hey_there": "Kaixo,",
"there": "han",
"forgot_your_password_calcom": "Pasahitza ahaztu duzu? - {{appName}}",
"delete_webhook_confirmation_message": "Ziur zaude webhook hau ezabatu nahi duzula? Ez duzu gehiago {{appName}} bileraren daturik jasoko zehaztutako URLan denbora errealean, gertaera bat programatzen edo bertan behera uzten denean.",
"confirm_delete_webhook": "Bai, ezabatu webhooka",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ezkutatu antolatzailearen helbide elektronikoa erreserba pantailatik, mezu elektronikoetatik eta egutegiko ekitaldietatik",
"you_and_conjunction": "Zu &",
"select_verified_email": "Hautatu egiaztatutako helbide elektronikoa",
"add_verified_email": "Gehitu egiaztatutako helbide elektronikoa",
"add_verified_emails": "Gehitu egiaztatutako helbide elektronikoak",
"custom_reply_to_email_title": "'Erantzun' helbide elektroniko pertsonalizatua",
"custom_reply_to_email_description": "Erabili beste helbide elektroniko bat baieztapen mezuen erantzun-helbide gisa antolatzailearen helbidearen ordez",
"email_survey_triggered_by_workflow": "Galdeketa hau Cal-eko Workflow baten bidez aktibatu da.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organisaatiosi päivitys onnistui!",
"use_link_to_reset_password": "Käytä alla olevaa linkkiä salasanan vaihtamiseen",
"hey_there": "Hei siellä",
"there": "siellä",
"forgot_your_password_calcom": "Unohditko salasanasi? - {{appName}}",
"delete_webhook_confirmation_message": "Haluatko varmasti poistaa tämän webhookin? Et enää vastaanota {{appName}}-kokoustietoja määritettyyn URL-osoitteeseen reaaliajassa, kun tapahtuma aikataulutetaan tai peruutetaan.",
"confirm_delete_webhook": "Kyllä, poista webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Piilota järjestäjän sähköpostiosoite varausnäytöltä, sähköposti-ilmoituksista ja kalenteritapahtumista",
"you_and_conjunction": "Sinä &",
"select_verified_email": "Valitse vahvistettu sähköposti",
"add_verified_email": "Lisää vahvistettu sähköposti",
"add_verified_emails": "Lisää vahvistettuja sähköposteja",
"custom_reply_to_email_title": "Mukautettu 'Vastaa'-sähköposti",
"custom_reply_to_email_description": "Käytä eri sähköpostiosoitetta vastausosoitteena vahvistussähköposteissa järjestäjän sähköpostin sijaan",
"email_survey_triggered_by_workflow": "Tämä kysely käynnistettiin Workflow-ohjelmalla Cal-palvelussa.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Votre organisation a été mise à niveau avec succès !",
"use_link_to_reset_password": "Utilisez le lien ci-dessous pour réinitialiser votre mot de passe",
"hey_there": "Bonjour",
"there": "là",
"forgot_your_password_calcom": "Mot de passe oublié ? - {{appName}}",
"delete_webhook_confirmation_message": "Voulez-vous vraiment supprimer ce webhook ? Vous ne recevrez plus les données de rendez-vous de {{appName}} à un lien spécifié, en temps réel, lorsqu'un événement est planifié ou annulé.",
"confirm_delete_webhook": "Oui, supprimer le webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Masquer l'adresse e-mail de l'organisateur sur l'écran de réservation, les notifications par e-mail et les événements du calendrier",
"you_and_conjunction": "Vous &",
"select_verified_email": "Sélectionner un e-mail vérifié",
"add_verified_email": "Ajouter une adresse e-mail vérifiée",
"add_verified_emails": "Ajouter des adresses e-mail vérifiées",
"custom_reply_to_email_title": "E-mail 'Répondre à' personnalisé",
"custom_reply_to_email_description": "Utiliser une adresse e-mail différente comme destinataire de réponse pour les e-mails de confirmation au lieu de l'e-mail de l'organisateur",
"email_survey_triggered_by_workflow": "Ce questionnaire a été déclenché par un Workflow dans Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "ה-Organization שלך שודרג בהצלחה!",
"use_link_to_reset_password": "נא להשתמש בקישור הבא כדי לאפס את הסיסמה",
"hey_there": "היי שם",
"there": "שם",
"forgot_your_password_calcom": "שכחתה את הסיסמה? - {{appName}}",
"delete_webhook_confirmation_message": "בטוח שברצונך למחוק את ה-Webhook הזה? אם אירוע יבוטל או שייקבע עבורו מועד, לא תקבל יותר נתוני פגישות מ-{{appName}} בזמן אמת או בכתובת ה-URL שציינת.",
"confirm_delete_webhook": "כן, אני רוצה למחוק את ה-Webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "הסתר את כתובת האימייל של המארגן ממסך ההזמנה, התראות האימייל ואירועי היומן",
"you_and_conjunction": "את/ה &",
"select_verified_email": "בחר אימייל מאומת",
"add_verified_email": "הוסף אימייל מאומת",
"add_verified_emails": "הוסף אימיילים מאומתים",
"custom_reply_to_email_title": "אימייל 'השב-אל' מותאם אישית",
"custom_reply_to_email_description": "השתמש בכתובת אימייל אחרת כ-replyTo עבור אימיילי אישור במקום האימייל של המארגן",
"email_survey_triggered_by_workflow": "הסקר הזה הופעל על ידי Workflow ב-Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Szervezete sikeresen frissítve!",
"use_link_to_reset_password": "Jelszó visszaállításához használja az alábbi linket",
"hey_there": "Szia",
"there": "ott",
"forgot_your_password_calcom": "Elfelejtette a jelszavát? - {{appName}}",
"delete_webhook_confirmation_message": "Biztosan törli ezt a webhookot? A továbbiakban nem kapja meg valós időben a(z) {{appName}} megbeszélés adatait egy megadott URL-címen, ha egy esemény ütemezett vagy lemondott.",
"confirm_delete_webhook": "Igen, webhook törlése",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Szervező e-mail címének elrejtése a foglalási képernyőn, e-mail értesítésekben és naptáreseményekben",
"you_and_conjunction": "Te &",
"select_verified_email": "Válasszon egy ellenőrzött e-mail címet",
"add_verified_email": "Hitelesített e-mail hozzáadása",
"add_verified_emails": "Hitelesített e-mailek hozzáadása",
"custom_reply_to_email_title": "Egyéni 'Válaszcím' e-mail",
"custom_reply_to_email_description": "Használjon másik e-mail címet válaszcímként a visszaigazoló e-mailekhez a szervező e-mail címe helyett",
"email_survey_triggered_by_workflow": "Ezt a kérdőívet egy Workflow indította a Cal-ban.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "È stato eseguito l'upgrade del piano Organization!",
"use_link_to_reset_password": "Usa il link qui sotto per reimpostare la tua password",
"hey_there": "Ciao",
"there": "lì",
"forgot_your_password_calcom": "Hai dimenticato la password? - {{appName}}",
"delete_webhook_confirmation_message": "Sei sicuro di voler eliminare questo webhook? Non riceverai più {{appName}} dati di riunione in un URL specificato, in tempo reale, quando un evento è programmato o annullato.",
"confirm_delete_webhook": "Sì, elimina webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Nascondi l'indirizzo email dell'organizzatore dalla schermata di prenotazione, dalle notifiche email e dagli eventi del calendario",
"you_and_conjunction": "Tu &",
"select_verified_email": "Seleziona un'email verificata",
"add_verified_email": "Aggiungi email verificata",
"add_verified_emails": "Aggiungi email verificate",
"custom_reply_to_email_title": "Email 'Rispondi a' personalizzata",
"custom_reply_to_email_description": "Usa un indirizzo email diverso come replyTo per le email di conferma invece dell'email dell'organizzatore",
"email_survey_triggered_by_workflow": "Questo sondaggio è stato attivato da un Workflow in Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "あなたの Organization プランは正常にアップグレードされました!",
"use_link_to_reset_password": "以下のリンクを使用してパスワードをリセットしてください",
"hey_there": "こんにちは",
"there": "そちら",
"forgot_your_password_calcom": "パスワードをお忘れですか? - {{appName}}",
"delete_webhook_confirmation_message": "この Webhook を削除してもよろしいですか? イベントがスケジュールまたはキャンセルされた場合、指定された URL で {{appName}} ミーティング データをリアルタイムで受信しなくなります。",
"confirm_delete_webhook": "はい、ウェブフックを削除します",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "予約画面、メール通知、カレンダーイベントから主催者のメールアドレスを非表示にします",
"you_and_conjunction": "あなた &",
"select_verified_email": "確認済みメールを選択",
"add_verified_email": "認証済みメールアドレスを追加",
"add_verified_emails": "認証済みメールアドレスを追加",
"custom_reply_to_email_title": "カスタム「返信先」メール",
"custom_reply_to_email_description": "主催者のメールの代わりに、確認メールの返信先として別のメールアドレスを使用します",
"email_survey_triggered_by_workflow": "このアンケートはCalのワークフローによってトリガーされました。",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization របស់អ្នកត្រូវបានដំឡើងកំណែដោយជោគជ័យ!",
"use_link_to_reset_password": "ប្រើតំណខាងក្រោមដើម្បីកំណត់ពាក្យសម្ងាត់របស់អ្នកឡើងវិញ",
"hey_there": "សួស្តី",
"there": "នៅទីនោះ",
"forgot_your_password_calcom": "ភ្លេចពាក្យសម្ងាត់? - {{appName}}",
"delete_webhook_confirmation_message": "តើអ្នកប្រាកដថាចង់លុប webhook នេះទេ? អ្នកនឹងលែងទទួលបានទិន្នន័យប្រជុំ {{appName}} តាម URL ដែលបានបញ្ជាក់, ក្នុងពេលវេលាជាក់ស្តែង នៅពេលដែលព្រឹត្តិការណ៍មួយត្រូវបានកំណត់ពេល ឬលុបចោល។",
"confirm_delete_webhook": "បាទ/ចាស លុប webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "លាក់អាសយដ្ឋានអ៊ីមែលរបស់អ្នករៀបចំពីអេក្រង់ការកក់ ការជូនដំណឹងតាមអ៊ីមែល និងព្រឹត្តិការណ៍ប្រតិទិន",
"you_and_conjunction": "អ្នក &",
"select_verified_email": "ជ្រើសរើសអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"add_verified_email": "បន្ថែមអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"add_verified_emails": "បន្ថែមអ៊ីមែលដែលបានផ្ទៀងផ្ទាត់",
"custom_reply_to_email_title": "អ៊ីមែល 'ឆ្លើយតប' ផ្ទាល់ខ្លួន",
"custom_reply_to_email_description": "ប្រើអាសយដ្ឋានអ៊ីមែលផ្សេងជា replyTo សម្រាប់អ៊ីមែលបញ្ជាក់ជំនួសឱ្យអ៊ីមែលរបស់អ្នករៀបចំ",
"email_survey_triggered_by_workflow": "ការស្ទង់មតិនេះត្រូវបានបង្កើតឡើងដោយលំហូរការងារនៅក្នុង Cal។",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization이 성공적으로 업그레이드되었습니다!",
"use_link_to_reset_password": "비밀번호를 재설정하시려면 아래 링크를 사용하세요.",
"hey_there": "안녕하세요",
"there": "거기",
"forgot_your_password_calcom": "비밀번호를 잊어버리셨나요? - {{appName}}",
"delete_webhook_confirmation_message": "웹훅을 정말로 삭제하시겠습니까? 이벤트 일정이 잡혔거나 취소되었을 때 {{appName}}의 회의 정보를 더 이상 실시간으로 받아보실 수 없게 됩니다.",
"confirm_delete_webhook": "네, 웹훅을 삭제하겠습니다.",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "예약 화면, 이메일 알림 및 캘린더 이벤트에서 주최자의 이메일 주소 숨기기",
"you_and_conjunction": "당신 &",
"select_verified_email": "인증된 이메일 선택",
"add_verified_email": "인증된 이메일 추가",
"add_verified_emails": "인증된 이메일 추가",
"custom_reply_to_email_title": "사용자 지정 '회신' 이메일",
"custom_reply_to_email_description": "주최자의 이메일 대신 확인 이메일의 회신 주소로 다른 이메일 주소 사용",
"email_survey_triggered_by_workflow": "이 설문은 Cal의 워크플로우에 의해 트리거되었습니다.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Uw organisatie is geüpgraded!",
"use_link_to_reset_password": "Gebruik de onderstaande link om uw wachtwoord te resetten",
"hey_there": "Hoi daar",
"there": "daar",
"forgot_your_password_calcom": "Wachtwoord vergeten? - {{appName}}",
"delete_webhook_confirmation_message": "Weet u zeker dat u deze webhook wilt verwijderen? U ontvangt dan niet langer {{appName}} vergadergegevens op een opgegeven URL, in real-time, wanneer een evenement is geboekt of geannuleerd.",
"confirm_delete_webhook": "Ja, webhook verwijderen",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Verberg het e-mailadres van de organisator op het boekingsscherm, in e-mailnotificaties en in agenda-afspraken",
"you_and_conjunction": "U &",
"select_verified_email": "Selecteer een geverifieerd e-mailadres",
"add_verified_email": "Geverifieerde e-mail toevoegen",
"add_verified_emails": "Geverifieerde e-mails toevoegen",
"custom_reply_to_email_title": "Aangepast 'Antwoord-aan' e-mailadres",
"custom_reply_to_email_description": "Gebruik een ander e-mailadres als antwoordadres voor bevestigingsmails in plaats van het e-mailadres van de organisator",
"email_survey_triggered_by_workflow": "Deze enquête is geactiveerd door een Workflow in Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organisasjonen din ble oppgradert!",
"use_link_to_reset_password": "Bruk lenken nedenfor for å tilbakestille passordet ditt",
"hey_there": "Hei der",
"there": "der",
"forgot_your_password_calcom": "Glemt passordet? - {{appName}}",
"delete_webhook_confirmation_message": "Er du sikker på at du vil slette denne webhooken? Du vil ikke lenger motta {{appName}} møtedata på en spesifisert URL, i sanntid, når en hendelse er planlagt eller avlyst.",
"confirm_delete_webhook": "Ja, slett webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Skjul arrangørens e-postadresse fra bestillingsskjermen, e-postvarsler og kalenderarrangementer",
"you_and_conjunction": "Du &",
"select_verified_email": "Velg en verifisert e-post",
"add_verified_email": "Legg til verifisert e-post",
"add_verified_emails": "Legg til verifiserte e-poster",
"custom_reply_to_email_title": "Egendefinert 'Svar-til' e-post",
"custom_reply_to_email_description": "Bruk en annen e-postadresse som svar-til for bekreftelsese-poster i stedet for arrangørens e-post",
"email_survey_triggered_by_workflow": "Denne undersøkelsen ble utløst av en arbeidsflyt i Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Twój zespół z planu Organization przeszedł na wyższą wersję!",
"use_link_to_reset_password": "Użyj poniższego linku, aby zresetować swoje hasło",
"hey_there": "Cześć",
"there": "tam",
"forgot_your_password_calcom": "Nie pamiętasz swojego hasła? - {{appName}}",
"delete_webhook_confirmation_message": "Czy na pewno chcesz usunąć ten webhook? Nie będziesz już otrzymywać danych o spotkaniach w {{appName}} w określonym URL, w czasie rzeczywistym, gdy zdarzenie jest zaplanowane lub anulowane.",
"confirm_delete_webhook": "Tak, usuń webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ukryj adres e-mail organizatora na ekranie rezerwacji, w powiadomieniach e-mail i wydarzeniach w kalendarzu.",
"you_and_conjunction": "Ty &",
"select_verified_email": "Wybierz zweryfikowany e-mail",
"add_verified_email": "Dodaj zweryfikowany e-mail",
"add_verified_emails": "Dodaj zweryfikowane e-maile",
"custom_reply_to_email_title": "Niestandardowy e-mail 'Odpowiedz do'",
"custom_reply_to_email_description": "Użyj innego adresu e-mail jako 'replyTo' dla e-maili potwierdzających zamiast e-maila organizatora.",
"email_survey_triggered_by_workflow": "Ta ankieta została uruchomiona przez Workflow w Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Sua Organização foi atualizada com êxito!",
"use_link_to_reset_password": "Use o link abaixo para redefinir a sua senha",
"hey_there": "Olá",
"there": "lá",
"forgot_your_password_calcom": "Esqueceu sua senha? - {{appName}}",
"delete_webhook_confirmation_message": "Você tem certeza que quer remover este Webhook? Você não receberá mais dados de reuniões de {{appName}} na URL especificada, em tempo real, quando um evento for agendado ou cancelado.",
"confirm_delete_webhook": "Sim, remover o Webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ocultar o endereço de e-mail do organizador na tela de agendamento, notificações por e-mail e eventos do calendário",
"you_and_conjunction": "Você &",
"select_verified_email": "Selecione um e-mail verificado",
"add_verified_email": "Adicionar e-mail verificado",
"add_verified_emails": "Adicionar e-mails verificados",
"custom_reply_to_email_title": "E-mail personalizado para 'Responder a'",
"custom_reply_to_email_description": "Use um endereço de e-mail diferente como resposta para e-mails de confirmação em vez do e-mail do organizador",
"email_survey_triggered_by_workflow": "Esta pesquisa foi acionada por um Workflow no Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "A sua Organização foi atualizada com sucesso!",
"use_link_to_reset_password": "Use a ligação abaixo para redefinir a sua senha",
"hey_there": "Olá",
"there": "lá",
"forgot_your_password_calcom": "Esqueceu-se da sua senha? - {{appName}}",
"delete_webhook_confirmation_message": "De certeza que quer eliminar este Webhook? Não receberá mais dados de reuniões de {{appName}} do URL especificado, em tempo real, quando um evento for agendado ou cancelado.",
"confirm_delete_webhook": "Sim, eliminar o Webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ocultar o endereço de email do organizador na tela de agendamento, notificações por email e eventos do calendário",
"you_and_conjunction": "Você &",
"select_verified_email": "Selecione um email verificado",
"add_verified_email": "Adicionar e-mail verificado",
"add_verified_emails": "Adicionar e-mails verificados",
"custom_reply_to_email_title": "Email personalizado para 'Responder a'",
"custom_reply_to_email_description": "Use um endereço de email diferente como resposta para emails de confirmação em vez do email do organizador",
"email_survey_triggered_by_workflow": "Este questionário foi acionado por um Workflow no Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Upgrade-ul pentru Organization a fost realizat cu succes!",
"use_link_to_reset_password": "Utilizați linkul de mai jos pentru a vă reseta parola",
"hey_there": "Salut",
"there": "acolo",
"forgot_your_password_calcom": "Ți-ai uitat parola? - {{appName}}",
"delete_webhook_confirmation_message": "Ești sigur că vrei să ștergi acest webhook? Nu vei mai primi {{appName}} date de întâlnire la un URL specificat, în timp real, atunci când un eveniment este programat sau anulat.",
"confirm_delete_webhook": "Da, șterge webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ascundeți adresa de email a organizatorului de pe ecranul de rezervare, notificările prin email și evenimentele din calendar",
"you_and_conjunction": "Tu &",
"select_verified_email": "Selectați un email verificat",
"add_verified_email": "Adaugă un email verificat",
"add_verified_emails": "Adaugă emailuri verificate",
"custom_reply_to_email_title": "Email personalizat 'Reply-To'",
"custom_reply_to_email_description": "Utilizați o altă adresă de email ca replyTo pentru emailurile de confirmare în locul emailului organizatorului",
"email_survey_triggered_by_workflow": "Acest sondaj a fost declanșat de un Workflow în Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Вы успешно перешли на тариф Organization!",
"use_link_to_reset_password": "Используйте ссылку ниже, чтобы сбросить пароль",
"hey_there": "Привет,",
"there": "там",
"forgot_your_password_calcom": "Забыли пароль? - {{appName}}",
"delete_webhook_confirmation_message": "Вы уверены, что хотите удалить этот вебхук? Вы больше не будете получать от {{appName}} данные о новых или отменённых встречах по указанному URL в реальном времени.",
"confirm_delete_webhook": "Да, удалить веб-хук",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Скрыть адрес электронной почты организатора на экране бронирования, в email-уведомлениях и событиях календаря",
"you_and_conjunction": "Вы &",
"select_verified_email": "Выбрать подтвержденный email",
"add_verified_email": "Добавить подтверждённый email",
"add_verified_emails": "Добавить подтверждённые email-адреса",
"custom_reply_to_email_title": "Пользовательский email для ответа",
"custom_reply_to_email_description": "Используйте другой адрес электронной почты в качестве адреса для ответа в подтверждающих письмах вместо email организатора",
"email_survey_triggered_by_workflow": "Этот опрос был запущен рабочим процессом в Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Vaša organizácia bola úspešne upgradovaná!",
"use_link_to_reset_password": "Použite nasledujúci odkaz na obnovenie hesla",
"hey_there": "Ahoj",
"there": "tam",
"forgot_your_password_calcom": "Zabudli ste heslo? - {{appName}}",
"delete_webhook_confirmation_message": "Naozaj chcete odstrániť tento webhook? Už nebudete dostávať údaje o stretnutiach {{appName}} v reálnom čase na určenú URL adresu pri naplánovaní alebo zrušení udalosti.",
"confirm_delete_webhook": "Áno, odstrániť webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Skryť e-mailovú adresu organizátora z obrazovky rezervácie, e-mailových upozornení a kalendárových udalostí",
"you_and_conjunction": "Vy &",
"select_verified_email": "Vyberte overený e-mail",
"add_verified_email": "Pridať overený e-mail",
"add_verified_emails": "Pridať overené e-maily",
"custom_reply_to_email_title": "Vlastný e-mail 'Odpovedať na'",
"custom_reply_to_email_description": "Použiť inú e-mailovú adresu ako adresu pre odpoveď v potvrdzovacích e-mailoch namiesto e-mailu organizátora",
"email_survey_triggered_by_workflow": "Tento prieskum bol aktivovaný pracovným postupom v Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Vaš Organization je uspešno nadograđen!",
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
"hey_there": "Zdravo",
"there": "tamo",
"forgot_your_password_calcom": "Zaboravili ste lozinku? - {{appName}}",
"delete_webhook_confirmation_message": "Da li ste sigurni da želite da obrišete ovaj webhook? Nećete više dobijati {{appName}} podatke o sastancima preko naznačenog URL, istovremeno kad je događaj zakazan ili otkazan.",
"confirm_delete_webhook": "Da, obriši webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Sakrij email adresu organizatora sa ekrana za rezervacije, email obaveštenja i kalendarske događaje",
"you_and_conjunction": "Ви &",
"select_verified_email": "Izaberite verifikovani email",
"add_verified_email": "Dodaj verifikovanu e-poštu",
"add_verified_emails": "Dodaj verifikovane e-pošte",
"custom_reply_to_email_title": "Prilagođeni 'Reply-To' email",
"custom_reply_to_email_description": "Koristite drugačiju email adresu kao replyTo za email potvrde umesto email-a organizatora",
"email_survey_triggered_by_workflow": "Ова анкета је покренута Воркфловом у Цал-у.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Din Organization har uppgraderats!",
"use_link_to_reset_password": "Använd länken nedan för att återställa ditt lösenord",
"hey_there": "Hej där",
"there": "där",
"forgot_your_password_calcom": "Glömt ditt lösenord? - {{appName}}",
"delete_webhook_confirmation_message": "Är du säker på att du vill ta bort denna webhook? Du kommer inte längre att få {{appName}} mötesdata på en angiven webbadress, i realtid, när en händelse är schemalagd eller inställd.",
"confirm_delete_webhook": "Ja, ta bort webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Dölj arrangörens e-postadress från bokningsskärmen, e-postmeddelanden och kalenderhändelser",
"you_and_conjunction": "Du &",
"select_verified_email": "Välj en verifierad e-postadress",
"add_verified_email": "Lägg till verifierad e-post",
"add_verified_emails": "Lägg till verifierade e-postadresser",
"custom_reply_to_email_title": "Anpassad 'Svara till'-adress",
"custom_reply_to_email_description": "Använd en annan e-postadress som 'Svara till' för bekräftelsemail istället för arrangörens e-postadress",
"email_survey_triggered_by_workflow": "Den här enkäten utlöstes av ett Workflow i Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization'ınız başarıyla yükseltildi!",
"use_link_to_reset_password": "Şifrenizi sıfırlamak için aşağıdaki bağlantıyı kullanın",
"hey_there": "Merhaba",
"there": "orada",
"forgot_your_password_calcom": "Şifrenizi mi unuttunuz? - {{appName}}",
"delete_webhook_confirmation_message": "Bu web kancasını silmek istediğinizden emin misiniz? Bir etkinlik planlandığında veya iptal edildiğinde, {{appName}} toplantı verilerini gerçek zamanlı olarak belirli bir URL'de almazsınız.",
"confirm_delete_webhook": "Evet, web kancasını sil",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Organizatörün e-posta adresini rezervasyon ekranından, e-posta bildirimlerinden ve takvim etkinliklerinden gizle",
"you_and_conjunction": "Sen &",
"select_verified_email": "Doğrulanmış bir e-posta seç",
"add_verified_email": "Doğrulanmış e-posta ekle",
"add_verified_emails": "Doğrulanmış e-postalar ekle",
"custom_reply_to_email_title": "Özel 'Yanıtla' e-postası",
"custom_reply_to_email_description": "Organizatörün e-postası yerine onay e-postaları için farklı bir e-posta adresini yanıtla adresi olarak kullan",
"email_survey_triggered_by_workflow": "Bu anket, Cal'da bir İş Akışı tarafından tetiklendi.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Рівень плану вашої організації підвищено!",
"use_link_to_reset_password": "Скористайтеся посиланням нижче, щоб скинути пароль",
"hey_there": "Привіт",
"there": "там",
"forgot_your_password_calcom": "Забули пароль? – {{appName}}",
"delete_webhook_confirmation_message": "Справді видалити цей вебгук? Ви більше не отримуватимете дані про нараду {{appName}} за вказаною URL-адресою: у реальному часі, а також під час планування та скасування.",
"confirm_delete_webhook": "Так, видалити вебгук",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Приховати електронну адресу організатора з екрану бронювання, сповіщень електронною поштою та подій календаря",
"you_and_conjunction": "Ви &",
"select_verified_email": "Виберіть підтверджену електронну пошту",
"add_verified_email": "Додати підтверджену електронну пошту",
"add_verified_emails": "Додати підтверджені електронні пошти",
"custom_reply_to_email_title": "Власна адреса 'Відповісти на'",
"custom_reply_to_email_description": "Використовувати іншу електронну адресу як адресу для відповіді в листах підтвердження замість електронної пошти організатора",
"email_survey_triggered_by_workflow": "Це опитування було запущено робочим процесом у Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "Organization của bạn đã được nâng cấp thành công!",
"use_link_to_reset_password": "Sử dụng đường link dưới đây để thay đổi mật khẩu của bạn",
"hey_there": "Chào bạn",
"there": "ở đó",
"forgot_your_password_calcom": "Quên mật khẩu của bạn? - {{appName}}",
"delete_webhook_confirmation_message": "Bạn có chắc bạn muốn xoá webhook này? Bạn sẽ không còn nhận dữ liệu cuộc họp {{appName}} tại đường URL này, trong thời gian thực, khi một sự kiện được lên lịch hoặc huỷ.",
"confirm_delete_webhook": "Vâng, hãy xoá webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "Ẩn địa chỉ email của người tổ chức khỏi màn hình đặt lịch, thông báo email và sự kiện lịch",
"you_and_conjunction": "Bạn &",
"select_verified_email": "Chọn email đã xác minh",
"add_verified_email": "Thêm email đã xác minh",
"add_verified_emails": "Thêm các email đã xác minh",
"custom_reply_to_email_title": "Email 'Trả lời-Cho' tùy chỉnh",
"custom_reply_to_email_description": "Sử dụng địa chỉ email khác làm email trả lời cho các email xác nhận thay vì email của người tổ chức",
"email_survey_triggered_by_workflow": "Khảo sát này được kích hoạt bởi Quy trình làm việc trong Cal.",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "您的 Organization 已成功升级!",
"use_link_to_reset_password": "使用下面的链接重置您的密码",
"hey_there": "您好",
"there": "那里",
"forgot_your_password_calcom": "忘记密码?- {{appName}}",
"delete_webhook_confirmation_message": "您确定要删除此Webhook吗?当活动被安排或取消时,您将不再会在指定的链接收到 {{appName}} 实时提供的会议数据。",
"confirm_delete_webhook": "是,删除 Webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "在预订页面、电子邮件通知和日历事件中隐藏组织者的电子邮件地址",
"you_and_conjunction": "你 &",
"select_verified_email": "选择已验证的电子邮件",
"add_verified_email": "添加已验证的邮箱",
"add_verified_emails": "添加已验证的邮箱",
"custom_reply_to_email_title": "自定义“回复至”电子邮件",
"custom_reply_to_email_description": "使用不同的电子邮件地址作为确认邮件的回复地址,而不是组织者的电子邮件",
"email_survey_triggered_by_workflow": "该调查由 Cal 中的工作流触发。",
@@ -230,6 +230,7 @@
"org_upgraded_successfully": "您的 Organization 成功升級!",
"use_link_to_reset_password": "使用以下連結重置密碼",
"hey_there": "嗨,您好",
"there": "那裡",
"forgot_your_password_calcom": "忘記密碼了? - {{appName}}",
"delete_webhook_confirmation_message": "您確定要刪除這組 Webhook 嗎?之後活動預定或取消的時候,您就不會即時經特定連結收到 {{appName}} 的會議資料。",
"confirm_delete_webhook": "是的,刪除 Webhook",
@@ -3344,6 +3345,8 @@
"hide_organizer_email_description": "在預訂畫面、電子郵件通知和日曆事件中隱藏組織者的電子郵件地址",
"you_and_conjunction": "你 &",
"select_verified_email": "選擇已驗證的電子郵件",
"add_verified_email": "新增已驗證的電子郵件",
"add_verified_emails": "新增已驗證的電子郵件地址",
"custom_reply_to_email_title": "自訂「回覆至」電子郵件",
"custom_reply_to_email_description": "使用不同的電子郵件地址作為確認電子郵件的回覆地址,而非組織者的電子郵件",
"email_survey_triggered_by_workflow": "該調查由 Cal 中的工作流程觸發。",
File diff suppressed because it is too large Load Diff
+3
View File
@@ -232,6 +232,7 @@ checksums:
org_upgraded_successfully: b803e2b8a503792206beaf1f4f2c861e
use_link_to_reset_password: 75eea563335b8773fa581c61307b4b22
hey_there: abc179eec007507d2e54e5ef922c04d7
there: 7d63e12738c8b7b3bea05fa04ca86552
forgot_your_password_calcom: e25822f11d138676f1151900a12be61f
delete_webhook_confirmation_message: e6c1794d4d697286d2b55eb24c08990f
confirm_delete_webhook: ef3672fcdca099306496ef20d846724d
@@ -3341,6 +3342,8 @@ checksums:
hide_organizer_email_description: 6e2cfd69fe84add77222a34bc8f5f157
you_and_conjunction: be0533b04f42df498d15c071896e6257
select_verified_email: 8a5d46d7989a61ba7248fbd528cf0b5f
add_verified_email: 9d07089e0e387f69c9686acb3d0c0f84
add_verified_emails: 62c56528e70d6818c1349089ce209ca8
custom_reply_to_email_title: 87587e5dca76926f94e532d76e661d0b
custom_reply_to_email_description: b94178b32d14b7c192aa5b4bb4b282ec
email_survey_triggered_by_workflow: 8ee502957ac3828ba4066e295f4ff93f
@@ -1,9 +1,9 @@
import type { Prisma } from "@prisma/client";
import type z from "zod";
import logger from "@calcom/lib/logger";
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import type { OAuth2UniversalSchemaWithCalcomBackwardCompatibility } from "./universalSchema";
@@ -23,7 +23,7 @@ export const updateTokenObject = async ({
id: credentialId,
},
data: {
key: tokenObject,
key: tokenObject as unknown as Prisma.InputJsonValue,
},
});
};
@@ -1,7 +1,6 @@
import { z } from "zod";
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
import { fetcher } from "@calcom/lib/dailyApiFetcher";
import { prisma } from "@calcom/prisma";
import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import {
@@ -16,6 +15,7 @@ import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapt
import { ZSubmitBatchProcessorJobRes, ZGetTranscriptAccessLink } from "../zod";
import type { TSubmitBatchProcessorJobRes, TGetTranscriptAccessLink, batchProcessorBody } from "../zod";
import { fetcher } from "./dailyApiFetcher";
import {
dailyReturnTypeSchema,
getTranscripts,
@@ -1,6 +1,7 @@
import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKeys";
import { handleErrorsJson } from "@calcom/lib/errors";
import { getDailyAppKeys } from "./getDailyAppKeys";
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key } = await getDailyAppKeys();
return fetch(`https://api.daily.co/v1${endpoint}`, {
@@ -5,6 +5,7 @@ import { z } from "zod";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -102,7 +103,7 @@ const NextcloudTalkVideoApiAdapter = (credential: CredentialPayload): VideoApiAd
id: credential.id,
},
data: {
key: newTokenObject,
key: newTokenObject as unknown as Prisma.InputJsonValue,
},
});
},

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