diff --git a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts index e04ee36b7f..d54f4b0df5 100644 --- a/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts +++ b/apps/api/v2/src/ee/calendars/services/apple-calendar.service.ts @@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common"; import { Injectable } from "@nestjs/common"; import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants"; -import { symmetricEncrypt, CalendarService } from "@calcom/platform-libraries"; +import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries"; +import { Credential } from "@calcom/prisma/client"; @Injectable() export class AppleCalendarService implements CredentialSyncCalendarApp { @@ -61,26 +62,70 @@ export class AppleCalendarService implements CredentialSyncCalendarApp { if (username.length <= 1 || password.length <= 1) throw new BadRequestException(`Username or password cannot be empty`); - const data = { - type: APPLE_CALENDAR_TYPE, - key: symmetricEncrypt( - JSON.stringify({ username, password }), - process.env.CALENDSO_ENCRYPTION_KEY || "" - ), - userId: userId, - teamId: null, - appId: APPLE_CALENDAR_ID, - invalid: false, - }; + const existingAppleCalendarCredentials = await this.credentialRepository.getAllUserCredentialsByTypeAndId( + APPLE_CALENDAR_TYPE, + userId + ); + + let hasMatchingUsernameAndPassword = false; + + if (existingAppleCalendarCredentials.length > 0) { + const hasCalendarWithGivenCredentials = existingAppleCalendarCredentials.find( + (calendarCredential: Credential) => { + const decryptedKey = JSON.parse( + symmetricDecrypt(calendarCredential.key as string, process.env.CALENDSO_ENCRYPTION_KEY || "") + ); + + if (decryptedKey.username === username) { + if (decryptedKey.password === password) { + hasMatchingUsernameAndPassword = true; + } + + return true; + } + } + ); + + if (!!hasCalendarWithGivenCredentials && hasMatchingUsernameAndPassword) { + return { + status: SUCCESS_STATUS, + }; + } + + if (!!hasCalendarWithGivenCredentials && !hasMatchingUsernameAndPassword) { + await this.credentialRepository.upsertAppCredential( + APPLE_CALENDAR_TYPE, + symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY || ""), + userId, + hasCalendarWithGivenCredentials.id + ); + + return { + status: SUCCESS_STATUS, + }; + } + } try { + const data = { + type: APPLE_CALENDAR_TYPE, + key: symmetricEncrypt( + JSON.stringify({ username, password }), + process.env.CALENDSO_ENCRYPTION_KEY || "" + ), + userId: userId, + teamId: null, + appId: APPLE_CALENDAR_ID, + invalid: false, + }; + const dav = new CalendarService({ id: 0, ...data, user: { email: userEmail }, }); await dav?.listCalendars(); - await this.credentialRepository.createAppCredential(APPLE_CALENDAR_TYPE, data.key, userId); + await this.credentialRepository.upsertAppCredential(APPLE_CALENDAR_TYPE, data.key, userId); } catch (reason) { throw new BadRequestException(`Could not add this apple calendar account: ${reason}`); } diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/ee/calendars/services/calendars.service.ts index 4f509f1935..ba2ce7b2a8 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/ee/calendars/services/calendars.service.ts @@ -6,6 +6,7 @@ import { } from "@/modules/credentials/credentials.repository"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { Injectable, @@ -15,9 +16,11 @@ import { } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { User } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import { DateTime } from "luxon"; import { z } from "zod"; +import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants"; import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries"; import { Calendar } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; @@ -33,7 +36,8 @@ export class CalendarsService { private readonly calendarsRepository: CalendarsRepository, private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService, - private readonly config: ConfigService + private readonly config: ConfigService, + private readonly selectedCalendarsRepository: SelectedCalendarsRepository ) {} async getCalendars(userId: number) { @@ -145,4 +149,32 @@ export class CalendarsService { throw new NotFoundException("Calendar credentials not found"); } } + + async createAndLinkCalendarEntry( + userId: number, + externalId: string, + key: Prisma.InputJsonValue, + calendarType: keyof typeof APPS_TYPE_ID_MAPPING, + credentialId?: number | null + ) { + const credential = await this.credentialsRepository.upsertAppCredential( + calendarType, + key, + userId, + credentialId + ); + + await this.selectedCalendarsRepository.upsertSelectedCalendar( + externalId, + credential.id, + userId, + calendarType + ); + } + + async checkCalendarCredentialValidity(userId: number, credentialId: number, type: string) { + const credential = await this.credentialsRepository.getUserCredentialById(userId, credentialId, type); + + return !credential?.invalid; + } } diff --git a/apps/api/v2/src/ee/calendars/services/gcal.service.ts b/apps/api/v2/src/ee/calendars/services/gcal.service.ts index 9f0d25471c..ea1d220d41 100644 --- a/apps/api/v2/src/ee/calendars/services/gcal.service.ts +++ b/apps/api/v2/src/ee/calendars/services/gcal.service.ts @@ -131,11 +131,6 @@ export class GoogleCalendarService implements OAuthCalendarApp { const token = await oAuth2Client.getToken(parsedCode); // Google oAuth Credentials are stored in token.tokens const key = token.tokens; - const credential = await this.credentialRepository.createAppCredential( - GOOGLE_CALENDAR_TYPE, - key as Prisma.InputJsonValue, - ownerId - ); oAuth2Client.setCredentials(key); @@ -149,10 +144,39 @@ export class GoogleCalendarService implements OAuthCalendarApp { const primaryCal = cals.data.items?.find((cal) => cal.primary); if (primaryCal?.id) { - await this.selectedCalendarsRepository.createSelectedCalendar( - primaryCal.id, - credential.id, + const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar( ownerId, + GOOGLE_CALENDAR_TYPE, + primaryCal.id + ); + + if (alreadyExistingSelectedCalendar) { + const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity( + ownerId, + alreadyExistingSelectedCalendar.credentialId ?? 0, + GOOGLE_CALENDAR_TYPE + ); + + // user credential probably got expired in this case + if (!isCredentialValid) { + await this.calendarsService.createAndLinkCalendarEntry( + ownerId, + alreadyExistingSelectedCalendar.externalId, + key as Prisma.InputJsonValue, + GOOGLE_CALENDAR_TYPE, + alreadyExistingSelectedCalendar.credentialId + ); + } + + return { + url: redir || origin, + }; + } + + await this.calendarsService.createAndLinkCalendarEntry( + ownerId, + primaryCal.id, + key as Prisma.InputJsonValue, GOOGLE_CALENDAR_TYPE ); } diff --git a/apps/api/v2/src/ee/calendars/services/outlook.service.ts b/apps/api/v2/src/ee/calendars/services/outlook.service.ts index 060427fdce..eeab3027e1 100644 --- a/apps/api/v2/src/ee/calendars/services/outlook.service.ts +++ b/apps/api/v2/src/ee/calendars/services/outlook.service.ts @@ -168,16 +168,39 @@ export class OutlookService implements OAuthCalendarApp { const defaultCalendar = await this.getDefaultCalendar(office365OAuthCredentials.access_token); if (defaultCalendar?.id) { - const credential = await this.credentialRepository.createAppCredential( + const alreadyExistingSelectedCalendar = await this.selectedCalendarsRepository.getUserSelectedCalendar( + ownerId, OFFICE_365_CALENDAR_TYPE, - office365OAuthCredentials, - ownerId + defaultCalendar.id ); - await this.selectedCalendarsRepository.createSelectedCalendar( - defaultCalendar.id, - credential.id, + if (alreadyExistingSelectedCalendar) { + const isCredentialValid = await this.calendarsService.checkCalendarCredentialValidity( + ownerId, + alreadyExistingSelectedCalendar.credentialId ?? 0, + OFFICE_365_CALENDAR_TYPE + ); + + // user credential probably got expired in this case + if (!isCredentialValid) { + await this.calendarsService.createAndLinkCalendarEntry( + ownerId, + alreadyExistingSelectedCalendar.externalId, + office365OAuthCredentials, + OFFICE_365_CALENDAR_TYPE, + alreadyExistingSelectedCalendar.credentialId + ); + } + + return { + url: redir || origin, + }; + } + + await this.calendarsService.createAndLinkCalendarEntry( ownerId, + defaultCalendar.id, + office365OAuthCredentials, OFFICE_365_CALENDAR_TYPE ); } diff --git a/apps/api/v2/src/modules/credentials/credentials.repository.ts b/apps/api/v2/src/modules/credentials/credentials.repository.ts index 5a496cc85f..3ae5dd5798 100644 --- a/apps/api/v2/src/modules/credentials/credentials.repository.ts +++ b/apps/api/v2/src/modules/credentials/credentials.repository.ts @@ -9,12 +9,12 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants"; export class CredentialsRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - async createAppCredential( + async upsertAppCredential( type: keyof typeof APPS_TYPE_ID_MAPPING, key: Prisma.InputJsonValue, - userId: number + userId: number, + credentialId?: number | null ) { - const credential = await this.getByTypeAndUserId(type, userId); return this.dbWrite.prisma.credential.upsert({ create: { type, @@ -27,7 +27,7 @@ export class CredentialsRepository { invalid: false, }, where: { - id: credential?.id ?? 0, + id: credentialId ?? 0, }, }); } @@ -36,6 +36,10 @@ export class CredentialsRepository { return this.dbWrite.prisma.credential.findFirst({ where: { type, userId } }); } + getAllUserCredentialsByTypeAndId(type: string, userId: number) { + return this.dbRead.prisma.credential.findMany({ where: { type, userId } }); + } + getUserCredentialsByIds(userId: number, credentialIds: number[]) { return this.dbRead.prisma.credential.findMany({ where: { @@ -60,6 +64,30 @@ export class CredentialsRepository { }, }); } + + async getUserCredentialById(userId: number, credentialId: number, type: string) { + return await this.dbRead.prisma.credential.findUnique({ + where: { + userId, + type, + id: credentialId, + }, + select: { + id: true, + type: true, + key: true, + userId: true, + teamId: true, + appId: true, + invalid: true, + user: { + select: { + email: true, + }, + }, + }, + }); + } } export type CredentialsWithUserEmail = Awaited< diff --git a/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts b/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts index 0c0c29085a..4610749075 100644 --- a/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts +++ b/apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts @@ -6,6 +6,7 @@ import { DestinationCalendarsController } from "@/modules/destination-calendars/ import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository"; import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @@ -19,6 +20,7 @@ import { Module } from "@nestjs/common"; UsersRepository, CredentialsRepository, AppsRepository, + SelectedCalendarsRepository, ], controllers: [DestinationCalendarsController], exports: [DestinationCalendarsRepository], diff --git a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts index bdf98d4fce..eb0b8d989e 100644 --- a/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts +++ b/apps/api/v2/src/modules/selected-calendars/selected-calendars.repository.ts @@ -6,7 +6,7 @@ import { Injectable } from "@nestjs/common"; export class SelectedCalendarsRepository { constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - createSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) { + upsertSelectedCalendar(externalId: string, credentialId: number, userId: number, integration: string) { return this.dbWrite.prisma.selectedCalendar.upsert({ create: { userId, @@ -38,6 +38,18 @@ export class SelectedCalendarsRepository { }); } + getUserSelectedCalendar(userId: number, integration: string, externalId: string) { + return this.dbRead.prisma.selectedCalendar.findUnique({ + where: { + userId_integration_externalId: { + userId, + externalId, + integration, + }, + }, + }); + } + async addUserSelectedCalendar( userId: number, integration: string, diff --git a/apps/api/v2/src/modules/users/users.repository.ts b/apps/api/v2/src/modules/users/users.repository.ts index b6ee18fa74..444b87a173 100644 --- a/apps/api/v2/src/modules/users/users.repository.ts +++ b/apps/api/v2/src/modules/users/users.repository.ts @@ -68,7 +68,6 @@ export class UsersRepository { } async findByIdWithProfile(userId: number): Promise { - console.log("findByIdWithProfile"); return this.dbRead.prisma.user.findUnique({ where: { id: userId, diff --git a/packages/platform/atoms/connect/apple/AppleConnect.tsx b/packages/platform/atoms/connect/apple/AppleConnect.tsx index 128cd97099..83e655f58a 100644 --- a/packages/platform/atoms/connect/apple/AppleConnect.tsx +++ b/packages/platform/atoms/connect/apple/AppleConnect.tsx @@ -76,7 +76,7 @@ export const AppleConnect: FC>> = ({