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,
};
}