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:
+64
-5
@@ -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) {
|
||||
|
||||
+4
-7
@@ -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,
|
||||
|
||||
+1
-2
@@ -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.`;
|
||||
|
||||
+4
-17
@@ -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);
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export class OAuthClientsOutputService {
|
||||
bookingCancelRedirectUri: client.bookingCancelRedirectUri ?? undefined,
|
||||
bookingRescheduleRedirectUri: client.bookingRescheduleRedirectUri ?? undefined,
|
||||
areEmailsEnabled: client.areEmailsEnabled,
|
||||
areDefaultEventTypesEnabled: client.areDefaultEventTypesEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user