From 15cb687307e30822fe49739726ee3d8086d9cc18 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:41:04 +0200 Subject: [PATCH] chore: ensure default calendars for delegated users (#20300) * chore: ensure default calendars for delegated users * fixup! chore: ensure default calendars for delegated users --- .../processors/calendars.processor.ts | 33 ++++++++++++ ...anizations-delegation-credential.module.ts | 19 ++++++- ...ations-delegation-credential.repository.ts | 12 +++++ ...nizations-delegation-credential.service.ts | 50 +++++++++++++++++-- 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 apps/api/v2/src/ee/calendars/processors/calendars.processor.ts diff --git a/apps/api/v2/src/ee/calendars/processors/calendars.processor.ts b/apps/api/v2/src/ee/calendars/processors/calendars.processor.ts new file mode 100644 index 0000000000..9422e6b3d8 --- /dev/null +++ b/apps/api/v2/src/ee/calendars/processors/calendars.processor.ts @@ -0,0 +1,33 @@ +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { Process, Processor } from "@nestjs/bull"; +import { Logger } from "@nestjs/common"; +import { Job } from "bull"; + +export const DEFAULT_CALENDARS_JOB = "default_calendars_job"; +export const CALENDARS_QUEUE = "calendars"; +export type DefaultCalendarsJobDataType = { + userId: number; +}; + +@Processor(CALENDARS_QUEUE) +export class CalendarsProcessor { + private readonly logger = new Logger(CalendarsProcessor.name); + + constructor(public readonly calendarsService: CalendarsService) {} + + @Process(DEFAULT_CALENDARS_JOB) + async handleEnsureDefaultCalendars(job: Job) { + const { userId } = job.data; + try { + // getCalendars calls getConnectedDestinationCalendarsAndEnsureDefaultsInDb from platform libraries + // which gets the calendars from third party providers and ensure default calendars are set in DB + await this.calendarsService.getCalendars(userId); + } catch (err) { + this.logger.error(`Failed to load default calendars of user with id: ${userId}`, { + userId, + err, + }); + } + return; + } +} diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts index a4fe8ebbf9..93bd58dcdd 100644 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts +++ b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.module.ts @@ -1,3 +1,5 @@ +import { CalendarsModule } from "@/ee/calendars/calendars.module"; +import { CALENDARS_QUEUE, CalendarsProcessor } from "@/ee/calendars/processors/calendars.processor"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OrganizationsDelegationCredentialController } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.controller"; import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; @@ -5,16 +7,31 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; +import { BullModule } from "@nestjs/bull"; import { Module } from "@nestjs/common"; import { OrganizationsDelegationCredentialService } from "./services/organizations-delegation-credential.service"; @Module({ - imports: [PrismaModule, StripeModule, RedisModule, MembershipsModule], + imports: [ + PrismaModule, + StripeModule, + RedisModule, + CalendarsModule, + MembershipsModule, + BullModule.registerQueue({ + name: CALENDARS_QUEUE, + limiter: { + max: 1, + duration: 1000, + }, + }), + ], providers: [ OrganizationsDelegationCredentialService, OrganizationsDelegationCredentialRepository, OrganizationsRepository, + CalendarsProcessor, ], controllers: [OrganizationsDelegationCredentialController], }) diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts index a5b3b447a3..a9cb1a297d 100644 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts +++ b/apps/api/v2/src/modules/organizations/delegation-credentials/organizations-delegation-credential.repository.ts @@ -28,4 +28,16 @@ export class OrganizationsDelegationCredentialRepository { include: { workspacePlatform: true }, }); } + + async findDelegatedUserProfiles(orgId: number, domain: string) { + return this.dbRead.prisma.profile.findMany({ + select: { + userId: true, + }, + where: { + organizationId: orgId, + user: { email: { endsWith: domain } }, + }, + }); + } } diff --git a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts index b42d63e0f9..eadaaad4af 100644 --- a/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts +++ b/apps/api/v2/src/modules/organizations/delegation-credentials/services/organizations-delegation-credential.service.ts @@ -1,3 +1,8 @@ +import { + CALENDARS_QUEUE, + DEFAULT_CALENDARS_JOB, + DefaultCalendarsJobDataType, +} from "@/ee/calendars/processors/calendars.processor"; import { CreateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input"; import { GoogleServiceAccountKeyInput, @@ -5,9 +10,10 @@ import { } from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input"; import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input"; import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository"; -import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { InjectQueue } from "@nestjs/bull"; +import { Injectable, NotFoundException, Logger } from "@nestjs/common"; import { User } from "@prisma/client"; +import { Queue } from "bull"; import { encryptServiceAccountKey } from "@calcom/platform-libraries"; import { @@ -17,8 +23,11 @@ import { @Injectable() export class OrganizationsDelegationCredentialService { + private logger = new Logger("OrganizationsDelegationCredentialService"); + constructor( - private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository + private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository, + @InjectQueue(CALENDARS_QUEUE) private readonly calendarsQueue: Queue ) {} async createDelegationCredential( orgId: number, @@ -59,9 +68,44 @@ export class OrganizationsDelegationCredentialService { await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey); } + // once delegation credentials are enabled, slowly set all the destination calendars of delegated users + if (body.enabled === true && delegationCredential.enabled === false) { + await this.ensureDefaultCalendars(orgId, delegationCredential.domain); + } + return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled }; } + async ensureDefaultCalendars(orgId: number, domain: string) { + try { + const delegatedUserProfiles = + await this.organizationsDelegationCredentialRepository.findDelegatedUserProfiles(orgId, domain); + + delegatedUserProfiles.forEach(async (profile) => { + if (profile.userId) { + const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`); + if (job) { + await job.remove(); + this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`); + } + this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`); + await this.calendarsQueue.add( + DEFAULT_CALENDARS_JOB, + { + userId: profile.userId, + } satisfies DefaultCalendarsJobDataType, + { jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true } + ); + } + }); + } catch (err) { + this.logger.error( + err, + `Could not ensure default calendars for delegated users in org with id:${orgId}` + ); + } + } + async updateDelegationCredentialEnabled( orgId: number, delegationCredentialId: string,