feat: Add booking and user creation source (#18768)

* migration and init to accept creationSource for new bookings

* V1 create booking

* V1 user creation

* webapp booking + V1 user

* user creation in V1, V2 and webapp

* booking source V2 and fix for v1 user

* fit type

* --fix type

* add test -- WIP

* fix type

* fix type

* ^

* Need more sleep zzz

* -_-

* bump libraries platform

* adds for v2 recurring booking

* fix lint

* instant meetings

* fix: api v2 creation source

* fixup! fix: api v2 creation source

* bump libraries

* add user

* fix test

* fixup! fix test

* add more source

* more source...

* fix type & test --1

* fix type & test --2

* typefix

* fixup test

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Syed Ali ShahbazandMorgan Vernay authored and GitHub committed 2025-01-27 11:01:33 +01:00
1 parent 3f622fe277
commit 41f63582b2
46 files changed
+261 -18

No files matched your search

+5
View File
@@ -5,6 +5,7 @@ import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { CreationSource } from "@calcom/prisma/enums";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -213,6 +214,10 @@ import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
*/
async function handler(req: NextApiRequest) {
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
req.body = {
...req.body,
creationSource: CreationSource.API_V1,
};
if (isSystemWideAdmin) req.userId = req.body.userId || userId;
if (isOrganizationOwnerOrAdmin) {
+2
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import type { TInviteMemberInputSchema } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.schema";
@@ -43,6 +44,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
language: data.language,
teamId: data.teamId,
usernameOrEmail: data.usernameOrEmail,
creationSource: CreationSource.API_V1,
});
return { success: true, message: `${data.usernameOrEmail} has been invited.` };
+2 -1
View File
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { schemaUserCreateBodyParams } from "~/lib/validations/user";
@@ -92,7 +93,7 @@ async function postHandler(req: NextApiRequest) {
// If user is not ADMIN, return unauthorized.
if (!isSystemWideAdmin) throw new HttpError({ statusCode: 401, message: "You are not authorized" });
const data = await schemaUserCreateBodyParams.parseAsync(req.body);
const user = await prisma.user.create({ data });
const user = await prisma.user.create({ data: { ...data, creationSource: CreationSource.API_V1 } });
req.statusCode = 201;
return { user };
}
@@ -12,6 +12,7 @@ import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import handler from "../../../pages/api/bookings/_post";
@@ -220,6 +221,34 @@ describe.skipIf(true)("POST /api/bookings", () => {
});
expect(previousBooking?.status).toBe("cancelled");
});
test("Creates source as api_v1", async () => {
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {
name: "test",
start: dayjs().format(),
end: dayjs().add(1, "day").format(),
eventTypeId: 2,
email: "test@example.com",
location: "Cal.com Video",
timeZone: "America/Montevideo",
language: "en",
customInputs: [],
metadata: {},
userId: 4,
},
prisma,
});
prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType());
prismaMock.booking.findMany.mockResolvedValue([]);
await handler(req, res);
createdBooking = JSON.parse(res._getData());
expect(createdBooking.creationSource).toEqual(CreationSource.API_V1);
expect(prismaMock.booking.create).toHaveBeenCalledTimes(1);
});
});
describe("Recurring event-type", () => {
+1 -1
View File
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.86",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.91",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -33,6 +33,7 @@ import {
import { ConfigService } from "@nestjs/config";
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { CreationSource } from "@prisma/client";
import { Request } from "express";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
@@ -383,7 +384,11 @@ export class BookingsController_2024_04_15 {
...oAuthParams,
});
Object.assign(clone, { userId, ...oAuthParams, platformBookingLocation });
clone.body = { ...clone.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
clone.body = {
...clone.body,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -411,6 +416,7 @@ export class BookingsController_2024_04_15 {
...oAuthParams,
platformBookingLocation,
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
});
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -12,6 +12,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CreationSource } from "@prisma/client";
import { Request } from "express";
import { DateTime } from "luxon";
import { NextApiRequest } from "next/types";
@@ -97,10 +98,14 @@ export class InputBookingsService_2024_08_13 {
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
@@ -220,6 +225,7 @@ export class InputBookingsService_2024_08_13 {
newRequest.body = bodyTransformed.map((event) => ({
...event,
creationSource: CreationSource.API_V2,
}));
return newRequest as unknown as BookingRequest;
@@ -317,10 +323,14 @@ export class InputBookingsService_2024_08_13 {
const location = await this.getRescheduleBookingLocation(bookingUid);
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
newRequest.body = { ...bodyTransformed, noEmail: false };
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
@@ -6,7 +6,7 @@ import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-us
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { User, CreationSource } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
@@ -46,6 +46,7 @@ export class OAuthClientUsersService {
role: "MEMBER",
},
],
creationSource: CreationSource.API_V2,
teamId: organizationId,
isOrg: true,
parentId: null,
@@ -3,6 +3,7 @@ import { UpdateOrganizationUserInput } from "@/modules/organizations/inputs/upda
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { CreationSource } from "@prisma/client";
@Injectable()
export class OrganizationsUsersRepository {
@@ -70,7 +71,7 @@ export class OrganizationsUsersRepository {
async createOrganizationUser(orgId: number, createUserBody: CreateOrganizationUserInput) {
const createdUser = await this.dbWrite.prisma.user.create({
data: createUserBody,
data: { ...createUserBody, creationSource: CreationSource.API_V2 },
});
return createdUser;
@@ -5,10 +5,10 @@ import { OrganizationsUsersRepository } from "@/modules/organizations/repositori
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { Injectable, ConflictException } from "@nestjs/common";
import { Team, CreationSource } from "@prisma/client";
import { plainToInstance } from "class-transformer";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
import { Team } from "@calcom/prisma/client";
@Injectable()
export class OrganizationsUsersService {
@@ -56,6 +56,7 @@ export class OrganizationsUsersService {
},
],
teamId: org.id,
creationSource: CreationSource.API_V2,
isOrg: true,
parentId: null,
autoAcceptEmailDomain: "not-required-for-this-endpoint",
@@ -4,6 +4,7 @@ import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-us
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
import { Injectable, NotFoundException } from "@nestjs/common";
import type { Profile, User, Team, Prisma } from "@prisma/client";
import { CreationSource } from "@prisma/client";
export type UserWithProfile = User & {
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
@@ -30,6 +31,7 @@ export class UsersRepository {
connect: { id: oAuthClientId },
},
isPlatformManaged,
creationSource: CreationSource.API_V2,
},
});
}
+2
View File
@@ -9,6 +9,7 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
const querySchema = z.object({
username: z
@@ -48,6 +49,7 @@ async function handler(req: NextApiRequest) {
emailVerified: new Date(),
locale: "en", // TODO: We should revisit this
identityProvider: IdentityProvider.CAL,
creationSource: CreationSource.WEBAPP,
},
});
@@ -3,6 +3,7 @@ import { organizationScenarios } from "@calcom/lib/server/repository/__mocks__/o
import { describe, it, expect, beforeEach, vi } from "vitest";
import { MembershipRole } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
import { moveUserToMatchingOrg } from "./verify-email";
@@ -29,6 +30,7 @@ describe("moveUserToMatchingOrg", () => {
const argToInviteMembersWithNoInviterPermissionCheck = {
inviterName: null,
language: "en",
creationSource: CreationSource.WEBAPP,
invitations: [
{
usernameOrEmail: email,
+2
View File
@@ -8,6 +8,7 @@ import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
@@ -29,6 +30,7 @@ export async function moveUserToMatchingOrg({ email }: { email: string }) {
inviterName: null,
teamId: org.id,
language: "en",
creationSource: CreationSource.WEBAPP,
invitations: [
{
usernameOrEmail: email,
+5
View File
@@ -6,6 +6,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
import getIP from "@calcom/lib/getIP";
import { defaultResponder } from "@calcom/lib/server";
import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";
import { CreationSource } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const userIp = getIP(req);
@@ -25,6 +26,10 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
req.body = {
...req.body,
creationSource: CreationSource.WEBAPP,
};
const booking = await handleNewBooking(req);
return booking;
}
+2
View File
@@ -5,6 +5,7 @@ import handleInstantMeeting from "@calcom/features/instant-meeting/handleInstant
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import getIP from "@calcom/lib/getIP";
import { defaultResponder } from "@calcom/lib/server";
import { CreationSource } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
const userIp = getIP(req);
@@ -16,6 +17,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
const session = await getServerSession({ req, res });
req.userId = session?.user?.id || -1;
req.body.creationSource = CreationSource.WEBAPP;
const booking = await handleInstantMeeting(req);
return booking;
}
@@ -1,6 +1,7 @@
import { getDate } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import type { SchedulingType } from "@calcom/prisma/client";
import type { CreationSource } from "@calcom/prisma/enums";
export const DEFAULT_TIMEZONE_BOOKER = "Asia/Kolkata";
export function getBasicMockRequestDataForBooking() {
@@ -40,6 +41,7 @@ export function getMockRequestDataForBooking({
data: Partial<ReturnType<typeof getBasicMockRequestDataForBooking>> & {
eventTypeId: number;
user?: string;
creationSource?: CreationSource;
} & CommonPropsMockRequestData;
}) {
return {
@@ -36,6 +36,7 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
@@ -979,6 +980,7 @@ export const getOptions = ({
create: { role: MembershipRole.MEMBER, accepted: true, team: { connect: { id: orgId } } },
},
}),
creationSource: CreationSource.WEBAPP,
},
});
@@ -14,6 +14,7 @@ import logger from "@calcom/lib/logger";
import { usernameHandler, type RequestWithUsernameStatus } from "@calcom/lib/server/username";
import { validateAndGetCorrectedUsernameAndEmail } from "@calcom/lib/validateUsername";
import { prisma } from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { IdentityProvider } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
@@ -199,6 +200,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
stripeCustomerId: customer.id,
checkoutSessionId,
},
creationSource: CreationSource.WEBAPP,
},
});
if (process.env.AVATARAPI_USERNAME && process.env.AVATARAPI_PASSWORD) {
@@ -68,6 +68,7 @@ import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import {
eventTypeAppMetadataOptionalSchema,
eventTypeMetaDataSchemaWithTypedApps,
@@ -277,6 +278,7 @@ const buildDryRunBooking = ({
ratingFeedback: null,
noShowHost: null,
cancelledBy: null,
creationSource: CreationSource.WEBAPP,
} as CreatedBooking;
/**
@@ -1195,6 +1197,7 @@ async function handler(
},
evt,
originalRescheduledBooking,
creationSource: req.body.creationSource,
});
if (booking?.userId) {
@@ -7,6 +7,7 @@ import dayjs from "@calcom/dayjs";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { TgetBookingDataSchema } from "../getBookingDataSchema";
@@ -51,6 +52,7 @@ type CreateBookingParams = {
};
evt: CalendarEvent;
originalRescheduledBooking: OriginalRescheduledBooking;
creationSource?: CreationSource;
};
function updateEventDetails(
@@ -85,6 +87,7 @@ export async function createBooking({
routingFormResponseId,
reroutingFormResponses,
rescheduledBy,
creationSource,
}: CreateBookingParams & { rescheduledBy: string | undefined }) {
updateEventDetails(evt, originalRescheduledBooking, input.changedOrganizer);
const associatedBookingForFormResponse = routingFormResponseId
@@ -101,6 +104,7 @@ export async function createBooking({
input,
evt,
originalRescheduledBooking,
creationSource,
});
return await saveBooking(
@@ -211,6 +215,7 @@ function buildNewBookingData(params: CreateBookingParams) {
routingFormResponseId,
reroutingFormResponses,
rescheduledBy,
creationSource,
} = params;
const attendeesData = getAttendeesData(evt);
@@ -258,6 +263,7 @@ function buildNewBookingData(params: CreateBookingParams) {
routedFromRoutingFormReponse: routingFormResponseId
? { connect: { id: routingFormResponseId } }
: undefined,
creationSource,
};
if (reqBody.recurringEventId) {
@@ -56,6 +56,7 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { resetTestEmails } from "@calcom/lib/testEmails";
import { CreationSource } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
@@ -1549,6 +1550,86 @@ describe("handleNewBooking", () => {
);
});
describe("Creation source tests", () => {
test(
`should create a booking with creation source as WEBAPP`,
async ({ emails }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getZoomAppCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { req } = createMockNextJsRequest({
method: "POST",
body: getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
},
creationSource: CreationSource.WEBAPP,
},
}),
});
const scenarioData = getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
locations: [
{
type: BookingLocations.ZoomVideo,
},
],
},
],
organizer,
apps: [TestData.apps["zoomvideo"]],
});
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "zoomvideo",
});
await createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
expect(createdBooking).toEqual(
expect.objectContaining({
creationSource: CreationSource.WEBAPP,
})
);
},
timeout
);
});
describe(
"Availability Check during booking",
() => {
@@ -3,6 +3,7 @@ import type { TFunction } from "next-i18next";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import type { getTeamOrThrow } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import { sendSignupToOrganizationEmail } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
@@ -46,6 +47,7 @@ const createUserAndInviteToOrg = async ({
],
},
},
creationSource: CreationSource.WEBAPP,
},
});
@@ -2,6 +2,7 @@ import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/cr
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { IdentityProvider } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import dSyncUserSelect from "./dSyncUserSelect";
@@ -37,6 +38,7 @@ const createUsersAndConnectToOrg = async (
organizationId,
identityProvider,
identityProviderId,
creationSource: CreationSource.WEBAPP,
};
}),
});
@@ -10,6 +10,7 @@ import { classNames } from "@calcom/lib";
import { IS_TEAM_BILLING_ENABLED_CLIENT } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { CreationSource } from "@calcom/prisma/enums";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Button, CheckboxField, Form, Icon, showToast, TextField } from "@calcom/ui";
@@ -151,7 +152,12 @@ const AddNewTeamsFormChild = ({
const handleFormSubmit = () => {
const fields = getValues("teams");
const moveTeams = getValues("moveTeams");
createTeamsMutation.mutate({ orgId: org.id, moveTeams, teamNames: fields.map((field) => field.name) });
createTeamsMutation.mutate({
orgId: org.id,
moveTeams,
teamNames: fields.map((field) => field.name),
creationSource: CreationSource.WEBAPP,
});
};
const moveTeams = watch("moveTeams");
@@ -13,6 +13,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import { Alert, Button, Form, Label, RadioGroup as RadioArea, TextField, ToggleGroup } from "@calcom/ui";
@@ -107,7 +108,7 @@ const CreateANewOrganizationFormChild = ({
handleSubmit={(v) => {
if (!createOrganizationMutation.isPending) {
setServerErrorMessage(null);
createOrganizationMutation.mutate(v);
createOrganizationMutation.mutate({ ...v, creationSource: CreationSource.WEBAPP });
}
}}>
<div>
@@ -9,6 +9,7 @@ import { useState, useEffect } from "react";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
@@ -216,6 +217,7 @@ const MembersView = () => {
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
creationSource: CreationSource.WEBAPP,
},
{
onSuccess: async (data) => {
@@ -13,6 +13,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import { Alert, Form, TextField, Button } from "@calcom/ui";
@@ -91,6 +92,7 @@ const CreateANewPlatformFormChild = ({ session }: { session: Ensure<SessionConte
createOrganizationMutation.mutate({
...v,
slug: `${v.name.toLocaleLowerCase()}-platform-${uuid().substring(0, 20)}`,
creationSource: CreationSource.API_V2,
});
}
}}>
@@ -10,6 +10,7 @@ import { IS_TEAM_BILLING_ENABLED_CLIENT, MAX_NB_INVITES } from "@calcom/lib/cons
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc";
import { trpc } from "@calcom/trpc";
import { isEmail } from "@calcom/trpc/server/routers/viewer/teams/util";
@@ -512,6 +513,7 @@ export const MemberInvitationModalWithoutMembers = ({
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
creationSource: CreationSource.WEBAPP,
},
{
onSuccess: async (data) => {
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, ButtonGroup, EmptyScreen, Icon, Label, showToast } from "@calcom/ui";
@@ -95,7 +96,7 @@ export function TeamsListing() {
useEffect(() => {
if (!router) return;
if (token) inviteMemberByToken({ token });
if (token) inviteMemberByToken({ token, creationSource: CreationSource.WEBAPP });
else setInviteTokenChecked(true);
}, [router, inviteMemberByToken, setInviteTokenChecked, token]);
@@ -1,6 +1,7 @@
import { z } from "zod";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { CreationSource } from "@calcom/prisma/enums";
import { RedirectType } from "@calcom/prisma/enums";
import { _UserModel as User } from "@calcom/prisma/zod";
import type { inferRouterOutputs } from "@calcom/trpc";
@@ -63,7 +64,7 @@ export const userAdminRouter = router({
}),
add: authedAdminProcedure.input(userBodySchema).mutation(async ({ ctx, input }) => {
const { prisma } = ctx;
const user = await prisma.user.create({ data: input });
const user = await prisma.user.create({ data: { ...input, creationSource: CreationSource.WEBAPP } });
return { user, message: `User with id: ${user.id} added successfully` };
}),
update: authedAdminProcedureWithRequestedUser
@@ -261,6 +261,7 @@ async function handler(req: NextApiRequest) {
data: attendeesList,
},
},
creationSource: req.body.creationSource,
};
const createBookingObj = {
@@ -3,6 +3,7 @@ import type { Dispatch } from "react";
import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import { showToast } from "@calcom/ui";
import usePlatformMe from "@calcom/web/components/settings/platform/hooks/usePlatformMe";
@@ -70,6 +71,7 @@ export function InviteMemberModal(props: Props) {
role: values.role,
usernameOrEmail: values.emailOrUsername,
isPlatform: platformUser?.organization.isPlatform,
creationSource: CreationSource.WEBAPP,
});
}}
/>
@@ -4,6 +4,7 @@ import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { createAProfileForAnExistingUser } from "../../createAProfileForAnExistingUser";
@@ -64,6 +65,7 @@ export class OrganizationRepository {
static async createWithNonExistentOwner({
orgData,
owner,
creationSource,
}: {
orgData: {
name: string;
@@ -79,6 +81,7 @@ export class OrganizationRepository {
owner: {
email: string;
};
creationSource: CreationSource;
}) {
logger.debug("createWithNonExistentOwner", safeStringify({ orgData, owner }));
const organization = await this.create(orgData);
@@ -87,6 +90,7 @@ export class OrganizationRepository {
email: owner.email,
username: ownerUsernameInOrg,
organizationId: organization.id,
creationSource,
});
await prisma.membership.create({
+5
View File
@@ -9,6 +9,7 @@ import prisma from "@calcom/prisma";
import { availabilityUserSelect } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import type { User as UserType } from "@calcom/prisma/client";
import type { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { userMetadata } from "@calcom/prisma/zod-utils";
@@ -584,11 +585,14 @@ export class UserRepository {
email,
username,
organizationId,
creationSource,
}: {
email: string;
username: string;
organizationId: number | null;
creationSource: CreationSource;
}) {
console.log("create user", { email, username, organizationId });
const password = createHash("md5").update(`${email}${process.env.CALENDSO_ENCRYPTION_KEY}`).digest("hex");
const hashedPassword = await hashPassword(password);
const t = await getTranslation("en", "common");
@@ -624,6 +628,7 @@ export class UserRepository {
},
}
: undefined,
creationSource,
},
});
}
+2
View File
@@ -3,6 +3,7 @@ import type { Booking, EventType, Prisma, Webhook, BookingReference } from "@pri
import type { TFunction } from "next-i18next";
import getICalUID from "@calcom/emails/lib/getICalUID";
import { CreationSource } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import type { CalendarEvent, Person, VideoCallData } from "@calcom/types/Calendar";
@@ -76,6 +77,7 @@ export const buildBooking = (
ratingFeedback: null,
attendees: [],
oneTimePassword: null,
creationSource: CreationSource.WEBAPP,
...booking,
};
};
@@ -0,0 +1,8 @@
-- CreateEnum
CREATE TYPE "CreationSource" AS ENUM ('api_v1', 'api_v2', 'webapp');
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "creationSource" "CreationSource";
-- AlterTable
ALTER TABLE "users" ADD COLUMN "creationSource" "CreationSource";
+8
View File
@@ -42,6 +42,12 @@ enum PeriodType {
RANGE @map("range")
}
enum CreationSource {
API_V1 @map("api_v1")
API_V2 @map("api_v2")
WEBAPP @map("webapp")
}
model Host {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
@@ -367,6 +373,7 @@ model User {
createdWatchlists Watchlist[] @relation("CreatedWatchlists")
updatedWatchlists Watchlist[] @relation("UpdatedWatchlists")
BookingInternalNote BookingInternalNote[]
creationSource CreationSource?
@@unique([email])
@@unique([email, username])
@@ -671,6 +678,7 @@ model Booking {
routedFromRoutingFormReponse App_RoutingForms_FormResponse?
assignmentReason AssignmentReason[]
internalNote BookingInternalNote[]
creationSource CreationSource?
@@index([eventTypeId])
@@index([userId])
@@ -89,6 +89,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
pricePerSeat,
isPlatform,
billingPeriod: billingPeriodRaw,
creationSource,
} = input;
const loggedInUser = await prisma.user.findUnique({
@@ -229,6 +230,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
owner: {
email: orgOwnerEmail,
},
creationSource,
});
orgOwner = data.orgOwner;
@@ -2,6 +2,7 @@ import { z } from "zod";
import { emailSchema } from "@calcom/lib/emailSchema";
import slugify from "@calcom/lib/slugify";
import { CreationSource } from "@calcom/prisma/enums";
export enum BillingPeriod {
MONTHLY = "MONTHLY",
@@ -17,6 +18,7 @@ export const ZCreateInputSchema = z.object({
pricePerSeat: z.number().optional(),
isPlatform: z.boolean().default(false),
billingPeriod: z.nativeEnum(BillingPeriod).default(BillingPeriod.MONTHLY).optional(),
creationSource: z.nativeEnum(CreationSource),
});
export type TCreateInputSchema = z.infer<typeof ZCreateInputSchema>;
@@ -8,6 +8,7 @@ import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import { prisma } from "@calcom/prisma";
import { MembershipRole, RedirectType } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -33,7 +34,7 @@ export const createTeamsHandler = async ({ ctx, input }: CreateTeamsOptions) =>
throw new NoUserError();
}
const { orgId, moveTeams } = input;
const { orgId, moveTeams, creationSource } = input;
// Remove empty team names that could be there due to the default empty team name
const teamNames = input.teamNames.filter((name) => name.trim().length > 0);
@@ -106,6 +107,7 @@ export const createTeamsHandler = async ({ ctx, input }: CreateTeamsOptions) =>
ownerId: organizationOwner.id,
},
ctx,
creationSource,
});
})
);
@@ -173,6 +175,7 @@ async function moveTeam({
newSlug,
org,
ctx,
creationSource,
}: {
teamId: number;
newSlug?: string | null;
@@ -183,6 +186,7 @@ async function moveTeam({
metadata: Prisma.JsonValue;
};
ctx: CreateTeamsOptions["ctx"];
creationSource: CreationSource;
}) {
const team = await prisma.team.findUnique({
where: {
@@ -240,6 +244,7 @@ async function moveTeam({
teamId: org.id,
language: "en",
usernameOrEmail: invitableMembers,
creationSource,
},
});
}
@@ -1,5 +1,7 @@
import { z } from "zod";
import { CreationSource } from "@calcom/prisma/enums";
export const ZCreateTeamsSchema = z.object({
teamNames: z.string().array(),
orgId: z.number(),
@@ -10,6 +12,7 @@ export const ZCreateTeamsSchema = z.object({
shouldMove: z.boolean(),
})
),
creationSource: z.nativeEnum(CreationSource),
});
export type TCreateTeamsSchema = z.infer<typeof ZCreateTeamsSchema>;
@@ -8,6 +8,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
import { isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { MembershipRole } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@@ -139,9 +140,10 @@ export const inviteMembersWithNoInviterPermissionCheck = async (
usernameOrEmail: string;
role: MembershipRole;
}[];
creationSource: CreationSource;
} & TargetTeam
) => {
const { inviterName, orgSlug, invitations, language } = data;
const { inviterName, orgSlug, invitations, language, creationSource } = data;
const myLog = log.getSubLogger({ prefix: ["inviteMembers"] });
const translation = await getTranslation(language ?? "en", "common");
const team = "team" in data ? data.team : await getTeamOrThrow(data.teamId);
@@ -188,6 +190,7 @@ export const inviteMembersWithNoInviterPermissionCheck = async (
isOrg: isTeamAnOrg,
inviter,
autoAcceptEmailDomain: orgState.autoAcceptEmailDomain,
creationSource,
});
}
@@ -235,7 +238,7 @@ export const inviteMembersWithNoInviterPermissionCheck = async (
const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => {
const { user: inviter } = ctx;
const { usernameOrEmail, role, isPlatform } = input;
const { usernameOrEmail, role, isPlatform, creationSource } = input;
const team = await getTeamOrThrow(input.teamId);
const requestedSlugForTeam = team?.metadata?.requestedSlug ?? null;
@@ -267,11 +270,11 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => {
teamId: inviterOrgId && isInviterOrgAdmin ? inviterOrgId : input.teamId,
isOrg: isTeamAnOrg,
});
const result = await inviteMembersWithNoInviterPermissionCheck({
inviterName: inviter.name,
team,
language: input.language,
creationSource,
orgSlug,
invitations,
});
@@ -3,6 +3,7 @@ import { z } from "zod";
import { MAX_NB_INVITES } from "@calcom/lib/constants";
import { emailSchema } from "@calcom/lib/emailSchema";
import { MembershipRole } from "@calcom/prisma/enums";
import { CreationSource } from "@calcom/prisma/enums";
export const ZInviteMemberInputSchema = z.object({
teamId: z.number(),
@@ -57,6 +58,7 @@ export const ZInviteMemberInputSchema = z.object({
role: z.nativeEnum(MembershipRole).optional(),
language: z.string(),
isPlatform: z.boolean().optional(),
creationSource: z.nativeEnum(CreationSource),
});
export type TInviteMemberInputSchema = z.infer<typeof ZInviteMemberInputSchema>;
@@ -20,6 +20,7 @@ import { prisma } from "@calcom/prisma";
import type { Membership, OrganizationSettings, Team } from "@calcom/prisma/client";
import { type User as UserType, type UserPassword, Prisma } from "@calcom/prisma/client";
import type { Profile as ProfileType } from "@calcom/prisma/client";
import type { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -274,6 +275,7 @@ export async function createNewUsersConnectToOrgIfExists({
weekStart,
timeZone,
language,
creationSource,
}: {
invitations: Invitation[];
isOrg: boolean;
@@ -286,6 +288,7 @@ export async function createNewUsersConnectToOrgIfExists({
weekStart?: string;
timeZone?: string;
language: string;
creationSource: CreationSource;
}) {
// fail if we have invalid emails
invitations.forEach((invitation) => checkInputEmailIsValid(invitation.usernameOrEmail));
@@ -323,6 +326,7 @@ export async function createNewUsersConnectToOrgIfExists({
timeFormat,
weekStart,
timeZone,
creationSource,
organizationId: orgId || null, // If the user is invited to a child team, they are automatically added to the parent org
...(orgId
? {
@@ -911,6 +915,7 @@ export async function handleNewUsersInvites({
isOrg,
autoAcceptEmailDomain,
inviter,
creationSource,
}: {
invitationsForNewUsers: Invitation[];
teamId: number;
@@ -922,6 +927,7 @@ export async function handleNewUsersInvites({
name: string | null;
};
isOrg: boolean;
creationSource: CreationSource;
}) {
const translation = await getTranslation(language, "common");
@@ -933,6 +939,7 @@ export async function handleNewUsersInvites({
autoAcceptEmailDomain: autoAcceptEmailDomain,
parentId: team.parentId,
language,
creationSource,
});
const sendVerifyEmailsPromises = invitationsForNewUsers.map((invitation) => {
@@ -1,7 +1,10 @@
import { z } from "zod";
import { CreationSource } from "@calcom/prisma/enums";
export const ZInviteMemberByTokenSchemaInputSchema = z.object({
token: z.string(),
creationSource: z.nativeEnum(CreationSource),
});
export type TInviteMemberByTokenSchemaInputSchema = z.infer<typeof ZInviteMemberByTokenSchemaInputSchema>;