## What does this PR do? This PR introduces the booking audit infrastructure with a viewer service to fetch, enrich, and format audit logs. This is the first iteration that includes only the CREATED action with versioned schema Note: Additional audit actions, UI improvements to match with Figma will be taken care of in followup PRs, to ensure the PR size remains as small as possible(as it is large enough already) Demo - Loom - https://www.loom.com/share/22c2f38b052b480b8be3716e1608eaf6 ### Key Changes **New Booking Audit Package** (`packages/features/booking-audit/`): - `BookingAuditViewerService` - Fetches, enriches, and formats audit logs for display - `BookingAuditTaskConsumer` - Processes audit tasks asynchronously via Tasker - `CreatedAuditActionService` (v1) - Handles RECORD_CREATED action with versioned schema **Repository Layer**: - `BookingAuditRepository` - CRUD operations for audit records - `AuditActorRepository` - Actor management (users, guests, system) - `UserRepository.findByUuid()` - User lookup for actor enrichment **UI Components**: - New page at `/booking/logs/[bookinguid]` for viewing audit history - Filterable audit log list with search, type, and actor filters - Expandable log entries showing detailed change information **Infrastructure**: - `bookingAudit` Tasker task type for async processing - `booking-audit` feature flag (disabled by default) - tRPC endpoint `viewer.bookings.getAuditLogs` ### Updates since last revision - **Refactored `Task` class to `TaskRepository`**: Converted all static methods to instance methods following the repository pattern. A singleton `Task` is exported for backward compatibility, so existing call sites continue to work unchanged. ### Important Notes for Reviewers - **Permission check is TODO**: `checkPermissions()` in `BookingAuditViewerService` is not yet implemented - **Only CREATED action supported**: Other actions will throw an error - additional actions will be added iteratively in follow-up PRs - **Feature flag controlled**: System is behind `booking-audit` flag, disabled by default - **UI strings need i18n**: Some UI text in `booking-logs-view.tsx` may need translation ### Human Review Checklist - [ ] Verify permission enforcement strategy for audit log access - [ ] Review DI wiring in module files - [ ] Confirm error handling in `BookingAuditTaskConsumer` is appropriate for retry scenarios - [ ] Check if UI strings need to be added to translation files - [ ] Verify `TaskRepository` refactoring maintains backward compatibility (singleton export `Task` should work for all existing call sites) ## Mandatory Tasks (DO NOT REMOVE) - [ ] 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 - internal infrastructure - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? Run the integration tests: ```bash TZ=UTC yarn test packages/features/booking-audit/lib/service/BookingAuditViewerService.integration-test.ts TZ=UTC yarn test packages/features/booking-audit/lib/service/BookingAuditTaskConsumer.integration-test.ts ``` To test the UI: 1. Enable the `booking-audit` feature flag in the database 2. Create a booking (only CREATED action is supported in this PR) 3. Navigate to `/booking/logs/{bookingUid}` to view the audit trail ## Checklist - [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - [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 <!-- Link to Devin run: https://app.devin.ai/sessions/a8ae61c253b549429401c9651dcbcf44 --> <!-- Requested by: hariom@cal.com (@hariombalhara) -->
84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
import type { BookingAuditProducerService } from "@calcom/features/booking-audit/lib/service/BookingAuditProducerService.interface";
|
|
import type { HashedLinkService } from "@calcom/features/hashedLink/lib/service/HashedLinkService";
|
|
import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import type { Actor } from "../types/actor";
|
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
|
|
|
import type { BookingCreatedPayload, BookingRescheduledPayload } from "./types";
|
|
|
|
interface BookingEventHandlerDeps {
|
|
log: ISimpleLogger;
|
|
hashedLinkService: HashedLinkService;
|
|
bookingAuditProducerService: BookingAuditProducerService;
|
|
}
|
|
|
|
export class BookingEventHandlerService {
|
|
private readonly log: BookingEventHandlerDeps["log"];
|
|
constructor(private readonly deps: BookingEventHandlerDeps) {
|
|
this.log = deps.log;
|
|
}
|
|
|
|
async onBookingCreated(payload: BookingCreatedPayload, actor: Actor) {
|
|
this.log.debug("onBookingCreated", safeStringify(payload));
|
|
if (payload.config.isDryRun) {
|
|
return;
|
|
}
|
|
await this.onBookingCreatedOrRescheduled(payload);
|
|
if (IS_PRODUCTION) {
|
|
// Skip queueing audit for production environments till we are absolutely sure that the payload schema is correct for CREATED action
|
|
// We might get more clarity as we implement more actions and test them
|
|
return;
|
|
}
|
|
try {
|
|
await this.deps.bookingAuditProducerService.queueAudit(payload.booking.uid, actor, payload.organizationId, {
|
|
action: "CREATED",
|
|
data: {
|
|
startTime: payload.booking.startTime.getTime(),
|
|
endTime: payload.booking.endTime.getTime(),
|
|
status: payload.booking.status,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
this.log.error("Error while queueing booking audit", safeStringify(error));
|
|
}
|
|
}
|
|
|
|
async onBookingRescheduled(payload: BookingRescheduledPayload) {
|
|
this.log.debug("onBookingRescheduled", safeStringify(payload));
|
|
if (payload.config.isDryRun) {
|
|
return;
|
|
}
|
|
await this.onBookingCreatedOrRescheduled(payload);
|
|
}
|
|
|
|
/**
|
|
* Handles common tasks that need to be executed in both booking created and rescheduled events
|
|
* A dedicated place because there are many tasks that need to be executed in both events.
|
|
*/
|
|
private async onBookingCreatedOrRescheduled(payload: BookingCreatedPayload | BookingRescheduledPayload) {
|
|
const results = await Promise.allSettled([
|
|
// TODO: Migrate other post-booking tasks here, to execute them in parallel, without affecting each other
|
|
this.updatePrivateLinkUsage(payload.bookingFormData.hashedLink),
|
|
]);
|
|
results.forEach((result) => {
|
|
if (result.status === "rejected") {
|
|
this.log.error(
|
|
"Error while executing onBookingCreatedOrRescheduled task",
|
|
safeStringify(result.reason)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async updatePrivateLinkUsage(hashedLink: string | null) {
|
|
try {
|
|
if (hashedLink) {
|
|
await this.deps.hashedLinkService.validateAndIncrementUsage(hashedLink);
|
|
}
|
|
} catch (error) {
|
|
this.log.error("Error while updating hashed link", safeStringify(error));
|
|
}
|
|
}
|
|
}
|