Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb020911d | ||
|
|
200b9d037c | ||
|
|
69667a7528 | ||
|
|
9f655ed819 | ||
|
|
5a1e1e231b | ||
|
|
6cccb1725f | ||
|
|
5bb2a904ce | ||
|
|
5e2f06cdea | ||
|
|
cf83038a7a | ||
|
|
c7662cad61 | ||
|
|
86912e0cbc | ||
|
|
74234b4ed1 | ||
|
|
2fcb8a750d | ||
|
|
2aaf672b10 | ||
|
|
34beadeef6 | ||
|
|
6de715b884 | ||
|
|
84717ca73e | ||
|
|
86b1db57d5 | ||
|
|
50c15f0d51 | ||
|
|
86d5c43307 | ||
|
|
e82bd45b55 | ||
|
|
13966e0ec8 | ||
|
|
14b9e0c032 | ||
|
|
f0f168e920 | ||
|
|
dd37caf32c | ||
|
|
856d223535 | ||
|
|
ad697365ce | ||
|
|
84171928cb | ||
|
|
e781a78eea | ||
|
|
7d08c6724b | ||
|
|
899da51f06 | ||
|
|
4e085e404a | ||
|
|
ff3aaadbaa | ||
|
|
11bddbb71d | ||
|
|
55a80ba253 | ||
|
|
32a89c30de | ||
|
|
5404eeedc2 | ||
|
|
d54ba90304 | ||
|
|
ca206915bf | ||
|
|
8461b82f0d | ||
|
|
07ffc3fade | ||
|
|
c2240f0bad | ||
|
|
36a712f979 | ||
|
|
842ba09896 | ||
|
|
b3c822abf7 | ||
|
|
1f52ed7290 | ||
|
|
a2c13e576e | ||
|
|
24bb52d2e9 | ||
|
|
d8145bbc78 | ||
|
|
8753a6e4d6 | ||
|
|
b91607b5b4 | ||
|
|
af0e839f27 | ||
|
|
b6de03de96 | ||
|
|
e115095198 | ||
|
|
c3add4bae8 | ||
|
|
3040e14624 | ||
|
|
34993e3f36 | ||
|
|
8d5c6b8877 | ||
|
|
c9eac73fec | ||
|
|
ab475b80f3 | ||
|
|
2b79039b70 | ||
|
|
044df04d3f | ||
|
|
2cbf805ffb | ||
|
|
d208074063 | ||
|
|
30541da1fa | ||
|
|
97af0af0ba | ||
|
|
e9b26921da | ||
|
|
e4434f199d | ||
|
|
f4b0219a0e | ||
|
|
6f1eaaa02e | ||
|
|
e5868837d5 | ||
|
|
71a5d8b261 | ||
|
|
dfc022d890 | ||
|
|
68cb0cf197 | ||
|
|
14592de6c4 | ||
|
|
d41dcc69ad | ||
|
|
0a28b5825b | ||
|
|
3131318b6a | ||
|
|
08e35e4698 | ||
|
|
ed4b3f082a | ||
|
|
a1007398f6 | ||
|
|
2aafa1c163 | ||
|
|
008178480a | ||
|
|
2334c3bef8 | ||
|
|
9542116ec1 | ||
|
|
f9c11c2856 | ||
|
|
30060f03eb | ||
|
|
b8ffcef45e | ||
|
|
999882b795 | ||
|
|
bda963b851 | ||
|
|
8511494d68 | ||
|
|
1c3ad9dad6 | ||
|
|
d43afe7dbd | ||
|
|
a06b2009ab | ||
|
|
2a786b4652 | ||
|
|
ef1844557f | ||
|
|
d1f7b043e8 | ||
|
|
3a8e40d029 | ||
|
|
bca46228e7 | ||
|
|
d15b24b8ea | ||
|
|
d9df9b3d01 | ||
|
|
fc1d6ae402 | ||
|
|
26b4b7a868 | ||
|
|
a839630ca3 | ||
|
|
e21dbb704e | ||
|
|
53a06797a9 | ||
|
|
99eb743772 | ||
|
|
73adfb0cec | ||
|
|
7127c60116 | ||
|
|
a6d37fd167 | ||
|
|
16ea15076d | ||
|
|
041871f1b6 | ||
|
|
8c8ae127fe | ||
|
|
357c82129c | ||
|
|
324ac691eb | ||
|
|
da99001a7a | ||
|
|
bd10d07083 | ||
|
|
0e9b419a07 | ||
|
|
d5bd08d279 | ||
|
|
4cc83530f9 | ||
|
|
4fcf4081d0 | ||
|
|
d84d80eea6 | ||
|
|
f3e1d67de1 | ||
|
|
2ce25f54d4 | ||
|
|
25eac5d29b | ||
|
|
1710e30719 | ||
|
|
6d970d0b57 | ||
|
|
41a6035a12 | ||
|
|
f84c8239e7 | ||
|
|
1dca160db0 | ||
|
|
4ff366cd99 | ||
|
|
21a4711f07 | ||
|
|
f1dd217de7 | ||
|
|
3e8f0b125d | ||
|
|
25246ef8ff | ||
|
|
bb98143e2e | ||
|
|
6258ca14d0 | ||
|
|
d0e17e8611 | ||
|
|
79dc76239b | ||
|
|
95d62354d9 | ||
|
|
b9fadf82a7 | ||
|
|
1740e34c79 | ||
|
|
9e09066e99 | ||
|
|
0a9cfd3a08 | ||
|
|
0be711ad6e | ||
|
|
1c507b8169 | ||
|
|
81bdb3dc91 | ||
|
|
5efc7f3856 | ||
|
|
0afecec607 | ||
|
|
920574de65 |
No files matched your search
@@ -188,6 +188,10 @@ TWILIO_TOKEN=
|
|||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
TWILIO_PHONE_NUMBER=
|
TWILIO_PHONE_NUMBER=
|
||||||
TWILIO_WHATSAPP_PHONE_NUMBER=
|
TWILIO_WHATSAPP_PHONE_NUMBER=
|
||||||
|
TWILIO_WHATSAPP_REMINDER_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_CANCELLED_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID=
|
||||||
|
TWILIO_WHATSAPP_COMPLETED_CONTENT_SID=
|
||||||
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
||||||
NEXT_PUBLIC_SENDER_ID=
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
TWILIO_VERIFY_SID=
|
TWILIO_VERIFY_SID=
|
||||||
@@ -441,6 +445,10 @@ NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM=0
|
|||||||
# Used to enable quick availability checks for x% of all visitors
|
# Used to enable quick availability checks for x% of all visitors
|
||||||
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
||||||
|
|
||||||
|
# Used to generate token for cal video recordings expiry token
|
||||||
|
# You can use: `openssl rand -base64 32` to generate one
|
||||||
|
CAL_VIDEO_RECORDING_TOKEN_SECRET=
|
||||||
|
|
||||||
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
|
# Deprecated. Use NEXT_PUBLIC_BODY_SCRIPTS instead. NEXT_PUBLIC_HEAD_SCRIPTS are now injected into the body only.
|
||||||
NEXT_PUBLIC_HEAD_SCRIPTS=
|
NEXT_PUBLIC_HEAD_SCRIPTS=
|
||||||
NEXT_PUBLIC_BODY_SCRIPTS=
|
NEXT_PUBLIC_BODY_SCRIPTS=
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
/packages/app-store/larkcalendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
/packages/app-store/office365calendar/**/* @calcom/Foundation
|
||||||
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
/packages/app-store/zohocalendar/**/* @calcom/Foundation
|
||||||
|
/packages/embeds/**/* @calcom/Foundation
|
||||||
/packages/features/bookings/lib/**/* @calcom/Foundation
|
/packages/features/bookings/lib/**/* @calcom/Foundation
|
||||||
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
|
||||||
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
/packages/lib/getUserAvailability.ts @calcom/Foundation
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
name: Submodule Sync
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Runs "At minute 15 past every 4th hour." (see https://crontab.guru)
|
|
||||||
- cron: "15 */4 * * *"
|
|
||||||
workflow_dispatch: ~
|
|
||||||
jobs:
|
|
||||||
submodule-sync:
|
|
||||||
name: Submodule update
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
- name: Commit
|
|
||||||
run: |
|
|
||||||
git config user.email "actions@github.com"
|
|
||||||
git config user.name "github-actions"
|
|
||||||
git commit -am "Auto updated submodule references" && git push || echo "No changes to commit"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
echo "Updating submodules recursively"
|
|
||||||
pwd
|
|
||||||
git submodule update --init --recursive
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/app-store-cli"
|
||||||
|
- "@calcom/platform-constants"
|
||||||
|
- "@calcom/platform-enums"
|
||||||
|
- "@calcom/platform-types"
|
||||||
|
- "@calcom/platform-utils"
|
||||||
|
- "@calcom/prisma"
|
||||||
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
Whitespace-only changes.
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
|
- "@calcom/prisma"
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
# Cal.com Permission Documentation
|
||||||
|
|
||||||
|
This document maps existing role-based permission checks to the new PBAC (Permission-Based Access Control) system's permission strings in the format `resource.action`.
|
||||||
|
|
||||||
|
## Permissions by Resource
|
||||||
|
|
||||||
|
### Team Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| team.create | Create teams | [packages/trpc/server/routers/viewer/teams/create.handler.ts](packages/trpc/server/routers/viewer/teams/create.handler.ts) | 55-57 |
|
||||||
|
| team.update | Update team settings | [packages/trpc/server/routers/viewer/teams/update.handler.ts](packages/trpc/server/routers/viewer/teams/update.handler.ts) | 18 |
|
||||||
|
| team.changeMemberRole | Change role of team members | [packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts](packages/trpc/server/routers/viewer/teams/changeMemberRole.handler.ts) | 18-21 |
|
||||||
|
| team.remove | Remove members from team | [packages/trpc/server/routers/viewer/teams/removeMember.handler.ts](packages/trpc/server/routers/viewer/teams/removeMember.handler.ts) | 35-36, 51-55 |
|
||||||
|
| team.invite | Invite members to team | [packages/trpc/server/routers/viewer/teams/invite.handler.ts](packages/trpc/server/routers/viewer/teams/invite.handler.ts) | - |
|
||||||
|
|
||||||
|
### Event Type Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| eventType.create | Create event types | [packages/trpc/server/routers/viewer/eventTypes/create.handler.ts](packages/trpc/server/routers/viewer/eventTypes/create.handler.ts) | - |
|
||||||
|
| eventType.update | Update event types | [packages/trpc/server/routers/viewer/eventTypes/update.handler.ts](packages/trpc/server/routers/viewer/eventTypes/update.handler.ts) | 162-164 |
|
||||||
|
| eventType.delete | Delete event types | [packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts](packages/trpc/server/routers/viewer/eventTypes/delete.handler.ts) | 13-31 |
|
||||||
|
|
||||||
|
### Booking Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| booking.read | Read booking details | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 95-107, 132-134 |
|
||||||
|
| booking.readTeamBookings | Read team bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
|
||||||
|
| booking.readOrgBookings | Read organization bookings | [packages/trpc/server/routers/viewer/bookings/get.handler.ts](packages/trpc/server/routers/viewer/bookings/get.handler.ts) | 240-252 |
|
||||||
|
|
||||||
|
### Organization Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| organization.read | Read organization details | [packages/trpc/server/routers/viewer/organizations/get.handler.ts](packages/trpc/server/routers/viewer/organizations/get.handler.ts) | - |
|
||||||
|
| organization.listMembers | List organization members | [packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts](packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts) | 68-76 |
|
||||||
|
| organization.create | Create organization | [packages/trpc/server/routers/viewer/organizations/create.handler.ts](packages/trpc/server/routers/viewer/organizations/create.handler.ts) | - |
|
||||||
|
|
||||||
|
### API Key Permissions
|
||||||
|
|
||||||
|
| Permission String | Description | File Path | Line |
|
||||||
|
|------------------|-------------|-----------|------|
|
||||||
|
| apiKey.create | Create API keys | [packages/trpc/server/routers/viewer/apiKeys/create.handler.ts](packages/trpc/server/routers/viewer/apiKeys/create.handler.ts) | 25-26 |
|
||||||
|
| apiKey.findKeyOfType | Find API keys by type | [packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts](packages/trpc/server/routers/viewer/apiKeys/findKeyOfType.handler.ts) | 18-19 |
|
||||||
|
|
||||||
|
## Helper Functions and Utilities
|
||||||
|
|
||||||
|
The following helper functions are commonly used for permission checks:
|
||||||
|
|
||||||
|
| Function | Description | File Path |
|
||||||
|
|----------|-------------|-----------|
|
||||||
|
| isTeamAdmin | Checks if user is admin or owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L385-L405) |
|
||||||
|
| isTeamOwner | Checks if user is owner of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L407-L416) |
|
||||||
|
| isTeamMember | Checks if user is a member of a team | [packages/lib/server/queries/teams/index.ts](packages/lib/server/queries/teams/index.ts#L418-L426) |
|
||||||
|
| canEditEntity | Checks if user can edit an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L13-L22) |
|
||||||
|
| canAccessEntity | Checks if user can access an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L24-L34) |
|
||||||
|
| getEntityPermissionLevel | Gets permission level for an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L36-L73) |
|
||||||
|
| canCreateEntity | Checks if user can create an entity | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L102-L115) |
|
||||||
|
| withRoleCanCreateEntity | Checks if role allows entity creation | [packages/lib/entityPermissionUtils.ts](packages/lib/entityPermissionUtils.ts#L119-L121) |
|
||||||
|
|
||||||
|
## Permission Level Enums
|
||||||
|
|
||||||
|
The codebase uses several enums to define permission levels:
|
||||||
|
|
||||||
|
### MembershipRole Enum
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum MembershipRole {
|
||||||
|
OWNER = "OWNER",
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
MEMBER = "MEMBER"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity Permission Level Enum
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ENTITY_PERMISSION_LEVEL {
|
||||||
|
NONE,
|
||||||
|
USER_ONLY_WRITE,
|
||||||
|
TEAM_READ_ONLY,
|
||||||
|
TEAM_WRITE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Permission Patterns
|
||||||
|
|
||||||
|
1. **Team Admin/Owner Check**: Many operations require the user to be a team admin or owner.
|
||||||
|
```typescript
|
||||||
|
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Team Owner Check**: Some operations (like changing an owner's role) require the user to be a team owner.
|
||||||
|
```typescript
|
||||||
|
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Organization Admin Check**: Operations within an organization require the user to be an organization admin.
|
||||||
|
```typescript
|
||||||
|
if (user.profile?.organizationId && !user.organization.isOrgAdmin) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Entity Permission Check**: Entity operations use permission level checks.
|
||||||
|
```typescript
|
||||||
|
const permissionLevel = await getEntityPermissionLevel(entity, userId);
|
||||||
|
return permissionLevel === ENTITY_PERMISSION_LEVEL.TEAM_WRITE ||
|
||||||
|
permissionLevel === ENTITY_PERMISSION_LEVEL.USER_ONLY_WRITE;
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Booking Access Control**: Complex permission checks for retrieving bookings based on user roles and team/organization membership.
|
||||||
|
```typescript
|
||||||
|
const membershipIdsWhereUserIsAdminOwner = (
|
||||||
|
await prisma.membership.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
role: {
|
||||||
|
in: ["ADMIN", "OWNER"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).map((membership) => membership.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration to PBAC
|
||||||
|
|
||||||
|
When migrating to the new PBAC system, these existing role-based checks should be replaced with permission string checks using the `permissionMatches` function:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { permissionMatches } from "@calcom/features/pbac/types/permission-registry";
|
||||||
|
|
||||||
|
// Instead of:
|
||||||
|
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
// Use:
|
||||||
|
if (!permissionMatches("team.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permission String Alternatives for Helper Functions
|
||||||
|
|
||||||
|
The following table provides permission string alternatives for common helper functions:
|
||||||
|
|
||||||
|
| Helper Function | Permission String Alternative | Description |
|
||||||
|
|-----------------|-------------------------------|-------------|
|
||||||
|
| isTeamAdmin | `team.*` | Grants all team permissions |
|
||||||
|
| isTeamAdmin | `team.update` | Update team settings |
|
||||||
|
| isTeamAdmin | `team.invite` | Invite team members |
|
||||||
|
| isTeamAdmin | `team.remove` | Remove team members |
|
||||||
|
| isTeamOwner | `team.changeMemberRole` | Change role of team members |
|
||||||
|
| isTeamOwner | `team.delete` | Delete team |
|
||||||
|
| isTeamMember | `team.read` | Read-only access to team |
|
||||||
|
|
||||||
|
## Entity Permission Functions and Resources
|
||||||
|
|
||||||
|
The following functions are used to check permissions for entities with userId and teamId properties:
|
||||||
|
|
||||||
|
| Function | Description | Permission String Alternative |
|
||||||
|
|----------|-------------|-------------------------------|
|
||||||
|
| canEditEntity | Checks if user can edit an entity | `{resource}.update` |
|
||||||
|
| canAccessEntity | Checks if user can access an entity | `{resource}.read` |
|
||||||
|
| getEntityPermissionLevel | Gets permission level for an entity | N/A - Implementation detail |
|
||||||
|
| canCreateEntity | Checks if user can create an entity | `{resource}.create` |
|
||||||
|
|
||||||
|
### Resources Used with Entity Permission Functions
|
||||||
|
|
||||||
|
| Resource | canEditEntity Usage | canAccessEntity Usage | Permission String |
|
||||||
|
|----------|-------------------|---------------------|------------------|
|
||||||
|
| routingForm | [packages/app-store/routing-forms/trpc/forms.handler.ts](packages/app-store/routing-forms/trpc/forms.handler.ts#L72) | [packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts](packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts#L78) | `routingForm.update`, `routingForm.read` |
|
||||||
|
| routingForm | [packages/app-store/routing-forms/trpc/formMutation.handler.ts](packages/app-store/routing-forms/trpc/formMutation.handler.ts#L315) | | `routingForm.update` |
|
||||||
|
| routingForm | [packages/app-store/routing-forms/api/responses/[formId].ts](packages/app-store/routing-forms/api/responses/[formId].ts#L96) | | `routingForm.update` |
|
||||||
|
|
||||||
|
These functions can be used with any entity that has userId and teamId properties. When migrating to PBAC, replace these checks with appropriate permission string checks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Instead of:
|
||||||
|
if (!(await canEditEntity(form, user.id))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
// Use:
|
||||||
|
if (!permissionMatches("routingForm.update", userPermissions)) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows for more granular permission control and the creation of custom roles with specific permissions.
|
||||||
+11
-6
@@ -1,4 +1,4 @@
|
|||||||
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
@@ -7,12 +7,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
|
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||||
|
import type { IDeploymentRepository } from "@calcom/lib/server/repository/deployment.interface";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { hashAPIKey } from "~/../../../packages/features/ee/api-keys/lib/apiKeys";
|
import { verifyApiKey } from "./verifyApiKey";
|
||||||
|
|
||||||
import { verifyApiKey } from "../../../lib/helpers/verifyApiKey";
|
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
type CustomNextApiResponse = NextApiResponse & Response;
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
@@ -21,11 +21,16 @@ afterEach(() => {
|
|||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Verify API key", () => {
|
const mockDeploymentRepository: IDeploymentRepository = {
|
||||||
|
getLicenseKeyWithId: vi.fn().mockResolvedValue("mockLicenseKey"), // Mocked return value
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Fix the skip condition for this test suite
|
||||||
|
describe.skip("Verify API key", () => {
|
||||||
let service: ILicenseKeyService;
|
let service: ILicenseKeyService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
service = await LicenseKeyService.create();
|
service = await LicenseKeyService.create(mockDeploymentRepository);
|
||||||
|
|
||||||
vi.spyOn(service, "checkLicense");
|
vi.spyOn(service, "checkLicense");
|
||||||
});
|
});
|
||||||
@@ -3,6 +3,7 @@ import type { NextMiddleware } from "next-api-middleware";
|
|||||||
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
|
||||||
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
import { hashAPIKey } from "@calcom/features/ee/api-keys/lib/apiKeys";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { DeploymentRepository } from "@calcom/lib/server/repository/deployment";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { isAdminGuard } from "../utils/isAdmin";
|
import { isAdminGuard } from "../utils/isAdmin";
|
||||||
@@ -19,11 +20,12 @@ export const dateNotInPast = function (date: Date) {
|
|||||||
|
|
||||||
// This verifies the apiKey and sets the user if it is valid.
|
// This verifies the apiKey and sets the user if it is valid.
|
||||||
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||||
const licenseKeyService = await LicenseKeySingleton.getInstance();
|
const deploymentRepo = new DeploymentRepository(prisma);
|
||||||
|
const licenseKeyService = await LicenseKeySingleton.getInstance(deploymentRepo);
|
||||||
const hasValidLicense = await licenseKeyService.checkLicense();
|
const hasValidLicense = await licenseKeyService.checkLicense();
|
||||||
|
|
||||||
if (!hasValidLicense && IS_PRODUCTION) {
|
if (!hasValidLicense && IS_PRODUCTION) {
|
||||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
return res.status(401).json({ message: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { MAX_SEATS_PER_TIME_SLOT } from "@calcom/lib/constants";
|
import {
|
||||||
|
MAX_SEATS_PER_TIME_SLOT,
|
||||||
|
MAX_EVENT_DURATION_MINUTES,
|
||||||
|
MIN_EVENT_DURATION_MINUTES,
|
||||||
|
} from "@calcom/lib/constants";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
|
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
|
||||||
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
@@ -80,7 +84,7 @@ const schemaEventTypeCreateParams = z
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
slug: z.string().transform((s) => slugify(s)),
|
slug: z.string().transform((s) => slugify(s)),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
length: z.number().int(),
|
length: z.number().int().min(MIN_EVENT_DURATION_MINUTES).max(MAX_EVENT_DURATION_MINUTES),
|
||||||
metadata: z.any().optional(),
|
metadata: z.any().optional(),
|
||||||
recurringEvent: recurringEventInputSchema.optional(),
|
recurringEvent: recurringEventInputSchema.optional(),
|
||||||
seatsPerTimeSlot: z.number().optional(),
|
seatsPerTimeSlot: z.number().optional(),
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyPa
|
|||||||
id: true,
|
id: true,
|
||||||
// No eventTypeId support in API v1
|
// No eventTypeId support in API v1
|
||||||
eventTypeId: true,
|
eventTypeId: true,
|
||||||
|
|
||||||
|
/** No watch related fields support in API v1 */
|
||||||
|
watchAttempts: true,
|
||||||
|
unwatchAttempts: true,
|
||||||
|
maxAttempts: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"next-axiom": "^0.17.0",
|
"next-axiom": "^0.17.0",
|
||||||
"next-swagger-doc": "^0.3.6",
|
"next-swagger-doc": "^0.3.6",
|
||||||
"next-validations": "^0.2.0",
|
"next-validations": "^0.2.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^5.8.3",
|
||||||
"tzdata": "^1.0.30",
|
"tzdata": "^1.0.30",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ describe("GET /api/bookings", async () => {
|
|||||||
|
|
||||||
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
|
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
|
||||||
expect(groupedUsers.size).toBe(1);
|
expect(groupedUsers.size).toBe(1);
|
||||||
expect(groupedUsers.entries().next().value[0]).toBe(memberUser.id);
|
const firstEntry = groupedUsers.entries().next().value;
|
||||||
|
expect(firstEntry?.[0]).toBe(memberUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Returns bookings for regular user", async () => {
|
it("Returns bookings for regular user", async () => {
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
googleChannelResourceUri: null,
|
googleChannelResourceUri: null,
|
||||||
googleChannelExpiration: null,
|
googleChannelExpiration: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
watchAttempts: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
unwatchAttempts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
@@ -128,6 +132,10 @@ describe("POST /api/selected-calendars", () => {
|
|||||||
domainWideDelegationCredentialId: null,
|
domainWideDelegationCredentialId: null,
|
||||||
eventTypeId: null,
|
eventTypeId: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
watchAttempts: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
unwatchAttempts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
|||||||
@@ -37,4 +37,8 @@ AXIOM_TOKEN=
|
|||||||
# INFO: 1
|
# INFO: 1
|
||||||
# WARN: 2
|
# WARN: 2
|
||||||
# ERROR: 3
|
# ERROR: 3
|
||||||
LOGGER_BRIDGE_LOG_LEVEL="1"
|
LOGGER_BRIDGE_LOG_LEVEL="1"
|
||||||
|
|
||||||
|
# 1: Rewrite /api/v2 to /v2
|
||||||
|
# 0: Don't rewrite
|
||||||
|
REWRITE_API_V2_PREFIX="1"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"@axiomhq/winston": "^1.2.0",
|
"@axiomhq/winston": "^1.2.0",
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
"@calcom/platform-enums": "*",
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.208",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.214",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/platform-utils": "*",
|
"@calcom/platform-utils": "*",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.0",
|
"tsconfig-paths": "^4.1.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "../../../packages/prisma/schema.prisma"
|
"schema": "../../../packages/prisma/schema.prisma"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const loadConfig = (): AppConfig => {
|
|||||||
app: {
|
app: {
|
||||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||||
},
|
},
|
||||||
e2e: getEnv("IS_E2E", false),
|
e2e: getEnv("IS_E2E", "false") === "true" ? true : false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,13 @@ export class BookingsController_2024_04_15 {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nextCursor = null;
|
||||||
|
if (bookings.totalCount > (cursor ?? 0) + (limit ?? 10)) {
|
||||||
|
nextCursor = (cursor ?? 0) + (limit ?? 10);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: bookings,
|
data: { ...bookings, nextCursor },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ import {
|
|||||||
|
|
||||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
enum Status {
|
const Status = {
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED: "CANCELLED",
|
||||||
REJECTED = "REJECTED",
|
REJECTED: "REJECTED",
|
||||||
ACCEPTED = "ACCEPTED",
|
ACCEPTED: "ACCEPTED",
|
||||||
PENDING = "PENDING",
|
PENDING: "PENDING",
|
||||||
AWAITING_HOST = "AWAITING_HOST",
|
AWAITING_HOST: "AWAITING_HOST",
|
||||||
}
|
} as const;
|
||||||
|
|
||||||
|
export type Status = (typeof Status)[keyof typeof Status];
|
||||||
|
|
||||||
class Attendee {
|
class Attendee {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
@@ -290,7 +290,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to auto-selected host",
|
summary: "Reassign a booking to auto-selected host",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description:
|
||||||
|
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
||||||
})
|
})
|
||||||
async reassignBooking(
|
async reassignBooking(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
@@ -311,7 +312,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Reassign a booking to a specific host",
|
summary: "Reassign a booking to a specific host",
|
||||||
description: "The provided authorization header refers to the owner of the booking.",
|
description:
|
||||||
|
"Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.",
|
||||||
})
|
})
|
||||||
async reassignBookingToUser(
|
async reassignBookingToUser(
|
||||||
@Param("bookingUid") bookingUid: string,
|
@Param("bookingUid") bookingUid: string,
|
||||||
|
|||||||
+1
-2
@@ -874,9 +874,8 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
|||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
expect(responseBody.data).toBeDefined();
|
expect(responseBody.data).toBeDefined();
|
||||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
|
||||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||||
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||||
|
|||||||
@@ -2399,6 +2399,146 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cant't reschedule cancelled booking", () => {
|
||||||
|
it("should not be able to reschedule cancelled booking", async () => {
|
||||||
|
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: RescheduleBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||||
|
reschedulingReason: "Flying to mars that day",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not be able to reschedule rescheduled booking", async () => {
|
||||||
|
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "CANCELLED",
|
||||||
|
rescheduled: true,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `cancelled-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newBooking = await bookingsRepositoryFixture.create({
|
||||||
|
status: "ACCEPTED",
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fromReschedule: rescheduledBooking.uid,
|
||||||
|
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||||
|
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||||
|
title: "peer coding lets goo",
|
||||||
|
uid: `new-booking-${randomString()}`,
|
||||||
|
eventType: {
|
||||||
|
connect: {
|
||||||
|
id: eventTypeId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: "integrations:daily",
|
||||||
|
customInputs: {},
|
||||||
|
metadata: {},
|
||||||
|
responses: {
|
||||||
|
name: "Oldie",
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
},
|
||||||
|
attendees: {
|
||||||
|
create: {
|
||||||
|
email: "oldie@gmail.com",
|
||||||
|
name: "Oldie",
|
||||||
|
locale: "lv",
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const body: RescheduleBookingInput_2024_08_13 = {
|
||||||
|
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||||
|
reschedulingReason: "Flying to mars that day",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||||
|
.send(body)
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||||
|
);
|
||||||
|
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||||
|
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||||
return (
|
return (
|
||||||
!Array.isArray(data) &&
|
!Array.isArray(data) &&
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servic
|
|||||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { getPagination } from "@/lib/pagination/pagination";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
@@ -108,6 +109,12 @@ export class BookingsService_2024_08_13 {
|
|||||||
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType.schedulingType === "MANAGED") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Event type with id=${eventType.id} is the parent managed event type that can't be booked. You have to provide the child event type id aka id of event type that has been assigned to one of the users.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
body.eventTypeId = eventType.id;
|
body.eventTypeId = eventType.id;
|
||||||
|
|
||||||
if ("instant" in body && body.instant) {
|
if ("instant" in body && body.instant) {
|
||||||
@@ -532,10 +539,13 @@ export class BookingsService_2024_08_13 {
|
|||||||
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const skip = Math.abs(queryParams?.skip ?? 0);
|
||||||
|
const take = Math.abs(queryParams?.take ?? 100);
|
||||||
|
|
||||||
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
|
const fetchedBookings: { bookings: { id: number }[]; totalCount: number } = await getAllUserBookings({
|
||||||
bookingListingByStatus: queryParams.status || [],
|
bookingListingByStatus: queryParams.status || [],
|
||||||
skip: queryParams.skip ?? 0,
|
skip,
|
||||||
take: queryParams.take ?? 100,
|
take,
|
||||||
filters: {
|
filters: {
|
||||||
...this.inputService.transformGetBookingsFilters(queryParams),
|
...this.inputService.transformGetBookingsFilters(queryParams),
|
||||||
...(userIds?.length ? { userIds } : {}),
|
...(userIds?.length ? { userIds } : {}),
|
||||||
@@ -588,28 +598,11 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const skip = Math.abs(queryParams?.skip ?? 0);
|
const pagination = getPagination({ skip, take, totalCount: fetchedBookings.totalCount });
|
||||||
const take = Math.abs(queryParams?.take ?? 100);
|
|
||||||
const itemsPerPage = take;
|
|
||||||
const totalPages = itemsPerPage !== 0 ? Math.ceil(fetchedBookings.totalCount / itemsPerPage) : 0;
|
|
||||||
const currentPage = Math.floor(skip / itemsPerPage) + 1;
|
|
||||||
const hasNextPage = skip + itemsPerPage < fetchedBookings.totalCount;
|
|
||||||
const hasPreviousPage = skip > 0;
|
|
||||||
return {
|
return {
|
||||||
bookings: formattedBookings,
|
bookings: formattedBookings,
|
||||||
pagination: {
|
pagination,
|
||||||
totalItems: fetchedBookings.totalCount,
|
|
||||||
// clamp remainingItems between 0 and totalCount
|
|
||||||
remainingItems: Math.min(
|
|
||||||
Math.max(fetchedBookings.totalCount - (skip + take), 0),
|
|
||||||
fetchedBookings.totalCount
|
|
||||||
),
|
|
||||||
itemsPerPage: itemsPerPage,
|
|
||||||
currentPage: currentPage,
|
|
||||||
totalPages: totalPages,
|
|
||||||
hasNextPage: hasNextPage,
|
|
||||||
hasPreviousPage: hasPreviousPage,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,6 +637,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
|
|
||||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||||
try {
|
try {
|
||||||
|
await this.canRescheduleBooking(bookingUid);
|
||||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||||
request,
|
request,
|
||||||
bookingUid,
|
bookingUid,
|
||||||
@@ -689,15 +683,29 @@ export class BookingsService_2024_08_13 {
|
|||||||
}
|
}
|
||||||
return this.outputService.getOutputBooking(databaseBooking);
|
return this.outputService.getOutputBooking(databaseBooking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
this.errorsBookingsService.handleBookingError(error, false);
|
||||||
if (error.message === "no_available_users_found_error") {
|
|
||||||
throw new BadRequestException("User either already has booking at this time or is not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async canRescheduleBooking(bookingUid: string) {
|
||||||
|
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error(`Booking with uid=${bookingUid} was not found in the database`);
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED" && !booking.rescheduled) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't reschedule booking with uid=${bookingUid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED" && booking.rescheduled) {
|
||||||
|
const rescheduledTo = await this.bookingsRepository.getByFromReschedule(bookingUid);
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Can't reschedule booking with uid=${bookingUid} because it has been cancelled and rescheduled already to booking with uid=${rescheduledTo?.uid}. You probably want to reschedule ${rescheduledTo?.uid} instead by passing it within the request URL.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return booking;
|
||||||
|
}
|
||||||
|
|
||||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||||
if (this.inputService.isCancelSeatedBody(body)) {
|
if (this.inputService.isCancelSeatedBody(body)) {
|
||||||
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
||||||
@@ -922,15 +930,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||||
|
const userCalendars = await this.usersRepository.findByIdWithCalendars(requestUser.id);
|
||||||
|
|
||||||
await confirmBookingHandler({
|
await confirmBookingHandler({
|
||||||
ctx: {
|
ctx: {
|
||||||
user: requestUser,
|
user: {
|
||||||
|
...requestUser,
|
||||||
|
destinationCalendar: userCalendars?.destinationCalendar ?? null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
recurringEventId: booking.recurringEventId,
|
recurringEventId: booking.recurringEventId ?? undefined,
|
||||||
emailsEnabled,
|
emailsEnabled,
|
||||||
platformClientParams,
|
platformClientParams,
|
||||||
},
|
},
|
||||||
@@ -950,15 +962,19 @@ export class BookingsService_2024_08_13 {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||||
|
const userCalendars = await this.usersRepository.findByIdWithCalendars(requestUser.id);
|
||||||
|
|
||||||
await confirmBookingHandler({
|
await confirmBookingHandler({
|
||||||
ctx: {
|
ctx: {
|
||||||
user: requestUser,
|
user: {
|
||||||
|
...requestUser,
|
||||||
|
destinationCalendar: userCalendars?.destinationCalendar ?? null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
recurringEventId: booking.recurringEventId,
|
recurringEventId: booking.recurringEventId ?? undefined,
|
||||||
reason,
|
reason,
|
||||||
emailsEnabled,
|
emailsEnabled,
|
||||||
platformClientParams,
|
platformClientParams,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||||
|
import { apiToInternalintegrationsMapping } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
||||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||||
@@ -29,10 +30,7 @@ import { NextApiRequest } from "next/types";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||||
EventTypeMetaDataSchema,
|
|
||||||
apiToInternalintegrationsMapping,
|
|
||||||
} from "@calcom/platform-libraries/event-types";
|
|
||||||
import {
|
import {
|
||||||
CancelBookingInput,
|
CancelBookingInput,
|
||||||
CancelBookingInput_2024_08_13,
|
CancelBookingInput_2024_08_13,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
|||||||
import {
|
import {
|
||||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
||||||
getBusyCalendarTimes,
|
getBusyCalendarTimes,
|
||||||
|
type EventBusyDate,
|
||||||
} from "@calcom/platform-libraries";
|
} from "@calcom/platform-libraries";
|
||||||
import { Calendar } from "@calcom/platform-types";
|
import { Calendar } from "@calcom/platform-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -45,6 +46,7 @@ export class CalendarsService {
|
|||||||
...credential,
|
...credential,
|
||||||
delegatedTo: null,
|
delegatedTo: null,
|
||||||
delegatedToId: null,
|
delegatedToId: null,
|
||||||
|
delegationCredentialId: null,
|
||||||
}))
|
}))
|
||||||
.filter((credential) => !!credential);
|
.filter((credential) => !!credential);
|
||||||
}
|
}
|
||||||
@@ -88,18 +90,19 @@ export class CalendarsService {
|
|||||||
dateTo,
|
dateTo,
|
||||||
composedSelectedCalendars
|
composedSelectedCalendars
|
||||||
);
|
);
|
||||||
// @ts-expect-error Element implicitly has any type
|
const calendarBusyTimesConverted = calendarBusyTimes.map(
|
||||||
const calendarBusyTimesConverted = calendarBusyTimes.map((busyTime) => {
|
(busyTime: EventBusyDate & { timeZone?: string }) => {
|
||||||
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
|
||||||
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
|
||||||
const busyTimeStartDate = busyTimeStart.toJSDate();
|
const busyTimeStartDate = busyTimeStart.toJSDate();
|
||||||
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
const busyTimeEndDate = busyTimeEnd.toJSDate();
|
||||||
return {
|
return {
|
||||||
...busyTime,
|
...busyTime,
|
||||||
start: busyTimeStartDate,
|
start: busyTimeStartDate,
|
||||||
end: busyTimeEndDate,
|
end: busyTimeEndDate,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return calendarBusyTimesConverted;
|
return calendarBusyTimesConverted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
|
|||||||
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||||
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||||
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
|
||||||
|
import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
updateEventType,
|
updateEventType,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
getEventTypesPublic,
|
getEventTypesPublic,
|
||||||
systemBeforeFieldEmail,
|
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@calcom/platform-libraries/event-types";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OrganizerIntegrationLocation } from "@calcom/lib/event-types/transformers/internal/locations";
|
import { OrganizerIntegrationLocation } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
|
|
||||||
type BaseEventType = {
|
type BaseEventType = {
|
||||||
length: number;
|
length: number;
|
||||||
|
|||||||
+3
@@ -114,6 +114,9 @@ export class EventTypesController_2024_06_14 {
|
|||||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||||
|
if (!eventTypes || eventTypes.length === 0) {
|
||||||
|
throw new NotFoundException(`Event types not found`);
|
||||||
|
}
|
||||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository_2024_06_14 {
|
export class EventTypesRepository_2024_06_14 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,7 @@
|
|||||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
|
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
|
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||||
|
import { SystemField, CustomField } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
@@ -14,10 +16,8 @@ import {
|
|||||||
updateEventType,
|
updateEventType,
|
||||||
getEventTypesPublic,
|
getEventTypesPublic,
|
||||||
EventTypesPublic,
|
EventTypesPublic,
|
||||||
SystemField,
|
|
||||||
CustomField,
|
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@calcom/platform-libraries/event-types";
|
||||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
import { GetEventTypesQuery_2024_06_14 } from "@calcom/platform-types";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
import { EventType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
+8
-12
@@ -1,13 +1,7 @@
|
|||||||
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
|
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
|
||||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
|
||||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { CONFERENCING_APPS } from "@calcom/platform-constants";
|
|
||||||
import { getUsersCredentials } from "@calcom/platform-libraries";
|
|
||||||
import { getApps } from "@calcom/platform-libraries/app-store";
|
|
||||||
import {
|
import {
|
||||||
transformBookingFieldsApiToInternal,
|
transformBookingFieldsApiToInternal,
|
||||||
transformLocationsApiToInternal,
|
transformLocationsApiToInternal,
|
||||||
@@ -21,21 +15,23 @@ import {
|
|||||||
systemAfterFieldNotes,
|
systemAfterFieldNotes,
|
||||||
systemAfterFieldGuests,
|
systemAfterFieldGuests,
|
||||||
systemAfterFieldRescheduleReason,
|
systemAfterFieldRescheduleReason,
|
||||||
EventTypeMetaDataSchema,
|
|
||||||
transformBookerLayoutsApiToInternal,
|
transformBookerLayoutsApiToInternal,
|
||||||
transformConfirmationPolicyApiToInternal,
|
transformConfirmationPolicyApiToInternal,
|
||||||
transformEventColorsApiToInternal,
|
transformEventColorsApiToInternal,
|
||||||
validateCustomEventName,
|
|
||||||
transformSeatsApiToInternal,
|
transformSeatsApiToInternal,
|
||||||
SystemField,
|
SystemField,
|
||||||
CustomField,
|
CustomField,
|
||||||
InternalLocation,
|
InternalLocation,
|
||||||
InternalLocationSchema,
|
InternalLocationSchema,
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { getApps, getUsersCredentialsIncludeServiceAccountKey } from "@calcom/platform-libraries/app-store";
|
||||||
|
import { validateCustomEventName, EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
DestinationCalendar_2024_06_14,
|
DestinationCalendar_2024_06_14,
|
||||||
InputEventTransformed_2024_06_14,
|
|
||||||
OutputUnknownLocation_2024_06_14,
|
OutputUnknownLocation_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
@@ -478,7 +474,7 @@ export class InputEventTypesService_2024_06_14 {
|
|||||||
appSlug = "msteams";
|
appSlug = "msteams";
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await getUsersCredentials(user);
|
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);
|
||||||
|
|
||||||
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
|
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
|
||||||
|
|
||||||
|
|||||||
+12
-13
@@ -1,14 +1,4 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
userMetadata,
|
|
||||||
parseBookingLimit,
|
|
||||||
parseRecurringEvent,
|
|
||||||
getBookingFieldsWithSystemFields,
|
|
||||||
} from "@calcom/platform-libraries";
|
|
||||||
import {
|
|
||||||
EventTypeMetaDataSchema,
|
|
||||||
transformLocationsInternalToApi,
|
transformLocationsInternalToApi,
|
||||||
transformBookingFieldsInternalToApi,
|
transformBookingFieldsInternalToApi,
|
||||||
InternalLocationSchema,
|
InternalLocationSchema,
|
||||||
@@ -20,11 +10,20 @@ import {
|
|||||||
transformBookerLayoutsInternalToApi,
|
transformBookerLayoutsInternalToApi,
|
||||||
transformRequiresConfirmationInternalToApi,
|
transformRequiresConfirmationInternalToApi,
|
||||||
transformEventTypeColorsInternalToApi,
|
transformEventTypeColorsInternalToApi,
|
||||||
parseEventTypeColor,
|
|
||||||
transformSeatsInternalToApi,
|
transformSeatsInternalToApi,
|
||||||
InternalLocation,
|
InternalLocation,
|
||||||
BookingFieldSchema,
|
BookingFieldSchema,
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userMetadata,
|
||||||
|
parseBookingLimit,
|
||||||
|
parseRecurringEvent,
|
||||||
|
getBookingFieldsWithSystemFields,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { EventTypeMetaDataSchema, parseEventTypeColor } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||||
BookerLayoutsTransformedSchema,
|
BookerLayoutsTransformedSchema,
|
||||||
@@ -39,7 +38,7 @@ type EventTypeRelations = {
|
|||||||
schedule: Schedule | null;
|
schedule: Schedule | null;
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
destinationCalendar?: DestinationCalendar | null;
|
||||||
};
|
};
|
||||||
export type DatabaseEventType = EventType & EventTypeRelations;
|
export type DatabaseEventType = Omit<EventType, "allowReschedulingCancelledBookings"> & EventTypeRelations;
|
||||||
|
|
||||||
type Input = Pick<
|
type Input = Pick<
|
||||||
DatabaseEventType,
|
DatabaseEventType,
|
||||||
|
|||||||
+6
-4
@@ -1,5 +1,3 @@
|
|||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
transformEventColorsApiToInternal,
|
transformEventColorsApiToInternal,
|
||||||
transformFutureBookingLimitsApiToInternal,
|
transformFutureBookingLimitsApiToInternal,
|
||||||
@@ -8,9 +6,13 @@ import type {
|
|||||||
transformSeatsApiToInternal,
|
transformSeatsApiToInternal,
|
||||||
transformBookingFieldsApiToInternal,
|
transformBookingFieldsApiToInternal,
|
||||||
InternalLocationsSchema,
|
InternalLocationsSchema,
|
||||||
} from "@calcom/lib/event-types/transformers";
|
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { CreateEventTypeInput_2024_06_14, ConfirmationPolicyTransformedSchema } from "../inputs";
|
import type {
|
||||||
|
CreateEventTypeInput_2024_06_14,
|
||||||
|
ConfirmationPolicyTransformedSchema,
|
||||||
|
} from "@calcom/platform-types";
|
||||||
|
|
||||||
export type InputEventTransformed_2024_06_14 = Omit<
|
export type InputEventTransformed_2024_06_14 = Omit<
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
File renamed without changes.
+1
-3
@@ -1,12 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BookerLayoutsInputEnum_2024_06_14,
|
BookerLayoutsInputEnum_2024_06_14,
|
||||||
BookerLayoutsOutputEnum_2024_06_14,
|
BookerLayoutsOutputEnum_2024_06_14,
|
||||||
ConfirmationPolicyEnum,
|
ConfirmationPolicyEnum,
|
||||||
NoticeThresholdUnitEnum,
|
NoticeThresholdUnitEnum,
|
||||||
FrequencyInput,
|
FrequencyInput,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import type {
|
import type {
|
||||||
InputBookingField_2024_06_14,
|
InputBookingField_2024_06_14,
|
||||||
InputLocation_2024_06_14,
|
InputLocation_2024_06_14,
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BookerLayoutsInputEnum_2024_06_14,
|
BookerLayoutsInputEnum_2024_06_14,
|
||||||
BookerLayoutsOutputEnum_2024_06_14,
|
BookerLayoutsOutputEnum_2024_06_14,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import { type CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
import { type CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
export function transformBookerLayoutsApiToInternal(
|
export function transformBookerLayoutsApiToInternal(
|
||||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ConfirmationPolicyEnum } from "@calcom/platform-enums/monorepo";
|
import { ConfirmationPolicyEnum } from "@calcom/platform-enums";
|
||||||
import type {
|
import type {
|
||||||
ConfirmationPolicyTransformedSchema,
|
ConfirmationPolicyTransformedSchema,
|
||||||
NoticeThresholdTransformedSchema,
|
NoticeThresholdTransformedSchema,
|
||||||
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
||||||
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
type CreateEventTypeInput_2024_06_14,
|
type CreateEventTypeInput_2024_06_14,
|
||||||
type BusinessDaysWindow_2024_06_14,
|
type BusinessDaysWindow_2024_06_14,
|
||||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums/monorepo";
|
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
type CreateEventTypeInput_2024_06_14,
|
type CreateEventTypeInput_2024_06_14,
|
||||||
type BookingLimitsKeyOutputType_2024_06_14,
|
type BookingLimitsKeyOutputType_2024_06_14,
|
||||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Frequency } from "@calcom/platform-enums/monorepo";
|
import { Frequency } from "@calcom/platform-enums";
|
||||||
import type { Recurrence_2024_06_14 } from "@calcom/platform-types";
|
import type { Recurrence_2024_06_14 } from "@calcom/platform-types";
|
||||||
import { type TransformRecurringEventSchema_2024_06_14 } from "@calcom/platform-types";
|
import { type TransformRecurringEventSchema_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BookerLayoutsInputEnum_2024_06_14,
|
BookerLayoutsInputEnum_2024_06_14,
|
||||||
BookerLayoutsOutputEnum_2024_06_14,
|
BookerLayoutsOutputEnum_2024_06_14,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import type { BookerLayoutsTransformedSchema } from "@calcom/platform-types";
|
import type { BookerLayoutsTransformedSchema } from "@calcom/platform-types";
|
||||||
|
|
||||||
export function transformBookerLayoutsInternalToApi(
|
export function transformBookerLayoutsInternalToApi(
|
||||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
||||||
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import type {
|
import type {
|
||||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||||
BookingWindow_2024_06_14,
|
BookingWindow_2024_06_14,
|
||||||
File renamed without changes.
+1
-3
@@ -1,11 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BookerLayoutsInputEnum_2024_06_14,
|
BookerLayoutsInputEnum_2024_06_14,
|
||||||
BookerLayoutsOutputEnum_2024_06_14,
|
BookerLayoutsOutputEnum_2024_06_14,
|
||||||
ConfirmationPolicyEnum,
|
ConfirmationPolicyEnum,
|
||||||
NoticeThresholdUnitEnum,
|
NoticeThresholdUnitEnum,
|
||||||
} from "@calcom/platform-enums/monorepo";
|
} from "@calcom/platform-enums";
|
||||||
import type {
|
import type {
|
||||||
TransformBookingLimitsSchema_2024_06_14,
|
TransformBookingLimitsSchema_2024_06_14,
|
||||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums/monorepo";
|
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums";
|
||||||
import type {
|
import type {
|
||||||
BookingLimitsKeysInputType,
|
BookingLimitsKeysInputType,
|
||||||
TransformBookingLimitsSchema_2024_06_14,
|
TransformBookingLimitsSchema_2024_06_14,
|
||||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Frequency, FrequencyInput } from "@calcom/platform-enums/monorepo";
|
import { Frequency, FrequencyInput } from "@calcom/platform-enums";
|
||||||
import type { Recurrence_2024_06_14, TransformRecurringEventSchema_2024_06_14 } from "@calcom/platform-types";
|
import type { Recurrence_2024_06_14, TransformRecurringEventSchema_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
export function transformRecurrenceInternalToApi(
|
export function transformRecurrenceInternalToApi(
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { ConfirmationPolicyEnum } from "@calcom/platform-enums/monorepo";
|
import { ConfirmationPolicyEnum } from "@calcom/platform-enums";
|
||||||
import type { NoticeThresholdTransformedSchema, ConfirmationPolicy_2024_06_14 } from "@calcom/platform-types";
|
import type { NoticeThresholdTransformedSchema, ConfirmationPolicy_2024_06_14 } from "@calcom/platform-types";
|
||||||
|
|
||||||
export function transformRequiresConfirmationInternalToApi(
|
export function transformRequiresConfirmationInternalToApi(
|
||||||
File renamed without changes.
File renamed without changes.
+45
-7
@@ -1,11 +1,7 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import {
|
|
||||||
transformApiScheduleOverrides,
|
|
||||||
transformApiScheduleAvailability,
|
|
||||||
} from "@calcom/platform-libraries/schedules";
|
|
||||||
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
|
||||||
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
|
import { ScheduleOverrideInput_2024_06_11, WeekDay } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InputSchedulesService_2024_06_11 {
|
export class InputSchedulesService_2024_06_11 {
|
||||||
@@ -34,10 +30,52 @@ export class InputSchedulesService_2024_06_11 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transformInputScheduleAvailability(inputAvailability: ScheduleAvailabilityInput_2024_06_11[]) {
|
transformInputScheduleAvailability(inputAvailability: ScheduleAvailabilityInput_2024_06_11[]) {
|
||||||
return transformApiScheduleAvailability(inputAvailability);
|
return inputAvailability.map((availability) => ({
|
||||||
|
days: availability.days.map(this.transformDayToNumber),
|
||||||
|
startTime: this.createDateFromHoursMinutes(availability.startTime),
|
||||||
|
endTime: this.createDateFromHoursMinutes(availability.endTime),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
transformInputOverrides(inputOverrides: ScheduleOverrideInput_2024_06_11[]) {
|
transformInputOverrides(inputOverrides: ScheduleOverrideInput_2024_06_11[]) {
|
||||||
return transformApiScheduleOverrides(inputOverrides);
|
return inputOverrides.map((override) => ({
|
||||||
|
date: new Date(override.date),
|
||||||
|
startTime: this.createDateFromHoursMinutes(override.startTime),
|
||||||
|
endTime: this.createDateFromHoursMinutes(override.endTime),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
createDateFromHoursMinutes(hoursMinutes: string): Date {
|
||||||
|
const parts = hoursMinutes.split(":");
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new Error("Invalid time format. Please use 'hh:mm'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = parseInt(parts[0], 10);
|
||||||
|
const minutes = parseInt(parts[1], 10);
|
||||||
|
|
||||||
|
if (isNaN(hours) || hours < 0 || hours > 23) {
|
||||||
|
throw new Error("Hours must be a number between 0 and 23.");
|
||||||
|
}
|
||||||
|
if (isNaN(minutes) || minutes < 0 || minutes > 59) {
|
||||||
|
throw new Error("Minutes must be a number between 0 and 59.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const utcDate = new Date(Date.UTC(1970, 0, 1, hours, minutes));
|
||||||
|
|
||||||
|
return utcDate;
|
||||||
|
}
|
||||||
|
transformDayToNumber(day: WeekDay): number {
|
||||||
|
const weekMap: { [key in WeekDay]: number } = {
|
||||||
|
Sunday: 0,
|
||||||
|
Monday: 1,
|
||||||
|
Tuesday: 2,
|
||||||
|
Wednesday: 3,
|
||||||
|
Thursday: 4,
|
||||||
|
Friday: 5,
|
||||||
|
Saturday: 6,
|
||||||
|
};
|
||||||
|
return weekMap[day];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export type Environment = {
|
|||||||
STRIPE_API_KEY: string;
|
STRIPE_API_KEY: string;
|
||||||
STRIPE_WEBHOOK_SECRET: string;
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
WEB_APP_URL: string;
|
WEB_APP_URL: string;
|
||||||
IS_E2E: boolean;
|
IS_E2E: string;
|
||||||
CALCOM_LICENSE_KEY: string;
|
CALCOM_LICENSE_KEY: string;
|
||||||
GET_LICENSE_KEY_URL: string;
|
GET_LICENSE_KEY_URL: string;
|
||||||
API_KEY_PREFIX: string;
|
API_KEY_PREFIX: string;
|
||||||
@@ -32,6 +32,8 @@ export type Environment = {
|
|||||||
AXIOM_TOKEN: string;
|
AXIOM_TOKEN: string;
|
||||||
STRIPE_TEAM_MONTHLY_PRICE_ID: string;
|
STRIPE_TEAM_MONTHLY_PRICE_ID: string;
|
||||||
IS_TEAM_BILLING_ENABLED: boolean;
|
IS_TEAM_BILLING_ENABLED: boolean;
|
||||||
|
// Used to enable/disable the rewrite of /api/v2 to /v2, active by default.
|
||||||
|
REWRITE_API_V2_PREFIX: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { getPagination, clamp } from "./pagination";
|
||||||
|
|
||||||
|
describe("getPagination", () => {
|
||||||
|
it("handles the first page correctly", () => {
|
||||||
|
const pagination = getPagination({ skip: 0, take: 10, totalCount: 35 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 10,
|
||||||
|
totalItems: 35,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
remainingItems: 25,
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 4,
|
||||||
|
hasNextPage: true,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles a middle page correctly", () => {
|
||||||
|
const pagination = getPagination({ skip: 10, take: 10, totalCount: 35 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 10,
|
||||||
|
totalItems: 35,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
remainingItems: 15,
|
||||||
|
currentPage: 2,
|
||||||
|
totalPages: 4,
|
||||||
|
hasNextPage: true,
|
||||||
|
hasPreviousPage: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles the last page when it is not completely full", () => {
|
||||||
|
const pagination = getPagination({ skip: 30, take: 10, totalCount: 35 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 5,
|
||||||
|
totalItems: 35,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
remainingItems: 0,
|
||||||
|
currentPage: 4,
|
||||||
|
totalPages: 4,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps skip values that exceed the total item count", () => {
|
||||||
|
const pagination = getPagination({ skip: 40, take: 10, totalCount: 35 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 0,
|
||||||
|
totalItems: 35,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
remainingItems: 0,
|
||||||
|
currentPage: 4,
|
||||||
|
totalPages: 4,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works when itemsPerPage (take) is zero", () => {
|
||||||
|
const pagination = getPagination({ skip: 0, take: 0, totalCount: 35 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 0,
|
||||||
|
totalItems: 35,
|
||||||
|
itemsPerPage: 0,
|
||||||
|
remainingItems: 35,
|
||||||
|
currentPage: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNextPage: true,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works when the collection is empty", () => {
|
||||||
|
const pagination = getPagination({ skip: 0, take: 10, totalCount: 0 });
|
||||||
|
expect(pagination).toEqual({
|
||||||
|
returnedItems: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
itemsPerPage: 10,
|
||||||
|
remainingItems: 0,
|
||||||
|
currentPage: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clamp", () => {
|
||||||
|
it("returns the value unchanged when it is inside the range", () => {
|
||||||
|
expect(clamp({ value: 5, min: 1, max: 10 })).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps values below the minimum", () => {
|
||||||
|
expect(clamp({ value: -3, min: 0, max: 10 })).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps values above the maximum", () => {
|
||||||
|
expect(clamp({ value: 42, min: 0, max: 10 })).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { PaginationMetaDto } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
type Pagination = {
|
||||||
|
skip: number;
|
||||||
|
take: number;
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPagination(pagination: Pagination): PaginationMetaDto {
|
||||||
|
const { skip, take, totalCount } = pagination;
|
||||||
|
|
||||||
|
const safeSkip = clamp({ value: skip, min: 0, max: totalCount });
|
||||||
|
const itemsPerPage = take;
|
||||||
|
const remainingItems = getRemainingItems(safeSkip, itemsPerPage, totalCount);
|
||||||
|
const totalPages = getTotalPages(itemsPerPage, totalCount);
|
||||||
|
const currentPage = getCurrentPage(safeSkip, itemsPerPage, totalPages);
|
||||||
|
const hasNextPage = getHasNextPage(safeSkip, itemsPerPage, totalCount);
|
||||||
|
const hasPreviousPage = getHasPreviousPage(safeSkip);
|
||||||
|
const returnedItems = getReturnedItems(safeSkip, itemsPerPage, totalCount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
returnedItems,
|
||||||
|
totalItems: totalCount,
|
||||||
|
itemsPerPage,
|
||||||
|
remainingItems,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage,
|
||||||
|
hasPreviousPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRemainingItems(skip: number, itemsPerPage: number, totalCount: number) {
|
||||||
|
return clamp({
|
||||||
|
value: totalCount - (skip + itemsPerPage),
|
||||||
|
min: 0,
|
||||||
|
max: totalCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalPages(itemsPerPage: number, totalCount: number) {
|
||||||
|
return itemsPerPage !== 0 ? Math.ceil(totalCount / itemsPerPage) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentPage(skip: number, itemsPerPage: number, totalPages: number) {
|
||||||
|
const rawCurrentPage = Math.floor(skip / itemsPerPage) + 1;
|
||||||
|
if (totalPages === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return clamp({ value: rawCurrentPage, min: 1, max: totalPages });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHasNextPage(skip: number, itemsPerPage: number, totalCount: number) {
|
||||||
|
return skip + itemsPerPage < totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHasPreviousPage(skip: number) {
|
||||||
|
return skip > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReturnedItems(skip: number, itemsPerPage: number, totalCount: number) {
|
||||||
|
const remaining = totalCount - skip;
|
||||||
|
return clamp({
|
||||||
|
value: remaining,
|
||||||
|
min: 0,
|
||||||
|
max: itemsPerPage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClampArgs = {
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clamp({ value, min, max }: ClampArgs): number {
|
||||||
|
return Math.max(min, Math.min(value, max));
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { getEnv } from "@/env";
|
||||||
import { Injectable, NestMiddleware } from "@nestjs/common";
|
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RewriterMiddleware implements NestMiddleware {
|
export class RewriterMiddleware implements NestMiddleware {
|
||||||
use(req: Request, res: Response, next: () => void) {
|
use(req: Request, res: Response, next: () => void) {
|
||||||
if (req.url.startsWith("/api/v2")) {
|
if (shouldRewriteApiV2Prefix() && req.url.startsWith("/api/v2")) {
|
||||||
req.url = req.url.replace("/api/v2", "/v2");
|
req.url = req.url.replace("/api/v2", "/v2");
|
||||||
}
|
}
|
||||||
if (req.url.startsWith("/v2/ee")) {
|
if (req.url.startsWith("/v2/ee")) {
|
||||||
@@ -16,3 +17,12 @@ export class RewriterMiddleware implements NestMiddleware {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldRewriteApiV2Prefix = () => {
|
||||||
|
try {
|
||||||
|
// rewrite is active even if REWRITE_API_V2_PREFIX is not set
|
||||||
|
return getEnv("REWRITE_API_V2_PREFIX", "1") === "1";
|
||||||
|
} catch (error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
|
||||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||||
|
import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/atoms.conferencing-apps.controller";
|
||||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||||
|
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||||
|
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||||
|
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
@@ -26,10 +29,15 @@ import { Module } from "@nestjs/common";
|
|||||||
UsersRepository,
|
UsersRepository,
|
||||||
AtomsRepository,
|
AtomsRepository,
|
||||||
UsersService,
|
UsersService,
|
||||||
SchedulesRepository_2024_06_11,
|
SchedulesAtomsService,
|
||||||
RedisService,
|
RedisService,
|
||||||
],
|
],
|
||||||
exports: [EventTypesAtomService],
|
exports: [EventTypesAtomService],
|
||||||
controllers: [AtomsController],
|
controllers: [
|
||||||
|
AtomsController,
|
||||||
|
AtomsEventTypesController,
|
||||||
|
AtomsConferencingAppsController,
|
||||||
|
AtomsSchedulesController,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AtomsModule {}
|
export class AtomsModule {}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Controller, Get, Param, ParseIntPipe, UseGuards, Version, VERSION_NEUTRAL } from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { ConnectedApps } from "@calcom/platform-libraries/app-store";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Conferencing endpoints for atoms, split from AtomsController for clarity and maintainability.
|
||||||
|
These endpoints should not be recommended for use by third party and are excluded from docs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - conferencing endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsConferencingAppsController {
|
||||||
|
constructor(private readonly conferencingService: ConferencingAtomsService) {}
|
||||||
|
|
||||||
|
@Get("/organizations/:orgId/teams/:teamId/conferencing")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
async listTeamInstalledConferencingApps(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<ApiResponse<ConnectedApps>> {
|
||||||
|
const conferencingApps = await this.conferencingService.getTeamConferencingApps(user, teamId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: conferencingApps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/conferencing")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async listUserInstalledConferencingApps(
|
||||||
|
@GetUser() user: UserWithProfile
|
||||||
|
): Promise<ApiResponse<ConnectedApps>> {
|
||||||
|
const conferencingApps = await this.conferencingService.getUserConferencingApps(user);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: conferencingApps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import {
|
|
||||||
BulkUpdateEventTypeToDefaultLocationDto,
|
|
||||||
EventTypesAppInput,
|
|
||||||
} from "@/modules/atoms/inputs/event-types-app.input";
|
|
||||||
import { FindTeamMembersMatchingAttributeQueryDto } from "@/modules/atoms/inputs/find-team-members-matching-attribute.input";
|
import { FindTeamMembersMatchingAttributeQueryDto } from "@/modules/atoms/inputs/find-team-members-matching-attribute.input";
|
||||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
|
||||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
|
||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
|
||||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
|
||||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
|
||||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
|
||||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -25,24 +13,17 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
Version,
|
Version,
|
||||||
VERSION_NEUTRAL,
|
VERSION_NEUTRAL,
|
||||||
Patch,
|
|
||||||
Body,
|
|
||||||
Query,
|
Query,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ConnectedApps } from "@calcom/platform-libraries/app-store";
|
|
||||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
|
||||||
import { ApiResponse } from "@calcom/platform-types";
|
|
||||||
|
|
||||||
import { FindTeamMembersMatchingAttributeResponseDto } from "../outputs/find-team-members-matching-attribute.output";
|
import { FindTeamMembersMatchingAttributeResponseDto } from "../outputs/find-team-members-matching-attribute.output";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Endpoints used only by platform atoms, reusing code from other modules, data is already formatted and ready to be used by frontend atoms
|
Endpoints used only by platform atoms, reusing code from other modules, data is already formatted and ready to be used by frontend atoms
|
||||||
these endpoints should not be recommended for use by third party and are excluded from docs
|
these endpoints should not be recommended for use by third party and are excluded from docs
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -53,180 +34,10 @@ these endpoints should not be recommended for use by third party and are exclude
|
|||||||
@DocsExcludeController(true)
|
@DocsExcludeController(true)
|
||||||
export class AtomsController {
|
export class AtomsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly eventTypesService: EventTypesAtomService,
|
|
||||||
private readonly conferencingService: ConferencingAtomsService,
|
private readonly conferencingService: ConferencingAtomsService,
|
||||||
private readonly attributesService: AttributesAtomsService
|
private readonly attributesService: AttributesAtomsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("event-types/:eventTypeId")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async getAtomEventType(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
|
||||||
): Promise<ApiResponse<unknown>> {
|
|
||||||
const eventType = await this.eventTypesService.getUserEventType(user, eventTypeId);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: eventType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("/organizations/:orgId/teams/:teamId/event-types")
|
|
||||||
@Roles("TEAM_ADMIN")
|
|
||||||
@PlatformPlan("ESSENTIALS")
|
|
||||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
async listTeamEventTypes(@Param("teamId", ParseIntPipe) teamId: number): Promise<ApiResponse<unknown>> {
|
|
||||||
const eventTypes = await this.eventTypesService.getTeamEventTypes(teamId);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: eventTypes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("/event-types")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async listUserEventTypes(@GetUser("id") userId: number): Promise<ApiResponse<unknown>> {
|
|
||||||
const eventTypes = await this.eventTypesService.getUserEventTypes(userId);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: eventTypes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("event-types-app/:appSlug")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async getAtomEventTypeApp(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Param("appSlug") appSlug: string,
|
|
||||||
@Query() queryParams: EventTypesAppInput
|
|
||||||
): Promise<ApiResponse<unknown>> {
|
|
||||||
const { teamId } = queryParams;
|
|
||||||
|
|
||||||
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: {
|
|
||||||
app,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("payment/:uid")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async getUserPaymentInfoById(@Param("uid") uid: string): Promise<ApiResponse<unknown>> {
|
|
||||||
const data = await this.eventTypesService.getUserPaymentInfo(uid);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch("/event-types/bulk-update-to-default-location")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async bulkUpdateAtomEventTypes(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Body() body: BulkUpdateEventTypeToDefaultLocationDto
|
|
||||||
): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> {
|
|
||||||
await this.eventTypesService.bulkUpdateEventTypesDefaultLocation(user, body.eventTypeIds);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch("/organizations/:orgId/teams/:teamId/event-types/bulk-update-to-default-location")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@Roles("TEAM_ADMIN")
|
|
||||||
@PlatformPlan("ESSENTIALS")
|
|
||||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
|
||||||
async bulkUpdateAtomTeamEventTypes(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Body() body: BulkUpdateEventTypeToDefaultLocationDto,
|
|
||||||
@Param("teamId", ParseIntPipe) teamId: number
|
|
||||||
): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> {
|
|
||||||
await this.eventTypesService.bulkUpdateTeamEventTypesDefaultLocation(body.eventTypeIds, teamId);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch("event-types/:eventTypeId")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async updateAtomEventType(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
|
||||||
@Body() body: UpdateEventTypeReturn
|
|
||||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
|
||||||
const eventType = await this.eventTypesService.updateEventType(
|
|
||||||
eventTypeId,
|
|
||||||
{ ...body, id: eventTypeId },
|
|
||||||
user
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: eventType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch("/organizations/:orgId/teams/:teamId/event-types/:eventTypeId")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@Roles("TEAM_ADMIN")
|
|
||||||
@PlatformPlan("ESSENTIALS")
|
|
||||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
|
||||||
async updateAtomTeamEventType(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
|
||||||
@Body() body: UpdateEventTypeReturn
|
|
||||||
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
|
||||||
const eventType = await this.eventTypesService.updateTeamEventType(
|
|
||||||
eventTypeId,
|
|
||||||
{ ...body, id: eventTypeId },
|
|
||||||
user,
|
|
||||||
teamId
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: eventType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@Get("/organizations/:orgId/teams/:teamId/conferencing")
|
|
||||||
@Roles("TEAM_ADMIN")
|
|
||||||
@PlatformPlan("ESSENTIALS")
|
|
||||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
async listTeamInstalledConferencingApps(
|
|
||||||
@GetUser() user: UserWithProfile,
|
|
||||||
@Param("teamId", ParseIntPipe) teamId: number
|
|
||||||
): Promise<ApiResponse<ConnectedApps>> {
|
|
||||||
const conferencingApps = await this.conferencingService.getTeamConferencingApps(user, teamId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: conferencingApps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("/conferencing")
|
|
||||||
@Version(VERSION_NEUTRAL)
|
|
||||||
@UseGuards(ApiAuthGuard)
|
|
||||||
async listUserInstalledConferencingApps(
|
|
||||||
@GetUser() user: UserWithProfile
|
|
||||||
): Promise<ApiResponse<ConnectedApps>> {
|
|
||||||
const conferencingApps = await this.conferencingService.getUserConferencingApps(user);
|
|
||||||
return {
|
|
||||||
status: SUCCESS_STATUS,
|
|
||||||
data: conferencingApps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@Get("/organizations/:orgId/teams/:teamId/members-matching-attribute")
|
@Get("/organizations/:orgId/teams/:teamId/members-matching-attribute")
|
||||||
@Version(VERSION_NEUTRAL)
|
@Version(VERSION_NEUTRAL)
|
||||||
@UseGuards(ApiAuthGuard)
|
@UseGuards(ApiAuthGuard)
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
|
||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import {
|
||||||
|
BulkUpdateEventTypeToDefaultLocationDto,
|
||||||
|
EventTypesAppInput,
|
||||||
|
} from "@/modules/atoms/inputs/event-types-app.input";
|
||||||
|
import { GetAtomPublicEventTypeQueryParams } from "@/modules/atoms/inputs/get-atom-public-event-type-query-params.input";
|
||||||
|
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||||
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
UseGuards,
|
||||||
|
Version,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
||||||
|
import { PublicEventType } from "@calcom/platform-libraries/event-types";
|
||||||
|
import { ApiResponse } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Event-types endpoints for atoms, split from AtomsController for clarity and maintainability.
|
||||||
|
These endpoints should not be recommended for use by third party and are excluded from docs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - event-types endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsEventTypesController {
|
||||||
|
constructor(private readonly eventTypesService: EventTypesAtomService) {}
|
||||||
|
|
||||||
|
@Get("/event-types/:eventSlug/public")
|
||||||
|
async getPublicEventType(
|
||||||
|
@Param("eventSlug") eventSlug: string,
|
||||||
|
@Query() queryParams: GetAtomPublicEventTypeQueryParams
|
||||||
|
): Promise<ApiResponse<PublicEventType>> {
|
||||||
|
const { username, teamId, orgId, isTeamEvent } = queryParams;
|
||||||
|
|
||||||
|
const event = await this.eventTypesService.getPublicEventTypeForAtoms({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
isTeamEvent: isTeamEvent ?? false,
|
||||||
|
teamId,
|
||||||
|
orgId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: event,
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("event-types/:eventTypeId")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async getAtomEventType(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
|
||||||
|
): Promise<ApiResponse<unknown>> {
|
||||||
|
const eventType = await this.eventTypesService.getUserEventType(user, eventTypeId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/organizations/:orgId/teams/:teamId/event-types")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
async listTeamEventTypes(@Param("teamId", ParseIntPipe) teamId: number): Promise<ApiResponse<unknown>> {
|
||||||
|
const eventTypes = await this.eventTypesService.getTeamEventTypes(teamId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventTypes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/event-types")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async listUserEventTypes(@GetUser("id") userId: number): Promise<ApiResponse<unknown>> {
|
||||||
|
const eventTypes = await this.eventTypesService.getUserEventTypes(userId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventTypes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("event-types-app/:appSlug")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async getAtomEventTypeApp(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("appSlug") appSlug: string,
|
||||||
|
@Query() queryParams: EventTypesAppInput
|
||||||
|
): Promise<ApiResponse<unknown>> {
|
||||||
|
const { teamId } = queryParams;
|
||||||
|
const app = await this.eventTypesService.getEventTypesAppIntegration(appSlug, user, teamId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: {
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("payment/:uid")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async getUserPaymentInfoById(@Param("uid") uid: string): Promise<ApiResponse<unknown>> {
|
||||||
|
const data = await this.eventTypesService.getUserPaymentInfo(uid);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/event-types/bulk-update-to-default-location")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async bulkUpdateAtomEventTypes(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() body: BulkUpdateEventTypeToDefaultLocationDto
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> {
|
||||||
|
await this.eventTypesService.bulkUpdateEventTypesDefaultLocation(user, body.eventTypeIds);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/organizations/:orgId/teams/:teamId/event-types/bulk-update-to-default-location")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
async bulkUpdateAtomTeamEventTypes(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() body: BulkUpdateEventTypeToDefaultLocationDto,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> {
|
||||||
|
await this.eventTypesService.bulkUpdateTeamEventTypesDefaultLocation(body.eventTypeIds, teamId);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("event-types/:eventTypeId")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
async updateAtomEventType(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Body() body: UpdateEventTypeReturn
|
||||||
|
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||||
|
const eventType = await this.eventTypesService.updateEventType(
|
||||||
|
eventTypeId,
|
||||||
|
{ ...body, id: eventTypeId },
|
||||||
|
user
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("/organizations/:orgId/teams/:teamId/event-types/:eventTypeId")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
async updateAtomTeamEventType(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Body() body: UpdateEventTypeReturn
|
||||||
|
): Promise<ApiResponse<UpdateEventTypeReturn>> {
|
||||||
|
const eventType = await this.eventTypesService.updateTeamEventType(
|
||||||
|
eventTypeId,
|
||||||
|
{ ...body, id: eventTypeId },
|
||||||
|
user,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: eventType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
|
import { GetAtomSchedulesQueryParams } from "@/modules/atoms/inputs/get-atom-schedules-query-params.input";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Patch,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
Version,
|
||||||
|
VERSION_NEUTRAL,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiTags as DocsTags,
|
||||||
|
ApiExcludeController as DocsExcludeController,
|
||||||
|
ApiOperation,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
|
||||||
|
import { ApiResponse, UpdateAtomScheduleDto } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
import { SchedulesAtomsService } from "../services/schedules-atom.service";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Endpoints used only by platform atoms, reusing code from other modules, data is already formatted and ready to be used by frontend atoms
|
||||||
|
these endpoints should not be recommended for use by third party and are excluded from docs
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/atoms",
|
||||||
|
version: API_VERSIONS_VALUES,
|
||||||
|
})
|
||||||
|
@DocsTags("Atoms - endpoints for atoms")
|
||||||
|
@DocsExcludeController(true)
|
||||||
|
export class AtomsSchedulesController {
|
||||||
|
constructor(private readonly schedulesService: SchedulesAtomsService) {}
|
||||||
|
|
||||||
|
@Get("/schedules")
|
||||||
|
@Version(VERSION_NEUTRAL)
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@Permissions([SCHEDULE_READ])
|
||||||
|
async getSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Query() queryParams: GetAtomSchedulesQueryParams
|
||||||
|
): Promise<ApiResponse<FindDetailedScheduleByIdReturnType | null>> {
|
||||||
|
const { isManagedEventType, scheduleId } = queryParams;
|
||||||
|
const schedule = await this.schedulesService.getSchedule({
|
||||||
|
scheduleId,
|
||||||
|
userId: user.id,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
isManagedEventType,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: schedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@Patch("schedules/:scheduleId")
|
||||||
|
@Permissions([SCHEDULE_WRITE])
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiOperation({ summary: "Update atom schedule" })
|
||||||
|
async updateSchedule(
|
||||||
|
@GetUser() user: UserWithProfile,
|
||||||
|
@Body() bodySchedule: UpdateAtomScheduleDto,
|
||||||
|
@Param("scheduleId", ParseIntPipe) scheduleId: number
|
||||||
|
): Promise<ApiResponse<any>> {
|
||||||
|
const updatedSchedule = await this.schedulesService.updateUserSchedule({
|
||||||
|
user,
|
||||||
|
input: bodySchedule,
|
||||||
|
scheduleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: updatedSchedule,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
|
||||||
import type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/platform-libraries";
|
import type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
export class FindTeamMembersMatchingAttributeQueryDto {
|
export class FindTeamMembersMatchingAttributeQueryDto {
|
||||||
@ApiPropertyOptional({
|
@ApiProperty({
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
attributesQueryValue: TFindTeamMembersMatchingAttributeLogicInputSchema["attributesQueryValue"];
|
attributesQueryValue!: TFindTeamMembersMatchingAttributeLogicInputSchema["attributesQueryValue"] | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform, Type } from "class-transformer";
|
||||||
|
import { IsBoolean, IsOptional, IsString, IsNumber } from "class-validator";
|
||||||
|
|
||||||
|
export class GetAtomPublicEventTypeQueryParams {
|
||||||
|
@Transform(({ value }: { value: string }) => value === "true")
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
isTeamEvent?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||||
|
@IsNumber()
|
||||||
|
@ApiPropertyOptional({ type: Number })
|
||||||
|
teamId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||||
|
@IsNumber()
|
||||||
|
@ApiPropertyOptional({ type: Number })
|
||||||
|
orgId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional({ type: String })
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform } from "class-transformer";
|
||||||
|
import { IsBoolean, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
export class GetAtomSchedulesQueryParams {
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||||
|
@ApiPropertyOptional({ type: Number })
|
||||||
|
scheduleId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@ApiPropertyOptional({ type: Boolean })
|
||||||
|
isManagedEventType?: boolean;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
|
||||||
|
import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
@@ -7,7 +8,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
|||||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
import { checkAdminOrOwner, getClientSecretFromPayment } from "@calcom/platform-libraries";
|
import { checkAdminOrOwner, getClientSecretFromPayment } from "@calcom/platform-libraries";
|
||||||
import type { TeamQuery } from "@calcom/platform-libraries";
|
import type { TeamQuery } from "@calcom/platform-libraries";
|
||||||
@@ -21,6 +22,7 @@ import type {
|
|||||||
CredentialDataWithTeamName,
|
CredentialDataWithTeamName,
|
||||||
LocationOption,
|
LocationOption,
|
||||||
} from "@calcom/platform-libraries/app-store";
|
} from "@calcom/platform-libraries/app-store";
|
||||||
|
import { type PublicEventType, getPublicEvent } from "@calcom/platform-libraries/event-types";
|
||||||
import {
|
import {
|
||||||
getEventTypeById,
|
getEventTypeById,
|
||||||
bulkUpdateEventsToDefaultLocation,
|
bulkUpdateEventsToDefaultLocation,
|
||||||
@@ -31,7 +33,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
updateEventType,
|
updateEventType,
|
||||||
TUpdateEventTypeInputSchema,
|
TUpdateEventTypeInputSchema,
|
||||||
systemBeforeFieldEmail,
|
|
||||||
EventTypeMetaDataSchema,
|
EventTypeMetaDataSchema,
|
||||||
} from "@calcom/platform-libraries/event-types";
|
} from "@calcom/platform-libraries/event-types";
|
||||||
import { PrismaClient } from "@calcom/prisma";
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
@@ -55,6 +56,18 @@ export class EventTypesAtomService {
|
|||||||
private readonly teamEventTypeService: TeamsEventTypesService
|
private readonly teamEventTypeService: TeamsEventTypesService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private async getTeamSlug(teamId: number): Promise<string> {
|
||||||
|
const team = await this.dbRead.prisma.team.findUnique({
|
||||||
|
where: { id: teamId },
|
||||||
|
select: { slug: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!team?.slug) {
|
||||||
|
throw new NotFoundException(`Team with id ${teamId} not found`);
|
||||||
|
}
|
||||||
|
return team.slug;
|
||||||
|
}
|
||||||
|
|
||||||
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
|
||||||
const organizationId = this.usersService.getUserMainOrgId(user);
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
@@ -226,20 +239,26 @@ export class EventTypesAtomService {
|
|||||||
} else {
|
} else {
|
||||||
credentials = credentials.concat(teamAppCredentials);
|
credentials = credentials.concat(teamAppCredentials);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (slug !== "stripe") {
|
||||||
|
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
|
||||||
|
const { credentials: allCredentials } =
|
||||||
|
await enrichUserWithDelegationConferencingCredentialsWithoutOrgId({
|
||||||
|
user: {
|
||||||
|
...user,
|
||||||
|
credentials,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
credentials = allCredentials;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// We only add delegationCredentials if the request for location options is for a user because DelegationCredential Credential is applicable to Users only.
|
|
||||||
const { credentials: allCredentials } = await enrichUserWithDelegationConferencingCredentialsWithoutOrgId(
|
const enabledApps = await getEnabledAppsFromCredentials(
|
||||||
|
credentials as unknown as CredentialDataWithTeamName[],
|
||||||
{
|
{
|
||||||
user: {
|
where: { slug },
|
||||||
...user,
|
|
||||||
credentials,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
credentials = allCredentials;
|
|
||||||
const enabledApps = await getEnabledAppsFromCredentials(allCredentials, {
|
|
||||||
where: { slug },
|
|
||||||
});
|
|
||||||
const apps = await Promise.all(
|
const apps = await Promise.all(
|
||||||
enabledApps
|
enabledApps
|
||||||
.filter(({ ...app }) => app.slug === slug)
|
.filter(({ ...app }) => app.slug === slug)
|
||||||
@@ -363,4 +382,61 @@ export class EventTypesAtomService {
|
|||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Returns the public event type for atoms, handling both team and user events.
|
||||||
|
*/
|
||||||
|
async getPublicEventTypeForAtoms({
|
||||||
|
username,
|
||||||
|
eventSlug,
|
||||||
|
isTeamEvent,
|
||||||
|
orgId,
|
||||||
|
teamId,
|
||||||
|
}: {
|
||||||
|
username?: string;
|
||||||
|
eventSlug: string;
|
||||||
|
isTeamEvent?: boolean;
|
||||||
|
orgId?: number;
|
||||||
|
teamId?: number;
|
||||||
|
}): Promise<PublicEventType> {
|
||||||
|
const orgSlug = orgId ? await this.getTeamSlug(orgId) : null;
|
||||||
|
|
||||||
|
let slug: string | null = null;
|
||||||
|
if (isTeamEvent) {
|
||||||
|
if (!teamId) {
|
||||||
|
throw new BadRequestException("teamId is required for team events, please provide a valid teamId");
|
||||||
|
}
|
||||||
|
slug = await this.getTeamSlug(teamId);
|
||||||
|
} else {
|
||||||
|
if (!username) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"username is required for non-team events, please provide a valid username"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
slug = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugLower = slug.toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = await getPublicEvent(
|
||||||
|
slugLower,
|
||||||
|
eventSlug,
|
||||||
|
isTeamEvent,
|
||||||
|
orgSlug,
|
||||||
|
this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
throw new NotFoundException(`Event type with slug ${eventSlug} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return event;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw new NotFoundException(err.message);
|
||||||
|
}
|
||||||
|
throw new NotFoundException(`Event type with slug ${eventSlug} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { ScheduleRepository, UpdateScheduleResponse } from "@calcom/platform-libraries/schedules";
|
||||||
|
import { updateSchedule } from "@calcom/platform-libraries/schedules";
|
||||||
|
import { UpdateAtomScheduleDto } from "@calcom/platform-types";
|
||||||
|
import { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulesAtomsService {
|
||||||
|
private logger = new Logger("SchedulesAtomService");
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly usersRepository: UsersRepository,
|
||||||
|
private readonly dbWrite: PrismaWriteService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getSchedule({
|
||||||
|
timeZone,
|
||||||
|
userId,
|
||||||
|
scheduleId,
|
||||||
|
isManagedEventType,
|
||||||
|
}: {
|
||||||
|
timeZone: string;
|
||||||
|
userId: number;
|
||||||
|
scheduleId?: number;
|
||||||
|
isManagedEventType?: boolean;
|
||||||
|
}) {
|
||||||
|
const user = await this.usersRepository.findById(userId);
|
||||||
|
|
||||||
|
if (!user?.defaultScheduleId) return null;
|
||||||
|
|
||||||
|
return await ScheduleRepository.findDetailedScheduleById({
|
||||||
|
scheduleId: scheduleId ?? user.defaultScheduleId,
|
||||||
|
isManagedEventType,
|
||||||
|
userId,
|
||||||
|
timeZone,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserSchedule({
|
||||||
|
input,
|
||||||
|
user,
|
||||||
|
scheduleId,
|
||||||
|
}: {
|
||||||
|
input: UpdateAtomScheduleDto;
|
||||||
|
user: UserWithProfile;
|
||||||
|
scheduleId: number;
|
||||||
|
}): Promise<UpdateScheduleResponse> {
|
||||||
|
return updateSchedule({
|
||||||
|
input: {
|
||||||
|
scheduleId,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||||
import { PlatformPlanType } from "@/modules/billing/types";
|
import { PlatformPlanType, orderedPlans } from "@/modules/billing/types";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { RedisService } from "@/modules/redis/redis.service";
|
import { RedisService } from "@/modules/redis/redis.service";
|
||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
@@ -61,7 +61,7 @@ export class PlatformPlanGuard implements CanActivate {
|
|||||||
!hasMinimumPlan({
|
!hasMinimumPlan({
|
||||||
currentPlan: organization.platformBilling?.plan as PlatformPlanType,
|
currentPlan: organization.platformBilling?.plan as PlatformPlanType,
|
||||||
minimumPlan: minimumPlan,
|
minimumPlan: minimumPlan,
|
||||||
plans: ["FREE", "STARTER", "ESSENTIALS", "SCALE", "ENTERPRISE"],
|
plans: orderedPlans,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
@@ -79,7 +79,7 @@ export class PlatformPlanGuard implements CanActivate {
|
|||||||
type HasMinimumPlanProp = {
|
type HasMinimumPlanProp = {
|
||||||
currentPlan: PlatformPlanType;
|
currentPlan: PlatformPlanType;
|
||||||
minimumPlan: PlatformPlanType;
|
minimumPlan: PlatformPlanType;
|
||||||
plans: PlatformPlanType[];
|
plans: readonly PlatformPlanType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hasMinimumPlan(props: HasMinimumPlanProp): boolean {
|
export function hasMinimumPlan(props: HasMinimumPlanProp): boolean {
|
||||||
@@ -87,7 +87,7 @@ export function hasMinimumPlan(props: HasMinimumPlanProp): boolean {
|
|||||||
const minimumPlanIndex = props.plans.indexOf(props.minimumPlan);
|
const minimumPlanIndex = props.plans.indexOf(props.minimumPlan);
|
||||||
|
|
||||||
if (currentPlanIndex === -1 || minimumPlanIndex === -1) {
|
if (currentPlanIndex === -1 || minimumPlanIndex === -1) {
|
||||||
throw new Error(
|
throw new ForbiddenException(
|
||||||
`PlatformPlanGuard - Invalid platform billing plan provided. Current plan: ${props.currentPlan}, Minimum plan: ${props.minimumPlan}`
|
`PlatformPlanGuard - Invalid platform billing plan provided. Current plan: ${props.currentPlan}, Minimum plan: ${props.minimumPlan}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ export class BillingController {
|
|||||||
case "checkout.session.completed":
|
case "checkout.session.completed":
|
||||||
await this.billingService.handleStripeCheckoutEvents(event);
|
await this.billingService.handleStripeCheckoutEvents(event);
|
||||||
break;
|
break;
|
||||||
|
case "customer.subscription.updated":
|
||||||
|
await this.billingService.handleStripePaymentPastDue(event);
|
||||||
|
break;
|
||||||
case "customer.subscription.deleted":
|
case "customer.subscription.deleted":
|
||||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -216,6 +216,30 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleStripePaymentPastDue(event: Stripe.Event) {
|
||||||
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||||
|
const customerId = this.getCustomerIdFromInvoice(invoice);
|
||||||
|
|
||||||
|
if (subscriptionId && customerId) {
|
||||||
|
const existingUserSubscription = await this.stripeService
|
||||||
|
.getStripe()
|
||||||
|
.subscriptions.retrieve(subscriptionId);
|
||||||
|
|
||||||
|
if (existingUserSubscription.status === "past_due") {
|
||||||
|
await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUserSubscription.status === "active") {
|
||||||
|
await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subscriptionId || !customerId) {
|
||||||
|
this.logger.log(`SubscriptionId: ${subscriptionId} or customerId: ${customerId} missing`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleStripeCheckoutEvents(event: Stripe.Event) {
|
async handleStripeCheckoutEvents(event: Stripe.Event) {
|
||||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,13 @@ export enum PlatformPlan {
|
|||||||
PER_ACTIVE_USER = "PER_ACTIVE_USER",
|
PER_ACTIVE_USER = "PER_ACTIVE_USER",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE" | "PER_ACTIVE_USER";
|
export const orderedPlans = [
|
||||||
|
"FREE",
|
||||||
|
"STARTER",
|
||||||
|
"ESSENTIALS",
|
||||||
|
"SCALE",
|
||||||
|
"PER_ACTIVE_USER",
|
||||||
|
"ENTERPRISE",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type PlatformPlanType = (typeof orderedPlans)[number];
|
||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
ZOOM,
|
ZOOM,
|
||||||
OFFICE_365_VIDEO,
|
OFFICE_365_VIDEO,
|
||||||
} from "@calcom/platform-constants";
|
} from "@calcom/platform-constants";
|
||||||
import { userMetadata, getUsersCredentials } from "@calcom/platform-libraries";
|
import { userMetadata } from "@calcom/platform-libraries";
|
||||||
|
import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/platform-libraries/app-store";
|
||||||
import { getApps, handleDeleteCredential } from "@calcom/platform-libraries/app-store";
|
import { getApps, handleDeleteCredential } from "@calcom/platform-libraries/app-store";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -90,7 +91,7 @@ export class ConferencingService {
|
|||||||
if (!CONFERENCING_APPS.includes(appSlug)) {
|
if (!CONFERENCING_APPS.includes(appSlug)) {
|
||||||
throw new BadRequestException("Invalid app, available apps are: ", CONFERENCING_APPS.join(", "));
|
throw new BadRequestException("Invalid app, available apps are: ", CONFERENCING_APPS.join(", "));
|
||||||
}
|
}
|
||||||
const credentials = await getUsersCredentials(user);
|
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);
|
||||||
|
|
||||||
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
|
const foundApp = getApps(credentials, true).filter((app) => app.slug === appSlug)[0];
|
||||||
|
|
||||||
|
|||||||
+25
-11
@@ -73,6 +73,7 @@ describe("Platform Destination Calendar Endpoints", () => {
|
|||||||
user.id,
|
user.id,
|
||||||
APPLE_CALENDAR_ID
|
APPLE_CALENDAR_ID
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
|
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
connectedCalendars: [
|
connectedCalendars: [
|
||||||
@@ -92,19 +93,32 @@ describe("Platform Destination Calendar Endpoints", () => {
|
|||||||
url: "",
|
url: "",
|
||||||
email: "",
|
email: "",
|
||||||
},
|
},
|
||||||
calendars: {
|
// calendars: {
|
||||||
externalId:
|
// externalId:
|
||||||
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
// "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||||
readOnly: false,
|
// readOnly: false,
|
||||||
integration: "apple_calendar",
|
// integration: "apple_calendar",
|
||||||
credentialId: appleCalendarCredentials.id,
|
// credentialId: appleCalendarCredentials.id,
|
||||||
primary: true,
|
// primary: true,
|
||||||
email: user.email,
|
// email: user.email,
|
||||||
},
|
// },
|
||||||
error: { message: "" },
|
error: { message: "" },
|
||||||
|
delegationCredentialId: null,
|
||||||
|
credentialId: appleCalendarCredentials.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
destinationCalendar: null,
|
destinationCalendar: {
|
||||||
|
name: "destinationCalendar",
|
||||||
|
eventTypeId: 1,
|
||||||
|
credentialId: appleCalendarCredentials.id,
|
||||||
|
primaryEmail: "primaryEmail",
|
||||||
|
integration: "apple_calendar",
|
||||||
|
externalId: "externalId",
|
||||||
|
userId: null,
|
||||||
|
id: 0,
|
||||||
|
delegationCredentialId: null,
|
||||||
|
domainWideDelegationCredentialId: null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -134,7 +148,7 @@ describe("Platform Destination Calendar Endpoints", () => {
|
|||||||
expect(user).toBeDefined();
|
expect(user).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
|
it.skip(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
|
||||||
const body = {
|
const body = {
|
||||||
integration: appleCalendarCredentials.type,
|
integration: appleCalendarCredentials.type,
|
||||||
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
|
|||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
import { WebhookRepositoryFixture } from "test/fixtures/repository/webhooks.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
import { withNextAuth } from "test/utils/withNextAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
|
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ describe("OAuth client WebhooksController (e2e)", () => {
|
|||||||
let webhook: OAuthClientWebhookOutputResponseDto["data"];
|
let webhook: OAuthClientWebhookOutputResponseDto["data"];
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await withNextAuth(
|
const moduleRef = await withApiAuth(
|
||||||
userEmail,
|
userEmail,
|
||||||
Test.createTestingModule({
|
Test.createTestingModule({
|
||||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||||
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
import { GetWebhook } from "@/modules/webhooks/decorators/get-webhook-decorator";
|
||||||
import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard";
|
import { IsOAuthClientWebhookGuard } from "@/modules/webhooks/guards/is-oauth-client-webhook-guard";
|
||||||
@@ -29,7 +29,7 @@ import { OAuthClientGuard } from "../../guards/oauth-client-guard";
|
|||||||
path: "/v2/oauth-clients/:clientId/webhooks",
|
path: "/v2/oauth-clients/:clientId/webhooks",
|
||||||
version: API_VERSIONS_VALUES,
|
version: API_VERSIONS_VALUES,
|
||||||
})
|
})
|
||||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
|
@UseGuards(ApiAuthGuard, OrganizationRolesGuard, OAuthClientGuard)
|
||||||
@DocsTags("Platform / Webhooks")
|
@DocsTags("Platform / Webhooks")
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: X_CAL_SECRET_KEY,
|
name: X_CAL_SECRET_KEY,
|
||||||
|
|||||||
+3
-2
@@ -1,11 +1,12 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsDelegationCredentialRepository {
|
export class OrganizationsDelegationCredentialRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
async findById(delegationCredentialId: string) {
|
async findById(delegationCredentialId: string) {
|
||||||
return this.dbRead.prisma.delegationCredential.findUnique({ where: { id: delegationCredentialId } });
|
return this.dbRead.prisma.delegationCredential.findUnique({ where: { id: delegationCredentialId } });
|
||||||
@@ -22,7 +23,7 @@ export class OrganizationsDelegationCredentialRepository {
|
|||||||
delegationCredentialId: string,
|
delegationCredentialId: string,
|
||||||
data: Prisma.DelegationCredentialUncheckedUpdateInput
|
data: Prisma.DelegationCredentialUncheckedUpdateInput
|
||||||
) {
|
) {
|
||||||
return this.dbRead.prisma.delegationCredential.update({
|
return this.dbWrite.prisma.delegationCredential.update({
|
||||||
where: { id: delegationCredentialId },
|
where: { id: delegationCredentialId },
|
||||||
data,
|
data,
|
||||||
include: { workspacePlatform: true },
|
include: { workspacePlatform: true },
|
||||||
|
|||||||
+19
-7
@@ -19,6 +19,7 @@ import { encryptServiceAccountKey } from "@calcom/platform-libraries";
|
|||||||
import {
|
import {
|
||||||
addDelegationCredential,
|
addDelegationCredential,
|
||||||
toggleDelegationCredentialEnabled,
|
toggleDelegationCredentialEnabled,
|
||||||
|
type TServiceAccountKeySchema,
|
||||||
} from "@calcom/platform-libraries/app-store";
|
} from "@calcom/platform-libraries/app-store";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -47,7 +48,7 @@ export class OrganizationsDelegationCredentialService {
|
|||||||
delegatedServiceAccountUser: User,
|
delegatedServiceAccountUser: User,
|
||||||
body: UpdateDelegationCredentialInput
|
body: UpdateDelegationCredentialInput
|
||||||
) {
|
) {
|
||||||
const delegationCredential =
|
let delegationCredential =
|
||||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||||
delegationCredentialId
|
delegationCredentialId
|
||||||
);
|
);
|
||||||
@@ -56,6 +57,14 @@ export class OrganizationsDelegationCredentialService {
|
|||||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.serviceAccountKey !== undefined) {
|
||||||
|
const updatedDelegationCredential = await this.updateDelegationCredentialServiceAccountKey(
|
||||||
|
delegationCredential.id,
|
||||||
|
body.serviceAccountKey
|
||||||
|
);
|
||||||
|
delegationCredential = updatedDelegationCredential ?? delegationCredential;
|
||||||
|
}
|
||||||
|
|
||||||
if (body.enabled !== undefined) {
|
if (body.enabled !== undefined) {
|
||||||
await this.updateDelegationCredentialEnabled(
|
await this.updateDelegationCredentialEnabled(
|
||||||
orgId,
|
orgId,
|
||||||
@@ -64,16 +73,13 @@ export class OrganizationsDelegationCredentialService {
|
|||||||
body.enabled
|
body.enabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (body.serviceAccountKey !== undefined) {
|
|
||||||
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// once delegation credentials are enabled, slowly set all the destination calendars of delegated users
|
// once delegation credentials are enabled, slowly set all the destination calendars of delegated users
|
||||||
if (body.enabled === true && delegationCredential.enabled === false) {
|
if (body.enabled === true && delegationCredential.enabled === false) {
|
||||||
await this.ensureDefaultCalendars(orgId, delegationCredential.domain);
|
await this.ensureDefaultCalendars(orgId, delegationCredential.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
|
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential.enabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureDefaultCalendars(orgId: number, domain: string) {
|
async ensureDefaultCalendars(orgId: number, domain: string) {
|
||||||
@@ -126,12 +132,18 @@ export class OrganizationsDelegationCredentialService {
|
|||||||
delegationCredentialId: string,
|
delegationCredentialId: string,
|
||||||
serviceAccountKey: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput
|
serviceAccountKey: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput
|
||||||
) {
|
) {
|
||||||
const encryptedServiceAccountKey = encryptServiceAccountKey(serviceAccountKey);
|
// First encrypt the service account key
|
||||||
|
const encryptedServiceAccountKey = encryptServiceAccountKey(
|
||||||
|
serviceAccountKey as TServiceAccountKeySchema
|
||||||
|
);
|
||||||
|
const prismaJsonValue = JSON.parse(JSON.stringify(encryptedServiceAccountKey));
|
||||||
|
|
||||||
const delegationCredential =
|
const delegationCredential =
|
||||||
await this.organizationsDelegationCredentialRepository.updateIncludeWorkspacePlatform(
|
await this.organizationsDelegationCredentialRepository.updateIncludeWorkspacePlatform(
|
||||||
delegationCredentialId,
|
delegationCredentialId,
|
||||||
{
|
{
|
||||||
serviceAccountKey: encryptedServiceAccountKey,
|
serviceAccountKey: prismaJsonValue,
|
||||||
|
enabled: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return delegationCredential;
|
return delegationCredential;
|
||||||
|
|||||||
+17
@@ -89,11 +89,13 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
teammate1 = await userRepositoryFixture.create({
|
teammate1 = await userRepositoryFixture.create({
|
||||||
email: teammate1Email,
|
email: teammate1Email,
|
||||||
username: teammate1Email,
|
username: teammate1Email,
|
||||||
|
name: "alice",
|
||||||
});
|
});
|
||||||
|
|
||||||
teammate2 = await userRepositoryFixture.create({
|
teammate2 = await userRepositoryFixture.create({
|
||||||
email: teammate2Email,
|
email: teammate2Email,
|
||||||
username: teammate2Email,
|
username: teammate2Email,
|
||||||
|
name: "bob",
|
||||||
});
|
});
|
||||||
|
|
||||||
falseTestUser = await userRepositoryFixture.create({
|
falseTestUser = await userRepositoryFixture.create({
|
||||||
@@ -432,6 +434,21 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
|
|
||||||
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
||||||
expect(responseTeamEvent).toBeDefined();
|
expect(responseTeamEvent).toBeDefined();
|
||||||
|
expect(responseTeamEvent?.hosts).toEqual([
|
||||||
|
{
|
||||||
|
userId: teammate1.id,
|
||||||
|
name: teammate1.name,
|
||||||
|
username: teammate1.username,
|
||||||
|
avatarUrl: teammate1.avatarUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: teammate2.id,
|
||||||
|
name: teammate2.name,
|
||||||
|
username: teammate2.username,
|
||||||
|
avatarUrl: teammate2.avatarUrl,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (!responseTeamEvent) {
|
if (!responseTeamEvent) {
|
||||||
throw new Error("Team event not found");
|
throw new Error("Team event not found");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||||
|
import { transformTeamLocationsApiToInternal } from "@/ee/event-types/event-types_2024_06_14/transformers/api-to-internal/locations";
|
||||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||||
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
|
||||||
@@ -6,7 +7,6 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
|||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { transformTeamLocationsApiToInternal } from "@calcom/platform-libraries/event-types";
|
|
||||||
import {
|
import {
|
||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
UpdateTeamEventTypeInput_2024_06_14,
|
UpdateTeamEventTypeInput_2024_06_14,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class OutputOrganizationsEventTypesService {
|
|||||||
const hosts =
|
const hosts =
|
||||||
databaseEventType.schedulingType === "MANAGED"
|
databaseEventType.schedulingType === "MANAGED"
|
||||||
? await this.getManagedEventTypeHosts(databaseEventType.id)
|
? await this.getManagedEventTypeHosts(databaseEventType.id)
|
||||||
: await this.transformHosts(databaseEventType.hosts, databaseEventType.schedulingType);
|
: await this.getNonManagedEventTypeHosts(databaseEventType.hosts, databaseEventType.schedulingType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -138,13 +138,18 @@ export class OutputOrganizationsEventTypesService {
|
|||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (child.userId) {
|
if (child.userId) {
|
||||||
const user = await this.usersRepository.findById(child.userId);
|
const user = await this.usersRepository.findById(child.userId);
|
||||||
transformedHosts.push({ userId: child.userId, name: user?.name || "" });
|
transformedHosts.push({
|
||||||
|
userId: child.userId,
|
||||||
|
name: user?.name || "",
|
||||||
|
username: user?.username || "",
|
||||||
|
avatarUrl: user?.avatarUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transformedHosts;
|
return transformedHosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
async transformHosts(
|
async getNonManagedEventTypeHosts(
|
||||||
databaseHosts: Host[],
|
databaseHosts: Host[],
|
||||||
schedulingType: SchedulingType | null
|
schedulingType: SchedulingType | null
|
||||||
): Promise<TeamEventTypeResponseHost[]> {
|
): Promise<TeamEventTypeResponseHost[]> {
|
||||||
@@ -160,6 +165,7 @@ export class OutputOrganizationsEventTypesService {
|
|||||||
transformedHosts.push({
|
transformedHosts.push({
|
||||||
userId: databaseHost.userId,
|
userId: databaseHost.userId,
|
||||||
name: databaseUser?.name || "",
|
name: databaseUser?.name || "",
|
||||||
|
username: databaseUser?.username || "",
|
||||||
mandatory: !!databaseHost.isFixed,
|
mandatory: !!databaseHost.isFixed,
|
||||||
priority: getPriorityLabel(databaseHost.priority || 2),
|
priority: getPriorityLabel(databaseHost.priority || 2),
|
||||||
avatarUrl: databaseUser?.avatarUrl,
|
avatarUrl: databaseUser?.avatarUrl,
|
||||||
@@ -168,6 +174,7 @@ export class OutputOrganizationsEventTypesService {
|
|||||||
transformedHosts.push({
|
transformedHosts.push({
|
||||||
userId: databaseHost.userId,
|
userId: databaseHost.userId,
|
||||||
name: databaseUser?.name || "",
|
name: databaseUser?.name || "",
|
||||||
|
username: databaseUser?.username || "",
|
||||||
avatarUrl: databaseUser?.avatarUrl,
|
avatarUrl: databaseUser?.avatarUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-6
@@ -2,6 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
|||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -34,11 +35,26 @@ export class ManagedOrganizationsRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByManagerOrganizationId(managerOrganizationId: number) {
|
async getByManagerOrganizationIdPaginated(managerOrganizationId: number, pagination: SkipTakePagination) {
|
||||||
return this.dbRead.prisma.managedOrganization.findMany({
|
const { skip, take } = pagination;
|
||||||
where: {
|
|
||||||
managerOrganizationId,
|
const where: Prisma.ManagedOrganizationWhereInput = {
|
||||||
},
|
managerOrganizationId,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const [totalItems, linkRows] = await this.dbRead.prisma.$transaction([
|
||||||
|
this.dbRead.prisma.managedOrganization.count({ where }),
|
||||||
|
this.dbRead.prisma.managedOrganization.findMany({
|
||||||
|
where,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
orderBy: { managedOrganizationId: "asc" },
|
||||||
|
include: { managedOrganization: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const items = linkRows.map((l) => l.managedOrganization);
|
||||||
|
|
||||||
|
return { totalItems, items };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-1
@@ -7,6 +7,7 @@ import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controller
|
|||||||
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto";
|
||||||
import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input";
|
import { CreateOrganizationInput } from "@/modules/organizations/organizations/inputs/create-managed-organization.input";
|
||||||
import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input";
|
import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input";
|
||||||
|
import { GetManagedOrganizationsOutput } from "@/modules/organizations/organizations/outputs/get-managed-organizations.output";
|
||||||
import {
|
import {
|
||||||
ManagedOrganizationWithApiKeyOutput,
|
ManagedOrganizationWithApiKeyOutput,
|
||||||
ManagedOrganizationOutput,
|
ManagedOrganizationOutput,
|
||||||
@@ -304,7 +305,7 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
.set("Authorization", `Bearer ${managerOrgAdminApiKey}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const responseBody: ApiSuccessResponse<ManagedOrganizationOutput[]> = response.body;
|
const responseBody: GetManagedOrganizationsOutput = response.body;
|
||||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
const responseManagedOrgs = responseBody.data;
|
const responseManagedOrgs = responseBody.data;
|
||||||
expect(responseManagedOrgs?.length).toEqual(1);
|
expect(responseManagedOrgs?.length).toEqual(1);
|
||||||
@@ -312,6 +313,14 @@ describe("Organizations Organizations Endpoints", () => {
|
|||||||
expect(responseManagedOrg?.id).toBeDefined();
|
expect(responseManagedOrg?.id).toBeDefined();
|
||||||
expect(responseManagedOrg?.name).toEqual(managedOrg.name);
|
expect(responseManagedOrg?.name).toEqual(managedOrg.name);
|
||||||
expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata);
|
expect(responseManagedOrg?.metadata).toEqual(managedOrg.metadata);
|
||||||
|
|
||||||
|
expect(responseBody.pagination).toBeDefined();
|
||||||
|
expect(responseBody.pagination.totalItems).toEqual(1);
|
||||||
|
expect(responseBody.pagination.remainingItems).toEqual(0);
|
||||||
|
expect(responseBody.pagination.returnedItems).toEqual(1);
|
||||||
|
expect(responseBody.pagination.itemsPerPage).toEqual(250);
|
||||||
|
expect(responseBody.pagination.currentPage).toEqual(1);
|
||||||
|
expect(responseBody.pagination.totalPages).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -28,10 +28,12 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Delete,
|
Delete,
|
||||||
|
Query,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
const SCALE = "SCALE";
|
const SCALE = "SCALE";
|
||||||
|
|
||||||
@@ -99,14 +101,15 @@ export class OrganizationsOrganizationsController {
|
|||||||
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
|
"Requires the user to have at least the 'ORG_ADMIN' role within the organization. Additionally, for platform, the plan must be 'SCALE' or higher to access this endpoint.",
|
||||||
})
|
})
|
||||||
async getOrganizations(
|
async getOrganizations(
|
||||||
@Param("orgId", ParseIntPipe) managerOrganizationId: number
|
@Param("orgId", ParseIntPipe) managerOrganizationId: number,
|
||||||
|
@Query() queryPagination: SkipTakePagination
|
||||||
): Promise<GetManagedOrganizationsOutput> {
|
): Promise<GetManagedOrganizationsOutput> {
|
||||||
const organizations = await this.managedOrganizationsService.getManagedOrganizations(
|
const { organizations, pagination: responsePagination } =
|
||||||
managerOrganizationId
|
await this.managedOrganizationsService.getManagedOrganizations(managerOrganizationId, queryPagination);
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: organizations,
|
data: organizations,
|
||||||
|
pagination: responsePagination,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -4,6 +4,7 @@ import { Expose, Type } from "class-transformer";
|
|||||||
import { IsEnum, ValidateNested } from "class-validator";
|
import { IsEnum, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { PaginationMetaDto } from "@calcom/platform-types";
|
||||||
|
|
||||||
export class GetManagedOrganizationsOutput {
|
export class GetManagedOrganizationsOutput {
|
||||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
@@ -17,4 +18,9 @@ export class GetManagedOrganizationsOutput {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => ManagedOrganizationOutput)
|
@Type(() => ManagedOrganizationOutput)
|
||||||
data!: ManagedOrganizationOutput[];
|
data!: ManagedOrganizationOutput[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: () => PaginationMetaDto })
|
||||||
|
@Type(() => PaginationMetaDto)
|
||||||
|
@ValidateNested()
|
||||||
|
pagination!: PaginationMetaDto;
|
||||||
}
|
}
|
||||||
Loaded 100 of 672 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user