feat: add uuid plumbing from API v2 to packages/features (#25721)
## What does this PR do? This PR adds user UUID plumbing from API v2 controllers through to packages/features functions. This is preparatory work extracted from PR #25125 to support future audit logging functionality. **Key changes:** - API v2 controllers (2024-04-15, 2024-08-13) now extract and pass `userUuid` to downstream functions - `handleMarkNoShow` and `CancelBookingInput` types now accept optional `userUuid` parameter - `UserRepository.findUnlockedUserForSession` now selects `uuid` field - Session middleware now includes `uuid` in the returned user object - Fixed lint warning: changed `PromiseSettledResult<any>` to `PromiseSettledResult<unknown>` **Refactoring (optimization):** - Renamed `getOwnerId` → `getOwner` and `getOwnerIdRescheduledBooking` → `getOwnerRescheduledBooking` - These methods now return `{ id: number; uuid: string } | null` instead of just `number | undefined` - This eliminates redundant database calls by fetching user id and uuid in a single query **Important:** This is plumbing-only - packages/features receives the `userUuid` but does not use it directly (note the `_userUuid` prefix). The actual audit logging usage will come in a follow-up PR. Requested by: @hariombalhara (hariom@cal.com) Link to Devin run: https://app.devin.ai/sessions/545209189f6347cd807bf1b336f9ac40 ## 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 - plumbing only, existing tests cover the functionality. ## How should this be tested? 1. Run type checks: `yarn type-check:ci --force` 2. Verify the changes compile without new type errors related to uuid 3. The `userUuid` parameters are optional, so existing functionality should work unchanged ## Checklist - [x] My code follows the style guidelines of this project - [x] I have checked if my changes generate no new warnings ## Human Review Checklist - [ ] Verify the `userUuid` parameter is intentionally unused (prefixed with `_userUuid`) - this is plumbing for future audit logging - [ ] Verify all callers of `getOwner` and `getOwnerRescheduledBooking` correctly handle the new `{ id, uuid } | null` return type - [ ] Confirm the change from `undefined` to `null` as the "not found" return value is handled consistently - [ ] Verify the `uuid` field exists in the User model schema
This commit is contained in:
1 parent
f350542d0d
commit
e20cc960f1
8 files changed
+136
-97
No files matched your search
@@ -125,7 +125,7 @@ export class BookingsController_2024_04_15 {
|
||||
private readonly instantBookingCreateService: InstantBookingCreateService,
|
||||
private readonly eventTypeRepository: PrismaEventTypeRepository,
|
||||
private readonly teamRepository: PrismaTeamRepository
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -270,6 +270,7 @@ export class BookingsController_2024_04_15 {
|
||||
const res = await handleCancelBooking({
|
||||
bookingData: bookingRequest.body,
|
||||
userId: bookingRequest.userId,
|
||||
userUuid: bookingRequest.userUuid,
|
||||
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
|
||||
platformClientId: bookingRequest.platformClientId,
|
||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||
@@ -300,7 +301,7 @@ export class BookingsController_2024_04_15 {
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async markNoShow(
|
||||
@GetUser("id") userId: number,
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() body: MarkNoShowInput_2024_04_15,
|
||||
@Param("bookingUid") bookingUid: string
|
||||
): Promise<MarkNoShowOutput_2024_04_15> {
|
||||
@@ -309,7 +310,8 @@ export class BookingsController_2024_04_15 {
|
||||
bookingUid: bookingUid,
|
||||
attendees: body.attendees,
|
||||
noShowHost: body.noShowHost,
|
||||
userId,
|
||||
userId: user.id,
|
||||
userUuid: user.uuid,
|
||||
});
|
||||
|
||||
return { status: SUCCESS_STATUS, data: markNoShowResponse };
|
||||
@@ -378,7 +380,7 @@ export class BookingsController_2024_04_15 {
|
||||
): Promise<ApiResponse<InstantBookingCreateResult>> {
|
||||
const oAuthClientId =
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
req.userId = (await this.getOwner(req))?.id ?? -1;
|
||||
try {
|
||||
const bookingReq = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||
const instantMeeting = await this.instantBookingCreateService.createBooking({
|
||||
@@ -405,30 +407,45 @@ export class BookingsController_2024_04_15 {
|
||||
throw new InternalServerErrorException("Could not create instant booking.");
|
||||
}
|
||||
|
||||
private async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
private async getOwner(req: Request): Promise<{ id: number; uuid: string } | null> {
|
||||
try {
|
||||
const bearerToken = req.get("Authorization")?.replace("Bearer ", "");
|
||||
if (bearerToken) {
|
||||
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = sha256Hash(strippedApiKey);
|
||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||
return keyData?.userId;
|
||||
} else {
|
||||
// Access Token
|
||||
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
return ownerId;
|
||||
}
|
||||
if (!bearerToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ownerId: number | null = null;
|
||||
|
||||
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = sha256Hash(strippedApiKey);
|
||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||
ownerId = keyData?.userId ?? null;
|
||||
} else {
|
||||
// Access Token
|
||||
ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
}
|
||||
|
||||
if (!ownerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findById(ownerId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { id: user.id, uuid: user.uuid };
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getOwnerIdRescheduledBooking(
|
||||
private async getOwnerRescheduledBooking(
|
||||
request: Request,
|
||||
platformClientId?: string
|
||||
): Promise<number | undefined> {
|
||||
): Promise<{ id: number; uuid: string } | null> {
|
||||
if (
|
||||
platformClientId &&
|
||||
request.body.rescheduledBy &&
|
||||
@@ -440,13 +457,15 @@ export class BookingsController_2024_04_15 {
|
||||
);
|
||||
}
|
||||
|
||||
if (request.body.rescheduledBy) {
|
||||
if (request.body.rescheduledBy !== request.body.responses.email) {
|
||||
return (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
|
||||
if (request.body.rescheduledBy && request.body.rescheduledBy !== request.body.responses.email) {
|
||||
const user = await this.usersRepository.findByEmail(request.body.rescheduledBy);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
return { id: user.id, uuid: user.uuid };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
|
||||
@@ -498,7 +517,8 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
}
|
||||
|
||||
const userId = await this.getOwnerId(req);
|
||||
const owner = await this.getOwner(req);
|
||||
const userId = owner?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException(
|
||||
@@ -561,12 +581,16 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
): Promise<NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams> {
|
||||
const requestId = req.get("X-Request-Id");
|
||||
const clone = { ...req };
|
||||
const userId = clone.body.rescheduleUid
|
||||
? await this.getOwnerIdRescheduledBooking(req, oAuthClientId)
|
||||
: await this.getOwnerId(req);
|
||||
const owner = clone.body.rescheduleUid
|
||||
? await this.getOwnerRescheduledBooking(req, oAuthClientId)
|
||||
: await this.getOwner(req);
|
||||
|
||||
const userId = owner?.id;
|
||||
const userUuid = owner?.uuid;
|
||||
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
@@ -577,7 +601,7 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId,
|
||||
...oAuthParams,
|
||||
});
|
||||
Object.assign(clone, { userId, ...oAuthParams, platformBookingLocation });
|
||||
Object.assign(clone, { userId, userUuid, ...oAuthParams, platformBookingLocation });
|
||||
clone.body = {
|
||||
...clone.body,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
@@ -586,7 +610,7 @@ export class BookingsController_2024_04_15 {
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
return clone as unknown as NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
async setPlatformAttendeesEmails(
|
||||
@@ -612,9 +636,12 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
): Promise<NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const owner = await this.getOwner(req);
|
||||
const userId = owner?.id ?? -1;
|
||||
const userUuid = owner?.uuid;
|
||||
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
@@ -628,6 +655,7 @@ export class BookingsController_2024_04_15 {
|
||||
});
|
||||
Object.assign(clone, {
|
||||
userId,
|
||||
userUuid,
|
||||
...oAuthParams,
|
||||
platformBookingLocation,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
@@ -636,7 +664,7 @@ export class BookingsController_2024_04_15 {
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
return clone as unknown as NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
private handleBookingErrors(
|
||||
|
||||
@@ -397,18 +397,18 @@ export class BookingsController_2024_08_13 {
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async markNoShow(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: MarkAbsentBookingInput_2024_08_13,
|
||||
@GetUser("id") ownerId: number
|
||||
): Promise<MarkAbsentBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.markAbsent(bookingUid, ownerId, body);
|
||||
async markNoShow(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: MarkAbsentBookingInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<MarkAbsentBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.markAbsent(bookingUid, user.id, body, user.uuid);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -919,34 +919,40 @@ export class BookingsService_2024_08_13 {
|
||||
return await this.getBooking(recurringBookingUid, authUser);
|
||||
}
|
||||
|
||||
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
|
||||
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
|
||||
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
|
||||
async markAbsent(
|
||||
bookingUid: string,
|
||||
bookingOwnerId: number,
|
||||
body: MarkAbsentBookingInput_2024_08_13,
|
||||
userUuid?: string
|
||||
) {
|
||||
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
|
||||
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
|
||||
|
||||
if (!bookingBefore) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
|
||||
}
|
||||
if (!bookingBefore) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
|
||||
}
|
||||
|
||||
const nowUtc = DateTime.utc();
|
||||
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
|
||||
const nowUtc = DateTime.utc();
|
||||
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
|
||||
|
||||
if (nowUtc < bookingStartTimeUtc) {
|
||||
throw new BadRequestException(
|
||||
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
|
||||
);
|
||||
}
|
||||
if (nowUtc < bookingStartTimeUtc) {
|
||||
throw new BadRequestException(
|
||||
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
|
||||
);
|
||||
}
|
||||
|
||||
const platformClientParams = bookingBefore?.eventTypeId
|
||||
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
: undefined;
|
||||
const platformClientParams = bookingBefore?.eventTypeId
|
||||
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
await handleMarkNoShow({
|
||||
bookingUid,
|
||||
attendees: bodyTransformed.attendees,
|
||||
noShowHost: bodyTransformed.noShowHost,
|
||||
userId: bookingOwnerId,
|
||||
platformClientParams,
|
||||
});
|
||||
await handleMarkNoShow({
|
||||
bookingUid,
|
||||
attendees: bodyTransformed.attendees,
|
||||
noShowHost: bodyTransformed.noShowHost,
|
||||
userId: bookingOwnerId,
|
||||
userUuid,
|
||||
platformClientParams,
|
||||
});
|
||||
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ export type BookingToDelete = Awaited<ReturnType<typeof getBookingToDelete>>;
|
||||
|
||||
export type CancelBookingInput = {
|
||||
userId?: number;
|
||||
userUuid?: string;
|
||||
bookingData: z.infer<typeof bookingCancelInput>;
|
||||
} & PlatformParams;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const buildResultPayload = async (
|
||||
};
|
||||
};
|
||||
|
||||
const logFailedResults = (results: PromiseSettledResult<any>[]) => {
|
||||
const logFailedResults = (results: PromiseSettledResult<unknown>[]) => {
|
||||
const failed = results.filter((x) => x.status === "rejected") as PromiseRejectedResult[];
|
||||
if (failed.length < 1) return;
|
||||
const failedMessage = failed.map((r) => r.reason);
|
||||
@@ -89,10 +89,12 @@ const handleMarkNoShow = async ({
|
||||
attendees,
|
||||
noShowHost,
|
||||
userId,
|
||||
userUuid: _userUuid,
|
||||
locale,
|
||||
platformClientParams,
|
||||
}: TNoShowInputSchema & {
|
||||
userId?: number;
|
||||
userUuid?: string;
|
||||
locale?: string;
|
||||
platformClientParams?: PlatformClientParams;
|
||||
}) => {
|
||||
|
||||
@@ -1014,18 +1014,19 @@ export class UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async findUnlockedUserForSession({ userId }: { userId: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
// Locked users can't login
|
||||
locked: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
async findUnlockedUserForSession({ userId }: { userId: number }) {
|
||||
const user = await this.prismaClient.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
// Locked users can't login
|
||||
locked: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uuid: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
bio: true,
|
||||
avatarUrl: true,
|
||||
|
||||
@@ -45,10 +45,10 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||
safeStringify({ user, userFromDb, upId })
|
||||
);
|
||||
|
||||
const { email, username, id } = user;
|
||||
if (!email || !id) {
|
||||
return null; // should we return null here?
|
||||
}
|
||||
const { email, username, id, uuid } = user;
|
||||
if (!email || !id) {
|
||||
return null; // should we return null here?
|
||||
}
|
||||
|
||||
const userMetaData = userMetadata.parse(user.metadata || {});
|
||||
const orgMetadata = teamMetadataSchema.parse(user.profile?.organization?.metadata || {});
|
||||
@@ -71,19 +71,20 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
||||
requestedSlug: orgMetadata?.requestedSlug ?? null,
|
||||
};
|
||||
|
||||
return {
|
||||
...user,
|
||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png${organization.id ? `?orgId=${organization.id}` : ""}`,
|
||||
// TODO: OrgNewSchema - later - We could consolidate the props in user.profile?.organization as organization is a profile thing now.
|
||||
organization,
|
||||
organizationId: organization.id,
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
defaultBookerLayouts: userMetaData?.defaultBookerLayouts || null,
|
||||
requiresBookerEmailVerification: user.requiresBookerEmailVerification,
|
||||
};
|
||||
return {
|
||||
...user,
|
||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png${organization.id ? `?orgId=${organization.id}` : ""}`,
|
||||
// TODO: OrgNewSchema - later - We could consolidate the props in user.profile?.organization as organization is a profile thing now.
|
||||
organization,
|
||||
organizationId: organization.id,
|
||||
id,
|
||||
uuid,
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
defaultBookerLayouts: userMetaData?.defaultBookerLayouts || null,
|
||||
requiresBookerEmailVerification: user.requiresBookerEmailVerification,
|
||||
};
|
||||
}
|
||||
|
||||
export type UserFromSession = Awaited<ReturnType<typeof getUserFromSession>>;
|
||||
|
||||
@@ -40,7 +40,7 @@ import type { TConfirmInputSchema } from "./confirm.schema";
|
||||
|
||||
type ConfirmOptions = {
|
||||
ctx: {
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "username" | "role" | "destinationCalendar">;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "uuid" | "email" | "username" | "role" | "destinationCalendar">;
|
||||
};
|
||||
input: TConfirmInputSchema;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user