feat: api endpoints for calendar settings atom (#15761)
* calendars repository * add disconnect endpoint * update platform libraries to include getCalendar fn * cleanup * fix typings * update calendars service to include delete credentials handler * update typings * add missing return * refactor delete calendar credentials handler * update disconnect endpoint * fix typing * fixup * add handlers to insert and remove selected calendar * init selected calendars controller * fix naming * output response dto for deleting calendar credentials * fix typing * fix merge conflicts * resolve merge conflicts * fixup * cleanup * capitalize controller name * include selected calendars controller in selected calendars module * fix type error * cleanup * fixup * add calendars repository * custom hook for calendar credentials * fix typing * cleanup * take input from query params instead of body in delete request * custom hook to add selected calendar * custom hook to remove selected calendar * better naming * fix typo * fixup * update input for delete calendar query params * address PR feedback * add method to check calendar credentials * abstract logic to check calendar credentials in calendars service * resolve module errors * export custom hooks for calendar settings * e2e for deleting calendar credentials endpoint * fix typo * set authorization header to calendars post * e2e tests for selected calendars controller * fix output typing * better error messages * restructuring * fix imports * remove unused not found exception * fixup! remove unused not found exception --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
co-authored by
Morgan
Morgan Vernay
parent
de2f51e104
commit
b46a902a3d
+156
@@ -0,0 +1,156 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { SelectedCalendarOutputResponseDto } from "@/modules/selected-calendars/outputs/selected-calendars.output";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { CalendarsServiceMock } from "test/mocks/calendars-service-mock";
|
||||
|
||||
import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:5555";
|
||||
|
||||
describe("Platform Selected Calendars Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
|
||||
let appleCalendarCredentials: Credential;
|
||||
let user: User;
|
||||
let accessTokenSecret: string;
|
||||
let refreshTokenSecret: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, TokensModule],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
|
||||
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
|
||||
accessTokenSecret = tokens.accessToken;
|
||||
refreshTokenSecret = tokens.refreshToken;
|
||||
appleCalendarCredentials = await credentialsRepositoryFixture.create(
|
||||
APPLE_CALENDAR_TYPE,
|
||||
{},
|
||||
user.id,
|
||||
APPLE_CALENDAR_ID
|
||||
);
|
||||
await app.init();
|
||||
jest
|
||||
.spyOn(CalendarsService.prototype, "getCalendars")
|
||||
.mockImplementation(CalendarsServiceMock.prototype.getCalendars);
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: [CLIENT_REDIRECT_URI],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(oauthClientRepositoryFixture).toBeDefined();
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(oAuthClient).toBeDefined();
|
||||
expect(accessTokenSecret).toBeDefined();
|
||||
expect(refreshTokenSecret).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it(`POST /v2/selected-calendars: it should respond with a 201 returning back the user added selected calendar`, async () => {
|
||||
const body = {
|
||||
integration: appleCalendarCredentials.type,
|
||||
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||
credentialId: appleCalendarCredentials.id,
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/selected-calendars")
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: SelectedCalendarOutputResponseDto = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.credentialId).toEqual(body.credentialId);
|
||||
expect(responseBody.data.integration).toEqual(body.integration);
|
||||
expect(responseBody.data.externalId).toEqual(body.externalId);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
it(`DELETE /v2/selected-calendars: it should respond with a 200 returning back the user deleted selected calendar`, async () => {
|
||||
const integration = appleCalendarCredentials.type;
|
||||
const externalId =
|
||||
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/";
|
||||
const credentialId = appleCalendarCredentials.id;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(
|
||||
`/v2/selected-calendars?credentialId=${credentialId}&integration=${integration}&externalId=${externalId}`
|
||||
)
|
||||
.set("Authorization", `Bearer ${accessTokenSecret}`)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: SelectedCalendarOutputResponseDto = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.credentialId).toEqual(credentialId);
|
||||
expect(responseBody.data.externalId).toEqual(externalId);
|
||||
expect(responseBody.data.integration).toEqual(integration);
|
||||
expect(responseBody.data.userId).toEqual(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import {
|
||||
SelectedCalendarsInputDto,
|
||||
SelectedCalendarsQueryParamsInputDto,
|
||||
} from "@/modules/selected-calendars/inputs/selected-calendars.input";
|
||||
import {
|
||||
SelectedCalendarOutputResponseDto,
|
||||
SelectedCalendarOutputDto,
|
||||
} from "@/modules/selected-calendars/outputs/selected-calendars.output";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Body, Controller, Post, UseGuards, Delete, Query } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/selected-calendars",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Selected-Calendars")
|
||||
export class SelectedCalendarsController {
|
||||
constructor(
|
||||
private readonly calendarsRepository: CalendarsRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async addSelectedCalendar(
|
||||
@Body() input: SelectedCalendarsInputDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<SelectedCalendarOutputResponseDto> {
|
||||
const { integration, externalId, credentialId } = input;
|
||||
await this.calendarsService.checkCalendarCredentials(Number(credentialId), user.id);
|
||||
|
||||
const newlyAddedCalendarEntry = await this.selectedCalendarsRepository.addUserSelectedCalendar(
|
||||
user.id,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(SelectedCalendarOutputDto, newlyAddedCalendarEntry, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async removeSelectedCalendar(
|
||||
@Query() queryParams: SelectedCalendarsQueryParamsInputDto,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<SelectedCalendarOutputResponseDto> {
|
||||
const { integration, externalId, credentialId } = queryParams;
|
||||
await this.calendarsService.checkCalendarCredentials(Number(credentialId), user.id);
|
||||
|
||||
const removedCalendarEntry = await this.selectedCalendarsRepository.removeUserSelectedCalendar(
|
||||
user.id,
|
||||
integration,
|
||||
externalId
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(SelectedCalendarOutputDto, removedCalendarEntry, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IsInt, IsString } from "class-validator";
|
||||
|
||||
export class SelectedCalendarsInputDto {
|
||||
@IsString()
|
||||
readonly integration!: string;
|
||||
|
||||
@IsString()
|
||||
readonly externalId!: string;
|
||||
|
||||
@IsInt()
|
||||
readonly credentialId!: number;
|
||||
}
|
||||
|
||||
export class SelectedCalendarsQueryParamsInputDto {
|
||||
@IsString()
|
||||
readonly integration!: string;
|
||||
|
||||
@IsString()
|
||||
readonly externalId!: string;
|
||||
|
||||
@IsString()
|
||||
readonly credentialId!: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsString, ValidateNested, IsEnum } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class SelectedCalendarOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly integration!: string;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly externalId!: string;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
readonly credentialId!: number | null;
|
||||
}
|
||||
|
||||
export class SelectedCalendarOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => SelectedCalendarOutputDto)
|
||||
data!: SelectedCalendarOutputDto;
|
||||
}
|
||||
@@ -1,10 +1,24 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [SelectedCalendarsRepository],
|
||||
providers: [
|
||||
SelectedCalendarsRepository,
|
||||
CalendarsRepository,
|
||||
CalendarsService,
|
||||
UsersRepository,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
],
|
||||
controllers: [SelectedCalendarsController],
|
||||
exports: [SelectedCalendarsRepository],
|
||||
})
|
||||
export class SelectedCalendarsModule {}
|
||||
|
||||
@@ -37,4 +37,41 @@ export class SelectedCalendarsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addUserSelectedCalendar(
|
||||
userId: number,
|
||||
integration: string,
|
||||
externalId: string,
|
||||
credentialId: number
|
||||
) {
|
||||
return await this.dbWrite.prisma.selectedCalendar.upsert({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
},
|
||||
// already exists
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
async removeUserSelectedCalendar(userId: number, integration: string, externalId: string) {
|
||||
return await this.dbWrite.prisma.selectedCalendar.delete({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId,
|
||||
externalId,
|
||||
integration,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user