Files
calendar/packages/features/credentials/repositories/CredentialRepository.ts
T
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f1011ddd08 chore: Improves the core booking audit architecture by adding new capabilities and simplifying the existing interfaces. (#25872)
* feat(booking-audit): extract core audit system changes from PR 25125

This PR extracts core audit infrastructure changes without integration changes:

1. New action services introduced:
   - SeatBookedAuditActionService
   - SeatRescheduledAuditActionService

2. Simplification of ActionService interface:
   - Streamlined IAuditActionService interface
   - Reduced TypeScript burden with cleaner type definitions

3. ActionSource support:
   - Added BookingAuditSource enum (API_V1, API_V2, WEBAPP, WEBHOOK, UNKNOWN)
   - Added source and operationId fields to BookingAudit model

4. New AuditAction types:
   - SEAT_BOOKED
   - SEAT_RESCHEDULED
   - APP actor type

5. New BookingAuditAccessService:
   - Permission-based access control for audit logs
   - Added readTeamAuditLogs and readOrgAuditLogs permissions

6. Fixes in the logs viewer flow:
   - Enhanced BookingAuditViewerService with improved filtering
   - Local AttendeeRepository for actor enrichment

Changes are contained within packages/features/booking-audit with minimal
outside changes (permission registry only).

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor(booking-audit): streamline action handling and enhance localization

- Replaced action icon retrieval with a mapping object for improved clarity and performance.
- Introduced constants for actor role labels to simplify role retrieval.
- Added new localization strings for audit log permission errors and organization requirements.
- Updated various service and repository interfaces to enhance type safety and clarity.
- Removed deprecated architecture documentation and adjusted related imports for consistency.

These changes aim to improve code maintainability and user experience in the booking audit system.

* fix(booking-audit): enhance actor role localization and operation ID tracking

- Updated actor role labels in the booking logs view to use lowercase for consistency.
- Improved localization by wrapping actor role display in a translation function.
- Added operationId field to audit logs for better correlation of actions across multiple bookings.
- Enhanced BookingAuditViewerService to include operationId in enriched audit logs.
- Updated integration tests to verify consistent operationId across related audit logs.

These changes aim to improve localization accuracy and facilitate better tracking of user actions in the booking audit system.

* feat: integrate credential repository and enhance app actor handling

- Added CredentialRepository to manage app credentials, including a method to find credentials by ID.
- Updated BookingAudit system to support app actors identified by credential ID, improving actor attribution and audit clarity.
- Introduced a new utility function to map app slugs to display names, enhancing the user experience in audit logs.
- Modified relevant interfaces and types to accommodate the new credential handling and app actor structure.
- Enhanced BookingAuditViewerService to display app names based on credentials, ensuring accurate representation in audit logs.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-18 12:46:24 +00:00

304 lines
7.5 KiB
TypeScript

import { buildNonDelegationCredential } from "@calcom/lib/delegationCredential";
import logger from "@calcom/lib/logger";
import { prisma } from "@calcom/prisma";
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
import { safeCredentialSelect } from "@calcom/prisma/selects/credential";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
const log = logger.getSubLogger({ prefix: ["CredentialRepository"] });
type CredentialCreateInput = {
type: string;
key: object;
userId: number;
appId: string;
delegationCredentialId?: string | null;
};
type CredentialUpdateInput = {
type?: string;
key?: object;
userId?: number;
appId?: string;
delegationCredentialId?: string | null;
invalid?: boolean;
};
export class CredentialRepository {
constructor(private primaClient: PrismaClient) { }
async findByCredentialId(id: number) {
return this.primaClient.credential.findUnique({
where: { id },
select: safeCredentialSelect,
});
}
async findByIdWithDelegationCredential(id: number) {
return this.primaClient.credential.findUnique({
where: { id },
select: { ...credentialForCalendarServiceSelect, delegationCredential: true },
});
}
static async create(data: CredentialCreateInput) {
const credential = await prisma.credential.create({ data: { ...data } });
return buildNonDelegationCredential(credential);
}
static async findByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) {
const credential = await prisma.credential.findFirst({
where: {
appId,
userId,
},
});
return buildNonDelegationCredential(credential);
}
/**
* Doesn't retrieve key field as that has credentials
*/
static async findFirstByIdWithUser({ id }: { id: number }) {
const credential = await prisma.credential.findUnique({ where: { id }, select: safeCredentialSelect });
return buildNonDelegationCredential(credential);
}
/**
* Includes 'key' field which is sensitive data.
*/
static async findFirstByIdWithKeyAndUser({ id }: { id: number }) {
const credential = await prisma.credential.findUnique({
where: { id },
select: { ...safeCredentialSelect, key: true },
});
return buildNonDelegationCredential(credential);
}
static async findFirstByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) {
return await prisma.credential.findFirst({
where: {
appId,
userId,
},
});
}
static async findFirstByUserIdAndType({ userId, type }: { userId: number; type: string }) {
const credential = await prisma.credential.findFirst({ where: { userId, type } });
return buildNonDelegationCredential(credential);
}
static async deleteById({ id }: { id: number }) {
await prisma.credential.delete({ where: { id } });
}
static async updateCredentialById({ id, data }: { id: number; data: CredentialUpdateInput }) {
await prisma.credential.update({
where: { id },
data,
});
}
static async deleteAllByDelegationCredentialId({
delegationCredentialId,
}: {
delegationCredentialId: string;
}) {
return prisma.credential.deleteMany({ where: { delegationCredentialId } });
}
static async findCredentialForCalendarServiceById({ id }: { id: number }) {
const dbCredential = await prisma.credential.findUnique({
where: { id },
select: credentialForCalendarServiceSelect,
});
if (!dbCredential) {
return dbCredential;
}
return buildNonDelegationCredential(dbCredential);
}
static async findByIdIncludeDelegationCredential({ id }: { id: number }) {
const dbCredential = await prisma.credential.findUnique({
where: { id },
select: { ...credentialForCalendarServiceSelect, delegationCredential: true },
});
return dbCredential;
}
static async findAllDelegationByUserIdsListAndDelegationCredentialIdAndType({
userIds,
delegationCredentialId,
type,
}: {
userIds: number[];
delegationCredentialId: string;
type: string;
}) {
return prisma.credential.findMany({
where: {
userId: {
in: userIds,
},
delegationCredentialId,
type,
},
select: {
userId: true,
},
});
}
static async findAllDelegationByTypeIncludeUserAndTake({ type, take }: { type: string; take: number }) {
const delegationUserCredentials = await prisma.credential.findMany({
where: {
delegationCredentialId: { not: null },
type,
},
include: {
user: {
select: {
email: true,
id: true,
},
},
},
take,
});
return delegationUserCredentials.map(({ delegationCredentialId, ...rest }) => {
return {
...rest,
// We queried only those where delegationCredentialId is not null
delegationCredentialId: delegationCredentialId!,
};
});
}
static async findUniqueByUserIdAndDelegationCredentialId({
userId,
delegationCredentialId,
}: {
userId: number;
delegationCredentialId: string;
}) {
const delegationUserCredentials = await prisma.credential.findMany({
where: {
userId,
delegationCredentialId,
},
});
if (delegationUserCredentials.length > 1) {
// Instead of crashing use the first one and log for observability
// TODO: Plan to add a unique constraint on userId and delegationCredentialId
log.error(`DelegationCredential: Multiple delegation user credentials found - this should not happen`, {
userId,
delegationCredentialId,
});
}
return delegationUserCredentials[0];
}
static async updateWhereUserIdAndDelegationCredentialId({
userId,
delegationCredentialId,
data,
}: {
userId: number;
delegationCredentialId: string;
data: {
key: Prisma.InputJsonValue;
};
}) {
return prisma.credential.updateMany({
where: {
userId,
delegationCredentialId,
},
data,
});
}
static async createDelegationCredential({
userId,
delegationCredentialId,
type,
key,
appId,
}: {
userId: number;
delegationCredentialId: string;
type: string;
key: Prisma.InputJsonValue;
appId: string;
}) {
return prisma.credential.create({ data: { userId, delegationCredentialId, type, key, appId } });
}
static async updateWhereId({ id, data }: { id: number; data: { key: Prisma.InputJsonValue } }) {
return prisma.credential.update({ where: { id }, data });
}
static async findPaymentCredentialByAppIdAndTeamId({
appId,
teamId,
}: {
appId: string | null;
teamId: number;
}) {
return await prisma.credential.findFirst({
where: {
teamId,
appId,
},
include: {
app: true,
},
});
}
static async findPaymentCredentialByAppIdAndUserId({
appId,
userId,
}: {
appId: string | null;
userId: number;
}) {
return await prisma.credential.findFirst({
where: {
userId,
appId,
},
include: {
app: true,
},
});
}
static async findPaymentCredentialByAppIdAndUserIdOrTeamId({
appId,
userId,
teamId,
}: {
appId: string | null;
userId: number;
teamId?: number | null;
}) {
const idToSearchObject = teamId ? { teamId } : { userId };
return await prisma.credential.findFirst({
where: {
...idToSearchObject,
appId,
},
include: {
app: true,
},
});
}
}