Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa09fd66ff | ||
|
|
b1cbeebe48 | ||
|
|
93bad282d7 | ||
|
|
47d941676e | ||
|
|
54c4b0b65e | ||
|
|
0f8356f417 | ||
|
|
cecfcc7c6f | ||
|
|
0b3f9d06a9 | ||
|
|
71e7300830 | ||
|
|
bccffb328b | ||
|
|
80fa172f79 | ||
|
|
50173658f8 | ||
|
|
dddbd7dad4 | ||
|
|
ae2d85d81b | ||
|
|
80c407a4b7 | ||
|
|
6e9ec113fe | ||
|
|
3ee6a7ecde | ||
|
|
8ad62d183c | ||
|
|
f047799299 | ||
|
|
3a9e353443 | ||
|
|
b73a3aeb0c | ||
|
|
b6ff64ed74 | ||
|
|
1b281fe269 | ||
|
|
024913592b | ||
|
|
78a34038d2 | ||
|
|
5f770a9789 | ||
|
|
24a43877ce | ||
|
|
4a96be4175 | ||
|
|
594bb606b8 | ||
|
|
1c088f65c5 | ||
|
|
7d7a429780 | ||
|
|
d3228441aa | ||
|
|
21d6bf1fca | ||
|
|
a4134aa5a3 | ||
|
|
61397e5184 | ||
|
|
6143cd60e5 | ||
|
|
ac821f6b13 | ||
|
|
2bce38fd2a | ||
|
|
390ee79134 | ||
|
|
cfc9f0bfc3 | ||
|
|
58d5f2a17c | ||
|
|
37f271bd74 | ||
|
|
d770cfc65e | ||
|
|
ab8556790e | ||
|
|
3d89b37cc6 | ||
|
|
78b213321c | ||
|
|
276ba1179f | ||
|
|
0109d166a1 | ||
|
|
779c014256 | ||
|
|
073faf9e10 | ||
|
|
d0e39cf2dd | ||
|
|
f9488a1866 | ||
|
|
d4a3894a9e | ||
|
|
91288b41cd | ||
|
|
1aa2f68e73 | ||
|
|
ad707b0ee4 | ||
|
|
49a2f4d4f7 | ||
|
|
f576979345 | ||
|
|
b45399ab78 | ||
|
|
306dc5e4fb | ||
|
|
605304b93b | ||
|
|
d6a17b2b1e | ||
|
|
4bee875a8c | ||
|
|
fe4ae3ae62 | ||
|
|
a245578ff2 | ||
|
|
f5ec696e15 | ||
|
|
ec9384858b | ||
|
|
47613f796e | ||
|
|
446c780d85 | ||
|
|
615968c640 | ||
|
|
fc89436b6a | ||
|
|
3c1015cb11 | ||
|
|
46c513330c | ||
|
|
ef1598faeb | ||
|
|
42a6903057 | ||
|
|
0f0c2b8b92 | ||
|
|
c400c36306 | ||
|
|
6410bd2eaf | ||
|
|
2ddc5317b8 | ||
|
|
6fffe3377d | ||
|
|
eedef35d02 | ||
|
|
08d232a816 | ||
|
|
d9b3f8657f | ||
|
|
99dc5837f5 | ||
|
|
5a35540522 | ||
|
|
c01fc08f67 | ||
|
|
7397722eb3 | ||
|
|
d51e03b14b | ||
|
|
c4af180a5b | ||
|
|
ee5f1dc879 | ||
|
|
15b43176ff | ||
|
|
03240709dc | ||
|
|
7ee451ca02 | ||
|
|
55b066c475 | ||
|
|
c52bbc4be3 | ||
|
|
31b1f31d82 | ||
|
|
ab1c1bb015 | ||
|
|
45219f4736 | ||
|
|
3296255f80 | ||
|
|
81b4651d6e | ||
|
|
e3bd90c4f2 | ||
|
|
17f1932467 | ||
|
|
1b04fe82a7 | ||
|
|
ffb36509ff | ||
|
|
dc361f7879 | ||
|
|
0b59a5d184 | ||
|
|
54c17eb827 | ||
|
|
bd1142ea7a | ||
|
|
26051833cf | ||
|
|
1d8bc6ca49 | ||
|
|
fbe8cf8223 | ||
|
|
ef8b3e83ac | ||
|
|
9d228a1d96 | ||
|
|
57df25505d | ||
|
|
31cbea0e23 | ||
|
|
edd80c056c | ||
|
|
6ebf0db389 |
No files matched your search
+11
-7
@@ -159,13 +159,16 @@ SENDGRID_EMAIL=
|
|||||||
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
|
SENTRY_ORG=
|
||||||
|
SENTRY_PROJECT=
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
# Used for capturing exceptions and logging messages
|
# Used for capturing exceptions and logging messages
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
SENTRY_DEBUG=
|
SENTRY_DEBUG=
|
||||||
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
|
|
||||||
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
|
|
||||||
SENTRY_MAX_SPANS=
|
SENTRY_MAX_SPANS=
|
||||||
SENTRY_TRACES_SAMPLE_RATE=
|
SENTRY_TRACES_SAMPLE_RATE=
|
||||||
|
SENTRY_REPLAYS_SESSION_SAMPLE_RATE=
|
||||||
|
SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=
|
||||||
|
|
||||||
# Formbricks Experience Management Integration
|
# Formbricks Experience Management Integration
|
||||||
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
|
NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com
|
||||||
@@ -363,9 +366,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
|||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# disable setry server source maps
|
|
||||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
|
||||||
|
|
||||||
# api v2
|
# api v2
|
||||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ DIRECTORY_IDS_TO_LOG=
|
|||||||
# Read more about it in the README.md
|
# Read more about it in the README.md
|
||||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||||
|
|
||||||
AKISMET_API_KEY=
|
IFFY_API_KEY=
|
||||||
|
|
||||||
## Env variables related to avoiding booking failures
|
## Env variables related to avoiding booking failures
|
||||||
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
||||||
@@ -443,4 +443,8 @@ NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT=10
|
|||||||
NEXT_PUBLIC_HEAD_SCRIPTS=
|
NEXT_PUBLIC_HEAD_SCRIPTS=
|
||||||
NEXT_PUBLIC_BODY_SCRIPTS=
|
NEXT_PUBLIC_BODY_SCRIPTS=
|
||||||
|
|
||||||
DATABASE_CHUNK_SIZE=
|
DATABASE_CHUNK_SIZE=
|
||||||
|
|
||||||
|
# Service Account Encryption Key for encrypting/decrypting service account keys
|
||||||
|
# You can use: `openssl rand -base64 24` to generate one
|
||||||
|
CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||||
+7
-5
@@ -1,9 +1,9 @@
|
|||||||
/**/package.json @calcom/Foundation
|
/**/package.json @calcom/Foundation
|
||||||
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
/apps/api/**/* @calcom/Platform @calcom/Foundation
|
||||||
/apps/ui-playground/**/* @calcom/Consumer
|
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/components/**/* @calcom/Consumer
|
/apps/web/components/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/modules/**/* @calcom/Consumer
|
/apps/web/modules/**/* @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/**/layout.tsx @calcom/Foundation
|
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||||
/apps/web/lib/**/* @calcom/Foundation
|
/apps/web/lib/**/* @calcom/Foundation
|
||||||
/apps/web/middleware.ts @calcom/Foundation
|
/apps/web/middleware.ts @calcom/Foundation
|
||||||
/deploy/**/* @calcom/Foundation
|
/deploy/**/* @calcom/Foundation
|
||||||
@@ -26,5 +26,7 @@
|
|||||||
/packages/lib/slots.ts @calcom/Foundation
|
/packages/lib/slots.ts @calcom/Foundation
|
||||||
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
/packages/platform/**/* @calcom/Platform @calcom/Foundation
|
||||||
/packages/prisma/**/* @calcom/Foundation
|
/packages/prisma/**/* @calcom/Foundation
|
||||||
|
/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @calcom/Foundation
|
||||||
|
/packages/trpc/server/routers/viewer/bookings/get.handler.ts @calcom/Foundation
|
||||||
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
|
||||||
/packages/ui/**/* @calcom/Consumer
|
/packages/ui/**/* @calcom/Consumer @calcom/Foundation
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
jobs:
|
||||||
|
cleanup-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout GitHub Pages Branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: calcom/test-results
|
||||||
|
ref: gh-pages
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set Git User
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Cleanup HTML reports on PR close/merge
|
||||||
|
env:
|
||||||
|
BRANCH_REPORTS_DIR: reports/${{ github.event.pull_request.head.ref }}
|
||||||
|
run: |
|
||||||
|
if [ -d "$BRANCH_REPORTS_DIR" ]; then
|
||||||
|
rm -rf "$BRANCH_REPORTS_DIR"
|
||||||
|
git add .
|
||||||
|
git commit -m "workflow: remove all reports for branch ${{ github.event.pull_request.head.ref }} (PR closed/merged)"
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "No reports found for branch ${{ github.event.pull_request.head.ref }}. Nothing to clean up."
|
||||||
|
fi
|
||||||
@@ -219,6 +219,17 @@ jobs:
|
|||||||
uses: ./.github/workflows/publish-report.yml
|
uses: ./.github/workflows/publish-report.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
cleanup-report:
|
||||||
|
name: Cleanup HTML report
|
||||||
|
if: ${{ !cancelled() && needs.check-label.outputs.run-e2e == 'true' && (github.event.pull_request.merged == true || github.event.pull_request.state == 'closed') }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
needs: [changes, check-label, publish-report]
|
||||||
|
uses: ./.github/workflows/cleanup-report.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
required:
|
required:
|
||||||
needs:
|
needs:
|
||||||
[
|
[
|
||||||
|
|||||||
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
+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
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- calcom-monorepo
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
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
+2
@@ -0,0 +1,2 @@
|
|||||||
|
undecided:
|
||||||
|
- "@calcom/prisma"
|
||||||
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"
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
Possible issue:
|
||||||
|
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
|
||||||
|
|
||||||
|
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
|
||||||
|
- Delete Credentials when DelegationCredential is disabled
|
||||||
|
|
||||||
|
|
||||||
|
TO Test
|
||||||
|
- New members beyond the batch size are processed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Constructs the WHERE clause for Prisma booking findMany operation.
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
||||||
|
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
||||||
|
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
||||||
|
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
||||||
|
*
|
||||||
|
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
||||||
|
*/
|
||||||
|
export function buildWhereClause(userId: number | null, attendeeEmails: string[], userIds: number[] = []) {
|
||||||
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
||||||
|
|
||||||
|
let whereClause = {};
|
||||||
|
|
||||||
|
if (filterByAttendeeEmails) {
|
||||||
|
whereClause = {
|
||||||
|
AND: [
|
||||||
|
userFilter,
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: attendeeEmails },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
whereClause = {
|
||||||
|
...userFilter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return whereClause;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma, Booking } from "@prisma/client";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -6,6 +6,7 @@ import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||||
|
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
|
||||||
import {
|
import {
|
||||||
getAccessibleUsers,
|
getAccessibleUsers,
|
||||||
retrieveOrgScopedAccessibleUsers,
|
retrieveOrgScopedAccessibleUsers,
|
||||||
@@ -151,58 +152,6 @@ type GetAdminArgsType = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the WHERE clause for Prisma booking findMany operation.
|
|
||||||
*
|
|
||||||
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
|
|
||||||
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
|
|
||||||
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
|
|
||||||
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
|
|
||||||
*
|
|
||||||
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
|
|
||||||
*/
|
|
||||||
function buildWhereClause(
|
|
||||||
userId: number | null,
|
|
||||||
attendeeEmails: string[],
|
|
||||||
userIds: number[] = [],
|
|
||||||
userEmails: string[] = []
|
|
||||||
) {
|
|
||||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
|
||||||
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {};
|
|
||||||
|
|
||||||
let whereClause = {};
|
|
||||||
|
|
||||||
if (filterByAttendeeEmails) {
|
|
||||||
whereClause = {
|
|
||||||
AND: [
|
|
||||||
userFilter,
|
|
||||||
{
|
|
||||||
attendees: {
|
|
||||||
some: {
|
|
||||||
email: { in: attendeeEmails },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
whereClause = {
|
|
||||||
OR: [
|
|
||||||
userFilter,
|
|
||||||
{
|
|
||||||
attendees: {
|
|
||||||
some: {
|
|
||||||
email: { in: userEmails },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return whereClause;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handler(req: NextApiRequest) {
|
export async function handler(req: NextApiRequest) {
|
||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
@@ -238,6 +187,7 @@ export async function handler(req: NextApiRequest) {
|
|||||||
? [queryFilterForAttendeeEmails.attendeeEmail]
|
? [queryFilterForAttendeeEmails.attendeeEmail]
|
||||||
: [];
|
: [];
|
||||||
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
const filterByAttendeeEmails = attendeeEmails.length > 0;
|
||||||
|
let userEmailsToFilterBy: string[] = [];
|
||||||
|
|
||||||
/** Only admins can query other users */
|
/** Only admins can query other users */
|
||||||
if (isSystemWideAdmin) {
|
if (isSystemWideAdmin) {
|
||||||
@@ -251,7 +201,8 @@ export async function handler(req: NextApiRequest) {
|
|||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
const { userId: argUserId, userIds, userEmails } = await handleSystemWideAdminArgs(systemWideAdminArgs);
|
||||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
userEmailsToFilterBy = userEmails;
|
||||||
|
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
|
||||||
}
|
}
|
||||||
} else if (isOrganizationOwnerOrAdmin) {
|
} else if (isOrganizationOwnerOrAdmin) {
|
||||||
let requestedUserIds = [userId];
|
let requestedUserIds = [userId];
|
||||||
@@ -265,7 +216,8 @@ export async function handler(req: NextApiRequest) {
|
|||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
const { userId: argUserId, userIds, userEmails } = await handleOrgWideAdminArgs(orgWideAdminArgs);
|
||||||
args.where = buildWhereClause(argUserId, attendeeEmails, userIds, userEmails);
|
userEmailsToFilterBy = userEmails;
|
||||||
|
args.where = buildWhereClause(argUserId, attendeeEmails, userIds);
|
||||||
} else {
|
} else {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
@@ -276,7 +228,7 @@ export async function handler(req: NextApiRequest) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new HttpError({ message: "User not found", statusCode: 404 });
|
throw new HttpError({ message: "User not found", statusCode: 404 });
|
||||||
}
|
}
|
||||||
args.where = buildWhereClause(userId, attendeeEmails, [], []);
|
args.where = buildWhereClause(userId, attendeeEmails, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateFrom) {
|
if (dateFrom) {
|
||||||
@@ -317,7 +269,77 @@ export async function handler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await prisma.booking.findMany(args);
|
let data: Booking[] = [];
|
||||||
|
|
||||||
|
if (!filterByAttendeeEmails && userEmailsToFilterBy.length > 0) {
|
||||||
|
const queryOne = prisma.booking.findMany(args);
|
||||||
|
|
||||||
|
const whereClauseForQueryTwo: Prisma.BookingWhereInput = {
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: userEmailsToFilterBy },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dateFrom) {
|
||||||
|
whereClauseForQueryTwo.startTime = { gte: dateFrom };
|
||||||
|
}
|
||||||
|
if (dateTo) {
|
||||||
|
whereClauseForQueryTwo.endTime = { lte: dateTo };
|
||||||
|
}
|
||||||
|
if (status === "upcoming") {
|
||||||
|
whereClauseForQueryTwo.startTime = { gte: new Date().toISOString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const argsForQueryTwo: Prisma.BookingFindManyArgs = {
|
||||||
|
...args,
|
||||||
|
where: whereClauseForQueryTwo,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTwo = prisma.booking.findMany(argsForQueryTwo);
|
||||||
|
|
||||||
|
const [resultOne, resultTwo] = await Promise.all([queryOne, queryTwo]);
|
||||||
|
|
||||||
|
const bookingMap = new Map();
|
||||||
|
[...resultOne, ...resultTwo].forEach((booking) => {
|
||||||
|
bookingMap.set(booking.id, booking);
|
||||||
|
});
|
||||||
|
|
||||||
|
const dedupedResults = Array.from(bookingMap.values());
|
||||||
|
|
||||||
|
if (args.orderBy) {
|
||||||
|
let sortField: keyof Booking;
|
||||||
|
let sortDirection: "asc" | "desc" = "asc";
|
||||||
|
|
||||||
|
if (typeof args.orderBy === "object" && !Array.isArray(args.orderBy)) {
|
||||||
|
const orderByKey = Object.keys(args.orderBy)[0] as keyof typeof args.orderBy;
|
||||||
|
sortField = orderByKey as keyof Booking;
|
||||||
|
sortDirection = args.orderBy[orderByKey] as "asc" | "desc";
|
||||||
|
} else {
|
||||||
|
sortField = "id";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortOrder = sortDirection === "desc" ? -1 : 1;
|
||||||
|
|
||||||
|
dedupedResults.sort((a, b) => {
|
||||||
|
const aValue = a[sortField];
|
||||||
|
const bValue = b[sortField];
|
||||||
|
|
||||||
|
if (aValue < bValue) return -1 * sortOrder;
|
||||||
|
if (aValue > bValue) return 1 * sortOrder;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.take !== undefined && args.skip !== undefined) {
|
||||||
|
data = dedupedResults.slice(args.skip, args.skip + args.take);
|
||||||
|
} else {
|
||||||
|
data = dedupedResults;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = await prisma.booking.findMany(args);
|
||||||
|
}
|
||||||
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,12 @@ async function verifyCredentialsAndGetId({
|
|||||||
currentCredentialId: number | null;
|
currentCredentialId: number | null;
|
||||||
}) {
|
}) {
|
||||||
if (parsedBody.integration && parsedBody.externalId) {
|
if (parsedBody.integration && parsedBody.externalId) {
|
||||||
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
|
const calendarCredentials = getCalendarCredentialsWithoutDelegation(
|
||||||
|
userCredentials.map((cred) => ({
|
||||||
|
...cred,
|
||||||
|
delegationCredentialId: null,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const { connectedCalendars } = await getConnectedCalendars(
|
const { connectedCalendars } = await getConnectedCalendars(
|
||||||
calendarCredentials,
|
calendarCredentials,
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ import { membershipCreateBodySchema, schemaMembershipPublic } from "~/lib/valida
|
|||||||
*/
|
*/
|
||||||
async function postHandler(req: NextApiRequest) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
const data = membershipCreateBodySchema.parse(req.body);
|
const data = membershipCreateBodySchema.parse(req.body);
|
||||||
const args: Prisma.MembershipCreateArgs = { data };
|
const args: Prisma.MembershipCreateArgs = {
|
||||||
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
await checkPermissions(req);
|
await checkPermissions(req);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -206,8 +207,155 @@ describe("GET /api/bookings", async () => {
|
|||||||
console.log("bookings=>", responseData.bookings);
|
console.log("bookings=>", responseData.bookings);
|
||||||
responseData.bookings.forEach((booking) => {
|
responseData.bookings.forEach((booking) => {
|
||||||
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
||||||
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
|
if (booking.id === 19) {
|
||||||
|
// The team field can be either null or undefined due to nullish() in the schema
|
||||||
|
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Date filtering", () => {
|
||||||
|
it("filters bookings by dateFrom", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateFrom: "2023-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(
|
||||||
|
new Date("2023-01-01T00:00:00Z").getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters bookings by dateTo", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
dateTo: "2024-12-31T23:59:59Z",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
expect(new Date(booking.endTime).getTime()).toBeLessThanOrEqual(
|
||||||
|
new Date("2024-12-31T23:59:59Z").getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Sorting and ordering", () => {
|
||||||
|
it("sorts bookings by createdAt in descending order", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
sortBy: "createdAt",
|
||||||
|
order: "desc",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
const timestamps = responseData.bookings.map((b) => new Date(b.createdAt).getTime());
|
||||||
|
expect(timestamps).toEqual([...timestamps].sort((a, b) => b - a));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Multiple attendee email filtering", () => {
|
||||||
|
it("filters bookings by multiple attendee emails", async () => {
|
||||||
|
const attendeeEmails = ["test1@example.com", "test2@example.com"];
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
attendeeEmail: attendeeEmails,
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
responseData.bookings.forEach((booking) => {
|
||||||
|
const bookingAttendeeEmails = booking.attendees?.map((a) => a.email);
|
||||||
|
expect(bookingAttendeeEmails?.some((email) => attendeeEmails.includes(email))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Error cases", () => {
|
||||||
|
it("throws error for invalid status parameter", async () => {
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
status: "invalid_status",
|
||||||
|
},
|
||||||
|
pagination: DefaultPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.userId = proUser.id;
|
||||||
|
|
||||||
|
await expect(handler(req)).rejects.toThrow(ZodError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Result merging", () => {
|
||||||
|
it("does not return duplicate bookings when merging results from multiple queries", async () => {
|
||||||
|
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||||
|
|
||||||
|
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||||
|
|
||||||
|
const testUserBooking = await prisma.booking.findFirstOrThrow({
|
||||||
|
where: { userId: testUser.id },
|
||||||
|
include: { attendees: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
userId: testUser.id, // This will make userEmailsToFilterBy contain the test user's email
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
take: 100,
|
||||||
|
skip: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isSystemWideAdmin = true;
|
||||||
|
req.userId = adminUser.id;
|
||||||
|
|
||||||
|
const responseData = await handler(req);
|
||||||
|
|
||||||
|
const bookingIds = responseData.bookings.map((booking) => booking.id);
|
||||||
|
|
||||||
|
const uniqueBookingIds = new Set(bookingIds);
|
||||||
|
|
||||||
|
expect(uniqueBookingIds.size).toBe(bookingIds.length);
|
||||||
|
|
||||||
|
if (uniqueBookingIds.size !== bookingIds.length) {
|
||||||
|
const counts = bookingIds.reduce((acc, id) => {
|
||||||
|
acc[id] = (acc[id] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, number>);
|
||||||
|
|
||||||
|
const duplicates = Object.entries(counts)
|
||||||
|
.filter(([_, count]) => count > 1)
|
||||||
|
.map(([id]) => id);
|
||||||
|
|
||||||
|
console.log(`Found duplicate booking IDs: ${duplicates.join(", ")}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { buildWhereClause } from "~/lib/utils/bookings/get/buildWhereClause";
|
||||||
|
|
||||||
|
describe("buildWhereClause", () => {
|
||||||
|
it("should return empty object when no filters are provided", () => {
|
||||||
|
const result = buildWhereClause(null, [], []);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by userId when only userId is provided", () => {
|
||||||
|
const userId = 123;
|
||||||
|
const result = buildWhereClause(userId, [], []);
|
||||||
|
expect(result).toEqual({ userId: 123 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by userIds when userIds array is provided", () => {
|
||||||
|
const userIds = [1, 2, 3];
|
||||||
|
const result = buildWhereClause(null, [], userIds);
|
||||||
|
expect(result).toEqual({ userId: { in: [1, 2, 3] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter by attendee emails when provided", () => {
|
||||||
|
const attendeeEmails = ["test@example.com", "user@example.com"];
|
||||||
|
const result = buildWhereClause(null, attendeeEmails, []);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com", "user@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine userId and attendee email filters", () => {
|
||||||
|
const userId = 123;
|
||||||
|
const attendeeEmails = ["test@example.com"];
|
||||||
|
const result = buildWhereClause(userId, attendeeEmails, []);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{ userId: 123 },
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine userIds and attendee email filters", () => {
|
||||||
|
const userIds = [1, 2, 3];
|
||||||
|
const attendeeEmails = ["test@example.com"];
|
||||||
|
const result = buildWhereClause(null, attendeeEmails, userIds);
|
||||||
|
expect(result).toEqual({
|
||||||
|
AND: [
|
||||||
|
{ userId: { in: [1, 2, 3] } },
|
||||||
|
{
|
||||||
|
attendees: {
|
||||||
|
some: {
|
||||||
|
email: { in: ["test@example.com"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
},
|
},
|
||||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||||
|
"setupFilesAfterEnv": ["<rootDir>/test/jest.setup-e2e.ts"],
|
||||||
"reporters": ["default", "jest-summarizing-reporter"],
|
"reporters": ["default", "jest-summarizing-reporter"],
|
||||||
"workerIdleMemoryLimit": "512MB",
|
"workerIdleMemoryLimit": "512MB",
|
||||||
"maxWorkers": 8
|
"maxWorkers": 8
|
||||||
|
|||||||
@@ -38,7 +38,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.176",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.197",
|
||||||
"@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": "*",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
|||||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
import { BillingModule } from "@/modules/billing/billing.module";
|
import { BillingModule } from "@/modules/billing/billing.module";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
KyselyModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
|||||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||||
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -99,6 +100,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly oAuthFlowService: OAuthFlowService,
|
private readonly oAuthFlowService: OAuthFlowService,
|
||||||
private readonly prismaReadService: PrismaReadService,
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly kyselyReadService: KyselyReadService,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@@ -129,6 +131,7 @@ export class BookingsController_2024_04_15 {
|
|||||||
ctx: {
|
ctx: {
|
||||||
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
|
user: { email: user.email, id: user.id, orgId: profile?.organizationId },
|
||||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
kysely: this.kyselyReadService.kysely,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BookingReferencesRepository_2024_08_13 {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService) {}
|
||||||
|
|
||||||
|
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||||
|
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
|
||||||
|
|
||||||
|
if (filter?.type) {
|
||||||
|
whereClause.type = filter.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dbRead.prisma.bookingReference.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
select: {
|
||||||
|
type: true,
|
||||||
|
uid: true,
|
||||||
|
id: true,
|
||||||
|
externalCalendarId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
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 { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||||
@@ -17,6 +20,7 @@ import { BillingModule } from "@/modules/billing/billing.module";
|
|||||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { KyselyModule } from "@/modules/kysely/kysely.module";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||||
@@ -36,6 +40,7 @@ import { Module } from "@nestjs/common";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
KyselyModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
TokensModule,
|
TokensModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
@@ -56,6 +61,7 @@ import { Module } from "@nestjs/common";
|
|||||||
BookingsService_2024_08_13,
|
BookingsService_2024_08_13,
|
||||||
InputBookingsService_2024_08_13,
|
InputBookingsService_2024_08_13,
|
||||||
OutputBookingsService_2024_08_13,
|
OutputBookingsService_2024_08_13,
|
||||||
|
OutputBookingReferencesService_2024_08_13,
|
||||||
BookingsRepository_2024_08_13,
|
BookingsRepository_2024_08_13,
|
||||||
EventTypesRepository_2024_06_14,
|
EventTypesRepository_2024_06_14,
|
||||||
BookingSeatRepository,
|
BookingSeatRepository,
|
||||||
@@ -69,6 +75,8 @@ import { Module } from "@nestjs/common";
|
|||||||
OrganizationsTeamsRepository,
|
OrganizationsTeamsRepository,
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
ErrorsBookingsService_2024_08_13,
|
ErrorsBookingsService_2024_08_13,
|
||||||
|
BookingReferencesService_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
],
|
],
|
||||||
controllers: [BookingsController_2024_08_13],
|
controllers: [BookingsController_2024_08_13],
|
||||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||||
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
|
||||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
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 { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.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 { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||||
import { UsersService } from "@/modules/users/services/users.service";
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
@@ -82,7 +87,8 @@ export class BookingsController_2024_08_13 {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly bookingsService: BookingsService_2024_08_13,
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
private readonly usersService: UsersService
|
private readonly usersService: UsersService,
|
||||||
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -386,4 +392,30 @@ export class BookingsController_2024_08_13 {
|
|||||||
data: calendarLinks,
|
data: calendarLinks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/references")
|
||||||
|
@PlatformPlan("SCALE")
|
||||||
|
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||||
|
@Permissions([BOOKING_READ])
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get 'Booking References' for a booking",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getBookingReferences(
|
||||||
|
@Param("bookingUid") bookingUid: string,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() filter: BookingReferencesFilterInput_2024_08_13
|
||||||
|
): Promise<BookingReferencesOutput_2024_08_13> {
|
||||||
|
const bookingReferences = await this.bookingReferencesService.getBookingReferences(
|
||||||
|
bookingUid,
|
||||||
|
userId,
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookingReferences,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -385,7 +385,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -430,7 +430,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -459,7 +459,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -502,7 +502,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
|||||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
expect(data.rating).toEqual(bookingInThePast.rating);
|
expect(data.rating).toEqual(bookingInThePast.rating);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -642,7 +642,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
|||||||
await bookingsRepositoryFixture.deleteById(mockBooking.id);
|
await bookingsRepositoryFixture.deleteById(mockBooking.id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsIn, IsOptional } from "class-validator";
|
||||||
|
|
||||||
|
export const BookingReferences = [
|
||||||
|
"google_calendar",
|
||||||
|
"office365_calendar",
|
||||||
|
"daily_video",
|
||||||
|
"google_video",
|
||||||
|
"office365_video",
|
||||||
|
"zoom_video",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export class BookingReferencesFilterInput_2024_08_13 {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Filter booking references by type",
|
||||||
|
required: false,
|
||||||
|
enum: BookingReferences,
|
||||||
|
example: "google_calendar",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(BookingReferences)
|
||||||
|
type?: (typeof BookingReferences)[number];
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsEnum, IsNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class BookingReference {
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The type of the booking reference",
|
||||||
|
})
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The event uid of the booking",
|
||||||
|
})
|
||||||
|
eventUid!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The id of the calendar the event is created in",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
destinationCalendarId!: string | null;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The id of the booking reference",
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BookingReferencesOutput_2024_08_13 {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The status of the request, always 'success' for successful responses",
|
||||||
|
example: SUCCESS_STATUS,
|
||||||
|
})
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Booking References",
|
||||||
|
type: [BookingReference],
|
||||||
|
})
|
||||||
|
data!: BookingReference[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
|
import { Injectable, NotFoundException, BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BookingReferencesService_2024_08_13 {
|
||||||
|
constructor(
|
||||||
|
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||||
|
private readonly bookingReferencesRepository: BookingReferencesRepository_2024_08_13,
|
||||||
|
private readonly outputBookingReferencesService: OutputBookingReferencesService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getBookingReferences(
|
||||||
|
bookingUid: string,
|
||||||
|
userId: number,
|
||||||
|
filter?: BookingReferencesFilterInput_2024_08_13
|
||||||
|
) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.user?.id !== userId) {
|
||||||
|
throw new BadRequestException(`Booking with uid ${bookingUid} does not belong to user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
|
||||||
|
|
||||||
|
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrgBookingReferences(bookingUid: string, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||||
|
const booking = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingReferences = await this.bookingReferencesRepository.getBookingReferences(booking.id, filter);
|
||||||
|
|
||||||
|
return this.outputBookingReferencesService.getOutputBookingReferences(bookingReferences);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.
|
|||||||
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 { 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 { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
@@ -78,6 +79,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||||
private readonly prismaReadService: PrismaReadService,
|
private readonly prismaReadService: PrismaReadService,
|
||||||
|
private readonly kyselyReadService: KyselyReadService,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingService: BillingService,
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
@@ -385,6 +387,7 @@ export class BookingsService_2024_08_13 {
|
|||||||
ctx: {
|
ctx: {
|
||||||
user,
|
user,
|
||||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||||
|
kysely: this.kyselyReadService.kysely,
|
||||||
},
|
},
|
||||||
sort: this.inputService.transformGetBookingsSort(queryParams),
|
sort: this.inputService.transformGetBookingsSort(queryParams),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
interface IBookingReference {
|
||||||
|
type: string;
|
||||||
|
uid: string;
|
||||||
|
id: number;
|
||||||
|
externalCalendarId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OutputBookingReferencesService_2024_08_13 {
|
||||||
|
getOutputBookingReferences(bookingReferences: IBookingReference[]) {
|
||||||
|
return bookingReferences.map((bookingReference) => ({
|
||||||
|
type: bookingReference.type,
|
||||||
|
eventUid: bookingReference.uid,
|
||||||
|
id: bookingReference.id,
|
||||||
|
destinationCalendarId: bookingReference?.externalCalendarId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
RecurringBookingOutput_2024_08_13,
|
RecurringBookingOutput_2024_08_13,
|
||||||
SeatedAttendee,
|
SeatedAttendee,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
import { Booking, BookingReference, BookingSeat } from "@calcom/prisma/client";
|
||||||
|
|
||||||
export const bookingResponsesSchema = z
|
export const bookingResponsesSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export class AppleCalendarService implements CredentialSyncCalendarApp {
|
|||||||
teamId: null,
|
teamId: null,
|
||||||
appId: APPLE_CALENDAR_ID,
|
appId: APPLE_CALENDAR_ID,
|
||||||
invalid: false,
|
invalid: false,
|
||||||
|
delegationCredentialId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const dav = new CalendarService({
|
const dav = new CalendarService({
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export class IcsFeedService implements ICSFeedCalendarApp {
|
|||||||
teamId: null,
|
teamId: null,
|
||||||
appId: ICS_CALENDAR,
|
appId: ICS_CALENDAR,
|
||||||
invalid: false,
|
invalid: false,
|
||||||
|
delegationCredentialId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+8
@@ -490,6 +490,7 @@ describe("Event types Endpoints", () => {
|
|||||||
requiresBookerEmailVerification: false,
|
requiresBookerEmailVerification: false,
|
||||||
hideCalendarNotes: false,
|
hideCalendarNotes: false,
|
||||||
hideCalendarEventDetails: false,
|
hideCalendarEventDetails: false,
|
||||||
|
hideOrganizerEmail: false,
|
||||||
lockTimeZoneToggleOnBookingPage: true,
|
lockTimeZoneToggleOnBookingPage: true,
|
||||||
color: {
|
color: {
|
||||||
darkThemeHex: "#292929",
|
darkThemeHex: "#292929",
|
||||||
@@ -529,6 +530,7 @@ describe("Event types Endpoints", () => {
|
|||||||
|
|
||||||
expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||||
expect(createdEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
expect(createdEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||||
|
expect(createdEventType.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||||
expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
@@ -610,6 +612,7 @@ describe("Event types Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||||
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||||
|
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
eventType.lockTimeZoneToggleOnBookingPage
|
eventType.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
@@ -1009,6 +1012,7 @@ describe("Event types Endpoints", () => {
|
|||||||
requiresBookerEmailVerification: true,
|
requiresBookerEmailVerification: true,
|
||||||
hideCalendarNotes: true,
|
hideCalendarNotes: true,
|
||||||
hideCalendarEventDetails: true,
|
hideCalendarEventDetails: true,
|
||||||
|
hideOrganizerEmail: true,
|
||||||
lockTimeZoneToggleOnBookingPage: true,
|
lockTimeZoneToggleOnBookingPage: true,
|
||||||
color: {
|
color: {
|
||||||
darkThemeHex: "#292929",
|
darkThemeHex: "#292929",
|
||||||
@@ -1073,6 +1077,7 @@ describe("Event types Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||||
expect(updatedEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
expect(updatedEventType.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||||
|
expect(updatedEventType.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||||
expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
body.lockTimeZoneToggleOnBookingPage
|
body.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
@@ -1093,6 +1098,7 @@ describe("Event types Endpoints", () => {
|
|||||||
eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification;
|
eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification;
|
||||||
eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes;
|
eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes;
|
||||||
eventType.hideCalendarEventDetails = updatedEventType.hideCalendarEventDetails;
|
eventType.hideCalendarEventDetails = updatedEventType.hideCalendarEventDetails;
|
||||||
|
eventType.hideOrganizerEmail = updatedEventType.hideOrganizerEmail;
|
||||||
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
|
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
|
||||||
eventType.color = updatedEventType.color;
|
eventType.color = updatedEventType.color;
|
||||||
eventType.bookingFields = updatedEventType.bookingFields;
|
eventType.bookingFields = updatedEventType.bookingFields;
|
||||||
@@ -1163,6 +1169,7 @@ describe("Event types Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||||
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||||
|
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
eventType.lockTimeZoneToggleOnBookingPage
|
eventType.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
@@ -1204,6 +1211,7 @@ describe("Event types Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
|
||||||
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
expect(fetchedEventType.hideCalendarEventDetails).toEqual(eventType.hideCalendarEventDetails);
|
||||||
|
expect(fetchedEventType.hideOrganizerEmail).toEqual(eventType.hideOrganizerEmail);
|
||||||
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
|
||||||
eventType.lockTimeZoneToggleOnBookingPage
|
eventType.lockTimeZoneToggleOnBookingPage
|
||||||
);
|
);
|
||||||
|
|||||||
+3
@@ -87,6 +87,7 @@ type Input = Pick<
|
|||||||
| "destinationCalendar"
|
| "destinationCalendar"
|
||||||
| "useEventTypeDestinationCalendarEmail"
|
| "useEventTypeDestinationCalendarEmail"
|
||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
|
| "hideOrganizerEmail"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -123,6 +124,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
seatsShowAttendees,
|
seatsShowAttendees,
|
||||||
useEventTypeDestinationCalendarEmail,
|
useEventTypeDestinationCalendarEmail,
|
||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
|
hideOrganizerEmail,
|
||||||
} = databaseEventType;
|
} = databaseEventType;
|
||||||
|
|
||||||
const locations = this.transformLocations(databaseEventType.locations);
|
const locations = this.transformLocations(databaseEventType.locations);
|
||||||
@@ -197,6 +199,7 @@ export class OutputEventTypesService_2024_06_14 {
|
|||||||
destinationCalendar,
|
destinationCalendar,
|
||||||
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
|
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
|
||||||
hideCalendarEventDetails,
|
hideCalendarEventDetails,
|
||||||
|
hideOrganizerEmail,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { RateLimitType } from "@/lib/throttler-guard";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
|
||||||
|
export const Throttle = Reflector.createDecorator<RateLimitType>();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getEnv } from "@/env";
|
import { getEnv } from "@/env";
|
||||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||||
import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||||
@@ -22,7 +23,7 @@ const rateLimitSchema = z.object({
|
|||||||
ttl: z.number(),
|
ttl: z.number(),
|
||||||
blockDuration: z.number(),
|
blockDuration: z.number(),
|
||||||
});
|
});
|
||||||
type RateLimitType = z.infer<typeof rateLimitSchema>;
|
export type RateLimitType = z.infer<typeof rateLimitSchema>;
|
||||||
const rateLimitsSchema = z.array(rateLimitSchema);
|
const rateLimitsSchema = z.array(rateLimitSchema);
|
||||||
|
|
||||||
const sixtySecondsMs = 60 * 1000;
|
const sixtySecondsMs = 60 * 1000;
|
||||||
@@ -52,7 +53,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
|
|
||||||
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
|
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
|
||||||
const { context } = requestProps;
|
const { context } = requestProps;
|
||||||
|
const throttleOptions = this.reflector.get(Throttle, context.getHandler());
|
||||||
const request = context.switchToHttp().getRequest<Request>();
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
||||||
const response = context.switchToHttp().getResponse<Response>();
|
const response = context.switchToHttp().getResponse<Response>();
|
||||||
@@ -63,6 +64,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
|
)}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (throttleOptions) {
|
||||||
|
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
|
||||||
|
}
|
||||||
|
|
||||||
if (tracker.startsWith("api_key_")) {
|
if (tracker.startsWith("api_key_")) {
|
||||||
return this.handleApiKeyRequest(tracker, response);
|
return this.handleApiKeyRequest(tracker, response);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,6 +75,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) {
|
||||||
|
const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response);
|
||||||
|
if (isBlocked) {
|
||||||
|
throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
|
||||||
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
|
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
|
Delete,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { ApiExcludeController } from "@nestjs/swagger";
|
import { ApiExcludeController } from "@nestjs/swagger";
|
||||||
@@ -96,6 +97,19 @@ export class BillingController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete("/:organizationId/unsubscribe")
|
||||||
|
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||||
|
@MembershipRoles(["OWNER", "ADMIN"])
|
||||||
|
async cancelTeamSubscriptionInStripe(
|
||||||
|
@Param("organizationId") organizationId: number
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
await this.billingService.cancelTeamSubscription(organizationId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "success",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/webhook")
|
@Post("/webhook")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async stripeWebhook(
|
async stripeWebhook(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
|
|||||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
import { InjectQueue } from "@nestjs/bull";
|
import { InjectQueue } from "@nestjs/bull";
|
||||||
import {
|
import {
|
||||||
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
Logger,
|
Logger,
|
||||||
@@ -313,6 +314,28 @@ export class BillingService implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelTeamSubscription(teamId: number) {
|
||||||
|
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||||
|
const customerId = teamWithBilling?.platformBilling?.customerId;
|
||||||
|
|
||||||
|
if (!customerId) {
|
||||||
|
throw new NotFoundException("No customer id found for team in Stripe");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) {
|
||||||
|
throw new NotFoundException("Team plan not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.stripeService
|
||||||
|
.getStripe()
|
||||||
|
.subscriptions.cancel(teamWithBilling?.platformBilling?.subscriptionId);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.log(error, "error while cancelling team subscription in stripe");
|
||||||
|
throw new BadRequestException("Failed to cancel team subscription");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
try {
|
try {
|
||||||
await this.billingQueue.close();
|
await this.billingQueue.close();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/
|
|||||||
import { RouterModule } from "@/modules/router/router.module";
|
import { RouterModule } from "@/modules/router/router.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||||
|
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
|||||||
OrganizationsUsersBookingsModule,
|
OrganizationsUsersBookingsModule,
|
||||||
OrganizationsBookingsModule,
|
OrganizationsBookingsModule,
|
||||||
OrganizationsRoutingFormsModule,
|
OrganizationsRoutingFormsModule,
|
||||||
|
VerifiedResourcesModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { OnModuleDestroy } from "@nestjs/common";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { DeduplicateJoinsPlugin, Kysely, ParseJSONResultsPlugin, PostgresDialect } from "kysely";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
import type { DB } from "@calcom/kysely/types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KyselyReadService implements OnModuleDestroy {
|
||||||
|
private logger = new Logger("KyselyReadService");
|
||||||
|
|
||||||
|
public kysely: Kysely<DB>;
|
||||||
|
|
||||||
|
constructor(readonly configService: ConfigService) {
|
||||||
|
const dbUrl = configService.get("db.readUrl", { infer: true });
|
||||||
|
const pool = new Pool({ connectionString: dbUrl });
|
||||||
|
|
||||||
|
// 3. Create the Dialect, passing the configured pool instance
|
||||||
|
const dialect = new PostgresDialect({
|
||||||
|
pool: pool, // Use the pool instance created above
|
||||||
|
});
|
||||||
|
this.kysely = new Kysely<DB>({
|
||||||
|
dialect,
|
||||||
|
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
try {
|
||||||
|
await this.kysely.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { OnModuleDestroy } from "@nestjs/common";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { Kysely, ParseJSONResultsPlugin, PostgresDialect, DeduplicateJoinsPlugin } from "kysely";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
import type { DB } from "@calcom/kysely/types";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KyselyWriteService implements OnModuleDestroy {
|
||||||
|
private logger = new Logger("KyselyWriteService");
|
||||||
|
|
||||||
|
public kysely: Kysely<DB>;
|
||||||
|
|
||||||
|
constructor(readonly configService: ConfigService) {
|
||||||
|
const dbUrl = configService.get("db.writeUrl", { infer: true });
|
||||||
|
const pool = new Pool({ connectionString: dbUrl });
|
||||||
|
|
||||||
|
// 3. Create the Dialect, passing the configured pool instance
|
||||||
|
const dialect = new PostgresDialect({
|
||||||
|
pool: pool, // Use the pool instance created above
|
||||||
|
});
|
||||||
|
this.kysely = new Kysely<DB>({
|
||||||
|
dialect,
|
||||||
|
plugins: [new ParseJSONResultsPlugin(), new DeduplicateJoinsPlugin()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
try {
|
||||||
|
await this.kysely.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||||
|
import { KyselyWriteService } from "@/modules/kysely/kysely-write.service";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [KyselyReadService, KyselyWriteService],
|
||||||
|
exports: [KyselyReadService, KyselyWriteService],
|
||||||
|
})
|
||||||
|
export class KyselyModule {}
|
||||||
@@ -63,6 +63,7 @@ export class MembershipsRepository {
|
|||||||
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
async createMembership(teamId: number, userId: number, role: MembershipRole, accepted: boolean) {
|
||||||
const membership = await this.dbRead.prisma.membership.create({
|
const membership = await this.dbRead.prisma.membership.create({
|
||||||
data: {
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
role,
|
role,
|
||||||
teamId,
|
teamId,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
+4
@@ -340,6 +340,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
requiresBookerEmailVerification: true,
|
requiresBookerEmailVerification: true,
|
||||||
hideCalendarNotes: true,
|
hideCalendarNotes: true,
|
||||||
hideCalendarEventDetails: true,
|
hideCalendarEventDetails: true,
|
||||||
|
hideOrganizerEmail: true,
|
||||||
lockTimeZoneToggleOnBookingPage: true,
|
lockTimeZoneToggleOnBookingPage: true,
|
||||||
color: {
|
color: {
|
||||||
darkThemeHex: "#292929",
|
darkThemeHex: "#292929",
|
||||||
@@ -371,6 +372,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
||||||
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||||
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||||
|
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||||
expect(data.color).toEqual(body.color);
|
expect(data.color).toEqual(body.color);
|
||||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||||
@@ -1099,6 +1101,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
requiresBookerEmailVerification: true,
|
requiresBookerEmailVerification: true,
|
||||||
hideCalendarNotes: true,
|
hideCalendarNotes: true,
|
||||||
hideCalendarEventDetails: true,
|
hideCalendarEventDetails: true,
|
||||||
|
hideOrganizerEmail: true,
|
||||||
lockTimeZoneToggleOnBookingPage: true,
|
lockTimeZoneToggleOnBookingPage: true,
|
||||||
color: {
|
color: {
|
||||||
darkThemeHex: "#292929",
|
darkThemeHex: "#292929",
|
||||||
@@ -1130,6 +1133,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
||||||
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||||
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||||
|
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||||
expect(data.color).toEqual(body.color);
|
expect(data.color).toEqual(body.color);
|
||||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type Input = Pick<
|
|||||||
| "eventName"
|
| "eventName"
|
||||||
| "useEventTypeDestinationCalendarEmail"
|
| "useEventTypeDestinationCalendarEmail"
|
||||||
| "hideCalendarEventDetails"
|
| "hideCalendarEventDetails"
|
||||||
|
| "hideOrganizerEmail"
|
||||||
| "team"
|
| "team"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
+196
-10
@@ -6,6 +6,7 @@ import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/o
|
|||||||
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
|
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
|
||||||
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
|
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
|
||||||
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
||||||
|
import { OrgUserAttribute } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
@@ -14,17 +15,16 @@ import { UsersModule } from "@/modules/users/users.module";
|
|||||||
import { INestApplication } from "@nestjs/common";
|
import { INestApplication } from "@nestjs/common";
|
||||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import { Test } from "@nestjs/testing";
|
import { Test } from "@nestjs/testing";
|
||||||
import { User } from "@prisma/client";
|
import { Attribute, AttributeOption, User } from "@prisma/client";
|
||||||
import * as request from "supertest";
|
import * as request from "supertest";
|
||||||
|
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
|
||||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
|
||||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
import { randomString } from "test/utils/randomString";
|
import { randomString } from "test/utils/randomString";
|
||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
|
||||||
import { Membership, Team } from "@calcom/prisma/client";
|
import { Membership, Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Organizations Memberships Endpoints", () => {
|
describe("Organizations Memberships Endpoints", () => {
|
||||||
@@ -33,7 +33,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let attributesRepositoryFixture: AttributeRepositoryFixture;
|
||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let membership: Membership;
|
let membership: Membership;
|
||||||
@@ -49,6 +50,17 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userToInviteViaApi: User;
|
let userToInviteViaApi: User;
|
||||||
|
|
||||||
|
let textAttribute: Attribute;
|
||||||
|
let multiSelectAttribute: Attribute;
|
||||||
|
let numberAttribute: Attribute;
|
||||||
|
let singleSelectAttribute: Attribute;
|
||||||
|
|
||||||
|
let textAttributeOption: AttributeOption;
|
||||||
|
let multiSelectAttributeOption: AttributeOption;
|
||||||
|
let multiSelectAttributeOption2: AttributeOption;
|
||||||
|
let numberAttributeOption: AttributeOption;
|
||||||
|
let singleSelectAttributeOption: AttributeOption;
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
some: "key",
|
some: "key",
|
||||||
};
|
};
|
||||||
@@ -64,7 +76,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -91,16 +104,42 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
membership = await membershipsRepositoryFixture.create({
|
await setupAttributes();
|
||||||
|
|
||||||
|
membership = await membershipRepositoryFixture.create({
|
||||||
role: "ADMIN",
|
role: "ADMIN",
|
||||||
user: { connect: { id: user.id } },
|
user: { connect: { id: user.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
membership2 = await membershipsRepositoryFixture.create({
|
membership2 = await membershipRepositoryFixture.create({
|
||||||
role: "MEMBER",
|
role: "MEMBER",
|
||||||
user: { connect: { id: user2.id } },
|
user: { connect: { id: user2.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
|
AttributeToUser: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: textAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: multiSelectAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: multiSelectAttributeOption2.id } },
|
||||||
|
weight: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: numberAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attributeOption: { connect: { id: singleSelectAttributeOption.id } },
|
||||||
|
weight: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
@@ -109,6 +148,87 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function setupAttributes() {
|
||||||
|
textAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "TEXT",
|
||||||
|
name: "team",
|
||||||
|
slug: `team-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "MULTI_SELECT",
|
||||||
|
name: "skills",
|
||||||
|
slug: `skills-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
numberAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "NUMBER",
|
||||||
|
name: "age",
|
||||||
|
slug: `age-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
singleSelectAttribute = await attributesRepositoryFixture.create({
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
type: "SINGLE_SELECT",
|
||||||
|
name: "frontend",
|
||||||
|
slug: `frontend-${randomString()}`,
|
||||||
|
enabled: true,
|
||||||
|
usersCanEditRelation: false,
|
||||||
|
isWeightsEnabled: false,
|
||||||
|
isLocked: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
textAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: textAttribute.id } },
|
||||||
|
value: "coders",
|
||||||
|
slug: "coders",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: multiSelectAttribute.id } },
|
||||||
|
value: "javascript",
|
||||||
|
slug: "javascript",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
multiSelectAttributeOption2 = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: multiSelectAttribute.id } },
|
||||||
|
value: "typescript",
|
||||||
|
slug: "typescript",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
numberAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: numberAttribute.id } },
|
||||||
|
value: "18",
|
||||||
|
slug: "18",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
singleSelectAttributeOption = await attributesRepositoryFixture.createOption({
|
||||||
|
attribute: { connect: { id: singleSelectAttribute.id } },
|
||||||
|
value: "yes",
|
||||||
|
slug: "yes",
|
||||||
|
isGroup: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("should be defined", () => {
|
it("should be defined", () => {
|
||||||
expect(userRepositoryFixture).toBeDefined();
|
expect(userRepositoryFixture).toBeDefined();
|
||||||
expect(organizationsRepositoryFixture).toBeDefined();
|
expect(organizationsRepositoryFixture).toBeDefined();
|
||||||
@@ -139,9 +259,54 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
expect(responseBody.data[1].user.email).toEqual(user2.email);
|
expect(responseBody.data[1].user.email).toEqual(user2.email);
|
||||||
expect(responseBody.data[1].user.username).toEqual(user2.username);
|
expect(responseBody.data[1].user.username).toEqual(user2.username);
|
||||||
expect(responseBody.data[1].teamId).toEqual(org.id);
|
expect(responseBody.data[1].teamId).toEqual(org.id);
|
||||||
|
userHasCorrectAttributes(responseBody.data[1].attributes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function userHasCorrectAttributes(attributes: OrgUserAttribute[]) {
|
||||||
|
expect(attributes.length).toEqual(4);
|
||||||
|
const responseNumberAttribute = attributes.find((attr) => attr.type === "number");
|
||||||
|
const responseSingleSelectAttribute = attributes.find((attr) => attr.type === "singleSelect");
|
||||||
|
const responseMultiSelectAttribute = attributes.find((attr) => attr.type === "multiSelect");
|
||||||
|
const responseTextAttribute = attributes.find((attr) => attr.type === "text");
|
||||||
|
expect(responseNumberAttribute).toEqual({
|
||||||
|
id: numberAttribute.id,
|
||||||
|
name: numberAttribute.name,
|
||||||
|
optionId: numberAttributeOption.id,
|
||||||
|
option: +numberAttributeOption.value,
|
||||||
|
type: "number",
|
||||||
|
});
|
||||||
|
expect(responseSingleSelectAttribute).toEqual({
|
||||||
|
id: singleSelectAttribute.id,
|
||||||
|
name: singleSelectAttribute.name,
|
||||||
|
optionId: singleSelectAttributeOption.id,
|
||||||
|
option: singleSelectAttributeOption.value,
|
||||||
|
type: "singleSelect",
|
||||||
|
});
|
||||||
|
expect(responseMultiSelectAttribute).toEqual({
|
||||||
|
id: multiSelectAttribute.id,
|
||||||
|
name: multiSelectAttribute.name,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
optionId: multiSelectAttributeOption2.id,
|
||||||
|
option: multiSelectAttributeOption2.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
optionId: multiSelectAttributeOption.id,
|
||||||
|
option: multiSelectAttributeOption.value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: "multiSelect",
|
||||||
|
});
|
||||||
|
expect(responseTextAttribute).toEqual({
|
||||||
|
id: textAttribute.id,
|
||||||
|
name: textAttribute.name,
|
||||||
|
optionId: textAttributeOption.id,
|
||||||
|
option: textAttributeOption.value,
|
||||||
|
type: "text",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("should get all the memberships of the org paginated", async () => {
|
it("should get all the memberships of the org paginated", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
|
||||||
@@ -182,6 +347,25 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should get the membership of the org", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/memberships/${membership2.id}`)
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const responseBody: GetOrgMembership = response.body;
|
||||||
|
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
|
expect(responseBody.data.id).toEqual(membership2.id);
|
||||||
|
expect(responseBody.data.userId).toEqual(user2.id);
|
||||||
|
expect(responseBody.data.role).toEqual("MEMBER");
|
||||||
|
expect(responseBody.data.user.bio).toEqual(bio);
|
||||||
|
expect(responseBody.data.user.metadata).toEqual(metadata);
|
||||||
|
expect(responseBody.data.user.email).toEqual(user2.email);
|
||||||
|
expect(responseBody.data.user.username).toEqual(user2.username);
|
||||||
|
expect(responseBody.data.teamId).toEqual(org.id);
|
||||||
|
userHasCorrectAttributes(responseBody.data.attributes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should create the membership of the org", async () => {
|
it("should create the membership of the org", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
.post(`/v2/organizations/${org.id}/memberships`)
|
.post(`/v2/organizations/${org.id}/memberships`)
|
||||||
@@ -259,7 +443,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
let userRepositoryFixture: UserRepositoryFixture;
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
let attributesRepositoryFixture: AttributeRepositoryFixture;
|
||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let membership: Membership;
|
let membership: Membership;
|
||||||
@@ -277,7 +462,8 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
|
|
||||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
user = await userRepositoryFixture.create({
|
user = await userRepositoryFixture.create({
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
@@ -289,7 +475,7 @@ describe("Organizations Memberships Endpoints", () => {
|
|||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
membership = await membershipsRepositoryFixture.create({
|
membership = await membershipRepositoryFixture.create({
|
||||||
role: "MEMBER",
|
role: "MEMBER",
|
||||||
user: { connect: { id: user.id } },
|
user: { connect: { id: user.id } },
|
||||||
team: { connect: { id: org.id } },
|
team: { connect: { id: org.id } },
|
||||||
|
|||||||
+5
-9
@@ -20,7 +20,6 @@ import { GetAllOrgMemberships } from "@/modules/organizations/memberships/output
|
|||||||
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
|
||||||
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -36,7 +35,6 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
} 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 { plainToClass } from "class-transformer";
|
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
import { SkipTakePagination } from "@calcom/platform-types";
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
@@ -70,9 +68,7 @@ export class OrganizationsMembershipsController {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: memberships.map((membership) =>
|
data: memberships,
|
||||||
plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" })
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +84,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
|
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +101,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
|
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +118,7 @@ export class OrganizationsMembershipsController {
|
|||||||
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
|
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +140,7 @@ export class OrganizationsMembershipsController {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
status: SUCCESS_STATUS,
|
status: SUCCESS_STATUS,
|
||||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
data: membership,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+59
-6
@@ -4,8 +4,31 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
|||||||
import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository";
|
import { MembershipUserSelect } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "./inputs/update-organization-membership.input";
|
||||||
|
|
||||||
|
export type DbOrgMembership = Awaited<ReturnType<OrganizationsMembershipRepository["findOrgMembership"]>>;
|
||||||
|
|
||||||
|
const attributeToUserSelect = {
|
||||||
|
createdByDSyncId: true,
|
||||||
|
weight: true,
|
||||||
|
attributeOption: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
value: true,
|
||||||
|
slug: true,
|
||||||
|
attribute: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Prisma.AttributeToUserSelect;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsMembershipRepository {
|
export class OrganizationsMembershipRepository {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
@@ -16,7 +39,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
id: membershipId,
|
id: membershipId,
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +54,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +69,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
id: membershipId,
|
id: membershipId,
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +83,24 @@ export class OrganizationsMembershipRepository {
|
|||||||
create: { ...data, teamId: organizationId },
|
create: { ...data, teamId: organizationId },
|
||||||
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
||||||
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||||
return this.dbWrite.prisma.membership.update({
|
return this.dbWrite.prisma.membership.update({
|
||||||
data: { ...data },
|
data: { ...data },
|
||||||
where: { id: membershipId, teamId: organizationId },
|
where: { id: membershipId, teamId: organizationId },
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +109,12 @@ export class OrganizationsMembershipRepository {
|
|||||||
where: {
|
where: {
|
||||||
teamId: organizationId,
|
teamId: organizationId,
|
||||||
},
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: {
|
||||||
|
user: { select: MembershipUserSelect },
|
||||||
|
AttributeToUser: {
|
||||||
|
select: attributeToUserSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class CreateOrgMembershipOutput {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsEnum } from "class-validator";
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
@@ -9,5 +9,5 @@ export class DeleteOrgMembership {
|
|||||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested, IsArray } from "class-validator";
|
||||||
@@ -11,11 +11,11 @@ export class GetAllOrgMemberships {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
data!: TeamMembershipOutput[];
|
data!: OrganizationMembershipOutput[];
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class GetOrgMembership {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||||
|
import { ApiExtraModels, ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
|
class BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "text";
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "number";
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SingleSelectAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "singleSelect";
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiSelectAttributeOption {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
optionId!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
option!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MultiSelectAttribute extends BaseAttribute {
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
type!: "multiSelect";
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
|
options!: MultiSelectAttributeOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrgUserAttribute = TextAttribute | NumberAttribute | SingleSelectAttribute | MultiSelectAttribute;
|
||||||
|
|
||||||
|
@ApiExtraModels(BaseAttribute, TextAttribute, NumberAttribute, SingleSelectAttribute, MultiSelectAttribute)
|
||||||
|
export class OrganizationMembershipOutput extends TeamMembershipOutput {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Expose()
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
oneOf: [
|
||||||
|
{ $ref: getSchemaPath(TextAttribute) },
|
||||||
|
{ $ref: getSchemaPath(NumberAttribute) },
|
||||||
|
{ $ref: getSchemaPath(SingleSelectAttribute) },
|
||||||
|
{ $ref: getSchemaPath(MultiSelectAttribute) },
|
||||||
|
],
|
||||||
|
type: "array",
|
||||||
|
})
|
||||||
|
@Type(() => Object)
|
||||||
|
attributes!: OrgUserAttribute[];
|
||||||
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||||
@@ -11,10 +11,10 @@ export class UpdateOrgMembership {
|
|||||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: TeamMembershipOutput,
|
type: OrganizationMembershipOutput,
|
||||||
})
|
})
|
||||||
@IsNotEmptyObject()
|
@IsNotEmptyObject()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => TeamMembershipOutput)
|
@Type(() => OrganizationMembershipOutput)
|
||||||
data!: TeamMembershipOutput;
|
data!: OrganizationMembershipOutput;
|
||||||
}
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
import { DbOrgMembership } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import {
|
||||||
|
MultiSelectAttribute,
|
||||||
|
NumberAttribute,
|
||||||
|
OrganizationMembershipOutput,
|
||||||
|
OrgUserAttribute,
|
||||||
|
SingleSelectAttribute,
|
||||||
|
TextAttribute,
|
||||||
|
} from "@/modules/organizations/memberships/outputs/organization-membership.output";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { groupMembershipAttributes } from "@calcom/platform-libraries";
|
||||||
|
import type { GroupedAttribute } from "@calcom/platform-libraries";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationsMembershipOutputService {
|
||||||
|
getOrgMembershipsOutput(memberships: NonNullable<DbOrgMembership>[]) {
|
||||||
|
return memberships.map((membership) => this.getOrgMembershipOutput(membership));
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrgMembershipOutput(organizationMembership: NonNullable<DbOrgMembership>) {
|
||||||
|
const { AttributeToUser, ...orgMembership } = organizationMembership;
|
||||||
|
|
||||||
|
const groupedMembershipAttributes: GroupedAttribute[] = groupMembershipAttributes(AttributeToUser);
|
||||||
|
const attributesOutput = this.getAttributesOutput(groupedMembershipAttributes);
|
||||||
|
|
||||||
|
return plainToClass(OrganizationMembershipOutput, { ...orgMembership, attributes: attributesOutput });
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAttributesOutput(attributes: GroupedAttribute[]): OrgUserAttribute[] {
|
||||||
|
return attributes.map((attribute) => {
|
||||||
|
switch (attribute.type) {
|
||||||
|
case "TEXT":
|
||||||
|
return this.getTextAttributeOutput(attribute);
|
||||||
|
case "NUMBER":
|
||||||
|
return this.getNumberAttributeOutput(attribute);
|
||||||
|
case "SINGLE_SELECT":
|
||||||
|
return this.getSingleSelectAttributeOutput(attribute);
|
||||||
|
case "MULTI_SELECT":
|
||||||
|
return this.getMultiSelectAttributeOutput(attribute);
|
||||||
|
default:
|
||||||
|
return this.getTextAttributeOutput(attribute);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTextAttributeOutput(attribute: GroupedAttribute): TextAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: attribute.options[0].value,
|
||||||
|
type: "text",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNumberAttributeOutput(attribute: GroupedAttribute): NumberAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: +attribute.options[0].value,
|
||||||
|
type: "number",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSingleSelectAttributeOutput(attribute: GroupedAttribute): SingleSelectAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
optionId: attribute.options[0].id,
|
||||||
|
option: attribute.options[0].value,
|
||||||
|
type: "singleSelect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMultiSelectAttributeOutput(attribute: GroupedAttribute): MultiSelectAttribute {
|
||||||
|
return {
|
||||||
|
id: attribute.id,
|
||||||
|
name: attribute.name,
|
||||||
|
options: attribute.options.map((option: GroupedAttribute["options"][number]) => ({
|
||||||
|
optionId: option.id,
|
||||||
|
option: option.value,
|
||||||
|
})),
|
||||||
|
type: "multiSelect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-8
@@ -1,19 +1,30 @@
|
|||||||
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||||
|
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrganizationsMembershipService {
|
export class OrganizationsMembershipService {
|
||||||
constructor(private readonly organizationsMembershipRepository: OrganizationsMembershipRepository) {}
|
constructor(
|
||||||
|
private readonly organizationsMembershipRepository: OrganizationsMembershipRepository,
|
||||||
|
private readonly organizationsMembershipOutputService: OrganizationsMembershipOutputService
|
||||||
|
) {}
|
||||||
|
|
||||||
async getOrgMembership(organizationId: number, membershipId: number) {
|
async getOrgMembership(organizationId: number, membershipId: number) {
|
||||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
return membership;
|
|
||||||
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
||||||
@@ -21,7 +32,13 @@ export class OrganizationsMembershipService {
|
|||||||
organizationId,
|
organizationId,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
return membership;
|
if (!membership) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Membership for user with id ${userId} within organization id ${organizationId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
async getPaginatedOrgMemberships(organizationId: number, skip = 0, take = 250) {
|
||||||
@@ -30,7 +47,7 @@ export class OrganizationsMembershipService {
|
|||||||
skip,
|
skip,
|
||||||
take
|
take
|
||||||
);
|
);
|
||||||
return memberships;
|
return this.organizationsMembershipOutputService.getOrgMembershipsOutput(memberships);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||||
@@ -38,7 +55,7 @@ export class OrganizationsMembershipService {
|
|||||||
organizationId,
|
organizationId,
|
||||||
membershipId
|
membershipId
|
||||||
);
|
);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||||
@@ -47,11 +64,11 @@ export class OrganizationsMembershipService {
|
|||||||
membershipId,
|
membershipId,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||||
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
|
||||||
return membership;
|
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
|
|||||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||||
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
|
import { OrganizationsMembershipsController } from "@/modules/organizations/memberships/organizations-membership.controller";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||||
@@ -98,6 +99,7 @@ import { Module } from "@nestjs/common";
|
|||||||
EmailService,
|
EmailService,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsEventTypesService,
|
OrganizationsEventTypesService,
|
||||||
InputOrganizationsEventTypesService,
|
InputOrganizationsEventTypesService,
|
||||||
OutputOrganizationsEventTypesService,
|
OutputOrganizationsEventTypesService,
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
||||||
import { OrganizationsOrganizationsController } from "@/modules/organizations/organizations/organizations-organizations.controller";
|
import { OrganizationsOrganizationsController } from "@/modules/organizations/organizations/organizations-organizations.controller";
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
ManagedOrganizationsBillingService,
|
ManagedOrganizationsBillingService,
|
||||||
OrganizationsRepository,
|
OrganizationsRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
ManagedOrganizationsOutputService,
|
ManagedOrganizationsOutputService,
|
||||||
],
|
],
|
||||||
|
|||||||
+40
-30
@@ -126,36 +126,6 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await prismaWriteService.prisma.app_RoutingForms_FormResponse.deleteMany({
|
|
||||||
where: {
|
|
||||||
formId: routingForm.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
|
|
||||||
where: {
|
|
||||||
teamId: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.apiKey.deleteMany({
|
|
||||||
where: {
|
|
||||||
teamId: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.team.delete({
|
|
||||||
where: {
|
|
||||||
id: team.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await prismaWriteService.prisma.team.delete({
|
|
||||||
where: {
|
|
||||||
id: org.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
describe(`GET /v2/organizations/:orgId/routing-forms/:routingFormId/responses`, () => {
|
||||||
it("should not get routing form responses for non existing org", async () => {
|
it("should not get routing form responses for non existing org", async () => {
|
||||||
return request(app.getHttpServer())
|
return request(app.getHttpServer())
|
||||||
@@ -261,4 +231,44 @@ describe("OrganizationsRoutingFormsResponsesController", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({
|
||||||
|
where: {
|
||||||
|
id: routingFormResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_FormResponse.delete({
|
||||||
|
where: {
|
||||||
|
id: routingFormResponse2.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.app_RoutingForms_Form.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.apiKey.deleteMany({
|
||||||
|
where: {
|
||||||
|
teamId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.team.delete({
|
||||||
|
where: {
|
||||||
|
id: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.team.delete({
|
||||||
|
where: {
|
||||||
|
id: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismaWriteService.prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+39
-2
@@ -1,13 +1,18 @@
|
|||||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||||
|
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||||
|
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||||
import {
|
import {
|
||||||
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
OPTIONAL_X_CAL_CLIENT_ID_HEADER,
|
||||||
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
OPTIONAL_X_CAL_SECRET_KEY_HEADER,
|
||||||
OPTIONAL_API_KEY_HEADER,
|
OPTIONAL_API_KEY_HEADER,
|
||||||
|
API_KEY_OR_ACCESS_TOKEN_HEADER,
|
||||||
} from "@/lib/docs/headers";
|
} from "@/lib/docs/headers";
|
||||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
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 { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||||
import { Roles } from "@/modules/auth/decorators/roles/roles.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 { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
@@ -20,7 +25,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
|||||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
|
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } 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, BOOKING_READ } from "@calcom/platform-constants";
|
||||||
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||||
|
|
||||||
@Controller({
|
@Controller({
|
||||||
@@ -33,7 +38,10 @@ import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
|||||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||||
export class OrganizationsTeamsBookingsController {
|
export class OrganizationsTeamsBookingsController {
|
||||||
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
|
constructor(
|
||||||
|
private readonly bookingsService: BookingsService_2024_08_13,
|
||||||
|
private readonly bookingReferencesService: BookingReferencesService_2024_08_13
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get("/")
|
@Get("/")
|
||||||
@ApiOperation({ summary: "Get organization team bookings" })
|
@ApiOperation({ summary: "Get organization team bookings" })
|
||||||
@@ -56,4 +64,33 @@ export class OrganizationsTeamsBookingsController {
|
|||||||
data: bookings,
|
data: bookings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("/:bookingUid/references")
|
||||||
|
@PlatformPlan("SCALE")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Permissions([BOOKING_READ])
|
||||||
|
@UseGuards(
|
||||||
|
ApiAuthGuard,
|
||||||
|
BookingUidGuard,
|
||||||
|
IsOrgGuard,
|
||||||
|
RolesGuard,
|
||||||
|
IsTeamInOrg,
|
||||||
|
PlatformPlanGuard,
|
||||||
|
IsAdminAPIEnabledGuard
|
||||||
|
)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get 'Booking References' for a booking",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getBookingReferences(
|
||||||
|
@Param("bookingUid") bookingUid: string,
|
||||||
|
@Query() filter: BookingReferencesFilterInput_2024_08_13
|
||||||
|
): Promise<BookingReferencesOutput_2024_08_13> {
|
||||||
|
const bookingReferences = await this.bookingReferencesService.getOrgBookingReferences(bookingUid, filter);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: bookingReferences,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+12
-1
@@ -1,4 +1,8 @@
|
|||||||
|
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||||
|
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||||
|
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||||
|
import { OutputBookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output-booking-references.service";
|
||||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
import { OrganizationsTeamsBookingsController } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.controller";
|
import { OrganizationsTeamsBookingsController } from "@/modules/organizations/teams/bookings/organizations-teams-bookings.controller";
|
||||||
@@ -10,7 +14,14 @@ import { Module } from "@nestjs/common";
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
||||||
providers: [OrganizationsRepository, OrganizationsTeamsRepository],
|
providers: [
|
||||||
|
OrganizationsRepository,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
BookingReferencesService_2024_08_13,
|
||||||
|
BookingReferencesRepository_2024_08_13,
|
||||||
|
BookingsRepository_2024_08_13,
|
||||||
|
OutputBookingReferencesService_2024_08_13,
|
||||||
|
],
|
||||||
controllers: [OrganizationsTeamsBookingsController],
|
controllers: [OrganizationsTeamsBookingsController],
|
||||||
})
|
})
|
||||||
export class OrganizationsTeamsBookingsModule {}
|
export class OrganizationsTeamsBookingsModule {}
|
||||||
+5
-1
@@ -69,7 +69,11 @@ export class OrganizationsTeamsMembershipsRepository {
|
|||||||
|
|
||||||
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
async createOrgTeamMembership(teamId: number, data: CreateOrgTeamMembershipDto) {
|
||||||
return this.dbWrite.prisma.membership.create({
|
return this.dbWrite.prisma.membership.create({
|
||||||
data: { ...data, teamId: teamId },
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
teamId: teamId,
|
||||||
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: { user: { select: MembershipUserSelect } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -35,7 +35,7 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
|
|
||||||
let org: Team;
|
let org: Team;
|
||||||
let orgTeam: Team;
|
let orgTeam: Team;
|
||||||
let routingFormResponseId: string;
|
let routingFormResponseId: number;
|
||||||
const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
|
const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`;
|
||||||
let user: User;
|
let user: User;
|
||||||
let apiKeyString: string;
|
let apiKeyString: string;
|
||||||
@@ -43,8 +43,7 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
let routingFormId: string;
|
let routingFormId: string;
|
||||||
const routingFormResponses = [
|
const routingFormResponses = [
|
||||||
{
|
{
|
||||||
id: 1,
|
formFillerId: `${randomString()}`,
|
||||||
formFillerId: "cm78tvkvd0001kh8jq0tu5iq9",
|
|
||||||
response: {
|
response: {
|
||||||
"participant-field": {
|
"participant-field": {
|
||||||
label: "participant",
|
label: "participant",
|
||||||
@@ -184,7 +183,6 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
const responseData = responseBody.data;
|
const responseData = responseBody.data;
|
||||||
expect(responseData).toBeDefined();
|
expect(responseData).toBeDefined();
|
||||||
expect(responseData.length).toEqual(1);
|
expect(responseData.length).toEqual(1);
|
||||||
expect(responseData[0].id).toEqual(routingFormResponses[0].id);
|
|
||||||
expect(responseData[0].response).toEqual(routingFormResponses[0].response);
|
expect(responseData[0].response).toEqual(routingFormResponses[0].response);
|
||||||
expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId);
|
expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId);
|
||||||
expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString());
|
expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString());
|
||||||
@@ -248,6 +246,8 @@ describe("Organizations Teams Routing Forms Responses", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await routingFormsRepositoryFixture.deleteResponse(routingFormResponseId);
|
||||||
|
await routingFormsRepositoryFixture.delete(routingFormId);
|
||||||
await userRepositoryFixture.deleteByEmail(user.email);
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
await organizationsRepositoryFixture.delete(org.id);
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
|||||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||||
|
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
|
import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard";
|
||||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||||
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
|
import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input";
|
||||||
@@ -29,6 +30,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
|||||||
IsTeamInOrg,
|
IsTeamInOrg,
|
||||||
IsRoutingFormInTeam,
|
IsRoutingFormInTeam,
|
||||||
PlatformPlanGuard,
|
PlatformPlanGuard,
|
||||||
|
RolesGuard,
|
||||||
IsAdminAPIEnabledGuard
|
IsAdminAPIEnabledGuard
|
||||||
)
|
)
|
||||||
@ApiHeader(API_KEY_HEADER)
|
@ApiHeader(API_KEY_HEADER)
|
||||||
|
|||||||
+236
@@ -0,0 +1,236 @@
|
|||||||
|
import { bootstrap } from "@/app";
|
||||||
|
import { AppModule } from "@/app.module";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { INestApplication } from "@nestjs/common";
|
||||||
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
|
import { Test } from "@nestjs/testing";
|
||||||
|
import { TOTP as TOTPtoMock } from "@otplib/core";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { totp } from "otplib";
|
||||||
|
import * as request from "supertest";
|
||||||
|
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||||
|
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||||
|
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||||
|
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||||
|
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||||
|
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||||
|
import { VerifiedResourcesRepositoryFixtures } from "test/fixtures/repository/verified-resources.repository.fixture";
|
||||||
|
import { randomString } from "test/utils/randomString";
|
||||||
|
|
||||||
|
import { AttendeeVerifyEmail } from "@calcom/platform-libraries/emails";
|
||||||
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
jest.spyOn(totp, "generate").mockImplementation(function () {
|
||||||
|
return "1234";
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(TOTPtoMock.prototype, "check").mockImplementation(function () {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(AttendeeVerifyEmail.prototype as any, "getNodeMailerPayload")
|
||||||
|
.mockImplementation(async function () {
|
||||||
|
return {
|
||||||
|
to: `testnotrealemail@notreal.com`,
|
||||||
|
from: `testnotrealemail@notreal.com`,
|
||||||
|
subject: "No Subject",
|
||||||
|
html: "<html><body>Mocked Email Content</body></html>",
|
||||||
|
text: "body",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Organizations Teams Verified Resources", () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
let userRepositoryFixture: UserRepositoryFixture;
|
||||||
|
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||||
|
let verifiedResourcesRepositoryFixtures: VerifiedResourcesRepositoryFixtures;
|
||||||
|
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||||
|
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||||
|
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||||
|
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||||
|
|
||||||
|
let org: Team;
|
||||||
|
let orgTeam: Team;
|
||||||
|
const emailToVerify = `org-team-e2e-verified-resources-${randomString()}@example.com`;
|
||||||
|
const phoneToVerify = "+37255556666";
|
||||||
|
const authEmail = `organizations-verified-resources-responses-user-${randomString()}@api.com`;
|
||||||
|
let user: User;
|
||||||
|
let apiKeyString: string;
|
||||||
|
let verifiedEmailId: number;
|
||||||
|
let verifiedPhoneId: number;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
verifiedResourcesRepositoryFixtures = new VerifiedResourcesRepositoryFixtures(moduleRef);
|
||||||
|
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||||
|
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||||
|
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||||
|
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||||
|
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||||
|
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||||
|
|
||||||
|
org = await organizationsRepositoryFixture.create({
|
||||||
|
name: `organizations-verified-resources-responses-organization-${randomString()}`,
|
||||||
|
isOrganization: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await userRepositoryFixture.create({
|
||||||
|
email: authEmail,
|
||||||
|
username: authEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||||
|
apiKeyString = keyString;
|
||||||
|
|
||||||
|
orgTeam = await teamsRepositoryFixture.create({
|
||||||
|
name: `organizations-verified-resources-responses-team-${randomString()}`,
|
||||||
|
isOrganization: false,
|
||||||
|
parent: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: org.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await membershipsRepositoryFixture.create({
|
||||||
|
role: "ADMIN",
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: { connect: { id: orgTeam.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
await profileRepositoryFixture.create({
|
||||||
|
uid: `usr-${user.id}`,
|
||||||
|
username: authEmail,
|
||||||
|
organization: {
|
||||||
|
connect: {
|
||||||
|
id: org.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
verifiedPhoneId = (
|
||||||
|
await verifiedResourcesRepositoryFixtures.createPhone({
|
||||||
|
phoneNumber: phoneToVerify,
|
||||||
|
user: { connect: { id: user.id } },
|
||||||
|
team: {
|
||||||
|
connect: {
|
||||||
|
id: orgTeam.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).id;
|
||||||
|
|
||||||
|
app = moduleRef.createNestApplication();
|
||||||
|
bootstrap(app as NestExpressApplication);
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trigger email verification code", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/request`
|
||||||
|
)
|
||||||
|
.send({ email: emailToVerify } satisfies RequestEmailVerificationInput)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should verify email", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post(
|
||||||
|
`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/verify`
|
||||||
|
)
|
||||||
|
.send({ email: emailToVerify, code: "1234" } satisfies VerifyEmailInput)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailOutput;
|
||||||
|
verifiedEmailId = response.data.id;
|
||||||
|
expect(response.data.email).toEqual(emailToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified email by id", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/${verifiedEmailId}`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailOutput;
|
||||||
|
expect(response.data.email).toEqual(emailToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified number by id", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones/${verifiedPhoneId}`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedPhoneOutput;
|
||||||
|
expect(response.data.phoneNumber).toEqual(phoneToVerify);
|
||||||
|
expect(response.data.teamId).toEqual(orgTeam.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified emails", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedEmailsOutput;
|
||||||
|
expect(response.data.find((verifiedEmail) => verifiedEmail.email === emailToVerify)).toBeDefined();
|
||||||
|
expect(response.data.find((verifiedEmail) => verifiedEmail.teamId === orgTeam.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch verified phones", async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones`)
|
||||||
|
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.body as TeamVerifiedPhonesOutput;
|
||||||
|
expect(
|
||||||
|
response.data.find((verifiedPhone) => verifiedPhone.phoneNumber === phoneToVerify)
|
||||||
|
).toBeDefined();
|
||||||
|
expect(response.data.find((verifiedPhone) => verifiedPhone.teamId === orgTeam.id)).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await verifiedResourcesRepositoryFixtures.deleteEmailById(verifiedEmailId);
|
||||||
|
await verifiedResourcesRepositoryFixtures.deletePhoneById(verifiedPhoneId);
|
||||||
|
await userRepositoryFixture.deleteByEmail(user.email);
|
||||||
|
await organizationsRepositoryFixture.delete(org.id);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
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 { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailOutputData,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhoneOutputData,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/organizations/:orgId/teams/:teamId/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||||
|
@ApiTags("Organization Team Verified Resources")
|
||||||
|
export class OrgTeamsVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the email.`,
|
||||||
|
})
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_emails_requests",
|
||||||
|
})
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_phones_requests",
|
||||||
|
})
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email for an org team.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(
|
||||||
|
userId,
|
||||||
|
body.email,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number for an org team.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@Throttle({
|
||||||
|
limit: 3,
|
||||||
|
ttl: 60000,
|
||||||
|
blockDuration: 60000,
|
||||||
|
name: "org_teams_verified_resources_phones_verify",
|
||||||
|
})
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(
|
||||||
|
userId,
|
||||||
|
body.phone,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails of an org team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers of an org team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/phones")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email of an org team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@Param("id") id: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number of an org team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@PlatformPlan("ESSENTIALS")
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-4
@@ -38,7 +38,7 @@ export class OrganizationsUsersRepository {
|
|||||||
member: {
|
member: {
|
||||||
teamId: orgId,
|
teamId: orgId,
|
||||||
...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }),
|
...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }),
|
||||||
// Filter to only get users which have ALL of the assigned attribue options
|
// Filter to only get users which have ALL of the assigned attribute options
|
||||||
...(attributeQueryOperator === "AND" && {
|
...(attributeQueryOperator === "AND" && {
|
||||||
AND: assignedOptionIds.map((optionId) => ({
|
AND: assignedOptionIds.map((optionId) => ({
|
||||||
AttributeToUser: { some: { attributeOptionId: optionId } },
|
AttributeToUser: { some: { attributeOptionId: optionId } },
|
||||||
@@ -46,11 +46,11 @@ export class OrganizationsUsersRepository {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
||||||
// Filter to get users which have AT LEAST ONE of the assigned attribue options
|
// Filter to get users which have AT LEAST ONE of the assigned attribute options
|
||||||
...(attributeQueryOperator === "OR" && {
|
...(attributeQueryOperator === "OR" && {
|
||||||
attributeOption: { id: { in: assignedOptionIds } },
|
attributeOption: { id: { in: assignedOptionIds } },
|
||||||
}),
|
}),
|
||||||
// Filter to get users that have NONE the assigned attribue options
|
// Filter to get users that have NONE the assigned attribute options
|
||||||
...(attributeQueryOperator === "NONE" && {
|
...(attributeQueryOperator === "NONE" && {
|
||||||
NOT: {
|
NOT: {
|
||||||
attributeOption: { id: { in: assignedOptionIds } },
|
attributeOption: { id: { in: assignedOptionIds } },
|
||||||
@@ -141,7 +141,16 @@ export class OrganizationsUsersRepository {
|
|||||||
return await this.dbWrite.prisma.user.delete({
|
return await this.dbWrite.prisma.user.delete({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
organizationId: orgId,
|
OR: [
|
||||||
|
{ organizationId: orgId },
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
some: {
|
||||||
|
organizationId: orgId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
profiles: {
|
profiles: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AppsRepository } from "@/modules/apps/apps.repository";
|
|||||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
||||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||||
|
import { OrganizationsMembershipOutputService } from "@/modules/organizations/memberships/services/organizations-membership-output.service";
|
||||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
import { SelectedCalendarsController } from "@/modules/selected-calendars/controllers/selected-calendars.controller";
|
||||||
@@ -22,6 +23,7 @@ import { Module } from "@nestjs/common";
|
|||||||
CredentialsRepository,
|
CredentialsRepository,
|
||||||
AppsRepository,
|
AppsRepository,
|
||||||
OrganizationsMembershipService,
|
OrganizationsMembershipService,
|
||||||
|
OrganizationsMembershipOutputService,
|
||||||
OrganizationsDelegationCredentialRepository,
|
OrganizationsDelegationCredentialRepository,
|
||||||
OrganizationsMembershipRepository,
|
OrganizationsMembershipRepository,
|
||||||
SelectedCalendarsService,
|
SelectedCalendarsService,
|
||||||
|
|||||||
+97
-12
@@ -181,7 +181,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
title: "frisbee match",
|
title: "frisbee match",
|
||||||
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
||||||
length: 15,
|
length: 15,
|
||||||
metadata: { multipleDuration: [15, 30, 45, 60] },
|
metadata: { multipleDuration: [15, 30, 45, 60, 180] },
|
||||||
},
|
},
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
@@ -506,6 +506,65 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
expect(reserveResponseBody.data).toEqual(rest);
|
expect(reserveResponseBody.data).toEqual(rest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("overlapping slot reservations", () => {
|
||||||
|
it("start of request slot overlaps already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 10:15-11:15 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).plus({ minutes: 15 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("end of request slot overlaps already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 9:45-10:45 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).minus({ minutes: 15 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("request slot is inside already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 10:10-11:10 when 10:00-11:00 is taken (60 min event)
|
||||||
|
const newSlotStart = DateTime.fromISO(reservedSlot.slotStart).plus({ minutes: 10 }).toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should update a reserved slot and it should not appear in available slots", async () => {
|
it("should update a reserved slot and it should not appear in available slots", async () => {
|
||||||
// note(Lauris): mock current date to test slots release time
|
// note(Lauris): mock current date to test slots release time
|
||||||
const now = "2049-09-05T14:00:00.000Z";
|
const now = "2049-09-05T14:00:00.000Z";
|
||||||
@@ -1266,6 +1325,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("variable length", () => {
|
describe("variable length", () => {
|
||||||
|
let responseReservedVariableSlot: ReserveSlotOutputData_2024_09_04;
|
||||||
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
||||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||||
const reserveResponse = await request(app.getHttpServer())
|
const reserveResponse = await request(app.getHttpServer())
|
||||||
@@ -1279,7 +1339,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
expect(reserveResponse.body.error.message).toEqual(
|
expect(reserveResponse.body.error.message).toEqual(
|
||||||
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
|
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60, 180"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1303,17 +1363,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
|
|
||||||
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
||||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||||
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
|
responseReservedVariableSlot = reserveResponseBody.data;
|
||||||
expect(responseReservedSlot.reservationUid).toBeDefined();
|
expect(responseReservedVariableSlot.reservationUid).toBeDefined();
|
||||||
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
expect(responseReservedVariableSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
||||||
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
|
expect(responseReservedVariableSlot.slotStart).toEqual(slotStartTime);
|
||||||
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
|
expect(responseReservedVariableSlot.slotDuration).toEqual(slotDuration);
|
||||||
expect(responseReservedSlot.slotEnd).toEqual(
|
expect(responseReservedVariableSlot.slotEnd).toEqual(
|
||||||
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
||||||
);
|
);
|
||||||
expect(responseReservedSlot.reservationDuration).toEqual(5);
|
expect(responseReservedVariableSlot.reservationDuration).toEqual(5);
|
||||||
|
|
||||||
if (!responseReservedSlot.reservationUid) {
|
if (!responseReservedVariableSlot.reservationUid) {
|
||||||
throw new Error("Reserved slot uid is undefined");
|
throw new Error("Reserved slot uid is undefined");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1337,16 +1397,41 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
|||||||
);
|
);
|
||||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||||
|
|
||||||
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
|
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(
|
||||||
|
responseReservedVariableSlot.reservationUid
|
||||||
|
);
|
||||||
expect(dbSlot).toBeDefined();
|
expect(dbSlot).toBeDefined();
|
||||||
if (dbSlot) {
|
if (dbSlot) {
|
||||||
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
||||||
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
||||||
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||||
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
|
expect(responseReservedVariableSlot.reservationUntil).toEqual(expectedReleaseAt);
|
||||||
}
|
}
|
||||||
clear();
|
clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("request slot contains already existing reserved slot", async () => {
|
||||||
|
// Try to reserve 9:45-12:45 when 10:00-11:00 is taken
|
||||||
|
const newSlotStart = DateTime.fromISO(responseReservedVariableSlot.slotStart)
|
||||||
|
.minus({ minutes: 15 })
|
||||||
|
.toISO();
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.post(`/v2/slots/reservations`)
|
||||||
|
.send({
|
||||||
|
eventTypeId: variableLengthEventType.id,
|
||||||
|
slotStart: newSlotStart,
|
||||||
|
slotDuration: 180,
|
||||||
|
})
|
||||||
|
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||||
|
.set("Authorization", `Bearer cal_test_${apiKeyString}`)
|
||||||
|
.expect(422)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.body.error.message).toEqual(
|
||||||
|
"This time slot is already reserved by another user. Please choose a different time."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("out of office", () => {
|
describe("out of office", () => {
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export class SlotsService_2024_09_04 {
|
|||||||
|
|
||||||
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
||||||
|
|
||||||
|
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
||||||
|
|
||||||
if (eventType.userId) {
|
if (eventType.userId) {
|
||||||
const slot = await this.slotsRepository.createSlot(
|
const slot = await this.slotsRepository.createSlot(
|
||||||
eventType.userId,
|
eventType.userId,
|
||||||
@@ -153,6 +155,20 @@ export class SlotsService_2024_09_04 {
|
|||||||
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkSlotOverlap(eventTypeId: number, startDate: string, endDate: string) {
|
||||||
|
const overlappingReservation = await this.slotsRepository.getOverlappingSlotReservation(
|
||||||
|
eventTypeId,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overlappingReservation) {
|
||||||
|
throw new UnprocessableEntityException(
|
||||||
|
`This time slot is already reserved by another user. Please choose a different time.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
||||||
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
||||||
if (!eventTypeMetadata?.multipleDuration) {
|
if (!eventTypeMetadata?.multipleDuration) {
|
||||||
@@ -257,6 +273,8 @@ export class SlotsService_2024_09_04 {
|
|||||||
|
|
||||||
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
const reservationDuration = input.reservationDuration ?? DEFAULT_RESERVATION_DURATION;
|
||||||
|
|
||||||
|
await this.checkSlotOverlap(input.eventTypeId, startDate.toISO(), endDate.toISO());
|
||||||
|
|
||||||
const slot = await this.slotsRepository.updateSlot(
|
const slot = await this.slotsRepository.updateSlot(
|
||||||
eventType.id,
|
eventType.id,
|
||||||
startDate.toISO(),
|
startDate.toISO(),
|
||||||
|
|||||||
@@ -19,6 +19,30 @@ export class SlotsRepository_2024_09_04 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOverlappingSlotReservation(eventTypeId: number, startDate: string, endDate: string) {
|
||||||
|
return this.dbRead.prisma.selectedSlots.findFirst({
|
||||||
|
where: {
|
||||||
|
eventTypeId,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
// Case 1: New slot starts during an existing slot
|
||||||
|
{ slotUtcStartDate: { lte: startDate }, slotUtcEndDate: { gt: startDate } },
|
||||||
|
// Case 2: New slot ends during an existing slot
|
||||||
|
{ slotUtcStartDate: { lt: endDate }, slotUtcEndDate: { gte: endDate } },
|
||||||
|
// Case 3: New slot is completely inside an existing slot
|
||||||
|
{ slotUtcStartDate: { lte: startDate }, slotUtcEndDate: { gte: endDate } },
|
||||||
|
// Case 4: New slot completely overlaps an existing slot
|
||||||
|
{ slotUtcStartDate: { gte: startDate }, slotUtcEndDate: { lte: endDate } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Only consider non-expired reservations
|
||||||
|
{ releaseAt: { gt: DateTime.utc().toJSDate() } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createSlot(
|
async createSlot(
|
||||||
userId: number,
|
userId: number,
|
||||||
eventTypeId: number,
|
eventTypeId: number,
|
||||||
|
|||||||
+2
@@ -312,6 +312,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
requiresBookerEmailVerification: true,
|
requiresBookerEmailVerification: true,
|
||||||
hideCalendarNotes: true,
|
hideCalendarNotes: true,
|
||||||
hideCalendarEventDetails: true,
|
hideCalendarEventDetails: true,
|
||||||
|
hideOrganizerEmail: true,
|
||||||
lockTimeZoneToggleOnBookingPage: true,
|
lockTimeZoneToggleOnBookingPage: true,
|
||||||
color: {
|
color: {
|
||||||
darkThemeHex: "#292929",
|
darkThemeHex: "#292929",
|
||||||
@@ -343,6 +344,7 @@ describe("Organizations Event Types Endpoints", () => {
|
|||||||
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
||||||
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||||
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||||
|
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||||
expect(data.color).toEqual(body.color);
|
expect(data.color).toEqual(body.color);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ export class TeamsMembershipsController {
|
|||||||
|
|
||||||
@Roles("TEAM_ADMIN")
|
@Roles("TEAM_ADMIN")
|
||||||
@Patch("/:membershipId")
|
@Patch("/:membershipId")
|
||||||
@ApiOperation({ summary: "Create a membership" })
|
@ApiOperation({ summary: "Update membership" })
|
||||||
async updateTeamMembership(
|
async updateTeamMembership(
|
||||||
@Param("teamId", ParseIntPipe) teamId: number,
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
@Param("membershipId", ParseIntPipe) membershipId: number,
|
@Param("membershipId", ParseIntPipe) membershipId: number,
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ export class TeamsMembershipsRepository {
|
|||||||
|
|
||||||
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
async createTeamMembership(teamId: number, data: CreateTeamMembershipInput) {
|
||||||
return this.dbWrite.prisma.membership.create({
|
return this.dbWrite.prisma.membership.create({
|
||||||
data: { ...data, teamId: teamId },
|
data: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
...data,
|
||||||
|
teamId: teamId,
|
||||||
|
},
|
||||||
include: { user: { select: MembershipUserSelect } },
|
include: { user: { select: MembershipUserSelect } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-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 { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedEmailOutput,
|
||||||
|
TeamVerifiedEmailOutputData,
|
||||||
|
TeamVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
TeamVerifiedPhoneOutput,
|
||||||
|
TeamVerifiedPhoneOutputData,
|
||||||
|
TeamVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/teams/:teamId/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||||
|
@ApiTags("Teams Verified Resources")
|
||||||
|
export class TeamsVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the Email.`,
|
||||||
|
})
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_emails_requests" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_requests" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email for a team.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(
|
||||||
|
userId,
|
||||||
|
body.email,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number for an org team.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_verify" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(
|
||||||
|
userId,
|
||||||
|
body.phone,
|
||||||
|
body.code,
|
||||||
|
teamId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails of a team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers of a team.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones")
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<TeamVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers(
|
||||||
|
teamId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email of a team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@Param("id") id: number,
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number
|
||||||
|
): Promise<TeamVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number of a team by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Roles("TEAM_ADMIN")
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@Param("teamId", ParseIntPipe) teamId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<TeamVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,7 +149,7 @@ export class TokensRepository {
|
|||||||
this.dbWrite.prisma.refreshToken.create({
|
this.dbWrite.prisma.refreshToken.create({
|
||||||
data: {
|
data: {
|
||||||
// note(Lauris): I am leaving userId because it was before adding ownerId to standardize payload like in the createOAuthTokens function for
|
// note(Lauris): I am leaving userId because it was before adding ownerId to standardize payload like in the createOAuthTokens function for
|
||||||
// backwards compatability.
|
// backwards compatibility.
|
||||||
secret: this.jwtService.signRefreshToken({
|
secret: this.jwtService.signRefreshToken({
|
||||||
clientId,
|
clientId,
|
||||||
ownerId: tokenUserId,
|
ownerId: tokenUserId,
|
||||||
|
|||||||
+196
@@ -0,0 +1,196 @@
|
|||||||
|
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||||
|
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||||
|
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
|
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||||
|
import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input";
|
||||||
|
import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input";
|
||||||
|
import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input";
|
||||||
|
import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input";
|
||||||
|
import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output";
|
||||||
|
import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output";
|
||||||
|
import {
|
||||||
|
UserVerifiedEmailOutput,
|
||||||
|
UserVerifiedEmailOutputData,
|
||||||
|
UserVerifiedEmailsOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-email.output";
|
||||||
|
import {
|
||||||
|
UserVerifiedPhoneOutput,
|
||||||
|
UserVerifiedPhoneOutputData,
|
||||||
|
UserVerifiedPhonesOutput,
|
||||||
|
} from "@/modules/verified-resources/outputs/verified-phone.output";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { SkipTakePagination } from "@calcom/platform-types";
|
||||||
|
|
||||||
|
@Controller({
|
||||||
|
path: "/v2/verified-resources",
|
||||||
|
})
|
||||||
|
@UseGuards(ApiAuthGuard)
|
||||||
|
@ApiTags("Verified Resources")
|
||||||
|
export class UserVerifiedResourcesController {
|
||||||
|
constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {}
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Email Verification Code",
|
||||||
|
description: `Sends a verification code to the email.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_emails_requests" })
|
||||||
|
async requestEmailVerificationCode(
|
||||||
|
@Body() body: RequestEmailVerificationInput,
|
||||||
|
@GetUser("username") username: string,
|
||||||
|
@GetUser("locale") locale: string
|
||||||
|
): Promise<RequestEmailVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode(
|
||||||
|
{ username, locale },
|
||||||
|
body.email
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request Phone Number Verification Code",
|
||||||
|
description: `Sends a verification code to the phone number.`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/phones/verification-code/request")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_requests" })
|
||||||
|
async requestPhoneVerificationCode(
|
||||||
|
@Body() body: RequestPhoneVerificationInput
|
||||||
|
): Promise<RequestPhoneVerificationOutput> {
|
||||||
|
const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode(
|
||||||
|
body.phone
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify an email.",
|
||||||
|
description: `Use code to verify an email`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/emails/verification-code/verify")
|
||||||
|
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_verify" })
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyEmail(
|
||||||
|
@Body() body: VerifyEmailInput,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<UserVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.verifyEmail(userId, body.email, body.code);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify a phone number.",
|
||||||
|
description: `Use code to verify a phone number`,
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Post("/phones/verification-code/verify")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async verifyPhoneNumber(
|
||||||
|
@Body() body: VerifyPhoneInput,
|
||||||
|
@GetUser("id") userId: number
|
||||||
|
): Promise<UserVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhone = await this.verifiedResourcesService.verifyPhone(userId, body.phone, body.code);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified emails.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/emails")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmails(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<UserVerifiedEmailsOutput> {
|
||||||
|
const verifiedEmails = await this.verifiedResourcesService.getUserVerifiedEmails(
|
||||||
|
userId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedEmails.map((verifiedEmail) => plainToClass(UserVerifiedEmailOutputData, verifiedEmail)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get list of verified phone numbers.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneNumbers(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Query() pagination: SkipTakePagination
|
||||||
|
): Promise<UserVerifiedPhonesOutput> {
|
||||||
|
const verifiedPhoneNumbers = await this.verifiedResourcesService.getUserVerifiedPhoneNumbers(
|
||||||
|
userId,
|
||||||
|
pagination?.skip ?? 0,
|
||||||
|
pagination?.take ?? 250
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: verifiedPhoneNumbers.map((verifiedPhoneNumber) =>
|
||||||
|
plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified email by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/emails/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedEmailById(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<UserVerifiedEmailOutput> {
|
||||||
|
const verifiedEmail = await this.verifiedResourcesService.getUserVerifiedEmailById(userId, id);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get verified phone number by id.",
|
||||||
|
})
|
||||||
|
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||||
|
@Get("/phones/:id")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async getVerifiedPhoneById(
|
||||||
|
@GetUser("id") userId: number,
|
||||||
|
@Param("id") id: number
|
||||||
|
): Promise<UserVerifiedPhoneOutput> {
|
||||||
|
const verifiedPhoneNumber = await this.verifiedResourcesService.getUserVerifiedPhoneNumberById(
|
||||||
|
userId,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: SUCCESS_STATUS,
|
||||||
|
data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class RequestEmailVerificationInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Email to verify.",
|
||||||
|
example: "acme@example.com",
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@Expose()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsPhoneNumber } from "class-validator";
|
||||||
|
|
||||||
|
export class RequestPhoneVerificationInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Phone number to verify.",
|
||||||
|
example: "+372 5555 6666",
|
||||||
|
})
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@Expose()
|
||||||
|
phone!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyEmailInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "Email to verify.",
|
||||||
|
example: "example@acme.com",
|
||||||
|
})
|
||||||
|
@IsEmail()
|
||||||
|
@Expose()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "verification code sent to the email to verify",
|
||||||
|
example: "1ABG2C",
|
||||||
|
})
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose } from "class-transformer";
|
||||||
|
import { IsEmail, IsPhoneNumber, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class VerifyPhoneInput {
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "phone number to verify.",
|
||||||
|
example: "+37255556666",
|
||||||
|
})
|
||||||
|
@IsPhoneNumber()
|
||||||
|
@Expose()
|
||||||
|
phone!: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: String,
|
||||||
|
description: "verification code sent to the phone number to verify",
|
||||||
|
example: "1ABG2C",
|
||||||
|
})
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class RequestEmailVerificationOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEnum } from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class RequestPhoneVerificationOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmptyObject,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UserVerifiedEmailOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified email address.",
|
||||||
|
example: "user@example.com",
|
||||||
|
format: "email",
|
||||||
|
})
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
})
|
||||||
|
userId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified email address.",
|
||||||
|
example: "user@example.com",
|
||||||
|
format: "email",
|
||||||
|
})
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated team, if applicable.",
|
||||||
|
example: 89,
|
||||||
|
})
|
||||||
|
teamId!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
nullable: true,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
userId?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => TeamVerifiedEmailOutputData)
|
||||||
|
data!: TeamVerifiedEmailOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedEmailOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => UserVerifiedEmailOutputData)
|
||||||
|
data!: UserVerifiedEmailOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedEmailsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TeamVerifiedEmailOutputData)
|
||||||
|
data!: TeamVerifiedEmailOutputData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedEmailsOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => UserVerifiedEmailOutputData)
|
||||||
|
data!: UserVerifiedEmailOutputData[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Expose, Type } from "class-transformer";
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmptyObject,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
IsOptional,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
|
||||||
|
export class UserVerifiedPhoneOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified phone number.",
|
||||||
|
example: "+37255556666",
|
||||||
|
format: "phone",
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
})
|
||||||
|
userId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhoneOutputData {
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The unique identifier for the verified email.",
|
||||||
|
example: 789,
|
||||||
|
})
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The verified phone number.",
|
||||||
|
example: "+37255556666",
|
||||||
|
format: "phone",
|
||||||
|
})
|
||||||
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated user, if applicable.",
|
||||||
|
example: 45,
|
||||||
|
nullable: true,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({
|
||||||
|
description: "The ID of the associated team, if applicable.",
|
||||||
|
example: 89,
|
||||||
|
})
|
||||||
|
teamId!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedPhoneOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => UserVerifiedPhoneOutputData)
|
||||||
|
data!: UserVerifiedPhoneOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhoneOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@Type(() => TeamVerifiedPhoneOutputData)
|
||||||
|
data!: TeamVerifiedPhoneOutputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserVerifiedPhonesOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => UserVerifiedPhoneOutputData)
|
||||||
|
data!: UserVerifiedPhoneOutputData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TeamVerifiedPhonesOutput {
|
||||||
|
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||||
|
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||||
|
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: ScheduleOutput,
|
||||||
|
})
|
||||||
|
@IsNotEmptyObject()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TeamVerifiedPhoneOutputData)
|
||||||
|
data!: TeamVerifiedPhoneOutputData[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository";
|
||||||
|
import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository";
|
||||||
|
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyPhoneNumber,
|
||||||
|
sendVerificationCode as sendPhoneVerificationCode,
|
||||||
|
} from "@calcom/platform-libraries";
|
||||||
|
import { sendEmailVerificationByCode, verifyEmailCodeHandler } from "@calcom/platform-libraries/emails";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerifiedResourcesService {
|
||||||
|
constructor(
|
||||||
|
private readonly usersVerifiedResourcesRepository: UsersVerifiedResourcesRepository,
|
||||||
|
private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async requestEmailVerificationCode(user: { username: string; locale: string }, email: string) {
|
||||||
|
const res = await sendEmailVerificationByCode({
|
||||||
|
email: email,
|
||||||
|
language: user.locale,
|
||||||
|
username: user.username,
|
||||||
|
isVerifyingEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok && !res.skipped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new BadRequestException("Email could not be verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.skipped) {
|
||||||
|
throw new ConflictException("Email is already verified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPhoneVerificationCode(phone: string) {
|
||||||
|
try {
|
||||||
|
await sendPhoneVerificationCode(phone);
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestException("Could not send verification code to this phone number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyPhone(userId: number, phone: string, code: string, teamId?: number) {
|
||||||
|
const result = await verifyPhoneNumber(phone, code, userId, teamId);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return teamId
|
||||||
|
? await this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phone, teamId)
|
||||||
|
: await this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumber(userId, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException("Could not verify phone number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyEmail(userId: number, email: string, code: string, teamId?: number) {
|
||||||
|
const isValidToken = await verifyEmailCodeHandler({
|
||||||
|
ctx: { user: { id: userId } },
|
||||||
|
input: { email, code, teamId },
|
||||||
|
});
|
||||||
|
if (isValidToken) {
|
||||||
|
const verifiedEmail = teamId
|
||||||
|
? await this.teamsVerifiedResourcesRepository.getTeamVerifiedEmail(userId, email, teamId)
|
||||||
|
: await this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email);
|
||||||
|
return verifiedEmail;
|
||||||
|
}
|
||||||
|
throw new BadRequestException("Invalid Verification Code");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmailById(userId: number, id: number) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmailById(userId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmailById(teamId: number, id: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmailById(id, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVerifiedEmail(userId: number, email: string) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmails(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmails(userId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmails(teamId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumberById(userId: number, id: number) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumberById(userId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumberById(teamId: number, id: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumberById(id, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumber(userId: number, phoneNumber: string) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumber(userId: number, phoneNumber: string, teamId: number) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phoneNumber, teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumbers(userId, skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumbers(teamId, skip, take);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TeamsVerifiedResourcesRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findMany({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmail(userId: number, email: string, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumber(userId: number, phone: string, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
phoneNumber: phone,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedEmailById(id: number, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeamVerifiedPhoneNumberById(id: number, teamId: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersVerifiedResourcesRepository {
|
||||||
|
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||||
|
|
||||||
|
async getUserVerifiedEmails(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumber(userId: number, phone: string) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
phoneNumber: phone,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmail(userId: number, email: string) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedEmailById(userId: number, id: number) {
|
||||||
|
return this.dbRead.prisma.verifiedEmail.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserVerifiedPhoneNumberById(userId: number, id: number) {
|
||||||
|
return this.dbRead.prisma.verifiedNumber.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||||
|
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||||
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
|
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||||
|
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||||
|
import { OrgTeamsVerifiedResourcesController } from "@/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller";
|
||||||
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
|
import { RedisModule } from "@/modules/redis/redis.module";
|
||||||
|
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||||
|
import { TeamsVerifiedResourcesController } from "@/modules/teams/verified-resources/teams-verified-resources.controller";
|
||||||
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
|
import { UserVerifiedResourcesController } from "@/modules/verified-resources/controllers/users-verified-resources.controller";
|
||||||
|
import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service";
|
||||||
|
import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository";
|
||||||
|
import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository";
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, RedisModule],
|
||||||
|
controllers: [
|
||||||
|
UserVerifiedResourcesController,
|
||||||
|
TeamsVerifiedResourcesController,
|
||||||
|
OrgTeamsVerifiedResourcesController,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
VerifiedResourcesService,
|
||||||
|
UsersVerifiedResourcesRepository,
|
||||||
|
TeamsVerifiedResourcesRepository,
|
||||||
|
MembershipsRepository,
|
||||||
|
OrganizationsTeamsRepository,
|
||||||
|
OrganizationsRepository,
|
||||||
|
StripeService,
|
||||||
|
AppsRepository,
|
||||||
|
CredentialsRepository,
|
||||||
|
UsersRepository,
|
||||||
|
],
|
||||||
|
exports: [VerifiedResourcesService],
|
||||||
|
})
|
||||||
|
export class VerifiedResourcesModule {}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -13,7 +13,7 @@ export class MembershipRepositoryFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(data: Prisma.MembershipCreateInput) {
|
async create(data: Prisma.MembershipCreateInput) {
|
||||||
return this.prismaWriteClient.membership.create({ data });
|
return this.prismaWriteClient.membership.create({ data: { createdAt: new Date(), ...data } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(membershipId: Membership["id"]) {
|
async delete(membershipId: Membership["id"]) {
|
||||||
@@ -30,7 +30,7 @@ export class MembershipRepositoryFixture {
|
|||||||
|
|
||||||
async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) {
|
async addUserToOrg(user: User, org: Team, role: MembershipRole, accepted: boolean) {
|
||||||
const membership = await this.prismaWriteClient.membership.create({
|
const membership = await this.prismaWriteClient.membership.create({
|
||||||
data: { teamId: org.id, userId: user.id, role, accepted },
|
data: { createdAt: new Date(), teamId: org.id, userId: user.id, role, accepted },
|
||||||
});
|
});
|
||||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||||
return membership;
|
return membership;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { TestingModule } from "@nestjs/testing";
|
import { TestingModule } from "@nestjs/testing";
|
||||||
import { Prisma, App_RoutingForms_Form } from "@prisma/client";
|
import { Prisma, App_RoutingForms_Form, App_RoutingForms_FormResponse } from "@prisma/client";
|
||||||
|
|
||||||
export class RoutingFormsRepositoryFixture {
|
export class RoutingFormsRepositoryFixture {
|
||||||
private prismaReadClient: PrismaReadService["prisma"];
|
private prismaReadClient: PrismaReadService["prisma"];
|
||||||
@@ -23,4 +23,10 @@ export class RoutingFormsRepositoryFixture {
|
|||||||
async delete(routingFormId: App_RoutingForms_Form["id"]) {
|
async delete(routingFormId: App_RoutingForms_Form["id"]) {
|
||||||
return this.prismaWriteClient.app_RoutingForms_Form.delete({ where: { id: routingFormId } });
|
return this.prismaWriteClient.app_RoutingForms_Form.delete({ where: { id: routingFormId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteResponse(routingFormResponseId: App_RoutingForms_FormResponse["id"]) {
|
||||||
|
return this.prismaWriteClient.app_RoutingForms_FormResponse.delete({
|
||||||
|
where: { id: routingFormResponseId },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { TestingModule } from "@nestjs/testing";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
export class VerifiedResourcesRepositoryFixtures {
|
||||||
|
private prismaReadClient: PrismaReadService["prisma"];
|
||||||
|
private prismaWriteClient: PrismaWriteService["prisma"];
|
||||||
|
|
||||||
|
constructor(private readonly module: TestingModule) {
|
||||||
|
this.prismaReadClient = module.get(PrismaReadService).prisma;
|
||||||
|
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPhone(data: Prisma.VerifiedNumberCreateInput) {
|
||||||
|
return this.prismaWriteClient.verifiedNumber.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEmailById(id: number) {
|
||||||
|
return this.prismaWriteClient.verifiedEmail.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePhoneById(id: number) {
|
||||||
|
return this.prismaWriteClient.verifiedNumber.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
jest.retryTimes(process.env.CI ? 2 : 0); // Global retry for all tests
|
||||||
@@ -2,9 +2,9 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="font-cal -mt-16 flex min-h-screen w-screen items-center justify-center border pt-16 text-center ">
|
<div className="font-cal -mt-16 flex min-h-screen w-screen items-center justify-center border pt-16 text-center ">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-7xl font-bold uppercase leading-none tracking-tight md:text-8xl">
|
<h1 className="text-7xl leading-none md:text-8xl">
|
||||||
Open UI for absolutely
|
Open UI for absolutely
|
||||||
<br /> Everyone
|
<br /> everyone
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const RenderComponentWithSnippet: React.FC<PropsWithChildren<Props>> = (p
|
|||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
return (
|
return (
|
||||||
<div className="border-gray-6 bg-gray-1 rounded-lg border">
|
<div className="border-gray-6 bg-default rounded-lg border">
|
||||||
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
|
<div className={className("not-prose p-8 xl:p-12", props.className)}>{props.children}</div>
|
||||||
|
|
||||||
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
|
<div className="bg-gray-3 border-gray-6 flex items-center justify-start border-b border-t p-2">
|
||||||
|
|||||||
@@ -65,3 +65,34 @@ Buttons can be grouped together using the ButtonGroup component. The ButtonGroup
|
|||||||
#### Combined
|
#### Combined
|
||||||
|
|
||||||
<ButtonGroupSizesCombined/>
|
<ButtonGroupSizesCombined/>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Primary button (default)
|
||||||
|
<Button>Primary Button</Button>
|
||||||
|
|
||||||
|
// Secondary button
|
||||||
|
<Button color="secondary">Secondary Button</Button>
|
||||||
|
|
||||||
|
// Minimal button
|
||||||
|
<Button color="minimal">Minimal Button</Button>
|
||||||
|
|
||||||
|
// Destructive button
|
||||||
|
<Button color="destructive">Destructive Button</Button>
|
||||||
|
|
||||||
|
// With start icon
|
||||||
|
<Button StartIcon="plus">Add Item</Button>
|
||||||
|
|
||||||
|
// With end icon
|
||||||
|
<Button EndIcon="chevronRight">View More</Button>
|
||||||
|
|
||||||
|
// With link
|
||||||
|
<Button href="/some-path">Navigate</Button>
|
||||||
|
|
||||||
|
// Different sizes
|
||||||
|
<Button size="xs">Extra Small</Button>
|
||||||
|
<Button size="sm">Small</Button>
|
||||||
|
<Button size="base">Base (Default)</Button>
|
||||||
|
<Button size="lg">Large</Button>
|
||||||
|
```
|
||||||
Loaded 100 of 533 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user