refactor: platform default event types (#19225)

* feat: add areDefaultEventTypesEnabled to PlatformOAuthClient

* feat: specify areDefaultEventTypesEnabled when creating OAuth client

* feat: specify areDefaultEventTypesEnabled when updating OAuth client

* feat: display areDefaultEventTypesEnabled in OAuth clients list

* refactor: set areDefaultEventTypesEnabled by default to false on API level

* feat: v2 API CREATE managed user toggle default event types

* refactor: centralize OAuth inputs and outputs in platform/types

* fix: correct response types for OAuth hooks

* refactor: web/lib/hooks/settings/platform/oauth-clients/useOAuthClients.ts

* refactor: web/lib/hooks/settings/platform/oauth-clients/useOAuthClients.ts

* refactor: split web OAuth hooks into separate files

* refactor: split web OAuth hooks into separate files

* docs: v2 OAuth client inputs and outputs

* refactor: update and create oauth client inputs
This commit is contained in:
Lauris Skraucis
2025-02-26 12:06:08 -03:00
committed by GitHub
parent 07789ba697
commit e437dfa000
35 changed files with 519 additions and 138 deletions
@@ -74,6 +74,7 @@ describe("OAuth Client Users Endpoints", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let oAuthClientEventTypesDisabled: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
@@ -84,6 +85,7 @@ describe("OAuth Client Users Endpoints", () => {
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let postResponseData: CreateManagedUserOutput["data"];
let postResponseData2: CreateManagedUserOutput["data"];
const platformAdminEmail = `oauth-client-users-admin-${randomString()}@api.com`;
let platformAdmin: User;
@@ -91,6 +93,9 @@ describe("OAuth Client Users Endpoints", () => {
const userEmail = `oauth-client-users-user-${randomString()}@api.com`;
const userTimeZone = "Europe/Rome";
const userEmail2 = `oauth-client-users-user2-${randomString()}@api.com`;
const userTimeZone2 = "America/New_York";
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
@@ -116,6 +121,7 @@ describe("OAuth Client Users Endpoints", () => {
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
oAuthClientEventTypesDisabled = await createOAuthClient(organization.id, false);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
@@ -136,12 +142,13 @@ describe("OAuth Client Users Endpoints", () => {
await app.init();
});
async function createOAuthClient(organizationId: number) {
async function createOAuthClient(organizationId: number, areDefaultEventTypesEnabled?: boolean) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 32,
areDefaultEventTypesEnabled,
};
const secret = "secret";
@@ -185,7 +192,7 @@ describe("OAuth Client Users Endpoints", () => {
.expect(400);
});
it(`/POST`, async () => {
it(`/POST with default event types`, async () => {
const requestBody: CreateManagedUserInput = {
email: userEmail,
timeZone: userTimeZone,
@@ -218,14 +225,55 @@ describe("OAuth Client Users Endpoints", () => {
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
await userConnectedToOAuth(responseBody.data.user.email);
await userConnectedToOAuth(oAuthClient.id, responseBody.data.user.email);
await userHasDefaultEventTypes(responseBody.data.user.id);
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
await userHasOnlyOneSchedule(responseBody.data.user.id);
});
async function userConnectedToOAuth(userEmail: string) {
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClient.id);
it(`/POST without default event types`, async () => {
const requestBody: CreateManagedUserInput = {
email: userEmail2,
timeZone: userTimeZone2,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClientEventTypesDisabled.id}/users`)
.set("x-cal-secret-key", oAuthClientEventTypesDisabled.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
postResponseData2 = responseBody.data;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
getOAuthUserEmail(oAuthClientEventTypesDisabled.id, requestBody.email)
);
expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone);
expect(responseBody.data.user.name).toEqual(requestBody.name);
expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart);
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
await userConnectedToOAuth(oAuthClientEventTypesDisabled.id, responseBody.data.user.email);
await userDoesNotHaveDefaultEventTypes(responseBody.data.user.id);
await userHasDefaultSchedule(responseBody.data.user.id, responseBody.data.user.defaultScheduleId);
await userHasOnlyOneSchedule(responseBody.data.user.id);
});
async function userConnectedToOAuth(oAuthClientId: string, userEmail: string) {
const oAuthUsers = await oauthClientRepositoryFixture.getUsers(oAuthClientId);
const newOAuthUser = oAuthUsers?.find((user) => user.email === userEmail);
expect(oAuthUsers?.length).toEqual(1);
@@ -251,6 +299,11 @@ describe("OAuth Client Users Endpoints", () => {
).toBeTruthy();
}
async function userDoesNotHaveDefaultEventTypes(userId: number) {
const defaultEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
expect(defaultEventTypes?.length).toEqual(0);
}
async function userHasDefaultSchedule(userId: number, scheduleId: number | null) {
expect(scheduleId).toBeDefined();
expect(scheduleId).not.toBeNull();
@@ -358,12 +411,18 @@ describe("OAuth Client Users Endpoints", () => {
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await oauthClientRepositoryFixture.delete(oAuthClientEventTypesDisabled.id);
await teamRepositoryFixture.delete(organization.id);
try {
await userRepositoryFixture.delete(postResponseData.user.id);
} catch (e) {
// User might have been deleted by the test
}
try {
await userRepositoryFixture.delete(postResponseData2.user.id);
} catch (e) {
// User might have been deleted by the test
}
try {
await userRepositoryFixture.delete(platformAdmin.id);
} catch (e) {
@@ -85,14 +85,11 @@ export class OAuthClientUsersController {
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
);
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
if (!client) {
throw new NotFoundException(`OAuth Client with ID ${oAuthClientId} not found`);
}
const isPlatformManaged = true;
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
oAuthClientId,
body,
isPlatformManaged,
client?.organizationId
);
const { user, tokens } = await this.oAuthClientUsersService.createOAuthClientUser(client, body);
return {
status: SUCCESS_STATUS,
@@ -37,8 +37,7 @@ import {
import { User, MembershipRole } from "@prisma/client";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { CreateOAuthClientInput, UpdateOAuthClientInput } from "@calcom/platform-types";
import { Pagination } from "@calcom/platform-types";
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
@@ -1,22 +1,9 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, ValidateNested, IsNotEmptyObject, IsString } from "class-validator";
import { IsIn, ValidateNested, IsNotEmptyObject } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS, REDIRECT_STATUS } from "@calcom/platform-constants";
class DataDto {
@ApiProperty({
example: "clsx38nbl0001vkhlwin9fmt0",
})
@IsString()
clientId!: string;
@ApiProperty({
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi",
})
@IsString()
clientSecret!: string;
}
import { CreateOAuthClientOutput } from "@calcom/platform-types";
export class CreateOAuthClientResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@@ -31,8 +18,8 @@ export class CreateOAuthClientResponseDto {
})
@IsNotEmptyObject()
@ValidateNested()
@Type(() => DataDto)
data!: DataDto;
@Type(() => CreateOAuthClientOutput)
data!: CreateOAuthClientOutput;
}
export class CreateOauthClientRedirect {
@@ -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, CreationSource } from "@prisma/client";
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
@@ -16,16 +16,13 @@ export class OAuthClientUsersService {
private readonly userRepository: UsersRepository,
private readonly tokensRepository: TokensRepository,
private readonly eventTypesService: EventTypesService_2024_04_15,
private readonly schedulesService: SchedulesService_2024_04_15,
private readonly organizationsTeamsService: OrganizationsTeamsService
private readonly schedulesService: SchedulesService_2024_04_15
) {}
async createOauthClientUser(
oAuthClientId: string,
body: CreateManagedUserInput,
isPlatformManaged: boolean,
organizationId?: number
) {
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
const oAuthClientId = oAuthClient.id;
const organizationId = oAuthClient.organizationId;
const existingUser = await this.getExistingUserByEmail(oAuthClientId, body.email);
if (existingUser) {
throw new ConflictException(
@@ -35,7 +32,9 @@ export class OAuthClientUsersService {
let user: User;
if (!organizationId) {
throw new BadRequestException("You cannot create a managed user outside of an organization");
throw new BadRequestException(
"You cannot create a managed user outside of an organization - the OAuth client does not belong to any organization."
);
} else {
const email = this.getOAuthUserEmail(oAuthClientId, body.email);
user = (
@@ -57,7 +56,7 @@ export class OAuthClientUsersService {
autoAccept: true,
},
},
isPlatformManaged,
isPlatformManaged: true,
timeFormat: body.timeFormat,
weekStart: body.weekStart,
timeZone: body.timeZone,
@@ -79,7 +78,9 @@ export class OAuthClientUsersService {
user.id
);
await this.eventTypesService.createUserDefaultEventTypes(user.id);
if (oAuthClient.areDefaultEventTypesEnabled) {
await this.eventTypesService.createUserDefaultEventTypes(user.id);
}
if (body.timeZone) {
const defaultSchedule = await this.schedulesService.createUserDefaultSchedule(user.id, body.timeZone);
@@ -33,6 +33,7 @@ export class OAuthClientsOutputService {
bookingCancelRedirectUri: client.bookingCancelRedirectUri ?? undefined,
bookingRescheduleRedirectUri: client.bookingRescheduleRedirectUri ?? undefined,
areEmailsEnabled: client.areEmailsEnabled,
areDefaultEventTypesEnabled: client.areDefaultEventTypesEnabled,
};
}
+23 -7
View File
@@ -7635,6 +7635,11 @@
},
"areEmailsEnabled": {
"type": "boolean"
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"default": false,
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
}
},
"required": [
@@ -7643,7 +7648,7 @@
"permissions"
]
},
"DataDto": {
"CreateOAuthClientOutput": {
"type": "object",
"properties": {
"clientId": {
@@ -7678,7 +7683,7 @@
},
"allOf": [
{
"$ref": "#/components/schemas/DataDto"
"$ref": "#/components/schemas/CreateOAuthClientOutput"
}
]
}
@@ -7748,6 +7753,15 @@
"type": "string",
"example": "2024-03-23T08:33:21.851Z"
},
"areEmailsEnabled": {
"type": "boolean",
"example": true
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"example": true,
"description": "If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user."
},
"bookingRedirectUri": {
"type": "string",
"example": "https://example.com/booking-redirect"
@@ -7759,10 +7773,6 @@
"bookingRescheduleRedirectUri": {
"type": "string",
"example": "https://example.com/booking-reschedule"
},
"areEmailsEnabled": {
"type": "boolean",
"example": false
}
},
"required": [
@@ -7772,7 +7782,9 @@
"permissions",
"redirectUris",
"organizationId",
"createdAt"
"createdAt",
"areEmailsEnabled",
"areDefaultEventTypesEnabled"
]
},
"GetOAuthClientsResponseDto": {
@@ -7844,6 +7856,10 @@
},
"areEmailsEnabled": {
"type": "boolean"
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
}
}
},
@@ -46,6 +46,7 @@ export const OAuthClientsList = ({ oauthClients, isDeleting, handleDelete }: OAu
isLoading={isDeleting}
onDelete={handleDelete}
areEmailsEnabled={client.areEmailsEnabled}
areDefaultEventTypesEnabled={client.areDefaultEventTypesEnabled}
organizationId={client.organizationId}
/>
);
@@ -1,4 +1,4 @@
import { useCheckTeamBilling } from "@calcom/web/lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { useCheckTeamBilling } from "@calcom/web/lib/hooks/settings/platform/billing/useCheckTeamBilling";
import { usePlatformMe } from "./usePlatformMe";
@@ -15,6 +15,7 @@ type OAuthClientCardProps = {
bookingCancelRedirectUri: string | null | undefined;
bookingRescheduleRedirectUri: string | null | undefined;
areEmailsEnabled: boolean | undefined;
areDefaultEventTypesEnabled: boolean;
permissions: Array<keyof typeof PERMISSION_MAP>;
lastItem: boolean;
id: string;
@@ -38,6 +39,7 @@ export const OAuthClientCard = ({
onDelete,
isLoading,
areEmailsEnabled,
areDefaultEventTypesEnabled,
organizationId,
}: OAuthClientCardProps) => {
const router = useRouter();
@@ -165,6 +167,10 @@ export const OAuthClientCard = ({
<div className="flex gap-1 text-sm">
<span className="text-sm font-semibold">Emails enabled:</span> {areEmailsEnabled ? "Yes" : "No"}
</div>
<div className="flex gap-1 text-sm">
<span className="text-sm font-semibold">Default event types enabled:</span>{" "}
{areDefaultEventTypesEnabled ? "Yes" : "No"}
</div>
</div>
<div className="flex items-start gap-4">
<Button
@@ -3,7 +3,7 @@ import { useForm, useFieldArray } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
import { TextField, Tooltip, Button, Label } from "@calcom/ui";
import { TextField, Tooltip, Button, Label, Icon } from "@calcom/ui";
type OAuthClientFormProps = {
defaultValues?: Partial<FormValues>;
@@ -33,6 +33,7 @@ export type FormValues = {
bookingCancelRedirectUri?: string;
bookingRescheduleRedirectUri?: string;
areEmailsEnabled?: boolean;
areDefaultEventTypesEnabled?: boolean;
};
export const OAuthClientForm = ({
@@ -232,6 +233,29 @@ export const OAuthClientForm = ({
Enable emails
</label>
</div>
<div className="mt-6">
<div className="flex items-center">
<input
{...register("areDefaultEventTypesEnabled")}
id="areDefaultEventTypesEnabled"
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
type="checkbox"
disabled={isFormDisabled}
/>
<label
htmlFor="areDefaultEventTypesEnabled"
className="cursor-pointer px-2 text-base font-semibold">
Enable managed user default event types
</label>
<Tooltip
className="max-w-[400px] whitespace-normal"
content="If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user.">
<div className="ml-1">
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
</div>
</Tooltip>
</div>
</div>
<div className="mt-6">
<div className="flex justify-between">
<h1 className="text-base font-semibold underline">Permissions</h1>
@@ -5,10 +5,8 @@ import type { ReactNode } from "react";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { showToast } from "@calcom/ui";
import {
useSubscribeTeamToStripe,
useUpgradeTeamSubscriptionInStripe,
} from "@lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { useSubscribeTeamToStripe } from "@lib/hooks/settings/platform/billing/useSubscribeTeamToStripe";
import { useUpgradeTeamSubscriptionInStripe } from "@lib/hooks/settings/platform/billing/useUpgradeTeamSubscriptionInStripe";
import { platformPlans } from "@components/settings/platform/platformUtils";
import { PlatformBillingCard } from "@components/settings/platform/pricing/billing-card";
@@ -0,0 +1,20 @@
import { useQuery } from "@tanstack/react-query";
export const useCheckTeamBilling = (teamId?: number | null, isPlatformTeam?: boolean | null) => {
const QUERY_KEY = "check-team-billing";
const isTeamBilledAlready = useQuery({
queryKey: [QUERY_KEY, teamId],
queryFn: async () => {
const response = await fetch(`/api/v2/billing/${teamId}/check`, {
method: "get",
headers: { "Content-type": "application/json" },
});
const data = await response.json();
return data.data;
},
enabled: !!teamId && !!isPlatformTeam,
});
return isTeamBilledAlready;
};
@@ -0,0 +1,41 @@
import { useMutation } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, SubscribeTeamInput } from "@calcom/platform-types";
export const useSubscribeTeamToStripe = (
{
onSuccess,
onError,
teamId,
}: { teamId?: number | null; onSuccess: (redirectUrl: string) => void; onError: () => void } = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<ApiResponse<{ action: string; url: string }>, unknown, SubscribeTeamInput>({
mutationFn: (data) => {
return fetch(`/api/v2/billing/${teamId}/subscribe`, {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.(data.data?.url);
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
@@ -0,0 +1,41 @@
import { useMutation } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, SubscribeTeamInput } from "@calcom/platform-types";
export const useUpgradeTeamSubscriptionInStripe = (
{
onSuccess,
onError,
teamId,
}: { teamId?: number | null; onSuccess: (redirectUrl: string) => void; onError: () => void } = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<ApiResponse<{ action: string; url: string }>, unknown, SubscribeTeamInput>({
mutationFn: (data) => {
return fetch(`/api/v2/billing/${teamId}/upgrade`, {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.(data.data?.url);
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
@@ -0,0 +1,40 @@
import { useMutation } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, CreateOAuthClientInput, CreateOAuthClientOutput } from "@calcom/platform-types";
export interface IPersistOAuthClient {
onSuccess?: () => void;
onError?: () => void;
}
export const useCreateOAuthClient = (
{ onSuccess, onError }: IPersistOAuthClient = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
return useMutation<ApiResponse<CreateOAuthClientOutput>, unknown, CreateOAuthClientInput>({
mutationFn: async (data) => {
return fetch("/api/v2/oauth-clients", {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.();
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
};
@@ -0,0 +1,39 @@
import { useMutation } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, DeleteOAuthClientInput, PlatformOAuthClientDto } from "@calcom/platform-types";
import type { IPersistOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useCreateOAuthClient";
export const useDeleteOAuthClient = (
{ onSuccess, onError }: IPersistOAuthClient = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<ApiResponse<PlatformOAuthClientDto>, unknown, DeleteOAuthClientInput>({
mutationFn: async (data) => {
const { id } = data;
return fetch(`/api/v2/oauth-clients/${id}`, {
method: "delete",
headers: { "Content-type": "application/json" },
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.();
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
@@ -0,0 +1,39 @@
import { useQuery } from "@tanstack/react-query";
import type { ApiSuccessResponse, PlatformOAuthClientDto } from "@calcom/platform-types";
export const useOAuthClient = (clientId?: string) => {
const {
isLoading,
error,
data: response,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
} = useQuery<ApiSuccessResponse<PlatformOAuthClientDto>>({
queryKey: ["oauth-client", clientId],
queryFn: () => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "get",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
},
enabled: Boolean(clientId),
staleTime: Infinity,
});
return {
isLoading,
error,
data: response?.data,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
};
};
@@ -2,17 +2,6 @@ import { useQuery } from "@tanstack/react-query";
import type { ApiSuccessResponse, PlatformOAuthClientDto } from "@calcom/platform-types";
export type ManagedUser = {
id: number;
email: string;
username: string | null;
timeZone: string;
weekStart: string;
createdDate: Date;
timeFormat: number | null;
defaultScheduleId: number | null;
};
export const useOAuthClients = () => {
const query = useQuery<ApiSuccessResponse<PlatformOAuthClientDto[]>>({
queryKey: ["oauth-clients"],
@@ -26,58 +15,3 @@ export const useOAuthClients = () => {
return { ...query, data: query.data?.data ?? [] };
};
export const useOAuthClient = (clientId?: string) => {
const {
isLoading,
error,
data: response,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
} = useQuery<ApiSuccessResponse<PlatformOAuthClientDto>>({
queryKey: ["oauth-client", clientId],
queryFn: () => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "get",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
},
enabled: Boolean(clientId),
staleTime: Infinity,
});
return {
isLoading,
error,
data: response?.data,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
};
};
export const useGetOAuthClientManagedUsers = (clientId: string) => {
const {
isLoading,
error,
data: response,
refetch,
} = useQuery<ApiSuccessResponse<ManagedUser[]>>({
queryKey: ["oauth-client-managed-users", clientId],
queryFn: () => {
return fetch(`/api/v2/oauth-clients/${clientId}/managed-users`, {
method: "get",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
},
enabled: Boolean(clientId),
});
return { isLoading, error, data: response?.data, refetch };
};
@@ -0,0 +1,43 @@
import { useMutation } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, CreateOAuthClientInput, PlatformOAuthClientDto } from "@calcom/platform-types";
import type { IPersistOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useCreateOAuthClient";
export const useUpdateOAuthClient = (
{ onSuccess, onError, clientId }: IPersistOAuthClient & { clientId?: string } = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<
ApiResponse<PlatformOAuthClientDto>,
unknown,
Omit<CreateOAuthClientInput, "permissions">
>({
mutationFn: async (data) => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "PATCH",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.();
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
@@ -9,8 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants";
import { showToast } from "@calcom/ui";
import { useOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
import { useUpdateOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { useOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClient";
import { useUpdateOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useUpdateOAuthClient";
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
@@ -59,6 +59,7 @@ export default function EditOAuthClient() {
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled,
});
};
@@ -85,6 +86,7 @@ export default function EditOAuthClient() {
defaultValues={{
name: data?.name ?? "",
areEmailsEnabled: data.areEmailsEnabled ?? false,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled ?? false,
redirectUris: data?.redirectUris?.map((uri) => ({ uri })) ?? [{ uri: "" }],
bookingRedirectUri: data?.bookingRedirectUri ?? "",
bookingCancelRedirectUri: data?.bookingCancelRedirectUri ?? "",
@@ -10,7 +10,7 @@ import type { PERMISSION_MAP } from "@calcom/platform-constants";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
import { showToast } from "@calcom/ui";
import { useCreateOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { useCreateOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useCreateOAuthClient";
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
@@ -55,6 +55,7 @@ export default function CreateOAuthClient() {
bookingCancelRedirectUri: data.bookingCancelRedirectUri,
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled,
});
};
@@ -7,8 +7,8 @@ import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { showToast } from "@calcom/ui";
import { useDeleteOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/useDeleteOAuthClient";
import { useOAuthClients } from "@lib/hooks/settings/platform/oauth-clients/useOAuthClients";
import { useDeleteOAuthClient } from "@lib/hooks/settings/platform/oauth-clients/usePersistOAuthClient";
import { HelpCards } from "@components/settings/platform/dashboard/HelpCards";
import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPlan";
+31 -7
View File
@@ -7181,11 +7181,16 @@
},
"areEmailsEnabled": {
"type": "boolean"
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"default": false,
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
}
},
"required": ["name", "redirectUris", "permissions"]
},
"DataDto": {
"CreateOAuthClientOutput": {
"type": "object",
"properties": {
"clientId": {
@@ -7214,7 +7219,7 @@
},
"allOf": [
{
"$ref": "#/components/schemas/DataDto"
"$ref": "#/components/schemas/CreateOAuthClientOutput"
}
]
}
@@ -7276,6 +7281,15 @@
"type": "string",
"example": "2024-03-23T08:33:21.851Z"
},
"areEmailsEnabled": {
"type": "boolean",
"example": true
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"example": true,
"description": "If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user."
},
"bookingRedirectUri": {
"type": "string",
"example": "https://example.com/booking-redirect"
@@ -7287,13 +7301,19 @@
"bookingRescheduleRedirectUri": {
"type": "string",
"example": "https://example.com/booking-reschedule"
},
"areEmailsEnabled": {
"type": "boolean",
"example": false
}
},
"required": ["id", "name", "secret", "permissions", "redirectUris", "organizationId", "createdAt"]
"required": [
"id",
"name",
"secret",
"permissions",
"redirectUris",
"organizationId",
"createdAt",
"areEmailsEnabled",
"areDefaultEventTypesEnabled"
]
},
"GetOAuthClientsResponseDto": {
"type": "object",
@@ -7352,6 +7372,10 @@
},
"areEmailsEnabled": {
"type": "boolean"
},
"areDefaultEventTypesEnabled": {
"type": "boolean",
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
}
}
},
+1
View File
@@ -10,5 +10,6 @@ export * from "./event-types";
export * from "./organizations";
export * from "./teams";
export * from "./embed";
export * from "./oauth-clients";
export * from "./routing-forms";
export * from "./me";
+1
View File
@@ -0,0 +1 @@
export * from "./outputs";
@@ -0,0 +1 @@
export * from "./me";
+15
View File
@@ -0,0 +1,15 @@
import { z } from "zod";
export const userSchemaResponse = z.object({
id: z.number().int(),
email: z.string(),
timeFormat: z.number().int().default(12),
defaultScheduleId: z.number().int().nullable(),
weekStart: z.string(),
timeZone: z.string().default("Europe/London"),
username: z.string(),
organizationId: z.number().nullable(),
organization: z.object({ isPlatform: z.boolean(), id: z.number() }).optional(),
});
export type UserResponse = z.infer<typeof userSchemaResponse>;
@@ -1,8 +1,11 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsArray, IsEnum, IsOptional, IsBoolean, IsString } from "class-validator";
import { PERMISSION_MAP } from "@calcom/platform-constants";
export const ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS =
"If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user.";
export class CreateOAuthClientInput {
@IsOptional()
@IsString()
@@ -47,4 +50,14 @@ export class CreateOAuthClientInput {
@IsBoolean()
@ApiPropertyOptional()
areEmailsEnabled?: boolean;
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value ?? false)
@ApiPropertyOptional({
type: Boolean,
default: false,
description: ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS,
})
areDefaultEventTypesEnabled? = false;
}
@@ -1,6 +1,8 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
import { ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS } from "./create-oauth-client.input";
export class UpdateOAuthClientInput {
@IsOptional()
@IsString()
@@ -37,4 +39,12 @@ export class UpdateOAuthClientInput {
@IsBoolean()
@ApiPropertyOptional()
areEmailsEnabled?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
type: Boolean,
description: ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS,
})
areDefaultEventTypesEnabled?: boolean;
}
@@ -0,0 +1,16 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class CreateOAuthClientOutput {
@ApiProperty({
example: "clsx38nbl0001vkhlwin9fmt0",
})
@IsString()
clientId!: string;
@ApiProperty({
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi",
})
@IsString()
clientSecret!: string;
}
@@ -1 +1,2 @@
export * from "./oauth-client.output";
export * from "./create-oauth-client.output";
@@ -44,6 +44,18 @@ export class PlatformOAuthClientDto {
@IsDate()
createdAt!: Date;
@IsBoolean()
@ApiProperty({ example: true })
areEmailsEnabled!: boolean;
@IsBoolean()
@ApiProperty({
example: true,
description:
"If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user.",
})
areDefaultEventTypesEnabled!: boolean;
@ApiPropertyOptional({ example: "https://example.com/booking-redirect" })
@IsOptional()
@IsUrl()
@@ -58,9 +70,4 @@ export class PlatformOAuthClientDto {
@IsOptional()
@IsUrl()
bookingRescheduleRedirectUri?: string;
@ApiPropertyOptional({ example: false })
@IsOptional()
@IsBoolean()
areEmailsEnabled?: boolean;
}
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "PlatformOAuthClient" ADD COLUMN "areDefaultEventTypesEnabled" BOOLEAN NOT NULL DEFAULT true;
+1
View File
@@ -1519,6 +1519,7 @@ model PlatformOAuthClient {
bookingCancelRedirectUri String?
bookingRescheduleRedirectUri String?
areEmailsEnabled Boolean @default(false)
areDefaultEventTypesEnabled Boolean @default(true)
createdAt DateTime @default(now())
}