Skip to content

Commit 7ec8601

Browse files
chore: Integrate edit location booking audit (calcom#26569)
## What does this PR do? Integrates booking audit logging for the edit location functionality, following the pattern established in PR calcom#26046 (booking creation/rescheduling audit). This PR adds audit logging when a booking's location is changed through: 1. **Web app** (tRPC handler): `packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts` 2. **API v2**: `apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts` ### Changes: - Added `actionSource` as a **required** parameter to `editLocationHandler` (no fallback) - Added optional `userUuid` parameter (defaults to logged-in user's uuid) - Added `ValidActionSource` type that excludes "UNKNOWN" for client-facing APIs - Captures old location before update for audit data - Calls `BookingEventHandlerService.onLocationChanged()` after successful location update - Web app uses `actionSource: "WEBAPP"`, API v2 uses `actionSource: "API_V2"` - Updated router to explicitly pass `actionSource: "WEBAPP"` - Updated test to pass `actionSource: "WEBAPP"` - **API v2**: Uses NestJS dependency injection pattern with `BookingEventHandlerService` injected via constructor ### Updates since last revision: - **Created `BookingEventHandlerModule`** (`apps/api/v2/src/lib/modules/booking-event-handler.module.ts`) to encapsulate `BookingEventHandlerService` and its dependencies (Logger, TaskerService, HashedLinkService, BookingAuditProducerService) - Updated both bookings modules (2024-04-15 and 2024-08-13) to import `BookingEventHandlerModule` instead of listing individual providers - This reduces code duplication and makes dependency management cleaner ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - no documentation changes needed. - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. N/A - using existing audit infrastructure that is already tested. ## How should this be tested? 1. Update a booking's location through the web app 2. Update a booking's location through API v2 3. Verify audit logs are created with: - Correct `bookingUid` - Correct `actor` (user who made the change) - Correct `source` ("WEBAPP" or "API_V2") - Correct `auditData.location.old` and `auditData.location.new` values ## Checklist - [x] My code follows the style guidelines of this project - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have checked if my changes generate no new warnings ## Human Review Checklist - [ ] Verify all callers of `editLocationHandler` pass `actionSource` (router updated, test updated) - [ ] Verify `organizationId` derivation is correct in both handlers (tRPC uses `booking.user?.profiles?.[0]?.organizationId`, API v2 uses `existingBookingHost.organizationId`) - [ ] Confirm audit call placement after location update is intentional (audit failures would fail the operation even though location was already updated) - [ ] Note: API v2 has its own implementation and does NOT reuse `editLocationHandler` - this is correct - [ ] Verify `BookingEventHandlerModule` properly exports `BookingEventHandlerService` and is imported in both bookings modules - [ ] Verify the `updateBookingLocationInDb` return value (`{ updatedLocation }`) is destructured and used correctly for audit data - [ ] Verify API v2 uses `bookingLocation` for audit `new` value, while tRPC uses `updatedLocation` from DB update --- Link to Devin run: https://app.devin.ai/sessions/fd1d439779674050a26ea3fa7d799943 Requested by: @hariombalhara
1 parent 39ae54d commit 7ec8601

10 files changed

Lines changed: 326 additions & 82 deletions

File tree

apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module";
2727
import { TokensRepository } from "@/modules/tokens/tokens.repository";
2828
import { UsersModule } from "@/modules/users/users.module";
2929
import { Module } from "@nestjs/common";
30+
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
3031

3132
@Module({
3233
imports: [
@@ -43,6 +44,7 @@ import { Module } from "@nestjs/common";
4344
RegularBookingModule,
4445
RecurringBookingModule,
4546
InstantBookingModule,
47+
BookingEventHandlerModule,
4648
],
4749
providers: [
4850
TokensRepository,

apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module";
5050
import { TokensRepository } from "@/modules/tokens/tokens.repository";
5151
import { UsersModule } from "@/modules/users/users.module";
5252
import { Module } from "@nestjs/common";
53+
import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module";
5354

5455
@Module({
5556
imports: [
@@ -71,6 +72,7 @@ import { Module } from "@nestjs/common";
7172
RegularBookingModule,
7273
RecurringBookingModule,
7374
InstantBookingModule,
75+
BookingEventHandlerModule,
7476
],
7577
providers: [
7678
TokensRepository,

apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import { makeUserActor } from "@calcom/platform-libraries/bookings";
2+
import type {
3+
BookingInputLocation_2024_08_13,
4+
UpdateBookingInputLocation_2024_08_13,
5+
UpdateBookingLocationInput_2024_08_13,
6+
} from "@calcom/platform-types";
7+
import { Booking } from "@calcom/prisma/client";
8+
import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common";
19
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
210
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
311
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
4-
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
512
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
13+
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
14+
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
615
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
716
import { UsersRepository } from "@/modules/users/users.repository";
8-
import { Injectable, Logger, NotFoundException, ForbiddenException } from "@nestjs/common";
9-
10-
import type {
11-
UpdateBookingLocationInput_2024_08_13,
12-
BookingInputLocation_2024_08_13,
13-
UpdateBookingInputLocation_2024_08_13,
14-
} from "@calcom/platform-types";
15-
import { Booking } from "@calcom/prisma/client";
1617

1718
@Injectable()
1819
export class BookingLocationService_2024_08_13 {
@@ -24,7 +25,8 @@ export class BookingLocationService_2024_08_13 {
2425
private readonly usersRepository: UsersRepository,
2526
private readonly inputService: InputBookingsService_2024_08_13,
2627
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
27-
private readonly eventTypeAccessService: EventTypeAccessService
28+
private readonly eventTypeAccessService: EventTypeAccessService,
29+
private readonly bookingEventHandlerService: BookingEventHandlerService
2830
) {}
2931

3032
async updateBookingLocation(
@@ -66,6 +68,7 @@ export class BookingLocationService_2024_08_13 {
6668
user: ApiAuthGuardUser
6769
) {
6870
const bookingUid = existingBooking.uid;
71+
const oldLocation = existingBooking.location;
6972
const bookingLocation = this.getLocationValue(inputLocation) ?? existingBooking.location;
7073

7174
if (!existingBooking.userId) {
@@ -96,6 +99,19 @@ export class BookingLocationService_2024_08_13 {
9699
responses: updatedBookingResponses,
97100
});
98101

102+
await this.bookingEventHandlerService.onLocationChanged({
103+
bookingUid: existingBooking.uid,
104+
actor: makeUserActor(user.uuid),
105+
organizationId: existingBookingHost.organizationId ?? null,
106+
source: "API_V2",
107+
auditData: {
108+
location: {
109+
old: oldLocation,
110+
new: bookingLocation,
111+
},
112+
},
113+
});
114+
99115
return this.bookingsService.getBooking(updatedBooking.uid, user);
100116
}
101117

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Logger } from "@/lib/logger.bridge";
2+
import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service";
3+
import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service";
4+
import { HashedLinkService } from "@/lib/services/hashed-link.service";
5+
import { TaskerService } from "@/lib/services/tasker.service";
6+
import { Module, Scope } from "@nestjs/common";
7+
8+
@Module({
9+
providers: [
10+
{
11+
provide: Logger,
12+
useFactory: () => {
13+
return new Logger();
14+
},
15+
scope: Scope.TRANSIENT,
16+
},
17+
TaskerService,
18+
HashedLinkService,
19+
BookingAuditProducerService,
20+
BookingEventHandlerService,
21+
],
22+
exports: [BookingEventHandlerService],
23+
})
24+
export class BookingEventHandlerModule {}

packages/features/booking-audit/lib/actions/LocationChangedAuditActionService.ts

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { z } from "zod";
33
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
44
import { StringChangeSchema } from "../common/changeSchemas";
55
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
6-
import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams } from "./IAuditActionService";
6+
import type {
7+
IAuditActionService,
8+
TranslationWithParams,
9+
GetDisplayTitleParams,
10+
} from "./IAuditActionService";
711
import { getTranslation } from "@calcom/lib/server/i18n";
812
/**
913
* Location Changed Audit Action Service
@@ -12,67 +16,67 @@ import { getTranslation } from "@calcom/lib/server/i18n";
1216

1317
// Module-level because it is passed to IAuditActionService type outside the class scope
1418
const fieldsSchemaV1 = z.object({
15-
location: StringChangeSchema,
19+
location: StringChangeSchema,
1620
});
1721

1822
export class LocationChangedAuditActionService implements IAuditActionService {
19-
readonly VERSION = 1;
20-
public static readonly TYPE = "LOCATION_CHANGED" as const;
21-
private static dataSchemaV1 = z.object({
22-
version: z.literal(1),
23-
fields: fieldsSchemaV1,
24-
});
25-
private static fieldsSchemaV1 = fieldsSchemaV1;
26-
public static readonly latestFieldsSchema = fieldsSchemaV1;
27-
// Union of all versions
28-
public static readonly storedDataSchema = LocationChangedAuditActionService.dataSchemaV1;
29-
// Union of all versions
30-
public static readonly storedFieldsSchema = LocationChangedAuditActionService.fieldsSchemaV1;
31-
private helper: AuditActionServiceHelper<
32-
typeof LocationChangedAuditActionService.latestFieldsSchema,
33-
typeof LocationChangedAuditActionService.storedDataSchema
34-
>;
23+
readonly VERSION = 1;
24+
public static readonly TYPE = "LOCATION_CHANGED" as const;
25+
private static dataSchemaV1 = z.object({
26+
version: z.literal(1),
27+
fields: fieldsSchemaV1,
28+
});
29+
private static fieldsSchemaV1 = fieldsSchemaV1;
30+
public static readonly latestFieldsSchema = fieldsSchemaV1;
31+
// Union of all versions
32+
public static readonly storedDataSchema = LocationChangedAuditActionService.dataSchemaV1;
33+
// Union of all versions
34+
public static readonly storedFieldsSchema = LocationChangedAuditActionService.fieldsSchemaV1;
35+
private helper: AuditActionServiceHelper<
36+
typeof LocationChangedAuditActionService.latestFieldsSchema,
37+
typeof LocationChangedAuditActionService.storedDataSchema
38+
>;
3539

36-
constructor() {
37-
this.helper = new AuditActionServiceHelper({
38-
latestVersion: this.VERSION,
39-
latestFieldsSchema: LocationChangedAuditActionService.latestFieldsSchema,
40-
storedDataSchema: LocationChangedAuditActionService.storedDataSchema,
41-
});
42-
}
40+
constructor() {
41+
this.helper = new AuditActionServiceHelper({
42+
latestVersion: this.VERSION,
43+
latestFieldsSchema: LocationChangedAuditActionService.latestFieldsSchema,
44+
storedDataSchema: LocationChangedAuditActionService.storedDataSchema,
45+
});
46+
}
4347

44-
getVersionedData(fields: unknown) {
45-
return this.helper.getVersionedData(fields);
46-
}
48+
getVersionedData(fields: unknown) {
49+
return this.helper.getVersionedData(fields);
50+
}
4751

48-
parseStored(data: unknown) {
49-
return this.helper.parseStored(data);
50-
}
52+
parseStored(data: unknown) {
53+
return this.helper.parseStored(data);
54+
}
5155

52-
getVersion(data: unknown): number {
53-
return this.helper.getVersion(data);
54-
}
56+
getVersion(data: unknown): number {
57+
return this.helper.getVersion(data);
58+
}
5559

56-
migrateToLatest(data: unknown) {
57-
// V1-only: validate and return as-is (no migration needed)
58-
const validated = fieldsSchemaV1.parse(data);
59-
return { isMigrated: false, latestData: validated };
60-
}
60+
migrateToLatest(data: unknown) {
61+
// V1-only: validate and return as-is (no migration needed)
62+
const validated = fieldsSchemaV1.parse(data);
63+
return { isMigrated: false, latestData: validated };
64+
}
6165

62-
async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise<TranslationWithParams> {
63-
const { fields } = this.parseStored(storedData);
64-
// TODO: Ideally we want to translate the location label to the user's locale
65-
// We currently don't accept requesting user's translate fn here, fix it later.
66-
const t = await getTranslation("en", "common");
66+
async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise<TranslationWithParams> {
67+
const { fields } = this.parseStored(storedData);
68+
// TODO: Ideally we want to translate the location label to the user's locale
69+
// We currently don't accept requesting user's translate fn here, fix it later.
70+
const t = await getTranslation("en", "common");
6771

68-
const fromLocation = getHumanReadableLocationValue(fields.location.old, t);
69-
const toLocation = getHumanReadableLocationValue(fields.location.new, t);
72+
const fromLocation = getHumanReadableLocationValue(fields.location.old, t);
73+
const toLocation = getHumanReadableLocationValue(fields.location.new, t);
7074

71-
return {
72-
key: "booking_audit_action.location_changed_from_to",
73-
params: { fromLocation, toLocation },
74-
};
75-
}
75+
return {
76+
key: "booking_audit_action.location_changed_from_to",
77+
params: { fromLocation, toLocation },
78+
};
79+
}
7680
}
7781

7882
export type LocationChangedAuditData = z.infer<typeof fieldsSchemaV1>;

0 commit comments

Comments
 (0)